Reorder canvaskit demos, add fixed 3d cube demo
Change-Id: I53ae7bec8470ac80c56780280a7d08974afdc35a
No-Try: true
Docs-Preview: https://skia.org/?cl=289784
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/289784
Commit-Queue: Nathaniel Nifong <nifong@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/site/user/modules/canvaskit.md b/site/user/modules/canvaskit.md
index 7142dc9..5795e05 100644
--- a/site/user/modules/canvaskit.md
+++ b/site/user/modules/canvaskit.md
@@ -29,12 +29,12 @@
margin: 2px;
}
- #patheffect, #ink, #shaping {
+ #patheffect, #ink, #shaping, #shader1, #camera3d {
width: 400px;
height: 400px;
}
- #sk_legos, #sk_drinks, #sk_party, #sk_onboarding, #shader1 {
+ #sk_legos, #sk_drinks, #sk_party, #sk_onboarding {
width: 300px;
height: 300px;
}
@@ -51,25 +51,33 @@
</style>
<div id=demo>
- <h3>Go beyond the HTML Canvas2D</h3>
+ <h3>Paragraph shaping, custom shaders, and perspective transformation</h3>
<figure>
- <canvas id=patheffect width=400 height=400></canvas>
+ <canvas id=shaping width=500 height=500></canvas>
<figcaption>
- <a href="https://jsfiddle.skia.org/canvaskit/43b38b83ca77dabe47f18f31cafe83f3018b3a24e569db27fe711c70bc3f7d62"
+ <a href="https://jsfiddle.skia.org/canvaskit/56cb197c724dfdfad0c3d8133d4fcab587e4c4e7f31576e62c17251637d3745c"
target=_blank rel=noopener>
- Star JSFiddle</a>
+ SkParagraph JSFiddle</a>
</figcaption>
</figure>
<figure>
- <canvas id=ink width=400 height=400></canvas>
+ <canvas id=shader1 width=512 height=512></canvas>
<figcaption>
- <a href="https://jsfiddle.skia.org/canvaskit/ad0a5454db3ac757684ed2fa8ce9f1f0175f1c043d2cbe33597d81481cdb4baa"
+ <a href="https://jsfiddle.skia.org/canvaskit/33ff9bed883cd5742b4770169da0b36fb0cbc18fd395ddd9563213e178362d30"
target=_blank rel=noopener>
- Ink JSFiddle</a>
+ Shader JSFiddle</a>
+ </figcaption>
+ </figure>
+ <figure>
+ <canvas id=camera3d width=400 height=400></canvas>
+ <figcaption>
+ <a href="https://jsfiddle.skia.org/canvaskit/4b7f2cb6683ad3254ac46e3bab62da9a09e994044b2e7512c93d166abeaa2549"
+ target=_blank rel=noopener>
+ 3D Cube JSFiddle</a>
</figcaption>
</figure>
- <h3>Skottie (click for fiddles)</h3>
+ <h3>Play back bodymovin lottie files with skottie (click for fiddles)</h3>
<a href="https://jsfiddle.skia.org/canvaskit/092690b273b41076d2f00f0d43d004893d6bb9992c387c0385efa8e6f6bc83d7"
target=_blank rel=noopener>
<canvas id=sk_legos width=300 height=300></canvas>
@@ -87,21 +95,23 @@
<canvas id=sk_onboarding width=500 height=500></canvas>
</a>
- <h3>SkParagraph (using ICU and Harfbuzz)</h3>
+ <h3>Go beyond the HTML Canvas2D</h3>
<figure>
- <canvas id=shaping width=500 height=500></canvas>
+ <canvas id=patheffect width=400 height=400></canvas>
<figcaption>
- <a href="https://jsfiddle.skia.org/canvaskit/56cb197c724dfdfad0c3d8133d4fcab587e4c4e7f31576e62c17251637d3745c"
+ <a href="https://jsfiddle.skia.org/canvaskit/43b38b83ca77dabe47f18f31cafe83f3018b3a24e569db27fe711c70bc3f7d62"
target=_blank rel=noopener>
- SkParagraph JSFiddle</a>
+ Star JSFiddle</a>
</figcaption>
</figure>
-
- <h3>SKSL for writing custom shaders</h3>
- <a href="https://jsfiddle.skia.org/canvaskit/33ff9bed883cd5742b4770169da0b36fb0cbc18fd395ddd9563213e178362d30"
- target=_blank rel=noopener>
- <canvas id=shader1 width=512 height=512></canvas>
- </a>
+ <figure>
+ <canvas id=ink width=400 height=400></canvas>
+ <figcaption>
+ <a href="https://jsfiddle.skia.org/canvaskit/ad0a5454db3ac757684ed2fa8ce9f1f0175f1c043d2cbe33597d81481cdb4baa"
+ target=_blank rel=noopener>
+ Ink JSFiddle</a>
+ </figcaption>
+ </figure>
</div>
@@ -129,9 +139,11 @@
let confettiJSON = null;
let onboardingJSON = null;
let fullBounds = {fLeft: 0, fTop: 0, fRight: 500, fBottom: 500};
- CanvasKitInit({
+ const ckLoaded = CanvasKitInit({
locateFile: (file) => locate_file + file,
- }).ready().then((CK) => {
+ }).ready();
+
+ ckLoaded.then((CK) => {
CanvasKit = CK;
DrawingExample(CanvasKit);
InkExample(CanvasKit);
@@ -173,6 +185,10 @@
});
});
+ const loadBrickTex = fetch('https://storage.googleapis.com/skia-cdn/misc/brickwork-texture.jpg').then((response) => response.arrayBuffer());
+ const loadBrickBump = fetch('https://storage.googleapis.com/skia-cdn/misc/brickwork_normal-map.jpg').then((response) => response.arrayBuffer());
+ Promise.all([ckLoaded, loadBrickTex, loadBrickBump]).then((results) => {Camera3D(...results)});
+
function preventScrolling(canvas) {
canvas.addEventListener('touchmove', (e) => {
// Prevents touch events in the canvas from scrolling the canvas.
@@ -508,6 +524,330 @@
requestAnimationFrame(drawFrame);
}
+ function Camera3D(canvas, textureImgData, normalImgData) {
+ const surface = CanvasKit.MakeCanvasSurface('camera3d');
+ if (!surface) {
+ console.error('Could not make surface');
+ return;
+ }
+
+ const sizeX = document.getElementById('camera3d').width;
+ const sizeY = document.getElementById('camera3d').height;
+
+ let clickToWorld = CanvasKit.SkM44.identity();
+ let worldToClick = CanvasKit.SkM44.identity();
+ // rotation of the cube shown in the demo
+ let rotation = CanvasKit.SkM44.identity();
+ // temporary during a click and drag
+ let clickRotation = CanvasKit.SkM44.identity();
+
+ // A virtual sphere used for tumbling the object on screen.
+ const vSphereCenter = [sizeX/2, sizeY/2];
+ const vSphereRadius = Math.min(...vSphereCenter);
+
+ // The rounded rect used for each face
+ const margin = vSphereRadius / 20;
+ const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(margin, margin,
+ vSphereRadius - margin, vSphereRadius - margin), margin*2.5, margin*2.5);
+
+ const camNear = 0.05;
+ const camFar = 4;
+ const camAngle = Math.PI / 12;
+
+ const camEye = [0, 0, 1 / Math.tan(camAngle/2) - 1];
+ const camCOA = [0, 0, 0];
+ const camUp = [0, 1, 0];
+
+ let mouseDown = false;
+ let clickDown = [0, 0]; // location of click down
+ let lastMouse = [0, 0]; // last mouse location
+
+ // keep spinning after mouse up. Also start spinning on load
+ let axis = [0.4, 1, 1];
+ let totalSpin = 0;
+ let spinRate = 0.1;
+ let lastRadians = 0;
+ let spinning = setInterval(keepSpinning, 30);
+
+ const imgscale = CanvasKit.SkMatrix.scaled(2, 2);
+ const textureShader = CanvasKit.MakeImageFromEncoded(textureImgData).makeShader(
+ CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, imgscale);
+ const normalShader = CanvasKit.MakeImageFromEncoded(normalImgData).makeShader(
+ CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, imgscale);
+ const children = [textureShader, normalShader];
+
+ const prog = `
+ in fragmentProcessor color_map;
+ in fragmentProcessor normal_map;
+
+ uniform float3 lightPos;
+ layout (marker=local_to_world) uniform float4x4 localToWorld;
+ layout (marker=normals(local_to_world)) uniform float4x4 localToWorldAdjInv;
+
+ float3 convert_normal_sample(half4 c) {
+ float3 n = 2 * c.rgb - 1;
+ n.y = -n.y;
+ return n;
+ }
+
+ void main(float2 p, inout half4 color) {
+ float3 norm = convert_normal_sample(sample(normal_map, p));
+ float3 plane_norm = normalize(localToWorldAdjInv * float4(norm, 0)).xyz;
+
+ float3 plane_pos = (localToWorld * float4(p, 0, 1)).xyz;
+ float3 light_dir = normalize(lightPos - plane_pos);
+
+ float ambient = 0.2;
+ float dp = dot(plane_norm, light_dir);
+ float scale = min(ambient + max(dp, 0), 1);
+
+ color = sample(color_map, p) * half4(float4(scale, scale, scale, 1));
+ }
+`;
+
+ const fact = CanvasKit.SkRuntimeEffect.Make(prog);
+
+ // properties of light
+ let lightLocation = [...vSphereCenter];
+ let lightDistance = vSphereRadius;
+ let lightIconRadius = 12;
+ let draggingLight = false;
+
+ function computeLightWorldPos() {
+ return CanvasKit.SkVector.add(CanvasKit.SkVector.mulScalar([...vSphereCenter, 0], 0.5),
+ CanvasKit.SkVector.mulScalar(vSphereUnitV3(lightLocation), lightDistance));
+ }
+
+ let lightWorldPos = computeLightWorldPos();
+
+ function drawLight(canvas) {
+ const paint = new CanvasKit.SkPaint();
+ paint.setAntiAlias(true);
+ paint.setColor(CanvasKit.WHITE);
+ canvas.drawCircle(...lightLocation, lightIconRadius + 2, paint);
+ paint.setColor(CanvasKit.BLACK);
+ canvas.drawCircle(...lightLocation, lightIconRadius, paint);
+ }
+
+ // Takes an x and y rotation in radians and a scale and returns a 4x4 matrix used to draw a
+ // face of the cube in that orientation.
+ function faceM44(rx, ry, scale) {
+ return CanvasKit.SkM44.multiply(
+ CanvasKit.SkM44.rotated([0,1,0], ry),
+ CanvasKit.SkM44.rotated([1,0,0], rx),
+ CanvasKit.SkM44.translated([0, 0, scale]));
+ }
+
+ const faceScale = vSphereRadius/2
+ const faces = [
+ {matrix: faceM44( 0, 0, faceScale ), color:CanvasKit.RED}, // front
+ {matrix: faceM44( 0, Math.PI, faceScale ), color:CanvasKit.GREEN}, // back
+
+ {matrix: faceM44( Math.PI/2, 0, faceScale ), color:CanvasKit.BLUE}, // top
+ {matrix: faceM44(-Math.PI/2, 0, faceScale ), color:CanvasKit.CYAN}, // bottom
+
+ {matrix: faceM44( 0, Math.PI/2, faceScale ), color:CanvasKit.MAGENTA}, // left
+ {matrix: faceM44( 0,-Math.PI/2, faceScale ), color:CanvasKit.YELLOW}, // right
+ ];
+
+ // Returns a component of the matrix m indicating whether it faces the camera.
+ // If it's positive for one of the matrices representing the face of the cube,
+ // that face is currently in front.
+ function front(m) {
+ // Is this invertible?
+ var m2 = CanvasKit.SkM44.invert(m);
+ if (m2 === null) {
+ m2 = CanvasKit.SkM44.identity();
+ }
+ // look at the sign of the z-scale of the inverse of m.
+ // that's the number in row 2, col 2.
+ return m2[10]
+ }
+
+ // Return the inverse of an SkM44. throw an error if it's not invertible
+ function mustInvert(m) {
+ var m2 = CanvasKit.SkM44.invert(m);
+ if (m2 === null) {
+ throw "Matrix not invertible";
+ }
+ return m2;
+ }
+
+ function saveCamera(canvas, /* rect */ area, /* scalar */ zscale) {
+ const camera = CanvasKit.SkM44.lookat(camEye, camCOA, camUp);
+ const perspective = CanvasKit.SkM44.perspective(camNear, camFar, camAngle);
+ // Calculate viewport scale. Even through we know these values are all constants in this
+ // example it might be handy to change the size later.
+ const center = [(area.fLeft + area.fRight)/2, (area.fTop + area.fBottom)/2, 0];
+ const viewScale = [(area.fRight - area.fLeft)/2, (area.fBottom - area.fTop)/2, zscale];
+ const viewport = CanvasKit.SkM44.multiply(
+ CanvasKit.SkM44.translated(center),
+ CanvasKit.SkM44.scaled(viewScale));
+
+ // want "world" to be in our big coordinates (e.g. area), so apply this inverse
+ // as part of our "camera".
+ canvas.concat(CanvasKit.SkM44.multiply(viewport, perspective));
+ canvas.concat(CanvasKit.SkM44.multiply(camera, mustInvert(viewport)));
+ // Mark the matrix to make it available to the shader by this name.
+ canvas.markCTM('local_to_world');
+ }
+
+ function setClickToWorld(canvas, matrix) {
+ const l2d = canvas.getLocalToDevice();
+ worldToClick = CanvasKit.SkM44.multiply(mustInvert(matrix), l2d);
+ clickToWorld = mustInvert(worldToClick);
+ }
+
+ function drawCubeFace(canvas, m, color) {
+ const trans = new CanvasKit.SkM44.translated([vSphereRadius/2, vSphereRadius/2, 0]);
+ canvas.concat(CanvasKit.SkM44.multiply(trans, m, mustInvert(trans)));
+ const znormal = front(canvas.getLocalToDevice());
+ if (znormal < 0) {
+ return; // skip faces facing backwards
+ }
+ // Pad with space for two 4x4 matrices. Even though the shader uses a layout()
+ // statement to populate them, we still have to reserve space for them.
+ const uniforms = [...lightWorldPos, ...Array(32).fill(0)];
+ const paint = new CanvasKit.SkPaint();
+ paint.setAntiAlias(true);
+ const shader = fact.makeShaderWithChildren(uniforms, true /*=opaque*/, children);
+ paint.setShader(shader);
+ canvas.drawRRect(rr, paint);
+ }
+
+ function drawFrame(canvas) {
+ const clickM = canvas.getLocalToDevice();
+ canvas.save();
+ canvas.translate(vSphereCenter[0] - vSphereRadius/2, vSphereCenter[1] - vSphereRadius/2);
+ // pass surface dimensions as viewport size.
+ saveCamera(canvas, CanvasKit.LTRBRect(0, 0, vSphereRadius, vSphereRadius), vSphereRadius/2);
+ setClickToWorld(canvas, clickM);
+ for (let f of faces) {
+ const saveCount = canvas.getSaveCount();
+ canvas.save();
+ drawCubeFace(canvas, CanvasKit.SkM44.multiply(clickRotation, rotation, f.matrix), f.color);
+ canvas.restoreToCount(saveCount);
+ }
+ canvas.restore(); // camera
+ canvas.restore(); // center the following content in the window
+
+ // draw virtual sphere outline.
+ const paint = new CanvasKit.SkPaint();
+ paint.setAntiAlias(true);
+ paint.setStyle(CanvasKit.PaintStyle.Stroke);
+ paint.setColor(CanvasKit.Color(64, 255, 0, 1.0));
+ canvas.drawCircle(vSphereCenter[0], vSphereCenter[1], vSphereRadius, paint);
+ canvas.drawLine(vSphereCenter[0], vSphereCenter[1] - vSphereRadius,
+ vSphereCenter[0], vSphereCenter[1] + vSphereRadius, paint);
+ canvas.drawLine(vSphereCenter[0] - vSphereRadius, vSphereCenter[1],
+ vSphereCenter[0] + vSphereRadius, vSphereCenter[1], paint);
+
+ drawLight(canvas);
+ }
+
+ // convert a 2D point in the circle displayed on screen to a 3D unit vector.
+ // the virtual sphere is a technique selecting a 3D direction by clicking on a the projection
+ // of a hemisphere.
+ function vSphereUnitV3(p) {
+ // v = (v - fCenter) * (1 / fRadius);
+ let v = CanvasKit.SkVector.mulScalar(CanvasKit.SkVector.sub(p, vSphereCenter), 1/vSphereRadius);
+
+ // constrain the clicked point within the circle.
+ let len2 = CanvasKit.SkVector.lengthSquared(v);
+ if (len2 > 1) {
+ v = CanvasKit.SkVector.normalize(v);
+ len2 = 1;
+ }
+ // the closer to the edge of the circle you are, the closer z is to zero.
+ const z = Math.sqrt(1 - len2);
+ v.push(z);
+ return v;
+ }
+
+ function computeVSphereRotation(start, end) {
+ const u = vSphereUnitV3(start);
+ const v = vSphereUnitV3(end);
+ // Axis is in the scope of the Camera3D function so it can be used in keepSpinning.
+ axis = CanvasKit.SkVector.cross(u, v);
+ const sinValue = CanvasKit.SkVector.length(axis);
+ const cosValue = CanvasKit.SkVector.dot(u, v);
+
+ let m = new CanvasKit.SkM44.identity();
+ if (Math.abs(sinValue) > 0.000000001) {
+ m = CanvasKit.SkM44.rotatedUnitSinCos(
+ CanvasKit.SkVector.mulScalar(axis, 1/sinValue), sinValue, cosValue);
+ const radians = Math.atan(cosValue / sinValue);
+ spinRate = lastRadians - radians;
+ lastRadians = radians;
+ }
+ return m;
+ }
+
+ function keepSpinning() {
+ totalSpin += spinRate;
+ clickRotation = CanvasKit.SkM44.rotated(axis, totalSpin);
+ spinRate *= .998;
+ if (spinRate < 0.01) {
+ stopSpinning();
+ }
+ surface.requestAnimationFrame(drawFrame);
+ }
+
+ function stopSpinning() {
+ clearInterval(spinning);
+ rotation = CanvasKit.SkM44.multiply(clickRotation, rotation);
+ clickRotation = CanvasKit.SkM44.identity();
+ }
+
+ function interact(e) {
+ const type = e.type;
+ let eventPos = [e.offsetX, e.offsetY];
+ if (type === 'lostpointercapture' || type === 'pointerup' || type == 'pointerleave') {
+ if (draggingLight) {
+ draggingLight = false;
+ } else if (mouseDown) {
+ mouseDown = false;
+ if (spinRate > 0.02) {
+ stopSpinning();
+ spinning = setInterval(keepSpinning, 30);
+ }
+ } else {
+ return;
+ }
+ return;
+ } else if (type === 'pointermove') {
+ if (draggingLight) {
+ lightLocation = eventPos;
+ lightWorldPos = computeLightWorldPos();
+ } else if (mouseDown) {
+ lastMouse = eventPos;
+ clickRotation = computeVSphereRotation(clickDown, lastMouse);
+ } else {
+ return;
+ }
+ } else if (type === 'pointerdown') {
+ // Are we repositioning the light?
+ if (CanvasKit.SkVector.dist(eventPos, lightLocation) < lightIconRadius) {
+ draggingLight = true;
+ return;
+ }
+ stopSpinning();
+ mouseDown = true;
+ clickDown = eventPos;
+ lastMouse = eventPos;
+ }
+ surface.requestAnimationFrame(drawFrame);
+ };
+
+ document.getElementById('camera3d').addEventListener('pointermove', interact);
+ document.getElementById('camera3d').addEventListener('pointerdown', interact);
+ document.getElementById('camera3d').addEventListener('lostpointercapture', interact);
+ document.getElementById('camera3d').addEventListener('pointerleave', interact);
+ document.getElementById('camera3d').addEventListener('pointerup', interact);
+
+ surface.requestAnimationFrame(drawFrame);
+ }
+
}
document.head.appendChild(s);
})();