/* * 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! */ @file:Suppress("DEPRECATION_ERROR") package creatures import jquery.* import org.w3c.dom.* import kotlin.browser.document import kotlin.browser.window import kotlin.math.* fun getImage(path: String): HTMLImageElement { val image = window.document.createElement("img") as HTMLImageElement image.src = path return image } val canvas: HTMLCanvasElement get() { return window.document.getElementsByTagName("canvas").item(0)!! as HTMLCanvasElement } 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.15 val shadowOffset = v(-3.0, 3.0) val imageSize = v(200.0, 200.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("https://try.kotlinlang.org/static/images/canvas/Kotlin-logo.png"), 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 by lazy { RadialGradientGenerator(context) } 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 * 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 = 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() 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 = "#FFFFFF" 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.asReversed()) { shape.draw(this) } Kotlin.draw(this) valid = true } } class RadialGradientGenerator(val context: CanvasRenderingContext2D) { val gradients = mutableListOf>>() var current = 0 fun newColorStops(vararg colorStops: Pair) { 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> { 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) = sqrt((this - v).sqr) fun rotatedBy(theta: Double): Vector { val sin = sin(theta) val cos = 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 / sqrt(sqr)) } fun main(args: Array) { jq { val state = CanvasState(canvas) state.addShape(Kotlin) state.addShape(Creature(state.size * 0.25, state)) state.addShape(Creature(state.size * 0.75, state)) window.setTimeout({ state.valid = false Unit }, 1000) } }