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.*
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)
data class XYZ(val x: Double = 0.0, val y: Double = 0.0, val z: Double = 0.0) {
val xy get() = x xy y
}
infix fun Double.xy(that: Double) = XY(this, that)
infix fun XY.yz(that: Double) = XYZ(x, y, 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
operator fun XYZ.plus(that: XYZ) = x + that.x xy y + that.y yz z + that.z
operator fun XYZ.minus(that: XYZ) = x - that.x xy y - that.y yz z - that.z
operator fun XYZ.times(that: Double) = x * that xy y * that yz z * that
operator fun XYZ.times(that: XYZ) = x * that.x xy y * that.y yz z * that.z
operator fun XYZ.div(that: Double) = x / that xy y / that yz z / that
operator fun XYZ.div(that: XYZ) = x / that.z xy y / that.y yz z / that.z
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
fun lerp(p1: XYZ, p2: XYZ, fraction: Double = 0.5) = lerp(p1.xy, p2.xy, fraction) yz lerp(p1.z, p2.z, fraction)
infix fun XY.avg(that: XY) = lerp(this, that)
infix fun XYZ.avg(that: XYZ) = lerp(this, that)
infix fun XY.rnd(to: XY) = (x rnd to.x) xy (y rnd to.y)
infix fun XYZ.rnd(to: XYZ) = (x rnd to.x) xy (y rnd to.y) yz (z rnd to.z)
fun XY.near(divisor: Double = 6.0) = this - this / divisor rnd this + this / divisor
fun XYZ.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 XYZ.around(spread: Double = 6.0) = this + ((-spread xy -spread yz -spread) rnd (spread xy spread yz 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())
}
}
}
}
//sampleStart
external class UObject3D {
val x: Double
val y: Double
val z: Double
fun pos(x: Double, y: Double, z: Double): UObject3D
fun add(vararg objs: UObject3D)
fun del(vararg objs: UObject3D)
}
external class USchool {
val scene: UObject3D
fun tryToInstallSchoolIn(rootElement: Element?)
fun tryToInstallSchoolInBody(clearBody: Boolean = definedExternally)
fun ucube(sizeX: Double, sizeY: Double, sizeZ: Double, color: Int = definedExternally): UObject3D
fun uline2D(color: Int, vararg xys: Double): UObject3D
fun uline3D(color: Int, vararg xyzs: Double): UObject3D
fun example1AddSpiral()
fun niceColorInt(idx: Int, offset: Int = definedExternally, factor: Int = definedExternally): Int
}
external val uschool: USchool
suspend fun main() {
js("window.uschool = kthreelhu.uschool")
ctx.paintSomeRects(9)
ctx.paintALotOfLines()
uschool.run {
tryToInstallSchoolInBody()
example1AddSpiral()
for (i in 1..16) scene.add(ucube(1.0, 0.2, 4.0, 0x00ff00 + i*0x000020).pos(-4.0, -2.0-i/2, -5.0))
}
}
//sampleEnd
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))
}
//sampleStart
fun main(args: Array<String>) {
CanvasState(canvas).apply {
addShape(Kotlin)
addShape(Creature(size * 0.25, this))
addShape(Creature(size * 0.75, this))
}
//sampleEnd
}