Canvas Playground

First canvas fun example

TODO: add some other cool examples to play with



package pl.mareklangiewicz.lib

import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.get
import kotlin.math.*
import kotlin.random.Random
import org.w3c.dom.CanvasRenderingContext2D as Ctx

const val WIDTH = 800.0
const val HEIGHT = 800.0

val ctx by lazy {
    val canvas = document.createElement("canvas") as HTMLCanvasElement
    val ctx = canvas.getContext("2d") as Ctx
    canvas.width = WIDTH.toInt()
    canvas.height = HEIGHT.toInt()
    document.body!!.appendChild(canvas)
    ctx
}

val Double.int get() = toInt()
val Int.dbl get() = toDouble()

infix fun Int.rnd(to: Int) = Random.nextInt(this, to + 1)
infix fun Double.rnd(to: Double) = Random.nextDouble(this, to)
val Double.max get() = Random.nextDouble(0.0, this)

fun Int.near(divisor: Int = 6) = this - this / divisor rnd this + this / divisor
fun Double.near(divisor: Double = 6.0) = this - this / divisor rnd this + this / divisor

data class XY(val x: Double, val y: Double)

infix fun Double.xy(that: Double) = XY(this, that)

operator fun XY.plus(that: XY) = x + that.x xy y + that.y
operator fun XY.minus(that: XY) = x - that.x xy y - that.y
operator fun XY.times(that: Double) = x * that xy y * that
operator fun XY.div(that: Double) = x / that xy y / that

fun lerp(v1: Double, v2: Double, fraction: Double = 0.5) = v1 + (v2 - v1) * fraction
fun lerp(p1: XY, p2: XY, fraction: Double = 0.5) = p1 + (p2 - p1) * fraction

infix fun XY.avg(that: XY) = lerp(this, that)


infix fun XY.rnd(to: XY) = (x rnd to.x) xy (y rnd to.y)

fun XY.near(divisor: Double = 6.0) = this - this / divisor rnd this + this / divisor

fun XY.around(spread: Double = 6.0) = this + ((-spread xy -spread) rnd (spread xy spread))

fun hsl(h: Int = 0, s: Int = 100, l: Int = 50) = "hsl($h, $s%, $l%)"
fun hsla(h: Int = 0, s: Int = 100, l: Int = 50, a: Double = 1.0) = "hsla($h, $s%, $l%, $a)"
fun rgb(r: Int = 0, g: Int = 0, b: Int = 0) = "rgb($r, $g, $b)"
fun rgba(r: Int = 0, g: Int = 0, b: Int = 0, a: Double = 1.0) = "rgb($r, $g, $b, $a)"

fun Ctx.rect(pos: XY, size: XY = (10.0 xy 10.0)) = fillRect(pos.x, pos.y, size.x, size.y)


val rndx get() = 0.0 rnd WIDTH
val rndy get() = 0.0 rnd HEIGHT

private fun Ctx.randomPainting() {
    repeat(4) { randomCurve(100, 140) }
    repeat(10) { randomCurve(1, 30) }
    repeat(4) { randomCircle(4, 10) }
    repeat(3) { randomPolyline(4, 4, 10) }
}

fun clearCanvas() = ctx.clearRect(0.0, 0.0, WIDTH, HEIGHT)

fun Ctx.randomCircle(minWidth: Int, maxWidth: Int = minWidth, minHue: Int = 0, maxHue: Int = 360) = strokePath {
    val radius = 40.0 rnd (100.0 rnd 300.0)
    arc(rndx, rndy, radius, 0.0, 2 * PI)
    lineWidth = (minWidth rnd maxWidth).toDouble()
    strokeStyle = "hsl(${minHue rnd maxHue}, 60%, 50%)"
}

fun Ctx.randomCurve(minWidth: Int, maxWidth: Int = minWidth, minHue: Int = 0, maxHue: Int = 360) = strokePath {
    moveTo(rndx, rndy)
    bezierCurveTo(rndx, rndy, rndx, rndy, rndx, rndy)
    lineWidth = (minWidth rnd maxWidth).toDouble()
    strokeStyle = "hsl(${minHue rnd maxHue}, 60%, 50%)"
}

fun Ctx.randomPolyline(
    segments: Int = 1,
    minWidth: Int = 4,
    maxWidth: Int = minWidth,
    minHue: Int = 0,
    maxHue: Int = 360,
    opacity: Int = 100
) = strokePath {
    moveTo(rndx, rndy)
    repeat(segments) { lineTo(rndx, rndy) }
    lineWidth = (minWidth rnd maxWidth).toDouble()
    strokeStyle = "hsl(${minHue rnd maxHue}, 60%, 50%, $opacity%)"
}

inline fun Ctx.strokePath(block: Ctx.() -> Unit) {
    beginPath()
    block()
    stroke()
}

suspend fun mydelay(timeMillis: Int) = suspendCoroutine<Unit> { cont ->
    val handler = { cont.resume(Unit) }
    window.setTimeout(handler, timeMillis)
}

fun Ctx.fancyPathA(apoints: List<XY>, hue: Int) {
    val startP = apoints[0]
    val startCurP = XY(startP.x-60, startP.y).around(90.0.max.max)
    repeat(50 + startP.y.int * 2) {
        strokePath {
            moveTo(startP.x, startP.y)
            var curP = startCurP
            for (inP in apoints) {
                val p = inP.around(16.0.max.max.max)
                quadraticCurveTo(curP.x, curP.y, p.x, p.y)
                curP = lerp(curP, p, 1.5.near())
            }
            lineWidth = 0.1
            strokeStyle = "hsla($hue, 100%, 30%, 0.2)"
        }
    }
}

suspend fun Ctx.paintSomeLines() {
    val DELAY = 1
    val LINES = 10
    val MARGIN = WIDTH / LINES

    for (nr in 1 until LINES) {
        val y = nr * MARGIN
        for (z in 1..3) {
            val path = buildList {
                add(MARGIN xy y)
                for (x in MARGIN.int .. WIDTH.int-MARGIN.int step 40.near()) {
                    mydelay(DELAY)
                    val nearX = x.dbl.near(12.0)
                    val nearYVar = if (nr % 2 == 0) 100 / WIDTH * x else 100 / WIDTH * (WIDTH-x)
                    val nearY = y.near(nearYVar)
                    add(nearX xy nearY)
                }
            }
            fancyPathA(path, 0 rnd 360)
        }
    }
}


suspend fun Ctx.paintALotOfLines() {
    scale(0.5, 0.5)
    translate(WIDTH, HEIGHT)
//    scale(3.5, 3.5)
    repeat(4) {
        paintSomeLines()
        rotate(PI/2)
    }
}

suspend fun Ctx.paintSomeRects(scrupulosity: Int = 3) {
    val DELAY = 10
    val LINES = 5
    val MARGIN = WIDTH / LINES

    for (nr in 1 until LINES) {
        val y = nr * MARGIN
        for (z in 1 until scrupulosity) {
            mydelay(DELAY)
            val sin1 = 5.0.max
            val sin2 = 1.0.max
            for (xi in MARGIN.int .. WIDTH.int-MARGIN.int step 4) {
                val xr = xi.dbl
                val yr = y + sin(sin1 + xr / 50) * 20
                val sr = (sin(sin2 + xr / 50) * 20)
                fillStyle = hsla(h = xi, s = 60, a = 0.1)
                val pos = xr xy yr
                val size = sr xy sr
                rect(pos.around(), size.around())
            }
        }
    }
}


suspend fun main() {
//    ctx.paintSomeRects()
    ctx.paintALotOfLines()
}


Example code snippet copied from: play.kotlinlang.org - canvas



package creatures

import org.w3c.dom.*
import org.w3c.dom.events.MouseEvent
import kotlinx.browser.document
import kotlinx.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 = 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 logoImage by lazy { getImage("https://play.kotlinlang.org/assets/kotlin-logo.svg") }

val logoImageSize = v(64.0, 64.0)

val Kotlin = Logo(v(canvas.width / 2.0 - logoImageSize.x / 2.0 - 64, canvas.height / 2.0 - logoImageSize.y / 2.0 - 64))

class Logo(override var pos: Vector) : Shape() {
    val relSize: Double = 0.18
    val shadowOffset = v(-3.0, 3.0)
    var size: Vector = logoImageSize * 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) {
        if (!logoImage.complete) {
            state.changed = true
            return
        }

        size = logoImageSize * (state.size.x / logoImageSize.x) * relSize
        state.context.drawImage(getImage("https://play.kotlinlang.org/assets/kotlin-logo.svg"), 0.0, 0.0,
                logoImageSize.x, logoImageSize.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 changed = true
    var shapes = mutableListOf<Shape>()
    var selection: Shape? = null
    var dragOff = Vector()
    val interval = 1000 / 30

    init {
        canvas.onmousedown = { e: MouseEvent ->
            changed = true
            selection = null
            val mousePos = mousePos(e)
            for (shape in shapes) {
                if (mousePos in shape) {
                    dragOff = mousePos - shape.pos
                    shape.selected = true
                    selection = shape
                    break
                }
            }
        }

        canvas.onmousemove = { e: MouseEvent ->
            if (selection != null) {
                selection!!.pos = mousePos(e) - dragOff
                changed = true
            }
        }

        canvas.onmouseup = { e: MouseEvent ->
            if (selection != null) {
                selection!!.selected = false
            }
            selection = null
            changed = true
            this
        }

        canvas.ondblclick = { e: MouseEvent ->
            val newCreature = Creature(mousePos(e), this@CanvasState)
            addShape(newCreature)
            changed = true
            this
        }

        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)
        changed = true
    }

    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 (!changed) return

        changed = false

        clear()
        for (shape in shapes.asReversed()) {
            shape.draw(this)
        }
        Kotlin.draw(this)
    }
}

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>) {
    CanvasState(canvas).apply {
        addShape(Kotlin)
        addShape(Creature(size * 0.25, this))
        addShape(Creature(size * 0.75, this))
    }
}