Embedding Kotlin Playground

Posted on by

fun main(args : Array<String>) { println("Hello Kotliner!") println("Click this green button at the top right!") }

Oh yes, this is a runnable Kotlin snippet embedded right in the blog post.

Note that you can not only run it, but you can also change the code:

// The code below doesn't compile. Add only one char to make it runnable again fun main(args : Array<String>) { val fix = "Kotlin " val me = "is great" println(fixme) }

Cool, isn’t it? Note that completion works too.

Often you don’t want to show all the code in the snippet, but instead only the most interesting and substantial parts of it. This is possible as well.

fun main(args: Array<String>) { //sampleStart val hint = "Click the plus button to see the full code" println (hint.length) //sampleEnd println("yes, 42") }

You can also add tests:

import org.junit.Test import org.junit.Assert class TestExtensionFunctions() { @Test fun testIntExtension() { Assert.assertEquals("Rational number creation error: ", RationalNumber(4, 1), 4.r()) } @Test fun testPairExtension() { Assert.assertEquals("Rational number creation error: ", RationalNumber(2, 3), Pair(2, 3).r()) } } //sampleStart // Implement extension functions Int.r() and Pair.r() // and make them convert Int and Pair to RationalNumber. fun Int.r(): RationalNumber = [mark]TODO()[/mark] fun Pair<Int, Int>.r(): RationalNumber = [mark]TODO()[/mark] data class RationalNumber(val numerator: Int, val denominator: Int) //sampleEnd

You can use JavaScript as a target or even draw on a canvas:

/** * In this example strange creatures are watching the kotlin logo. * You can drag'n'drop them as well as the logo. Doubleclick to add * more creatures but be careful. They may be watching you! */ package creatures import jquery.* import org.w3c.dom.* import kotlin.browser.document import kotlin.browser.window import kotlin.js.Math fun getImage(path: String): HTMLImageElement { val image = window.document.createElement("img") as HTMLImageElement image.src = path return image } val canvas = initalizeCanvas() fun initalizeCanvas(): HTMLCanvasElement { val canvas = document.createElement("canvas") as HTMLCanvasElement val context = canvas.getContext("2d") as CanvasRenderingContext2D context.canvas.width = window.innerWidth.toInt(); context.canvas.height = window.innerHeight.toInt(); document.body!!.appendChild(canvas) return canvas } val context: CanvasRenderingContext2D get() { return canvas.getContext("2d") as CanvasRenderingContext2D } abstract class Shape() { abstract fun draw(state: CanvasState) // these two abstract methods defines that our shapes can be dragged operator abstract fun contains(mousePos: Vector): Boolean abstract var pos: Vector var selected: Boolean = false // a couple of helper extension methods we'll be using in the derived classes fun CanvasRenderingContext2D.shadowed(shadowOffset: Vector, alpha: Double, render: CanvasRenderingContext2D.() -> Unit) { save() shadowColor = "rgba(100, 100, 100, $alpha)" shadowBlur = 5.0 shadowOffsetX = shadowOffset.x shadowOffsetY = shadowOffset.y render() restore() } fun CanvasRenderingContext2D.fillPath(constructPath: CanvasRenderingContext2D.() -> Unit) { beginPath() constructPath() closePath() fill() } } val Kotlin = Logo(v(250.0, 75.0)) class Logo(override var pos: Vector) : Shape() { val relSize: Double = 0.18 val shadowOffset = v(-3.0, 3.0) val imageSize = v(150.0, 150.0) var size: Vector = imageSize * relSize // get-only properties like this saves you lots of typing and are very expressive val position: Vector get() = if (selected) pos - shadowOffset else pos fun drawLogo(state: CanvasState) { size = imageSize * (state.size.x / imageSize.x) * relSize // getKotlinLogo() is a 'magic' function here defined only for purposes of demonstration but in fact it just find an element containing the logo state.context.drawImage(getImage("http://try.kotlinlang.org/static/images/kotlin_logo.svg"), 0.0, 0.0, imageSize.x, imageSize.y, position.x, position.y, size.x, size.y) } override fun draw(state: CanvasState) { val context = state.context if (selected) { // using helper we defined in Shape class context.shadowed(shadowOffset, 0.2) { drawLogo(state) } } else { drawLogo(state) } } override fun contains(mousePos: Vector): Boolean = mousePos.isInRect(pos, size) val centre: Vector get() = pos + size * 0.5 } val gradientGenerator: RadialGradientGenerator? = null get() { if (field == null) { field = RadialGradientGenerator(context) } return field } class Creature(override var pos: Vector, val state: CanvasState) : Shape() { val shadowOffset = v(-5.0, 5.0) val colorStops = gradientGenerator!!.getNext() val relSize = 0.05 // these properties have no backing fields and in java/javascript they could be represented as little helper functions val radius: Double get() = state.width * relSize val position: Vector get() = if (selected) pos - shadowOffset else pos val directionToLogo: Vector get() = (Kotlin.centre - position).normalized //notice how the infix call can make some expressions extremely expressive override fun contains(mousePos: Vector) = pos distanceTo mousePos < radius // defining more nice extension functions fun CanvasRenderingContext2D.circlePath(position: Vector, rad: Double) { arc(position.x, position.y, rad, 0.0, 2 * Math.PI, false) } //notice we can use an extension function we just defined inside another extension function fun CanvasRenderingContext2D.fillCircle(position: Vector, rad: Double) { fillPath { circlePath(position, rad) } } override fun draw(state: CanvasState) { val context = state.context if (!selected) { drawCreature(context) } else { drawCreatureWithShadow(context) } } fun drawCreature(context: CanvasRenderingContext2D) { context.fillStyle = getGradient(context) context.fillPath { tailPath(context) circlePath(position, radius) } drawEye(context) } fun getGradient(context: CanvasRenderingContext2D): CanvasGradient { val gradientCentre = position + directionToLogo * (radius / 4) val gradient = context.createRadialGradient(gradientCentre.x, gradientCentre.y, 1.0, gradientCentre.x, gradientCentre.y, 2 * radius) for (colorStop in colorStops) { gradient.addColorStop(colorStop.first, colorStop.second) } return gradient } fun tailPath(context: CanvasRenderingContext2D) { val tailDirection = -directionToLogo val tailPos = position + tailDirection * radius * 1.0 val tailSize = radius * 1.6 val angle = Math.PI / 6.0 val p1 = tailPos + tailDirection.rotatedBy(angle) * tailSize val p2 = tailPos + tailDirection.rotatedBy(-angle) * tailSize val middlePoint = position + tailDirection * radius * 1.0 context.moveTo(tailPos.x, tailPos.y) context.lineTo(p1.x, p1.y) context.quadraticCurveTo(middlePoint.x, middlePoint.y, p2.x, p2.y) context.lineTo(tailPos.x, tailPos.y) } fun drawEye(context: CanvasRenderingContext2D) { val eyePos = directionToLogo * radius * 0.6 + position val eyeRadius = radius / 3 val eyeLidRadius = eyeRadius / 2 context.fillStyle = "#FFFFFF" context.fillCircle(eyePos, eyeRadius) context.fillStyle = "#000000" context.fillCircle(eyePos, eyeLidRadius) } fun drawCreatureWithShadow(context: CanvasRenderingContext2D) { context.shadowed(shadowOffset, 0.7) { context.fillStyle = getGradient(context) fillPath { tailPath(context) context.circlePath(position, radius) } } drawEye(context) } } class CanvasState(val canvas: HTMLCanvasElement) { var width = canvas.width var height = canvas.height val size: Vector get() = v(width.toDouble(), height.toDouble()) val context = creatures.context var valid = false var shapes = mutableListOf<Shape>() var selection: Shape? = null var dragOff = Vector() val interval = 1000 / 30 init { jq(canvas).mousedown { valid = false selection = null val mousePos = mousePos(it) for (shape in shapes) { if (mousePos in shape) { dragOff = mousePos - shape.pos shape.selected = true selection = shape break } } } jq(canvas).mousemove { if (selection != null) { selection!!.pos = mousePos(it) - dragOff valid = false } } jq(canvas).mouseup { if (selection != null) { selection!!.selected = false } selection = null valid = false } jq(canvas).dblclick { val newCreature = Creature(mousePos(it), this@CanvasState) addShape(newCreature) valid = false } window.setInterval({ draw() }, interval) } fun mousePos(e: MouseEvent): Vector { var offset = Vector() var element: HTMLElement? = canvas while (element != null) { val el: HTMLElement = element offset += Vector(el.offsetLeft.toDouble(), el.offsetTop.toDouble()) element = el.offsetParent as HTMLElement? } return Vector(e.pageX, e.pageY) - offset } fun addShape(shape: Shape) { shapes.add(shape) valid = false } fun clear() { context.fillStyle = "#D0D0D0" context.fillRect(0.0, 0.0, width.toDouble(), height.toDouble()) context.strokeStyle = "#000000" context.lineWidth = 4.0 context.strokeRect(0.0, 0.0, width.toDouble(), height.toDouble()) } fun draw() { if (valid) return clear() for (shape in shapes.reversed()) { shape.draw(this) } Kotlin.draw(this) valid = true } } class RadialGradientGenerator(val context: CanvasRenderingContext2D) { val gradients = mutableListOf<Array<out Pair<Double, String>>>() var current = 0 fun newColorStops(vararg colorStops: Pair<Double, String>) { gradients.add(colorStops) } init { newColorStops(Pair(0.0, "#F59898"), Pair(0.5, "#F57373"), Pair(1.0, "#DB6B6B")) newColorStops(Pair(0.39, "rgb(140,167,209)"), Pair(0.7, "rgb(104,139,209)"), Pair(0.85, "rgb(67,122,217)")) newColorStops(Pair(0.0, "rgb(255,222,255)"), Pair(0.5, "rgb(255,185,222)"), Pair(1.0, "rgb(230,154,185)")) newColorStops(Pair(0.0, "rgb(255,209,114)"), Pair(0.5, "rgb(255,174,81)"), Pair(1.0, "rgb(241,145,54)")) newColorStops(Pair(0.0, "rgb(132,240,135)"), Pair(0.5, "rgb(91,240,96)"), Pair(1.0, "rgb(27,245,41)")) newColorStops(Pair(0.0, "rgb(250,147,250)"), Pair(0.5, "rgb(255,80,255)"), Pair(1.0, "rgb(250,0,217)")) } fun getNext(): Array<out Pair<Double, String>> { val result = gradients.get(current) current = (current + 1) % gradients.size return result } } fun v(x: Double, y: Double) = Vector(x, y) class Vector(val x: Double = 0.0, val y: Double = 0.0) { operator fun plus(v: Vector) = v(x + v.x, y + v.y) operator fun unaryMinus() = v(-x, -y) operator fun minus(v: Vector) = v(x - v.x, y - v.y) operator fun times(koef: Double) = v(x * koef, y * koef) infix fun distanceTo(v: Vector) = Math.sqrt((this - v).sqr) fun rotatedBy(theta: Double): Vector { val sin = Math.sin(theta) val cos = Math.cos(theta) return v(x * cos - y * sin, x * sin + y * cos) } fun isInRect(topLeft: Vector, size: Vector) = (x >= topLeft.x) && (x <= topLeft.x + size.x) && (y >= topLeft.y) && (y <= topLeft.y + size.y) val sqr: Double get() = x * x + y * y val normalized: Vector get() = this * (1.0 / Math.sqrt(sqr)) } fun main(args: Array<String>) { //sampleStart val state = CanvasState(canvas).apply { addShape(Kotlin) addShape(Creature(size * 0.25, this)) addShape(Creature(size * 0.75, this)) } window.setTimeout({ state.valid = false }, 1000) // You can drag this objects //sampleEnd } fun <T> List<T>.reversed(): List<T> { val result = mutableListOf<T>() var i = size while (i > 0) { result.add(get(--i)) } return result }

Sometimes you don’t need or can’t make a runnable sample. In that case you can apply a highlight-only attribute and get the snippet exactly in the same style, but without the ability to run it.

val simpleText1 = "It's just an ordinary Kotlin snippet" ... val simpleText239 = "It doesn't compile actually"

Embedded Kotlin playground and how it’s done

Historically, thousands of newcomers used try.kotlinlang.org as an interactive way of learning the language. In particular, Kotlin Koans online have been extremely popular. More advanced users use this playground for trying small snippets without opening an IDE, for example before pasting code as an answer on StackOverflow.

Embedded Kotlin Playground works on the same technology, but lets you write and run samples on your webpages. It compiles code on our backend server and then runs either in your browser (if the target platform is JS) or on a server (if the target is set to JVM).

Frontend

Adding an embedded Kotlin playground is as easy as writing a single line in the page header:

<script src="https://unpkg.com/kotlin-playground@1" data-selector="code"></script>

Now all the code blocks on the page will be converted to runnable Kotlin snippets. Of course, data-selector is customizable and you can apply the script only to some particular class. There’s also an option to configure a Kotlin playground manually:

<script src="https://unpkg.com/kotlin-playground@1"></script> <script> document.addEventListener('DOMContentLoaded', function() { KotlinPlayground('.code-blocks-selector'); }); </script>

There’re also a lot of different installation and customization options. Read more in the documentation.

Backend

The backend part of the playground compiles the code and provides information for completion and highlighting. Generally, you shouldn’t need to bother about the backend and you may stick with our server unless you want to reference custom JVM libraries.

For writing examples that use some external library, for example when you’re creating interactive documentation for your library, you will have to configure and run your instance of the playground backend. It’s very easy to do: you’ll just need to add any dependencies, run two predefined Gradle tasks, then docker-compose up , and voila – the server is running. See these instructions for details.

Where it’s already used

We already extensively use this technology for writing Kotlin documentation on the official website. All new bits of documentation are written using runnable samples (see Basic syntax, What’s new in 1.1 and 1.2, Lambdas and Coroutines. For some functions from the standard library, there are live examples as well (see groupBy for example).

for example). Kotlin By Example is written with Kotlin-Playground live samples.

We’ve also released a plugin for WordPress. It adds a [kotlin] shortcode which allows embedding an interactive Kotlin playground in any post. All the samples on this page are written with the help of this plugin.



shortcode which allows embedding an interactive Kotlin playground in any post. All the samples on this page are written with the help of this plugin. On the Kotlin forum, you can use the run-kotlin language in markdown syntax to answer questions, with full correctness guaranteed.



Where this can be used

Kotlin Playground improves the reading experience and increases the expressiveness of code examples. It allows readers to not only see the code but also run it, change it, play with it, and run it again. We encourage all authors to use runnable Kotlin snippets, especially when creating:

Learning courses

Supplementary materials for slides and books

Documentation for libraries and frameworks

Examples in blog posts

Later we are going to support scripting in Kotlin Playground as well.

fun main(args: Array<String>) { //sampleStart println("Let's Kotlin!") //sampleEnd }