2018-09-18 20:17:34 +00:00
CanvasKit - Skia + WebAssembly
2018-09-20 20:30:21 +00:00
==============================
2018-09-18 20:17:34 +00:00
2018-09-20 20:30:21 +00:00
Skia now offers a WebAssembly build for easy deployment of our graphics APIs on
the web.
2018-09-18 20:17:34 +00:00
2018-09-20 20:30:21 +00:00
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
2018-09-20 21:39:31 +00:00
animation](https://skia.org/user/modules/skottie) support.
2018-09-20 20:30:21 +00:00
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
-------
2018-09-18 20:17:34 +00:00
< style >
#demo canvas {
border: 1px dashed #AAA ;
2018-09-20 21:39:31 +00:00
margin: 2px;
2018-09-18 20:17:34 +00:00
}
2020-05-14 20:04:49 +00:00
#patheffect , #ink , #shaping , #shader1 , #camera3d {
2018-09-20 21:39:31 +00:00
width: 400px;
2018-09-18 20:17:34 +00:00
height: 400px;
}
2020-05-14 20:04:49 +00:00
#sk_legos , #sk_drinks , #sk_party , #sk_onboarding {
2018-09-18 20:17:34 +00:00
width: 300px;
height: 300px;
}
2018-09-21 14:14:30 +00:00
figure {
display: inline-block;
margin: 0;
}
figcaption > a {
margin: 2px 10px;
}
2018-09-18 20:17:34 +00:00
< / style >
< div id = demo >
2020-05-14 20:04:49 +00:00
< h3 > Paragraph shaping, custom shaders, and perspective transformation< / h3 >
2018-09-21 14:14:30 +00:00
< figure >
2020-05-14 20:04:49 +00:00
< canvas id = shaping width = 500 height = 500 > < / canvas >
2018-09-21 14:14:30 +00:00
< figcaption >
2020-05-14 20:04:49 +00:00
< a href = "https://jsfiddle.skia.org/canvaskit/56cb197c724dfdfad0c3d8133d4fcab587e4c4e7f31576e62c17251637d3745c"
2018-09-21 14:14:30 +00:00
target=_blank rel=noopener>
2020-05-14 20:04:49 +00:00
SkParagraph JSFiddle< / a >
2018-09-21 14:14:30 +00:00
< / figcaption >
< / figure >
< figure >
2020-05-14 20:04:49 +00:00
< canvas id = shader1 width = 512 height = 512 > < / canvas >
2018-09-21 14:14:30 +00:00
< figcaption >
2020-05-14 20:04:49 +00:00
< a href = "https://jsfiddle.skia.org/canvaskit/33ff9bed883cd5742b4770169da0b36fb0cbc18fd395ddd9563213e178362d30"
2018-09-21 14:14:30 +00:00
target=_blank rel=noopener>
2020-05-14 20:04:49 +00:00
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 >
2018-09-21 14:14:30 +00:00
< / figcaption >
< / figure >
2020-05-14 20:04:49 +00:00
< h3 > Play back bodymovin lottie files with skottie (click for fiddles)< / h3 >
2018-10-30 19:05:04 +00:00
< a href = "https://jsfiddle.skia.org/canvaskit/092690b273b41076d2f00f0d43d004893d6bb9992c387c0385efa8e6f6bc83d7"
2018-09-21 14:14:30 +00:00
target=_blank rel=noopener>
< canvas id = sk_legos width = 300 height = 300 > < / canvas >
< / a >
2018-10-30 19:05:04 +00:00
< a href = "https://jsfiddle.skia.org/canvaskit/e7ac983d9859f89aff1b6d385190919202c2eb53d028a79992892cacceffd209"
2018-09-21 14:14:30 +00:00
target=_blank rel=noopener>
< canvas id = sk_drinks width = 500 height = 500 > < / canvas >
< / a >
2018-10-30 19:05:04 +00:00
< a href = "https://jsfiddle.skia.org/canvaskit/0e06547181759731e7369d3e3613222a0826692f48c41b16504ed68d671583e1"
2018-09-21 14:14:30 +00:00
target=_blank rel=noopener>
< canvas id = sk_party width = 500 height = 500 > < / canvas >
< / a >
2018-10-30 19:05:04 +00:00
< a href = "https://jsfiddle.skia.org/canvaskit/be3fc1c5c351e7f43cc2840033f80b44feb3475925264808f321bb9e2a21174a"
2018-09-21 14:14:30 +00:00
target=_blank rel=noopener>
< canvas id = sk_onboarding width = 500 height = 500 > < / canvas >
< / a >
2020-05-14 20:04:49 +00:00
< h3 > Go beyond the HTML Canvas2D< / h3 >
2019-02-27 13:42:33 +00:00
< figure >
2020-05-14 20:04:49 +00:00
< canvas id = patheffect width = 400 height = 400 > < / canvas >
2019-02-27 13:42:33 +00:00
< figcaption >
2020-05-14 20:04:49 +00:00
< a href = "https://jsfiddle.skia.org/canvaskit/43b38b83ca77dabe47f18f31cafe83f3018b3a24e569db27fe711c70bc3f7d62"
2019-02-27 13:42:33 +00:00
target=_blank rel=noopener>
2020-05-14 20:04:49 +00:00
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 >
2019-02-27 13:42:33 +00:00
< / figcaption >
< / figure >
2020-01-29 19:21:00 +00:00
2018-09-18 20:17:34 +00:00
< / 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');
2020-01-29 19:21:00 +00:00
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.
2018-09-18 20:17:34 +00:00
if (window.WebAssembly & & typeof window.WebAssembly.compile === 'function') {
console.log('WebAssembly is supported!');
2020-01-29 19:21:00 +00:00
locate_file = 'https://particles.skia.org/static/';
2018-09-18 20:17:34 +00:00
} 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;
}
2018-10-09 13:36:35 +00:00
s.src = locate_file + 'canvaskit.js';
2018-09-18 20:17:34 +00:00
s.onload = () => {
2020-01-29 19:21:00 +00:00
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};
2020-05-14 20:04:49 +00:00
const ckLoaded = CanvasKitInit({
2018-09-18 20:17:34 +00:00
locateFile: (file) => locate_file + file,
2020-05-26 23:10:48 +00:00
});
2020-05-14 20:04:49 +00:00
ckLoaded.then((CK) => {
2018-09-18 20:17:34 +00:00
CanvasKit = CK;
DrawingExample(CanvasKit);
2018-09-20 21:39:31 +00:00
InkExample(CanvasKit);
2019-02-27 13:42:33 +00:00
ShapingExample(CanvasKit);
2018-09-20 21:39:31 +00:00
// Set bounds to fix the 4:3 resolution of the legos
SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300});
2018-09-21 14:14:30 +00:00
// Re-size to fit
SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
2020-01-29 19:21:00 +00:00
ShaderExample1(CanvasKit);
2018-09-18 20:17:34 +00:00
});
fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
resp.text().then((str) => {
legoJSON = str;
2018-09-21 14:14:30 +00:00
SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300});
2018-09-18 20:17:34 +00:00
});
});
fetch('https://storage.googleapis.com/skia-cdn/misc/drinks.json').then((resp) => {
resp.text().then((str) => {
drinksJSON = str;
2018-09-21 14:14:30 +00:00
SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
2018-09-18 20:17:34 +00:00
});
});
fetch('https://storage.googleapis.com/skia-cdn/misc/confetti.json').then((resp) => {
resp.text().then((str) => {
confettiJSON = str;
2018-09-21 14:14:30 +00:00
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);
2018-09-18 20:17:34 +00:00
});
});
2020-05-14 20:04:49 +00:00
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)});
2018-10-02 15:33:52 +00:00
function preventScrolling(canvas) {
canvas.addEventListener('touchmove', (e) => {
// Prevents touch events in the canvas from scrolling the canvas.
e.preventDefault();
e.stopPropagation();
});
}
2018-09-18 20:17:34 +00:00
function DrawingExample(CanvasKit) {
2018-10-30 19:05:04 +00:00
const surface = CanvasKit.MakeCanvasSurface('patheffect');
2018-09-18 20:17:34 +00:00
if (!surface) {
console.log('Could not make surface');
}
const context = CanvasKit.currentContext();
const canvas = surface.getCanvas();
const paint = new CanvasKit.SkPaint();
2018-10-02 15:33:52 +00:00
const textPaint = new CanvasKit.SkPaint();
textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0));
textPaint.setAntiAlias(true);
2019-02-27 13:42:33 +00:00
const textFont = new CanvasKit.SkFont(null, 30);
2018-09-18 20:17:34 +00:00
let i = 0;
2018-09-21 14:14:30 +00:00
let X = 200;
let Y = 200;
2018-09-18 20:17:34 +00:00
function drawFrame() {
const path = starPath(CanvasKit, X, Y);
CanvasKit.setCurrentContext(context);
2020-03-04 20:43:50 +00:00
const dpe = CanvasKit.SkPathEffect.MakeDash([15, 5, 5, 10], i/5);
2018-09-18 20:17:34 +00:00
i++;
paint.setPathEffect(dpe);
2018-11-07 15:43:42 +00:00
paint.setStyle(CanvasKit.PaintStyle.Stroke);
2018-09-18 20:17:34 +00:00
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);
2019-02-27 13:42:33 +00:00
canvas.drawText('Try Clicking!', 10, 380, textPaint, textFont);
2018-09-18 20:17:34 +00:00
canvas.flush();
dpe.delete();
path.delete();
window.requestAnimationFrame(drawFrame);
}
window.requestAnimationFrame(drawFrame);
// Make animation interactive
2018-10-02 15:33:52 +00:00
let interact = (e) => {
2018-09-21 14:14:30 +00:00
if (!e.buttons) {
return;
}
2018-09-18 20:17:34 +00:00
X = e.offsetX;
Y = e.offsetY;
2018-10-02 15:33:52 +00:00
};
document.getElementById('patheffect').addEventListener('pointermove', interact);
document.getElementById('patheffect').addEventListener('pointerdown', interact);
preventScrolling(document.getElementById('patheffect'));
2018-09-18 20:17:34 +00:00
2019-02-27 13:42:33 +00:00
// A client would need to delete this if it didn't go on forever.
// font.delete();
// paint.delete();
2018-09-18 20:17:34 +00:00
}
2018-09-20 21:39:31 +00:00
function InkExample(CanvasKit) {
2018-10-30 19:05:04 +00:00
const surface = CanvasKit.MakeCanvasSurface('ink');
2018-09-20 21:39:31 +00:00
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));
2018-11-07 15:43:42 +00:00
paint.setStyle(CanvasKit.PaintStyle.Stroke);
2018-09-20 21:39:31 +00:00
paint.setStrokeWidth(4.0);
// This effect smooths out the drawn lines a bit.
2020-03-27 16:27:06 +00:00
paint.setPathEffect(CanvasKit.SkPathEffect.MakeCorner(50));
2018-09-20 21:39:31 +00:00
2018-09-21 14:14:30 +00:00
// Draw I N K
2018-09-20 21:39:31 +00:00
let path = new CanvasKit.SkPath();
2018-09-21 14:14:30 +00:00
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);
2018-09-20 21:39:31 +00:00
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;
2018-10-02 15:33:52 +00:00
let interact = (e) => {
let type = e.type;
if (type === 'lostpointercapture' || type === 'pointerup' || !e.pressure ) {
2018-09-20 21:39:31 +00:00
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;
2018-10-02 15:33:52 +00:00
};
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'));
2018-09-20 21:39:31 +00:00
window.requestAnimationFrame(drawFrame);
}
2019-02-27 13:42:33 +00:00
function ShapingExample(CanvasKit) {
const surface = CanvasKit.MakeCanvasSurface('shaping');
if (!surface) {
console.log('Could not make surface');
return;
}
2019-12-04 20:25:51 +00:00
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);
});
});
2019-02-27 13:42:33 +00:00
const skcanvas = surface.getCanvas();
2019-12-04 20:25:51 +00:00
const font = new CanvasKit.SkFont(null, 18);
const fontPaint = new CanvasKit.SkPaint();
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
fontPaint.setAntiAlias(true);
2019-02-27 13:42:33 +00:00
2019-12-04 20:25:51 +00:00
skcanvas.drawText(`Fetching Font data...`, 5, 450, fontPaint, font);
surface.flush();
2019-02-27 13:42:33 +00:00
2019-12-04 20:25:51 +00:00
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.';
2019-02-27 13:42:33 +00:00
function drawFrame() {
2019-12-04 20:25:51 +00:00
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;
}
2019-02-27 13:42:33 +00:00
CanvasKit.setCurrentContext(context);
2019-12-04 20:25:51 +00:00
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);
}
2019-02-27 13:42:33 +00:00
surface.flush();
2019-12-04 20:25:51 +00:00
requestAnimationFrame(drawFrame);
2019-02-27 13:42:33 +00:00
}
// Make animation interactive
let interact = (e) => {
2019-12-04 20:25:51 +00:00
// 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;
2019-02-27 13:42:33 +00:00
};
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);
}
2018-09-18 20:17:34 +00:00
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;
}
2018-09-20 21:39:31 +00:00
function SkottieExample(CanvasKit, id, jsonStr, bounds) {
2018-09-18 20:17:34 +00:00
if (!CanvasKit || !jsonStr) {
return;
}
2018-09-20 21:39:31 +00:00
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};
2018-10-30 19:05:04 +00:00
const surface = CanvasKit.MakeCanvasSurface(id);
2018-09-18 20:17:34 +00:00
if (!surface) {
console.log('Could not make surface');
}
const context = CanvasKit.currentContext();
const canvas = surface.getCanvas();
2018-09-20 21:39:31 +00:00
let firstFrame = new Date().getTime();
2018-09-18 20:17:34 +00:00
function drawFrame() {
2018-09-20 21:39:31 +00:00
let now = new Date().getTime();
let seek = ((now - firstFrame) / duration) % 1.0;
2018-09-18 20:17:34 +00:00
CanvasKit.setCurrentContext(context);
2018-09-20 21:39:31 +00:00
animation.seek(seek);
animation.render(canvas, bounds);
2018-09-18 20:17:34 +00:00
canvas.flush();
window.requestAnimationFrame(drawFrame);
}
window.requestAnimationFrame(drawFrame);
//animation.delete();
}
2020-01-29 19:21:00 +00:00
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;
2020-02-07 18:37:12 +00:00
void main(float2 p, inout half4 color) {
float2 pp = p - in_center;
float radius = sqrt(dot(pp, pp));
2020-01-29 19:21:00 +00:00
radius = sqrt(radius);
2020-02-07 18:37:12 +00:00
float angle = atan(pp.y / pp.x);
2020-01-29 19:21:00 +00:00
float t = (angle + 3.1415926/2) / (3.1415926);
t += radius * rad_scale;
t = fract(t);
color = 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);
}
2020-05-14 20:04:49 +00:00
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 = `
2020-03-23 20:59:08 +00:00
in shader color_map;
in shader normal_map;
2020-05-14 20:04:49 +00:00
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);
}
2018-09-18 20:17:34 +00:00
}
document.head.appendChild(s);
})();
< / script >
2018-09-20 20:30:21 +00:00
Lottie files courtesy of the lottiefiles.com community:
2018-10-30 19:05:04 +00:00
[Lego Loader ](https://www.lottiefiles.com/410-lego-loader ),
[I'm thirsty ](https://www.lottiefiles.com/77-im-thirsty ),
2018-09-21 14:14:30 +00:00
[Confetti ](https://www.lottiefiles.com/1370-confetti ),
[Onboarding ](https://www.lottiefiles.com/1134-onboarding-1 )
2018-09-18 20:17:34 +00:00
2018-09-20 20:30:21 +00:00
Test server
-----------
Test your code on our [CanvasKit Fiddle ](https://jsfiddle.skia.org/canvaskit )
2018-09-18 20:17:34 +00:00
2018-09-20 20:30:21 +00:00
Download
--------
2018-12-18 13:28:59 +00:00
Get [CanvasKit on NPM ](https://www.npmjs.com/package/canvaskit-wasm )