Back to Skia

CanvasKit - Skia + WebAssembly

site/docs/user/modules/canvaskit.md

latest27.3 KB
Original Source

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 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/6a5c211a8cb4a7752297674b3533f7e1bbc2a78dd37f117c29a77bcc68411f31" 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/ac0574825f9e517f2dfa8e822126ee75b005e8156c3de4a95d4ffd17ab6ca57b" 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/289946b783390c3242cb5cc117d7bcaf2bcb610bf3d1e67a1dd9c46c1e66b968" 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/cb0b72eadb45f7e75b4015db7251e9da2cc202a2ce1cbec8eb2e453d83a619a6" target=_blank rel=noopener> <canvas id=sk_legos width=300 height=300></canvas> </a> <a href="https://jsfiddle.skia.org/canvaskit/e77274c30d63645d3bb82fd366991e27c1e1c3df39def04e999b4fcce9f425a2" target=_blank rel=noopener> <canvas id=sk_drinks width=500 height=500></canvas> </a> <a href="https://jsfiddle.skia.org/canvaskit/e42700132d80efd3470b0f08334556028490ac08d1938210fa618504c6109c99" target=_blank rel=noopener> <canvas id=sk_party width=500 height=500></canvas> </a> <a href="https://jsfiddle.skia.org/canvaskit/987b1f99f4703f9f44dbfb2f43a5ed107672334f68d6262cd53ba44ed7a09236" 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/3588b3b0a7cc93f36d9fa4f08b397c38971dcb1f80a36107f9ad93c051f2cb28" 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/bd42c174a0dcb2f65ff1f3c803397df14014d1e66b92185e9980dc631a49f258" 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 = ''; if (window.WebAssembly && typeof window.WebAssembly.compile === 'function') { console.log('WebAssembly is supported!'); locate_file = 'https://unpkg.com/[email protected]/bin/full/'; } 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 = [0, 0, 500, 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, [-183, -100, 483, 400]); // 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://cdn.skia.org/misc/lego_loader.json').then((resp) => { resp.text().then((str) => { legoJSON = str; SkottieExample(CanvasKit, 'sk_legos', legoJSON, [-183, -100, 483, 400]); }); }); fetch('https://cdn.skia.org/misc/drinks.json').then((resp) => { resp.text().then((str) => { drinksJSON = str; SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds); }); }); fetch('https://cdn.skia.org/misc/confetti.json').then((resp) => { resp.text().then((str) => { confettiJSON = str; SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds); }); }); fetch('https://cdn.skia.org/misc/onboarding.json').then((resp) => { resp.text().then((str) => { onboardingJSON = str; SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds); }); }); const loadBrickTex = fetch('https://cdn.skia.org/misc/brickwork-texture.jpg').then((response) => response.arrayBuffer()); const loadBrickBump = fetch('https://cdn.skia.org/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 paint = new CanvasKit.Paint(); const textPaint = new CanvasKit.Paint(); textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0)); textPaint.setAntiAlias(true); const textFont = new CanvasKit.Font(null, 30); let i = 0; let X = 200; let Y = 200; function drawFrame(canvas) { const path = starPath(CanvasKit, X, Y); const dpe = CanvasKit.PathEffect.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); dpe.delete(); path.delete(); surface.requestAnimationFrame(drawFrame); } surface.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'); } let paint = new CanvasKit.Paint(); 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.PathEffect.MakeCorner(50)); // Draw I N K let path = new CanvasKit.Path(); 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(canvas) { canvas.clear(CanvasKit.WHITE); for (let i = 0; i < paints.length && i < paths.length; i++) { canvas.drawPath(paths[i], paints[i]); } surface.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.Path(); 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')); surface.requestAnimationFrame(drawFrame); } function ShapingExample(CanvasKit) { const surface = CanvasKit.MakeCanvasSurface('shaping'); if (!surface) { console.log('Could not make surface'); return; } let robotoData = null; fetch('https://cdn.skia.org/google-web-fonts/Roboto-Regular.ttf').then((resp) => { resp.arrayBuffer().then((buffer) => { robotoData = buffer; }); }); let emojiData = null; fetch('https://cdn.skia.org/misc/NotoColorEmoji.ttf').then((resp) => { resp.arrayBuffer().then((buffer) => { emojiData = buffer; }); }); const font = new CanvasKit.Font(null, 18); const fontPaint = new CanvasKit.Paint(); fontPaint.setStyle(CanvasKit.PaintStyle.Fill); fontPaint.setAntiAlias(true); let paragraph = null; let X = 250; let Y = 250; const str = 'The quick brown fox ๐ŸฆŠ ate a zesty hamburgerfons ๐Ÿ”.\nThe ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง laughed.'; function drawFrame(canvas) { surface.requestAnimationFrame(drawFrame); if (robotoData && emojiData && !paragraph) { const fontMgr = CanvasKit.FontMgr.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) { canvas.drawText(`Fetching Font data...`, 5, 450, fontPaint, font); return; } canvas.clear(CanvasKit.WHITE); let wrapTo = 350 + 150 * Math.sin(Date.now() / 2000); paragraph.layout(wrapTo); canvas.drawParagraph(paragraph, 0, 0); canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint); const posA = paragraph.getGlyphPositionAtCoordinate(X, Y); const cp = str.codePointAt(posA.pos); if (cp) { const glyph = String.fromCodePoint(cp); canvas.drawText(`At (${X.toFixed(2)}, ${Y.toFixed(2)}) glyph is '${glyph}'`, 5, 450, fontPaint, font); } } surface.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')); surface.requestAnimationFrame(drawFrame); } function starPath(CanvasKit, X=128, Y=128, R=116) { let p = new CanvasKit.Path(); 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'); } let firstFrame = new Date().getTime(); function drawFrame(canvas) { let now = new Date().getTime(); let seek = ((now - firstFrame) / duration) % 1.0; animation.seek(seek); animation.render(canvas, bounds); surface.requestAnimationFrame(drawFrame); } surface.requestAnimationFrame(drawFrame); //animation.delete(); } function ShaderExample1(CanvasKit) { if (!CanvasKit) { return; } const surface = CanvasKit.MakeCanvasSurface('shader1'); if (!surface) { throw 'Could not make surface'; } const paint = new CanvasKit.Paint(); 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)); } `; const fact = CanvasKit.RuntimeEffect.Make(prog); function drawFrame(canvas) { canvas.clear(CanvasKit.WHITE); const shader = fact.makeShader([ Math.sin(Date.now() / 2000) / 5, 256, 256, 1, 0, 0, 1, 0, 1, 0, 1]); paint.setShader(shader); canvas.drawRect(CanvasKit.LTRBRect(0, 0, 512, 512), paint); shader.delete(); surface.requestAnimationFrame(drawFrame); } surface.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.M44.identity(); let worldToClick = CanvasKit.M44.identity(); // rotation of the cube shown in the demo let rotation = CanvasKit.M44.identity(); // temporary during a click and drag let clickRotation = CanvasKit.M44.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 camAngle = Math.PI / 12; const cam = { 'eye' : [0, 0, 1 / Math.tan(camAngle/2) - 1], 'coa' : [0, 0, 0], 'up' : [0, 1, 0], 'near' : 0.05, 'far' : 4, 'angle': camAngle, }; 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.Matrix.scaled(2, 2); const textureShader = CanvasKit.MakeImageFromEncoded(textureImgData).makeShaderCubic( CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3, imgscale); const normalShader = CanvasKit.MakeImageFromEncoded(normalImgData).makeShaderCubic( CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3, imgscale); const children = [textureShader, normalShader]; const prog = ` uniform shader color_map; uniform shader normal_map; uniform float3 lightPos; uniform float4x4 localToWorld; 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(normal_map.eval(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 color_map.eval(p) * half4(float4(scale, scale, scale, 1)); } `; const fact = CanvasKit.RuntimeEffect.Make(prog); // properties of light let lightLocation = [...vSphereCenter]; let lightDistance = vSphereRadius; let lightIconRadius = 12; let draggingLight = false; function computeLightWorldPos() { return CanvasKit.Vector.add(CanvasKit.Vector.mulScalar([...vSphereCenter, 0], 0.5), CanvasKit.Vector.mulScalar(vSphereUnitV3(lightLocation), lightDistance)); } let lightWorldPos = computeLightWorldPos(); function drawLight(canvas) { const paint = new CanvasKit.Paint(); 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.M44.multiply( CanvasKit.M44.rotated([0,1,0], ry), CanvasKit.M44.rotated([1,0,0], rx), CanvasKit.M44.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.M44.invert(m); if (m2 === null) { m2 = CanvasKit.M44.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] } function setClickToWorld(canvas, matrix) { const l2d = canvas.getLocalToDevice(); worldToClick = CanvasKit.M44.multiply(CanvasKit.M44.mustInvert(matrix), l2d); clickToWorld = CanvasKit.M44.mustInvert(worldToClick); } function normalMatrix(m) { m[3] = 0; m[7] = 0; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; return CanvasKit.M44.transpose(CanvasKit.M44.mustInvert(m)); } function drawCubeFace(canvas, m, color) { const trans = new CanvasKit.M44.translated([vSphereRadius/2, vSphereRadius/2, 0]); const localToWorld = new CanvasKit.M44.multiply(m, CanvasKit.M44.mustInvert(trans)); canvas.concat(CanvasKit.M44.multiply(trans, localToWorld)); const znormal = front(canvas.getLocalToDevice()); if (znormal < 0) { return; // skip faces facing backwards } const uniforms = [...lightWorldPos, ...localToWorld, ...normalMatrix(localToWorld)]; const paint = new CanvasKit.Paint(); paint.setAntiAlias(true); const shader = fact.makeShaderWithChildren(uniforms, 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. canvas.concat(CanvasKit.M44.setupCamera( CanvasKit.LTRBRect(0, 0, vSphereRadius, vSphereRadius), vSphereRadius/2, cam)); setClickToWorld(canvas, clickM); for (let f of faces) { const saveCount = canvas.getSaveCount(); canvas.save(); drawCubeFace(canvas, CanvasKit.M44.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.Paint(); 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.Vector.mulScalar(CanvasKit.Vector.sub(p, vSphereCenter), 1/vSphereRadius); // constrain the clicked point within the circle. let len2 = CanvasKit.Vector.lengthSquared(v); if (len2 > 1) { v = CanvasKit.Vector.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.Vector.cross(u, v); const sinValue = CanvasKit.Vector.length(axis); const cosValue = CanvasKit.Vector.dot(u, v); let m = new CanvasKit.M44.identity(); if (Math.abs(sinValue) > 0.000000001) { m = CanvasKit.M44.rotatedUnitSinCos( CanvasKit.Vector.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.M44.rotated(axis, totalSpin); spinRate *= .998; if (spinRate < 0.01) { stopSpinning(); } surface.requestAnimationFrame(drawFrame); } function stopSpinning() { clearInterval(spinning); rotation = CanvasKit.M44.multiply(clickRotation, rotation); clickRotation = CanvasKit.M44.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.Vector.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, I'm thirsty, Confetti, Onboarding

Test server

Test your code on our CanvasKit Fiddle

Download

Get CanvasKit on NPM. Documentation and Typescript definitions are available in the types/ subfolder of the npm package or from the Skia repo.

Check out the quickstart guide as well.