| <!DOCTYPE html> |
| <title>CanvasKit Extra features (Skia via Web Assembly)</title> |
| <meta charset="utf-8" /> |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| |
| <style> |
| canvas { |
| border: 1px dashed #AAA; |
| } |
| |
| </style> |
| |
| <h2> Skottie </h2> |
| <canvas id=sk_legos width=300 height=300></canvas> |
| <canvas id=sk_drinks width=500 height=500></canvas> |
| <canvas id=sk_party width=500 height=500></canvas> |
| <canvas id=sk_onboarding width=500 height=500></canvas> |
| <canvas id=sk_animated_gif width=500 height=500 |
| title='This is an animated gif being animated in Skottie'></canvas> |
| |
| <h2> RT Shader </h2> |
| <canvas id=rtshader width=300 height=300></canvas> |
| <canvas id=rtshader2 width=300 height=300></canvas> |
| |
| <h2> Particles </h2> |
| <canvas id=particles width=500 height=500></canvas> |
| |
| <h2> Paragraph </h2> |
| <canvas id=para1 width=600 height=600></canvas> |
| |
| |
| <h2> CanvasKit can serialize/deserialize .skp files</h2> |
| <canvas id=skp width=500 height=500></canvas> |
| |
| <h2> 3D perspective transformations </h2> |
| <canvas id=camera3d width=500 height=500></canvas> |
| |
| <script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script> |
| |
| <script type="text/javascript" charset="utf-8"> |
| |
| var CanvasKit = null; |
| var legoJSON = null; |
| var drinksJSON = null; |
| var confettiJSON = null; |
| var onboardingJSON = null; |
| var multiFrameJSON = null; |
| var fullBounds = {fLeft: 0, fTop: 0, fRight: 500, fBottom: 500}; |
| |
| var robotoData = null; |
| var notoserifData = null; |
| |
| var flightAnimGif = null; |
| var skpData = null; |
| var cdn = 'https://storage.googleapis.com/skia-cdn/misc/'; |
| const ckLoaded = CanvasKitInit({ |
| locateFile: (file) => '/node_modules/canvaskit/bin/'+file, |
| }).ready(); |
| ckLoaded.then((CK) => { |
| CanvasKit = CK; |
| // 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); |
| SkottieExample(CanvasKit, 'sk_animated_gif', multiFrameJSON, fullBounds, { |
| 'image_0.png': flightAnimGif, |
| }); |
| ParticlesAPI1(CanvasKit); |
| |
| ParagraphAPI1(CanvasKit, robotoData); |
| |
| RTShaderAPI1(CanvasKit); |
| |
| SkpExample(CanvasKit, skpData); |
| }); |
| |
| fetch(cdn + '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(cdn + 'drinks.json').then((resp) => { |
| resp.text().then((str) => { |
| drinksJSON = str; |
| SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds); |
| }); |
| }); |
| |
| fetch(cdn + 'confetti.json').then((resp) => { |
| resp.text().then((str) => { |
| confettiJSON = str; |
| SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds); |
| }); |
| }); |
| |
| fetch(cdn + 'onboarding.json').then((resp) => { |
| resp.text().then((str) => { |
| onboardingJSON = str; |
| SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds); |
| }); |
| }); |
| |
| fetch(cdn + 'skottie_sample_multiframe.json').then((resp) => { |
| resp.text().then((str) => { |
| multiFrameJSON = str; |
| SkottieExample(CanvasKit, 'sk_animated_gif', multiFrameJSON, fullBounds, { |
| 'image_0.png': flightAnimGif, |
| }); |
| }); |
| }); |
| |
| fetch(cdn + 'flightAnim.gif').then((resp) => { |
| resp.arrayBuffer().then((buffer) => { |
| flightAnimGif = buffer; |
| SkottieExample(CanvasKit, 'sk_animated_gif', multiFrameJSON, fullBounds, { |
| 'image_0.png': flightAnimGif, |
| }); |
| }); |
| }); |
| |
| fetch(cdn + 'Roboto-Regular.ttf').then((resp) => { |
| resp.arrayBuffer().then((buffer) => { |
| robotoData = buffer; |
| ParagraphAPI1(CanvasKit, robotoData); |
| }); |
| }); |
| |
| fetch(cdn + 'picture2.skp').then((response) => response.arrayBuffer()).then((buffer) => { |
| skpData = buffer; |
| SkpExample(CanvasKit, skpData); |
| }); |
| |
| const loadDog = fetch(cdn + 'dog.jpg').then((response) => response.arrayBuffer()); |
| const loadMandrill = fetch(cdn + 'mandrill_256.png').then((response) => response.arrayBuffer()); |
| const loadBrickTex = fetch(cdn + 'brickwork-texture.jpg').then((response) => response.arrayBuffer()); |
| const loadBrickBump = fetch(cdn + 'brickwork_normal-map.jpg').then((response) => response.arrayBuffer()); |
| Promise.all([ckLoaded, loadBrickTex, loadBrickBump]).then((results) => {Camera3D(...results)}); |
| |
| function SkottieExample(CanvasKit, id, jsonStr, bounds, assets) { |
| if (!CanvasKit || !jsonStr) { |
| return; |
| } |
| const animation = CanvasKit.MakeManagedAnimation(jsonStr, assets); |
| 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}; |
| |
| // Basic managed animation test. |
| if (id === 'sk_drinks') { |
| animation.setColor('BACKGROUND_FILL', CanvasKit.Color(0, 163, 199, 1.0)); |
| } |
| |
| const surface = CanvasKit.MakeCanvasSurface(id); |
| if (!surface) { |
| console.error('Could not make surface'); |
| return; |
| } |
| |
| let firstFrame = Date.now(); |
| |
| function drawFrame(canvas) { |
| let seek = ((Date.now() - firstFrame) / duration) % 1.0; |
| let damage = animation.seek(seek); |
| // TODO: SkRect.isEmpty()? |
| if (damage.fRight > damage.fLeft && damage.fBottom > damage.fTop) { |
| canvas.clear(CanvasKit.WHITE); |
| animation.render(canvas, bounds); |
| } |
| surface.requestAnimationFrame(drawFrame); |
| } |
| surface.requestAnimationFrame(drawFrame); |
| |
| //animation.delete(); |
| return surface; |
| } |
| |
| function ParticlesAPI1(CanvasKit) { |
| const surface = CanvasKit.MakeCanvasSurface('particles'); |
| if (!surface) { |
| console.error('Could not make surface'); |
| return; |
| } |
| const context = CanvasKit.currentContext(); |
| const canvas = surface.getCanvas(); |
| canvas.translate(250, 450); |
| |
| const particles = CanvasKit.MakeParticles(JSON.stringify(curves)); |
| particles.start(Date.now() / 1000.0, true); |
| |
| function drawFrame(canvas) { |
| canvas.clear(CanvasKit.BLACK); |
| |
| particles.update(Date.now() / 1000.0); |
| particles.draw(canvas); |
| surface.requestAnimationFrame(drawFrame); |
| } |
| surface.requestAnimationFrame(drawFrame); |
| } |
| |
| const curves = { |
| "MaxCount": 1000, |
| "Drawable": { |
| "Type": "SkCircleDrawable", |
| "Radius": 2 |
| }, |
| "EffectCode": [ |
| "void effectSpawn(inout Effect effect) {", |
| " effect.rate = 200;", |
| " effect.color = float4(1, 0, 0, 1);", |
| "}", |
| "" |
| ], |
| "Code": [ |
| "void spawn(inout Particle p) {", |
| " p.lifetime = 3 + rand;", |
| " p.vel.y = -50;", |
| "}", |
| "", |
| "void update(inout Particle p) {", |
| " float w = mix(15, 3, p.age);", |
| " p.pos.x = sin(radians(p.age * 320)) * mix(25, 10, p.age) + mix(-w, w, rand);", |
| " if (rand < 0.5) { p.pos.x = -p.pos.x; }", |
| "", |
| " p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand)) / 255;", |
| "}", |
| "" |
| ], |
| "Bindings": [] |
| }; |
| |
| function SurfaceAPI1(CanvasKit) { |
| const surface = CanvasKit.MakeCanvasSurface('surfaces'); |
| if (!surface) { |
| console.error('Could not make surface'); |
| return; |
| } |
| const context = CanvasKit.currentContext(); |
| const canvas = surface.getCanvas(); |
| |
| // create a subsurface as a temporary workspace. |
| const subSurface = surface.makeSurface({ |
| width: 50, |
| height: 50, |
| alphaType: CanvasKit.AlphaType.Premul, |
| colorType: CanvasKit.ColorType.RGBA_8888, |
| }); |
| |
| if (!subSurface) { |
| console.error('Could not make subsurface'); |
| return; |
| } |
| |
| // draw a small "scene" |
| const paint = new CanvasKit.SkPaint(); |
| paint.setColor(CanvasKit.Color(139, 228, 135, 0.95)); // greenish |
| paint.setStyle(CanvasKit.PaintStyle.Fill); |
| paint.setAntiAlias(true); |
| |
| const subCanvas = subSurface.getCanvas(); |
| subCanvas.clear(CanvasKit.BLACK); |
| subCanvas.drawRect(CanvasKit.LTRBRect(5, 15, 45, 40), paint); |
| |
| paint.setColor(CanvasKit.Color(214, 93, 244)); // purplish |
| for (let i = 0; i < 10; i++) { |
| const x = Math.random() * 50; |
| const y = Math.random() * 50; |
| |
| subCanvas.drawOval(CanvasKit.XYWHRect(x, y, 6, 6), paint); |
| } |
| |
| // Snap it off as an SkImage - this image will be in the form the |
| // parent surface prefers (e.g. Texture for GPU / Raster for CPU). |
| const img = subSurface.makeImageSnapshot(); |
| |
| // clean up the temporary surface |
| subSurface.delete(); |
| paint.delete(); |
| |
| // Make it repeat a bunch with a shader |
| const pattern = img.makeShader(CanvasKit.TileMode.Repeat, CanvasKit.TileMode.Mirror); |
| const patternPaint = new CanvasKit.SkPaint(); |
| patternPaint.setShader(pattern); |
| |
| let i = 0; |
| |
| function drawFrame() { |
| i++; |
| CanvasKit.setCurrentContext(context); |
| canvas.clear(CanvasKit.WHITE); |
| |
| canvas.drawOval(CanvasKit.LTRBRect(i % 60, i % 60, 300 - (i% 60), 300 - (i % 60)), patternPaint); |
| surface.flush(); |
| window.requestAnimationFrame(drawFrame); |
| } |
| window.requestAnimationFrame(drawFrame); |
| |
| } |
| |
| function ParagraphAPI1(CanvasKit, fontData) { |
| if (!CanvasKit || !fontData) { |
| return; |
| } |
| |
| const surface = CanvasKit.MakeCanvasSurface('para1'); |
| if (!surface) { |
| console.error('Could not make surface'); |
| return; |
| } |
| |
| const canvas = surface.getCanvas(); |
| const fontMgr = CanvasKit.SkFontMgr.FromData([fontData]); |
| |
| const paraStyle = new CanvasKit.ParagraphStyle({ |
| textStyle: { |
| color: CanvasKit.BLACK, |
| fontFamilies: ['Roboto'], |
| fontSize: 50, |
| }, |
| textAlign: CanvasKit.TextAlign.Left, |
| maxLines: 5, |
| }); |
| |
| const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); |
| builder.addText('The quick brown fox ate a hamburgerfons and got sick.'); |
| const paragraph = builder.build(); |
| |
| let wrapTo = 0; |
| |
| let X = 100; |
| let Y = 100; |
| |
| const font = new CanvasKit.SkFont(null, 18); |
| const fontPaint = new CanvasKit.SkPaint(); |
| fontPaint.setStyle(CanvasKit.PaintStyle.Fill); |
| fontPaint.setAntiAlias(true); |
| |
| function drawFrame(canvas) { |
| canvas.clear(CanvasKit.WHITE); |
| wrapTo = 350 + 150 * Math.sin(Date.now() / 2000); |
| paragraph.layout(wrapTo); |
| canvas.drawParagraph(paragraph, 0, 0); |
| |
| canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint); |
| |
| let posA = paragraph.getGlyphPositionAtCoordinate(X, Y); |
| canvas.drawText(`At (${X.toFixed(2)}, ${Y.toFixed(2)}) glyph is ${posA.pos}`, 5, 450, fontPaint, font); |
| |
| surface.requestAnimationFrame(drawFrame); |
| } |
| surface.requestAnimationFrame(drawFrame); |
| |
| let interact = (e) => { |
| X = e.offsetX*2; // multiply by 2 because the canvas is 300 css pixels wide, |
| Y = e.offsetY*2; // but the canvas itself is 600px wide |
| }; |
| |
| document.getElementById('para1').addEventListener('pointermove', interact); |
| return surface; |
| } |
| |
| const spiralSkSL = ` |
| uniform float rad_scale; |
| uniform float2 in_center; |
| uniform float4 in_colors0; |
| uniform float4 in_colors1; |
| |
| void main(float2 p, inout half4 color) { |
| 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); |
| color = half4(mix(in_colors0, in_colors1, t)); |
| }`; |
| |
| function RTShaderAPI1(CanvasKit) { |
| if (!CanvasKit) { |
| return; |
| } |
| |
| const surface = CanvasKit.MakeCanvasSurface('rtshader'); |
| if (!surface) { |
| console.error('Could not make surface'); |
| return; |
| } |
| |
| const canvas = surface.getCanvas(); |
| |
| const effect = CanvasKit.SkRuntimeEffect.Make(spiralSkSL); |
| const shader = effect.makeShader([ |
| 0.5, |
| 150, 150, |
| 0, 1, 0, 1, |
| 1, 0, 0, 1], true); |
| const paint = new CanvasKit.SkPaint(); |
| paint.setShader(shader); |
| canvas.drawRect(CanvasKit.LTRBRect(0, 0, 300, 300), paint); |
| |
| surface.flush(); |
| shader.delete(); |
| paint.delete(); |
| effect.delete(); |
| } |
| |
| // RTShader2 demo |
| Promise.all([ckLoaded, loadDog, loadMandrill]).then((values) => { |
| const [CanvasKit, dogData, mandrillData] = values; |
| const dogImg = CanvasKit.MakeImageFromEncoded(dogData); |
| if (!dogImg) { |
| console.error('could not decode dog'); |
| return; |
| } |
| const mandrillImg = CanvasKit.MakeImageFromEncoded(mandrillData); |
| if (!mandrillImg) { |
| console.error('could not decode mandrill'); |
| return; |
| } |
| const quadrantSize = 150; |
| |
| const dogShader = dogImg.makeShader(CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, |
| CanvasKit.SkMatrix.scaled(quadrantSize/dogImg.width(), |
| quadrantSize/dogImg.height())); |
| const mandrillShader = mandrillImg.makeShader(CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, |
| CanvasKit.SkMatrix.scaled( |
| quadrantSize/mandrillImg.width(), |
| quadrantSize/mandrillImg.height())); |
| |
| const surface = CanvasKit.MakeCanvasSurface('rtshader2'); |
| if (!surface) { |
| console.error('Could not make surface'); |
| return; |
| } |
| |
| const prog = ` |
| in fragmentProcessor before_map; |
| in fragmentProcessor after_map; |
| in fragmentProcessor threshold_map; |
| |
| uniform float cutoff; |
| uniform float slope; |
| |
| float smooth_cutoff(float x) { |
| x = x * slope + (0.5 - slope * cutoff); |
| return clamp(x, 0, 1); |
| } |
| |
| void main(float2 xy, inout half4 color) { |
| half4 before = sample(before_map, xy); |
| half4 after = sample(after_map, xy); |
| |
| float m = smooth_cutoff(sample(threshold_map, xy).r); |
| color = mix(before, after, half(m)); |
| }`; |
| |
| const canvas = surface.getCanvas(); |
| |
| const thresholdEffect = CanvasKit.SkRuntimeEffect.Make(prog); |
| const spiralEffect = CanvasKit.SkRuntimeEffect.Make(spiralSkSL); |
| |
| const draw = (x, y, shader) => { |
| const paint = new CanvasKit.SkPaint(); |
| paint.setShader(shader); |
| canvas.save(); |
| canvas.translate(x, y); |
| canvas.drawRect(CanvasKit.LTRBRect(0, 0, quadrantSize, quadrantSize), paint); |
| canvas.restore(); |
| paint.delete(); |
| }; |
| |
| const offscreenSurface = CanvasKit.MakeSurface(quadrantSize, quadrantSize); |
| const getBlurrySpiralShader = (rad_scale) => { |
| const oCanvas = offscreenSurface.getCanvas(); |
| |
| const spiralShader = spiralEffect.makeShader([ |
| rad_scale, |
| quadrantSize/2, quadrantSize/2, |
| 1, 1, 1, 1, |
| 0, 0, 0, 1], true); |
| |
| return spiralShader; |
| // TODO(kjlubick): The raster backend does not like atan or fract, so we can't |
| // draw the shader into the offscreen canvas and mess with it. When we can, that |
| // would be cool to show off. |
| |
| const blur = CanvasKit.SkImageFilter.MakeBlur(0.1, 0.1, CanvasKit.TileMode.Clamp, null); |
| |
| const paint = new CanvasKit.SkPaint(); |
| paint.setShader(spiralShader); |
| paint.setImageFilter(blur); |
| oCanvas.drawRect(CanvasKit.LTRBRect(0, 0, quadrantSize, quadrantSize), paint); |
| |
| paint.delete(); |
| blur.delete(); |
| spiralShader.delete(); |
| return offscreenSurface.makeImageSnapshot() |
| .makeShader(CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp); |
| |
| }; |
| |
| const drawFrame = () => { |
| surface.requestAnimationFrame(drawFrame); |
| const thresholdShader = getBlurrySpiralShader(Math.sin(Date.now() / 5000) / 2); |
| |
| const blendShader = thresholdEffect.makeShaderWithChildren( |
| [0.5, 10], |
| true, [dogShader, mandrillShader, thresholdShader]); |
| draw(0, 0, blendShader); |
| draw(quadrantSize, 0, thresholdShader); |
| draw(0, quadrantSize, dogShader); |
| draw(quadrantSize, quadrantSize, mandrillShader); |
| |
| blendShader.delete(); |
| }; |
| |
| surface.requestAnimationFrame(drawFrame); |
| }); |
| |
| function SkpExample(CanvasKit, skpData) { |
| if (!skpData || !CanvasKit) { |
| return; |
| } |
| |
| const surface = CanvasKit.MakeSWCanvasSurface('skp'); |
| if (!surface) { |
| console.error('Could not make surface'); |
| return; |
| } |
| |
| const pic = CanvasKit.MakeSkPicture(skpData); |
| |
| function drawFrame(canvas) { |
| canvas.clear(CanvasKit.TRANSPARENT); |
| // this particular file has a path drawing at (68,582) that's 1300x1300 pixels |
| // scale it down to 500x500 and translate it to fit. |
| const scale = 500.0/1300; |
| canvas.scale(scale, scale); |
| canvas.translate(-68, -582); |
| canvas.drawPicture(pic); |
| } |
| // Intentionally just draw frame once |
| surface.drawOnce(drawFrame); |
| } |
| |
| function Camera3D(canvas, textureImgData, normalImgData) { |
| if (!canvas) { |
| |
| } |
| 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 textPaint = new CanvasKit.SkPaint(); |
| textPaint.setColor(CanvasKit.BLACK); |
| textPaint.setAntiAlias(true); |
| const roboto = CanvasKit.SkFontMgr.RefDefault().MakeTypefaceFromData(robotoData); |
| const textFont = new CanvasKit.SkFont(roboto, 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 float4x4 localToWorld; |
| uniform float4x4 localToWorldAdjInv; |
| uniform float3 lightPos; |
| |
| 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(localToWorld * 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.saveCamera( |
| CanvasKit.SkM44.multiply(viewport, perspective), |
| CanvasKit.SkM44.multiply(camera, mustInvert(viewport))); |
| } |
| |
| 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.concat44(CanvasKit.SkM44.multiply(trans, m, mustInvert(trans))); |
| const znormal = front(canvas.getLocalToDevice()); |
| if (znormal < 0) { |
| return; // skip faces facing backwards |
| } |
| const ltw = canvas.getLocalToWorld(); |
| // shader expects the 4x4 matrices in column major order. |
| const uniforms = [...CanvasKit.SkM44.transpose(ltw), ...mustInvert(ltw), ...lightWorldPos]; |
| const paint = new CanvasKit.SkPaint(); |
| paint.setAntiAlias(true); |
| const shader = fact.makeShaderWithChildren(uniforms, true /*=opaque*/, children); |
| paint.setShader(shader); |
| canvas.drawRRect(rr, paint); |
| canvas.drawText(znormal.toFixed(2), faceScale*0.25, faceScale*0.4, textPaint, textFont); |
| } |
| |
| 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); |
| } |
| </script> |