skia2/site/user/modules/canvaskit.md
Brian Osman 767f444feb SkRuntimeEffect SkSL has a new signature for main()
There is no more 'inout half4 color'. Effects return their output color.
If an effect wants the input color, it must use the (already existing)
approach of sampling a nullptr input shader.

The change is guarded for Chromium (so we can update their runtime color
filters in skia_renderer.cc).

For the GPU backend, FPs can now override usesExplicitReturn to indicate
that their emitCode will generate a return statement. If that's true,
then writeProcessorFunction doesn't inject the automatic return of the
output color, and emitFragProc will *always* wrap that FP in a helper
function, even as a top-level FP. GrSkSLFP opts in to this behavior, so
that the user-supplied return becomes the actual return in the FP's
emitCode.

Adapting the skvm code to this wasn't too bad: It looks fragile (what
happens if there are multiple returns?), but that's not really possible
today, without varying control flow.

Bug: skia:10613

Change-Id: I205b81fd87dd32bab30b6d6d5fc78853485da036
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/310756
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Reviewed-by: John Stiles <johnstiles@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
Reviewed-by: Mike Klein <mtklein@google.com>
2020-08-25 13:36:28 +00:00

869 lines
30 KiB
Markdown

CanvasKit - Skia + WebAssembly
==============================
Skia now offers a WebAssembly build for easy deployment of our graphics APIs on
the web.
CanvasKit provides a playground for testing new Canvas and SVG platform APIs,
enabling fast-paced development on the web platform.
It can also be used as a deployment mechanism for custom web apps requiring
cutting-edge features, like Skia's [Lottie
animation](https://skia.org/user/modules/skottie) support.
Features
--------
- WebGL context encapsulated as an SkSurface, allowing for direct drawing to
an HTML canvas
- Core set of Skia canvas/paint/path/text APIs available, see bindings
- Draws to a hardware-accelerated backend
- Security tested with Skia's fuzzers
Samples
-------
<style>
#demo canvas {
border: 1px dashed #AAA;
margin: 2px;
}
#patheffect, #ink, #shaping, #shader1, #camera3d {
width: 400px;
height: 400px;
}
#sk_legos, #sk_drinks, #sk_party, #sk_onboarding {
width: 300px;
height: 300px;
}
figure {
display: inline-block;
margin: 0;
}
figcaption > a {
margin: 2px 10px;
}
</style>
<div id=demo>
<h3>Paragraph shaping, custom shaders, and perspective transformation</h3>
<figure>
<canvas id=shaping width=500 height=500></canvas>
<figcaption>
<a href="https://jsfiddle.skia.org/canvaskit/56cb197c724dfdfad0c3d8133d4fcab587e4c4e7f31576e62c17251637d3745c"
target=_blank rel=noopener>
SkParagraph JSFiddle</a>
</figcaption>
</figure>
<figure>
<canvas id=shader1 width=512 height=512></canvas>
<figcaption>
<a href="https://jsfiddle.skia.org/canvaskit/8ab89ac8f24840509debec604030b9abded5a73de8f6dbc376433f08ed3fba56"
target=_blank rel=noopener>
Shader JSFiddle</a>
</figcaption>
</figure>
<figure>
<canvas id=camera3d width=400 height=400></canvas>
<figcaption>
<a href="https://jsfiddle.skia.org/canvaskit/518a1694fed734b4533f6ffb62e47c10a45289d63aa749e6062c372076f11a12"
target=_blank rel=noopener>
3D Cube JSFiddle</a>
</figcaption>
</figure>
<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>
</a>
<a href="https://jsfiddle.skia.org/canvaskit/e7ac983d9859f89aff1b6d385190919202c2eb53d028a79992892cacceffd209"
target=_blank rel=noopener>
<canvas id=sk_drinks width=500 height=500></canvas>
</a>
<a href="https://jsfiddle.skia.org/canvaskit/0e06547181759731e7369d3e3613222a0826692f48c41b16504ed68d671583e1"
target=_blank rel=noopener>
<canvas id=sk_party width=500 height=500></canvas>
</a>
<a href="https://jsfiddle.skia.org/canvaskit/be3fc1c5c351e7f43cc2840033f80b44feb3475925264808f321bb9e2a21174a"
target=_blank rel=noopener>
<canvas id=sk_onboarding width=500 height=500></canvas>
</a>
<h3>Go beyond the HTML Canvas2D</h3>
<figure>
<canvas id=patheffect width=400 height=400></canvas>
<figcaption>
<a href="https://jsfiddle.skia.org/canvaskit/43b38b83ca77dabe47f18f31cafe83f3018b3a24e569db27fe711c70bc3f7d62"
target=_blank rel=noopener>
Star JSFiddle</a>
</figcaption>
</figure>
<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>
<script type="text/javascript" charset="utf-8">
(function() {
// Tries to load the WASM version if supported, shows error otherwise
let s = document.createElement('script');
let locate_file = '';
// Hey, if you are looking at this code for an example of how to do it yourself, please use
// an actual CDN, such as https://unpkg.com/canvaskit-wasm - it will have better reliability
// and niceties like brotli compression.
if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') {
console.log('WebAssembly is supported!');
locate_file = 'https://particles.skia.org/static/';
} else {
console.log('WebAssembly is not supported (yet) on this browser.');
document.getElementById('demo').innerHTML = "<div>WASM not supported by your browser. Try a recent version of Chrome, Firefox, Edge, or Safari.</div>";
return;
}
s.src = locate_file + 'canvaskit.js';
s.onload = () => {
let CanvasKit = null;
let legoJSON = null;
let drinksJSON = null;
let confettiJSON = null;
let onboardingJSON = null;
let fullBounds = {fLeft: 0, fTop: 0, fRight: 500, fBottom: 500};
const ckLoaded = CanvasKitInit({
locateFile: (file) => locate_file + file,
});
ckLoaded.then((CK) => {
CanvasKit = CK;
DrawingExample(CanvasKit);
InkExample(CanvasKit);
ShapingExample(CanvasKit);
// Set bounds to fix the 4:3 resolution of the legos
SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300});
// Re-size to fit
SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
ShaderExample1(CanvasKit);
});
fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
resp.text().then((str) => {
legoJSON = str;
SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300});
});
});
fetch('https://storage.googleapis.com/skia-cdn/misc/drinks.json').then((resp) => {
resp.text().then((str) => {
drinksJSON = str;
SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
});
});
fetch('https://storage.googleapis.com/skia-cdn/misc/confetti.json').then((resp) => {
resp.text().then((str) => {
confettiJSON = str;
SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
});
});
fetch('https://storage.googleapis.com/skia-cdn/misc/onboarding.json').then((resp) => {
resp.text().then((str) => {
onboardingJSON = str;
SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
});
});
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.
e.preventDefault();
e.stopPropagation();
});
}
function DrawingExample(CanvasKit) {
const surface = CanvasKit.MakeCanvasSurface('patheffect');
if (!surface) {
console.log('Could not make surface');
}
const context = CanvasKit.currentContext();
const canvas = surface.getCanvas();
const paint = new CanvasKit.SkPaint();
const textPaint = new CanvasKit.SkPaint();
textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0));
textPaint.setAntiAlias(true);
const textFont = new CanvasKit.SkFont(null, 30);
let i = 0;
let X = 200;
let Y = 200;
function drawFrame() {
const path = starPath(CanvasKit, X, Y);
CanvasKit.setCurrentContext(context);
const dpe = CanvasKit.SkPathEffect.MakeDash([15, 5, 5, 10], i/5);
i++;
paint.setPathEffect(dpe);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0 + -3 * Math.cos(i/30));
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
canvas.drawPath(path, paint);
canvas.drawText('Try Clicking!', 10, 380, textPaint, textFont);
canvas.flush();
dpe.delete();
path.delete();
window.requestAnimationFrame(drawFrame);
}
window.requestAnimationFrame(drawFrame);
// Make animation interactive
let interact = (e) => {
if (!e.buttons) {
return;
}
X = e.offsetX;
Y = e.offsetY;
};
document.getElementById('patheffect').addEventListener('pointermove', interact);
document.getElementById('patheffect').addEventListener('pointerdown', interact);
preventScrolling(document.getElementById('patheffect'));
// A client would need to delete this if it didn't go on forever.
// font.delete();
// paint.delete();
}
function InkExample(CanvasKit) {
const surface = CanvasKit.MakeCanvasSurface('ink');
if (!surface) {
console.log('Could not make surface');
}
const context = CanvasKit.currentContext();
const canvas = surface.getCanvas();
let paint = new CanvasKit.SkPaint();
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(4.0);
// This effect smooths out the drawn lines a bit.
paint.setPathEffect(CanvasKit.SkPathEffect.MakeCorner(50));
// Draw I N K
let path = new CanvasKit.SkPath();
path.moveTo(80, 30);
path.lineTo(80, 80);
path.moveTo(100, 80);
path.lineTo(100, 15);
path.lineTo(130, 95);
path.lineTo(130, 30);
path.moveTo(150, 30);
path.lineTo(150, 80);
path.moveTo(170, 30);
path.lineTo(150, 55);
path.lineTo(170, 80);
let paths = [path];
let paints = [paint];
function drawFrame() {
CanvasKit.setCurrentContext(context);
for (let i = 0; i < paints.length && i < paths.length; i++) {
canvas.drawPath(paths[i], paints[i]);
}
canvas.flush();
window.requestAnimationFrame(drawFrame);
}
let hold = false;
let interact = (e) => {
let type = e.type;
if (type === 'lostpointercapture' || type === 'pointerup' || !e.pressure ) {
hold = false;
return;
}
if (hold) {
path.lineTo(e.offsetX, e.offsetY);
} else {
paint = paint.copy();
paint.setColor(CanvasKit.Color(Math.random() * 255, Math.random() * 255, Math.random() * 255, Math.random() + .2));
paints.push(paint);
path = new CanvasKit.SkPath();
paths.push(path);
path.moveTo(e.offsetX, e.offsetY);
}
hold = true;
};
document.getElementById('ink').addEventListener('pointermove', interact);
document.getElementById('ink').addEventListener('pointerdown', interact);
document.getElementById('ink').addEventListener('lostpointercapture', interact);
document.getElementById('ink').addEventListener('pointerup', interact);
preventScrolling(document.getElementById('ink'));
window.requestAnimationFrame(drawFrame);
}
function ShapingExample(CanvasKit) {
const surface = CanvasKit.MakeCanvasSurface('shaping');
if (!surface) {
console.log('Could not make surface');
return;
}
let robotoData = null;
fetch('https://storage.googleapis.com/skia-cdn/google-web-fonts/Roboto-Regular.ttf').then((resp) => {
resp.arrayBuffer().then((buffer) => {
robotoData = buffer;
requestAnimationFrame(drawFrame);
});
});
let emojiData = null;
fetch('https://storage.googleapis.com/skia-cdn/misc/NotoColorEmoji.ttf').then((resp) => {
resp.arrayBuffer().then((buffer) => {
emojiData = buffer;
requestAnimationFrame(drawFrame);
});
});
const skcanvas = surface.getCanvas();
const font = new CanvasKit.SkFont(null, 18);
const fontPaint = new CanvasKit.SkPaint();
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
fontPaint.setAntiAlias(true);
skcanvas.drawText(`Fetching Font data...`, 5, 450, fontPaint, font);
surface.flush();
const context = CanvasKit.currentContext();
let paragraph = null;
let X = 10;
let Y = 10;
const str = 'The quick brown fox 🦊 ate a zesty hamburgerfons 🍔.\nThe 👩‍👩‍👧‍👧 laughed.';
function drawFrame() {
if (robotoData && emojiData && !paragraph) {
const fontMgr = CanvasKit.SkFontMgr.FromData([robotoData, emojiData]);
const paraStyle = new CanvasKit.ParagraphStyle({
textStyle: {
color: CanvasKit.BLACK,
fontFamilies: ['Roboto', 'Noto Color Emoji'],
fontSize: 50,
},
textAlign: CanvasKit.TextAlign.Left,
maxLines: 7,
ellipsis: '...',
});
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
builder.addText(str);
paragraph = builder.build();
}
if (!paragraph) {
requestAnimationFrame(drawFrame);
return;
}
CanvasKit.setCurrentContext(context);
skcanvas.clear(CanvasKit.WHITE);
const wrapTo = 350 + 150 * Math.sin(Date.now() / 2000);
paragraph.layout(wrapTo);
skcanvas.drawParagraph(paragraph, 0, 0);
skcanvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint);
let posA = paragraph.getGlyphPositionAtCoordinate(X, Y);
const cp = str.codePointAt(posA.pos);
if (cp) {
const glyph = String.fromCodePoint(cp);
skcanvas.drawText(`At (${X.toFixed(2)}, ${Y.toFixed(2)}) glyph is '${glyph}'`, 5, 450, fontPaint, font);
}
surface.flush();
requestAnimationFrame(drawFrame);
}
// Make animation interactive
let interact = (e) => {
// multiply by 4/5 to account for the difference in the canvas width and the CSS width.
// The 10 accounts for where the mouse actually is compared to where it is drawn.
X = (e.offsetX * 4/5) - 10;
Y = e.offsetY * 4/5;
};
document.getElementById('shaping').addEventListener('pointermove', interact);
document.getElementById('shaping').addEventListener('pointerdown', interact);
document.getElementById('shaping').addEventListener('lostpointercapture', interact);
document.getElementById('shaping').addEventListener('pointerup', interact);
preventScrolling(document.getElementById('shaping'));
window.requestAnimationFrame(drawFrame);
}
function starPath(CanvasKit, X=128, Y=128, R=116) {
let p = new CanvasKit.SkPath();
p.moveTo(X + R, Y);
for (let i = 1; i < 8; i++) {
let a = 2.6927937 * i;
p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a));
}
return p;
}
function SkottieExample(CanvasKit, id, jsonStr, bounds) {
if (!CanvasKit || !jsonStr) {
return;
}
const animation = CanvasKit.MakeAnimation(jsonStr);
const duration = animation.duration() * 1000;
const size = animation.size();
let c = document.getElementById(id);
bounds = bounds || {fLeft: 0, fTop: 0, fRight: size.w, fBottom: size.h};
const surface = CanvasKit.MakeCanvasSurface(id);
if (!surface) {
console.log('Could not make surface');
}
const context = CanvasKit.currentContext();
const canvas = surface.getCanvas();
let firstFrame = new Date().getTime();
function drawFrame() {
let now = new Date().getTime();
let seek = ((now - firstFrame) / duration) % 1.0;
CanvasKit.setCurrentContext(context);
animation.seek(seek);
animation.render(canvas, bounds);
canvas.flush();
window.requestAnimationFrame(drawFrame);
}
window.requestAnimationFrame(drawFrame);
//animation.delete();
}
function ShaderExample1(CanvasKit) {
if (!CanvasKit) {
return;
}
const surface = CanvasKit.MakeCanvasSurface('shader1');
if (!surface) {
throw 'Could not make surface';
}
const skcanvas = surface.getCanvas();
const paint = new CanvasKit.SkPaint();
const prog = `
uniform float rad_scale;
uniform float2 in_center;
uniform float4 in_colors0;
uniform float4 in_colors1;
half4 main(float2 p) {
float2 pp = p - in_center;
float radius = sqrt(dot(pp, pp));
radius = sqrt(radius);
float angle = atan(pp.y / pp.x);
float t = (angle + 3.1415926/2) / (3.1415926);
t += radius * rad_scale;
t = fract(t);
return half4(mix(in_colors0, in_colors1, t));
}
`;
// If there are multiple contexts on the screen, we need to make sure
// we switch to this one before we draw.
const context = CanvasKit.currentContext();
const fact = CanvasKit.SkRuntimeEffect.Make(prog);
function drawFrame() {
CanvasKit.setCurrentContext(context);
skcanvas.clear(CanvasKit.WHITE);
const shader = fact.makeShader([
Math.sin(Date.now() / 2000) / 5,
256, 256,
1, 0, 0, 1,
0, 1, 0, 1],
true/*=opaque*/);
paint.setShader(shader);
skcanvas.drawRect(CanvasKit.LTRBRect(0, 0, 512, 512), paint);
surface.flush();
requestAnimationFrame(drawFrame);
shader.delete();
}
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 shader color_map;
in shader 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;
}
half4 main(float2 p) {
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);
return 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);
})();
</script>
Lottie files courtesy of the lottiefiles.com community:
[Lego Loader](https://www.lottiefiles.com/410-lego-loader),
[I'm thirsty](https://www.lottiefiles.com/77-im-thirsty),
[Confetti](https://www.lottiefiles.com/1370-confetti),
[Onboarding](https://www.lottiefiles.com/1134-onboarding-1)
Test server
-----------
Test your code on our [CanvasKit Fiddle](https://jsfiddle.skia.org/canvaskit)
Download
--------
Get [CanvasKit on NPM](https://www.npmjs.com/package/canvaskit-wasm)