blob: efb256298506298562697e07d81e8cf892a0d63f [file] [log] [blame]
<!DOCTYPE html>
<title>Mesh2D Demo</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
canvas {
width: 1024px;
height: 1024px;
background-color: #ccc;
display: none;
}
.root {
display: flex;
}
.controls {
display: flex;
}
.controls-left { width: 50%; }
.controls-right { width: 50%; }
.controls-right select { width: 100%; }
#loader {
width: 1024px;
height: 1024px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #f1f2f3;
font: bold 2em monospace;
color: #85a2b6;
}
</style>
<div class="root">
<div id="loader">
<img src="BeanEater-1s-200px.gif">
<div>Fetching <a href="https://skia.org/docs/user/modules/canvaskit/">CanvasKit</a>...</div>
</div>
<div id="canvas_wrapper">
<canvas id="canvas2d" width="1024" height="1024"></canvas>
<canvas id="canvas3d" width="1024" height="1024"></canvas>
</div>
<div class="controls">
<div class="controls-left">
<div>Show mesh</div>
<div>Level of detail</div>
<div>Animator</div>
<div>Renderer</div>
</div>
<div class="controls-right">
<div>
<input type="checkbox" id="show_mesh"/>
</div>
<div>
<select id="lod">
<option value="4">4x4</option>
<option value="8" selected>8x8</option>
<option value="16">16x16</option>
<option value="32">32x32</option>
<option value="64">64x64</option>
<option value="128">128x128</option>
<option value="255">255x255</option>
</select>
</div>
<div>
<select id="animator">
<option value="">Manual</option>
<option value="squircleAnimator">Squircle</option>
<option value="twirlAnimator">Twirl</option>
<option value="wiggleAnimator">Wiggle</option>
<option value="cylinderAnimator" selected>Cylinder</option>
</select>
</div>
<div>
<select id="renderer" disabled>
<option value="ckRenderer" selected>CanvasKit (polyfill)</option>
<option value="nativeRenderer">Canvas2D (native)</option>
</select>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="canvaskit.js"></script>
<script type="text/javascript">
class MeshData {
constructor(size, renderer) {
const vertex_count = size*size;
// 2 floats per point
this.verts = new Float32Array(vertex_count*2);
this.animated_verts = new Float32Array(vertex_count*2);
this.uvs = new Float32Array(vertex_count*2);
let i = 0;
for (let y = 0; y < size; ++y) {
for (let x = 0; x < size; ++x) {
// To keep things simple, all vertices are normalized.
this.verts[i + 0] = this.uvs[i + 0] = x / (size - 1);
this.verts[i + 1] = this.uvs[i + 1] = y / (size - 1);
i += 2;
}
}
// 2 triangles per LOD square, 3 indices per triangle
this.indices = new Uint16Array((size - 1)*(size - 1)*6);
i = 0;
for (let y = 0; y < size - 1; ++y) {
for (let x = 0; x < size - 1; ++x) {
const vidx0 = x + y*size;
const vidx1 = vidx0 + size;
this.indices[i++] = vidx0;
this.indices[i++] = vidx0 + 1;
this.indices[i++] = vidx1 + 1;
this.indices[i++] = vidx0;
this.indices[i++] = vidx1;
this.indices[i++] = vidx1 + 1;
}
}
// These can be cached upfront (constant during animation).
this.uvBuffer = renderer.makeUVBuffer(this.uvs);
this.indexBuffer = renderer.makeIndexBuffer(this.indices);
}
animate(animator) {
function bezier(t, p0, p1, p2, p3){
return (1 - t)*(1 - t)*(1 - t)*p0 +
3*(1 - t)*(1 - t)*t*p1 +
3*(1 - t)*t*t*p2 +
t*t*t*p3;
}
// Tuned for non-linear transition.
function ease(t) { return bezier(t, 0, 0.4, 1, 1); }
if (!animator) {
return;
}
const ms = Date.now() - timeBase;
const t = Math.abs((ms / 1000) % 2 - 1);
animator(this.verts, this.animated_verts, t);
}
generateTriangles(func) {
for (let i = 0; i < this.indices.length; i += 3) {
const i0 = 2*this.indices[i + 0];
const i1 = 2*this.indices[i + 1];
const i2 = 2*this.indices[i + 2];
func(this.animated_verts[i0 + 0], this.animated_verts[i0 + 1],
this.animated_verts[i1 + 0], this.animated_verts[i1 + 1],
this.animated_verts[i2 + 0], this.animated_verts[i2 + 1]);
}
}
}
class PatchControls {
constructor() {
this.controls = [
{ pos: [ 0.00, 0.33], color: '#0ff', deps: [] },
{ pos: [ 0.00, 0.00], color: '#0f0', deps: [0, 2] },
{ pos: [ 0.33, 0.00], color: '#0ff', deps: [] },
{ pos: [ 0.66, 0.00], color: '#0ff', deps: [] },
{ pos: [ 1.00, 0.00], color: '#0f0', deps: [3, 5] },
{ pos: [ 1.00, 0.33], color: '#0ff', deps: [] },
{ pos: [ 1.00, 0.66], color: '#0ff', deps: [] },
{ pos: [ 1.00, 1.00], color: '#0f0', deps: [6, 8] },
{ pos: [ 0.66, 1.00], color: '#0ff', deps: [] },
{ pos: [ 0.33, 1.00], color: '#0ff', deps: [] },
{ pos: [ 0.00, 1.00], color: '#0f0', deps: [9, 11] },
{ pos: [ 0.00, 0.66], color: '#0ff', deps: [] },
];
this.radius = 0.01;
this.drag_target = null;
}
mapMouse(ev) {
const w = canvas2d.width,
h = canvas2d.height;
return [
(ev.offsetX - w*(1 - meshScale)*0.5)/(w*meshScale),
(ev.offsetY - h*(1 - meshScale)*0.5)/(h*meshScale),
];
}
onMouseDown(ev) {
const mouse_pos = this.mapMouse(ev);
for (let i = this.controls.length - 1; i >= 0; --i) {
const dx = this.controls[i].pos[0] - mouse_pos[0],
dy = this.controls[i].pos[1] - mouse_pos[1];
if (dx*dx + dy*dy <= this.radius*this.radius) {
this.drag_target = this.controls[i];
this.drag_offset = [dx, dy];
break;
}
}
}
onMouseMove(ev) {
if (!this.drag_target) return;
const mouse_pos = this.mapMouse(ev),
dx = mouse_pos[0] + this.drag_offset[0] - this.drag_target.pos[0],
dy = mouse_pos[1] + this.drag_offset[1] - this.drag_target.pos[1];
this.drag_target.pos = [ this.drag_target.pos[0] + dx, this.drag_target.pos[1] + dy ];
for (let dep_index of this.drag_target.deps) {
const dep = this.controls[dep_index];
dep.pos = [ dep.pos[0] + dx, dep.pos[1] + dy ];
}
this.updateVerts();
}
onMouseUp(ev) {
this.drag_target = null;
}
updateVerts() {
this.samplePatch(parseInt(lodSelectUI.value), meshData.animated_verts);
}
drawUI(line_func, circle_func) {
for (let i = 0; i < this.controls.length; i += 3) {
const c0 = this.controls[i + 0],
c1 = this.controls[i + 1],
c2 = this.controls[i + 2];
line_func(c0.pos, c1.pos, '#f00');
line_func(c1.pos, c2.pos, '#f00');
circle_func(c0.pos, this.radius, c0.color);
circle_func(c1.pos, this.radius, c1.color);
circle_func(c2.pos, this.radius, c2.color);
}
}
// Based on https://github.com/google/skia/blob/de56f293eb41d65786b9e6224fdf9a4702b30f51/src/utils/SkPatchUtils.cpp#L84
sampleCubic(cind, lod) {
const divisions = lod - 1,
h = 1/divisions,
h2 = h*h,
h3 = h*h2,
pts = [
this.controls[cind[0]].pos,
this.controls[cind[1]].pos,
this.controls[cind[2]].pos,
this.controls[cind[3]].pos,
],
coeffs = [
[
pts[3][0] + 3*(pts[1][0] - pts[2][0]) - pts[0][0],
pts[3][1] + 3*(pts[1][1] - pts[2][1]) - pts[0][1],
],
[
3*(pts[2][0] - 2*pts[1][0] + pts[0][0]),
3*(pts[2][1] - 2*pts[1][1] + pts[0][1]),
],
[
3*(pts[1][0] - pts[0][0]),
3*(pts[1][1] - pts[0][1]),
],
pts[0],
],
fwDiff3 = [
6*h3*coeffs[0][0],
6*h3*coeffs[0][1],
];
let fwDiff = [
coeffs[3],
[
h3*coeffs[0][0] + h2*coeffs[1][0] + h*coeffs[2][0],
h3*coeffs[0][1] + h2*coeffs[1][1] + h*coeffs[2][1],
],
[
fwDiff3[0] + 2*h2*coeffs[1][0],
fwDiff3[1] + 2*h2*coeffs[1][1],
],
fwDiff3,
];
let verts = [];
for (let i = 0; i <= divisions; ++i) {
verts.push(fwDiff[0]);
fwDiff[0] = [ fwDiff[0][0] + fwDiff[1][0], fwDiff[0][1] + fwDiff[1][1] ];
fwDiff[1] = [ fwDiff[1][0] + fwDiff[2][0], fwDiff[1][1] + fwDiff[2][1] ];
fwDiff[2] = [ fwDiff[2][0] + fwDiff[3][0], fwDiff[2][1] + fwDiff[3][1] ];
}
return verts;
}
// Based on https://github.com/google/skia/blob/de56f293eb41d65786b9e6224fdf9a4702b30f51/src/utils/SkPatchUtils.cpp#L256
samplePatch(lod, verts) {
const top_verts = this.sampleCubic([ 1, 2, 3, 4 ], lod),
right_verts = this.sampleCubic([ 4, 5, 6, 7 ], lod),
bottom_verts = this.sampleCubic([ 10, 9, 8, 7 ], lod),
left_verts = this.sampleCubic([ 1, 0, 11, 10 ], lod);
let i = 0;
for (let y = 0; y < lod; ++y) {
const v = y/(lod - 1),
left = left_verts[y],
right = right_verts[y];
for (let x = 0; x < lod; ++x) {
const u = x/(lod - 1),
top = top_verts[x],
bottom = bottom_verts[x],
s0 = [
(1 - v)*top[0] + v*bottom[0],
(1 - v)*top[1] + v*bottom[1],
],
s1 = [
(1 - u)*left[0] + u*right[0],
(1 - u)*left[1] + u*right[1],
],
s2 = [
(1 - v)*((1 - u)*this.controls[ 1].pos[0] + u*this.controls[4].pos[0]) +
v*((1 - u)*this.controls[10].pos[0] + u*this.controls[7].pos[0]),
(1 - v)*((1 - u)*this.controls[ 1].pos[1] + u*this.controls[4].pos[1]) +
v*((1 - u)*this.controls[10].pos[1] + u*this.controls[7].pos[1]),
];
verts[i++] = s0[0] + s1[0] - s2[0];
verts[i++] = s0[1] + s1[1] - s2[1];
}
}
}
}
class CKRenderer {
constructor(ck, img, canvasElement) {
this.ck = ck;
this.surface = ck.MakeCanvasSurface(canvasElement);
this.meshPaint = new ck.Paint();
// UVs are normalized, so we scale the image shader down to 1x1.
const skimg = ck.MakeImageFromCanvasImageSource(img);
const localMatrix = [1/skimg.width(), 0, 0,
0, 1/skimg.height(), 0,
0, 0, 1];
this.meshPaint.setShader(skimg.makeShaderOptions(ck.TileMode.Decal,
ck.TileMode.Decal,
ck.FilterMode.Linear,
ck.MipmapMode.None,
localMatrix));
this.gridPaint = new ck.Paint();
this.gridPaint.setColor(ck.BLUE);
this.gridPaint.setAntiAlias(true);
this.gridPaint.setStyle(ck.PaintStyle.Stroke);
this.controlsPaint = new ck.Paint();
this.controlsPaint.setAntiAlias(true);
this.controlsPaint.setStyle(ck.PaintStyle.Fill);
}
// Unlike the native renderer, CK drawVertices() takes typed arrays directly - so
// we don't need to allocate separate buffers.
makeVertexBuffer(buf) { return buf; }
makeUVBuffer (buf) { return buf; }
makeIndexBuffer (buf) { return buf; }
meshPath(mesh) {
// 4 commands per triangle, 3 floats per cmd
const cmds = new Float32Array(mesh.indices.length*12);
let ci = 0;
mesh.generateTriangles((x0, y0, x1, y1, x2, y2) => {
cmds[ci++] = this.ck.MOVE_VERB; cmds[ci++] = x0; cmds[ci++] = y0;
cmds[ci++] = this.ck.LINE_VERB; cmds[ci++] = x1; cmds[ci++] = y1;
cmds[ci++] = this.ck.LINE_VERB; cmds[ci++] = x2; cmds[ci++] = y2;
cmds[ci++] = this.ck.LINE_VERB; cmds[ci++] = x0; cmds[ci++] = y0;
});
return this.ck.Path.MakeFromCmds(cmds);
}
drawMesh(mesh, ctrls) {
const vertices = this.ck.MakeVertices(this.ck.VertexMode.Triangles,
this.makeVertexBuffer(mesh.animated_verts),
mesh.uvBuffer, null, mesh.indexBuffer, false);
const canvas = this.surface.getCanvas();
const w = this.surface.width(),
h = this.surface.height();
canvas.save();
canvas.translate(w*(1-meshScale)*0.5, h*(1-meshScale)*0.5);
canvas.scale(w*meshScale, h*meshScale);
canvas.drawVertices(vertices, this.ck.BlendMode.Dst, this.meshPaint);
if (showMeshUI.checked) {
canvas.drawPath(this.meshPath(mesh), this.gridPaint);
}
ctrls?.drawUI(
(p0, p1, color) => {
this.controlsPaint.setColor(this.ck.parseColorString(color));
canvas.drawLine(p0[0], p0[1], p1[0], p1[1], this.controlsPaint);
},
(c, r, color) => {
this.controlsPaint.setColor(this.ck.parseColorString(color));
canvas.drawCircle(c[0], c[1], r, this.controlsPaint);
}
);
canvas.restore();
this.surface.flush();
}
}
class NativeRenderer {
constructor(img, canvasElement) {
this.img = img;
this.ctx = canvasElement.getContext("2d");
}
// New Mesh2D API: https://github.com/fserb/canvas2D/blob/master/spec/mesh2d.md#mesh2d-api
makeVertexBuffer(buf) { return new Mesh2DVertexBuffer(buf); }
makeUVBuffer(buf) {
// Temp workaround for lack of uv normalization support in the current prototype.
for (let i = 0; i < buf.length; i+= 2) {
buf[i + 0] *= this.img.width;
buf[i + 1] *= this.img.height;
}
return new Mesh2DVertexBuffer(buf);
}
makeIndexBuffer(buf) { return new Mesh2DIndexBuffer(buf); }
meshPath(mesh) {
const path = new Path2D();
mesh.generateTriangles((x0, y0, x1, y1, x2, y2) => {
path.moveTo(x0, y0);
path.lineTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x0, y0);
});
return path;
}
drawMesh(mesh, ctrls) {
const vbuf = new Mesh2DVertexBuffer(mesh.animated_verts);
const w = canvas2d.width,
h = canvas2d.height;
this.ctx.clearRect(0, 0, canvas2d.width, canvas2d.height);
this.ctx.save();
this.ctx.translate(w*(1-meshScale)*0.5, h*(1-meshScale)*0.5);
this.ctx.scale(w*meshScale, h*meshScale);
this.ctx.drawMesh(vbuf, mesh.uvBuffer, mesh.indexBuffer, this.img);
if (showMeshUI.checked) {
this.ctx.strokeStyle = "blue";
this.ctx.lineWidth = 0.001;
this.ctx.stroke(this.meshPath(mesh));
}
ctrls?.drawUI(
(p0, p1, color) => {
this.ctx.lineWidth = 0.001;
this.ctx.strokeStyle = color;
this.ctx.beginPath();
this.ctx.moveTo(p0[0], p0[1]);
this.ctx.lineTo(p1[0], p1[1]);
this.ctx.stroke();
},
(c, r, color) => {
this.ctx.fillStyle = color;
this.ctx.beginPath();
this.ctx.arc(c[0], c[1], r, 0, 2*Math.PI);
this.ctx.fill();
}
);
this.ctx.restore();
}
}
function squircleAnimator(verts, animated_verts, t) {
function lerp(a, b, t) { return a + t*(b - a); }
for (let i = 0; i < verts.length; i += 2) {
const uvx = verts[i + 0] - 0.5,
uvy = verts[i + 1] - 0.5,
d = Math.sqrt(uvx*uvx + uvy*uvy)*0.5/Math.max(Math.abs(uvx), Math.abs(uvy)),
s = d > 0 ? lerp(1, (0.5/ d), t) : 1;
animated_verts[i + 0] = uvx*s + 0.5;
animated_verts[i + 1] = uvy*s + 0.5;
}
}
function twirlAnimator(verts, animated_verts, t) {
const kMaxRotate = Math.PI*4;
for (let i = 0; i < verts.length; i += 2) {
const uvx = verts[i + 0] - 0.5,
uvy = verts[i + 1] - 0.5,
r = Math.sqrt(uvx*uvx + uvy*uvy),
a = kMaxRotate * r * t;
animated_verts[i + 0] = uvx*Math.cos(a) - uvy*Math.sin(a) + 0.5;
animated_verts[i + 1] = uvy*Math.cos(a) + uvx*Math.sin(a) + 0.5;
}
}
function wiggleAnimator(verts, animated_verts, t) {
const radius = t*0.2/(Math.sqrt(verts.length/2) - 1);
for (let i = 0; i < verts.length; i += 2) {
const phase = i*Math.PI*0.1505;
const angle = phase + t*Math.PI*2;
animated_verts[i + 0] = verts[i + 0] + radius*Math.cos(angle);
animated_verts[i + 1] = verts[i + 1] + radius*Math.sin(angle);
}
}
function cylinderAnimator(verts, animated_verts, t) {
const kCylRadius = .2;
const cyl_pos = t;
for (let i = 0; i < verts.length; i += 2) {
const uvx = verts[i + 0],
uvy = verts[i + 1];
if (uvx <= cyl_pos) {
animated_verts[i + 0] = uvx;
animated_verts[i + 1] = uvy;
continue;
}
const arc_len = uvx - cyl_pos,
arc_ang = arc_len/kCylRadius;
animated_verts[i + 0] = cyl_pos + Math.sin(arc_ang)*kCylRadius;
animated_verts[i + 1] = uvy;
}
}
function drawFrame() {
meshData.animate(animator);
currentRenderer.drawMesh(meshData, patchControls);
requestAnimationFrame(drawFrame);
}
function switchRenderer(renderer) {
currentRenderer = renderer;
meshData = new MeshData(parseInt(lodSelectUI.value), currentRenderer);
const showCanvas = renderer == ckRenderer ? canvas3d : canvas2d;
const hideCanvas = renderer == ckRenderer ? canvas2d : canvas3d;
showCanvas.style.display = 'block';
hideCanvas.style.display = 'none';
patchControls?.updateVerts();
}
const canvas2d = document.getElementById("canvas2d");
const canvas3d = document.getElementById("canvas3d");
const hasMesh2DAPI = 'drawMesh' in CanvasRenderingContext2D.prototype;
const showMeshUI = document.getElementById("show_mesh");
const lodSelectUI = document.getElementById("lod");
const animatorSelectUI = document.getElementById("animator");
const rendererSelectUI = document.getElementById("renderer");
const meshScale = 0.75;
const loadCK = CanvasKitInit({ locateFile: (file) => 'https://demos.skia.org/demo/mesh2d/' + file });
const loadImage = new Promise(resolve => {
const image = new Image();
image.addEventListener('load', () => { resolve(image); });
image.src = 'baby_tux.png';
});
var ckRenderer;
var nativeRenderer;
var currentRenderer;
var meshData;
var image;
const timeBase = Date.now();
var animator = window[animatorSelectUI.value];
var patchControls = animator ? null : new PatchControls();
Promise.all([loadCK, loadImage]).then(([ck, img]) => {
ckRenderer = new CKRenderer(ck, img, canvas3d);
nativeRenderer = 'drawMesh' in CanvasRenderingContext2D.prototype
? new NativeRenderer(img, canvas2d)
: null;
rendererSelectUI.disabled = !nativeRenderer;
rendererSelectUI.value = nativeRenderer ? "nativeRenderer" : "ckRenderer";
document.getElementById('loader').style.display = 'none';
switchRenderer(nativeRenderer ? nativeRenderer : ckRenderer);
requestAnimationFrame(drawFrame);
});
lodSelectUI.onchange = () => { switchRenderer(currentRenderer); }
rendererSelectUI.onchange = () => { switchRenderer(window[rendererSelectUI.value]); }
animatorSelectUI.onchange = () => {
animator = window[animatorSelectUI.value];
patchControls = animator ? null : new PatchControls();
patchControls?.updateVerts();
}
const cwrapper = document.getElementById('canvas_wrapper');
cwrapper.onmousedown = (ev) => { patchControls?.onMouseDown(ev); }
cwrapper.onmousemove = (ev) => { patchControls?.onMouseMove(ev); }
cwrapper.onmouseup = (ev) => { patchControls?.onMouseUp(ev); }
</script>