341 lines
11 KiB
Kotlin
Vendored
341 lines
11 KiB
Kotlin
Vendored
/*
|
|
* 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.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<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 = "#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<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) = 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<String>) {
|
|
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)
|
|
}
|
|
}
|
|
|