Unverified Commit d7320949 authored by Shagen Ogandzhanian's avatar Shagen Ogandzhanian Committed by GitHub

Introduce minimal viable api for renderer in web, update web sample (#221)

* Introduce minimal viable api for renderer in web, update web sample

* Fix bouncing logic

* Process Position.INSIDE case so that we won't see a warning on non-exhausting "when"
Co-authored-by: 's avatarNikolay Igotti <igotti@gmail.com>
parent f63103ed
......@@ -2,46 +2,128 @@ package org.jetbrains.skiko.sample.js
import kotlinx.browser.document
import kotlinx.browser.window
import org.jetbrains.skia.*
import org.jetbrains.skiko.wasm.GetWebGLContext
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color4f
import org.jetbrains.skia.Paint
import org.jetbrains.skia.PaintMode
import org.jetbrains.skia.Rect
import org.jetbrains.skiko.wasm.api.CanvasRenderer
import org.jetbrains.skiko.wasm.onWasmReady
import org.w3c.dom.HTMLCanvasElement
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
private data class Circle(var x: Float, var y: Float, var r: Float)
fun main() {
onWasmReady {
val paint = Paint().apply {
color4f = Color4f(0f, 1f, 0f, 1.0f)
mode = PaintMode.FILL
isAntiAlias = true
private fun Canvas.draw(circle: Circle, paint: Paint) {
drawCircle(circle.x, circle.y, circle.r, paint)
}
private fun Circle.move(s: Float, angle: Float, width: Int, height: Int, r: Float) {
x = (x + s * sin(angle)).coerceAtLeast(r).coerceAtMost(width.toFloat() - r)
y = (y + s * cos(angle)).coerceAtLeast(r).coerceAtMost(height.toFloat() - r)
}
private enum class Position {
INSIDE,
TOUCHES_SOUTH,
TOUCHES_NORTH,
TOUCHES_WEST,
TOUCHES_EAST
}
private fun calculatePosition(circle: Circle, boundingWidth: Int, boundingHeight: Int): Position {
val southmost = circle.y + circle.r
val northmost = circle.y - circle.r
val westmost = circle.x - circle.r
val eastmost = circle.x + circle.r
return when {
southmost >= boundingHeight -> Position.TOUCHES_SOUTH
northmost <= 0 -> Position.TOUCHES_NORTH
eastmost >= boundingWidth -> Position.TOUCHES_EAST
westmost <= 0 -> Position.TOUCHES_WEST
else -> Position.INSIDE
}
}
private class BouncingBall(
val circle: Circle,
val width: Int,
val height: Int,
val velocity: Float,
var angle: Double
) {
fun recalculate(dt: Float) {
val position = calculatePosition(circle, width, height)
when (position) {
Position.TOUCHES_SOUTH -> angle = PI - angle
Position.TOUCHES_EAST -> angle = -angle
Position.TOUCHES_WEST -> angle = -angle
Position.TOUCHES_NORTH -> angle = PI - angle
Position.INSIDE -> angle
}
val canvas = document.getElementById("c") as HTMLCanvasElement
canvas.getContext("webgl")
GetWebGLContext(canvas)
val grContext = DirectContext.makeGL()
val surface = Surface.makeFromBackendRenderTarget(
grContext,
BackendRenderTarget.makeGL(
canvas.width, canvas.height, 1, 8, 0, 0x8058
),
SurfaceOrigin.BOTTOM_LEFT,
SurfaceColorFormat.RGBA_8888,
ColorSpace.sRGB
)
fun draw() {
surface.canvas.drawCircle(50f, 50f, 25f, paint)
paint.color4f = Color4f(1f, 1f, 0f, 1.0f)
surface.canvas.drawCircle(100f, 100f, 10f, paint)
surface.canvas.drawCircle(200f, 200f, 30f, paint)
window.requestAnimationFrame { draw() }
surface.flushAndSubmit()
circle.move(velocity * (dt.coerceAtMost(500f) / 1000), angle.toFloat(), width, height, circle.r)
}
}
private fun Canvas.draw(ball: BouncingBall, paint: Paint) {
draw(ball.circle, paint)
}
private class BouncingBallsApp(htmlCanvas: HTMLCanvasElement, private var prevTimestamp: Double) : CanvasRenderer(htmlCanvas) {
private fun Color4f.asPaint() = Paint().apply {
color4f = this@asPaint
mode = PaintMode.FILL
isAntiAlias = true
}
val data = listOf(
BouncingBall(Circle(200f, 50f, 25f), width, height, 172f, PI / 4),
BouncingBall(Circle(100f, 100f, 10f), width, height, 162f, -PI / 3),
BouncingBall(Circle(150f, 120f, 30f), width, height, 168f, 3 * PI / 4),
BouncingBall(Circle(100f, 10f, 25f), width, height, 208f, -PI / 6),
BouncingBall(Circle(120f, 100f, 40f), width, height, 120f, -1.1 * PI)
).zip(listOf(
Color4f(0f, 1f, 0f, 0.8f).asPaint(),
Color4f(0f, 0f, 1f, 0.8f).asPaint(),
Color4f(1f, 0f, 0f, 0.8f).asPaint(),
Color4f(1f, 0f, 1f, 0.8f).asPaint(),
Color4f(0f, 1f, 1f, 0.8f).asPaint()
))
override fun drawFrame(currentTimestamp: Double) {
val dtime = (currentTimestamp - prevTimestamp)
prevTimestamp = currentTimestamp
data.forEach { (ball, paint) ->
ball.recalculate(dtime.toFloat())
canvas.draw(ball, paint)
}
}
}
draw()
private class DemoApp(htmlCanvas: HTMLCanvasElement) : CanvasRenderer(htmlCanvas) {
private val paint = Paint()
override fun drawFrame(currentTimestamp: Double) {
canvas.draw(Circle(200f, 50f, 25f), paint)
canvas.drawLine(100f, 100f, 200f, 200f, paint)
canvas.drawRect(Rect(10f, 20f, 50f, 70f), paint)
canvas.drawOval(Rect(110f, 220f, 50f, 70f), paint)
canvas.drawOval(Rect(110f, 220f, 50f, 70f), paint)
}
}
fun main() {
onWasmReady {
val syncNow = window.performance.now()
BouncingBallsApp(document.getElementById("c") as HTMLCanvasElement, syncNow).draw()
BouncingBallsApp(document.getElementById("b") as HTMLCanvasElement, syncNow).draw()
DemoApp(document.getElementById("a") as HTMLCanvasElement).draw()
}
}
......@@ -5,9 +5,15 @@
<title>skiko for web demo</title>
<script src="skiko.js"> </script>
<script src="SkiaJsSample.js"> </script>
<link type="text/css" rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Skiko For Web</h1>
<canvas id="c" width="300" height="300" style="outline: 2px solid red"></canvas>
<canvas id="c" width="300" height="300"></canvas>
<canvas id="b" width="300" height="300"></canvas>
<div>
<canvas id="a" width="606" height="400"></canvas>
</div>
</body>
</html>
canvas {
border: 1px solid black;
}
\ No newline at end of file
package org.jetbrains.skiko.wasm
import kotlinx.dom.createElement
import kotlinx.browser.document
import org.jetbrains.skia.impl.NativePointer
import org.khronos.webgl.WebGLRenderingContext
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.RenderingContext
import kotlin.js.Promise
external val wasmSetup: Promise<Boolean>
......@@ -16,7 +12,7 @@ external interface GLInterface {
fun makeContextCurrent(contextPointer: NativePointer): Boolean;
}
external object GL: GLInterface {
external object GL : GLInterface {
override fun createContext(context: HTMLCanvasElement, contextAttributes: dynamic): Int = definedExternally
override fun makeContextCurrent(contextPointer: NativePointer): Boolean = definedExternally
}
......@@ -54,7 +50,7 @@ fun ContextAttributes.asJsObject(): dynamic {
}
fun GetWebGLContext(canvas: HTMLCanvasElement, attr: ContextAttributes? = null): Boolean {
fun CreateWebGLContext(canvas: HTMLCanvasElement, attr: ContextAttributes? = null): NativePointer {
val contextAttributes = object : ContextAttributes {
override val alpha = attr?.alpha ?: 1
override val depth = attr?.depth ?: 1
......@@ -70,6 +66,5 @@ fun GetWebGLContext(canvas: HTMLCanvasElement, attr: ContextAttributes? = null):
override val majorVersion = attr?.majorVersion ?: 1
}
val contextPointer = GL.createContext(canvas, contextAttributes.asJsObject())
return GL.makeContextCurrent(contextPointer)
return GL.createContext(canvas, contextAttributes.asJsObject())
}
\ No newline at end of file
package org.jetbrains.skiko.wasm.api
import kotlinx.browser.window
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.ColorSpace
import org.jetbrains.skia.DirectContext
import org.jetbrains.skia.Surface
import org.jetbrains.skia.SurfaceColorFormat
import org.jetbrains.skia.SurfaceOrigin
import org.jetbrains.skiko.wasm.CreateWebGLContext
import org.jetbrains.skiko.wasm.GL
import org.w3c.dom.HTMLCanvasElement
fun Surface.Companion.createFromGL(width: Int, height: Int) = makeFromBackendRenderTarget(
DirectContext.makeGL(),
BackendRenderTarget.makeGL(
width, height, 1, 8, 0, 0x8058
),
SurfaceOrigin.BOTTOM_LEFT,
SurfaceColorFormat.RGBA_8888,
ColorSpace.sRGB
)
abstract class CanvasRenderer constructor(htmlCanvas: HTMLCanvasElement, val width: Int, val height: Int) {
constructor(canvas: HTMLCanvasElement): this(canvas, canvas.width, canvas.height)
private val contextPointer = CreateWebGLContext(htmlCanvas)
init {
GL.makeContextCurrent(contextPointer)
}
val surface = Surface.createFromGL(width, height)
val canvas: Canvas
get() = surface.canvas
abstract fun drawFrame(currentTimestamp: Double)
fun draw() {
window.requestAnimationFrame { timestamp ->
GL.makeContextCurrent(contextPointer)
drawFrame(timestamp)
surface.flushAndSubmit()
draw()
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment