blob: 720273cf175394c8968d977e50f9fb19775d41dd [file] [log] [blame]
<!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>
<canvas id=glyphgame width=500 height=500></canvas>
<h2> Support for extended color spaces </h2>
<a href="chrome://flags/#force-color-profile">Force P3 profile</a>
<canvas id=colorsupport width=300 height=300></canvas>
<script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
<script type="text/javascript" charset="utf-8">
var CanvasKit = null;
var cdn = 'https://storage.googleapis.com/skia-cdn/misc/';
const ckLoaded = CanvasKitInit({locateFile: (file) => '/node_modules/canvaskit/bin/'+file});
const loadLegoJSON = fetch(cdn + 'lego_loader.json').then((response) => response.text());
const loadDrinksJSON = fetch(cdn + 'drinks.json').then((response) => response.text());
const loadConfettiJSON = fetch(cdn + 'confetti.json').then((response) => response.text());
const loadOnboardingJSON = fetch(cdn + 'onboarding.json').then((response) => response.text());
const loadMultiframeJSON = fetch(cdn + 'skottie_sample_multiframe.json').then((response) => response.text());
const loadFlightGif = fetch(cdn + 'flightAnim.gif').then((response) => response.arrayBuffer());
const loadSkp = fetch(cdn + 'picture2.skp').then((response) => response.arrayBuffer());
const loadFont = fetch(cdn + 'Roboto-Regular.ttf').then((response) => response.arrayBuffer());
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());
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.seed);
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(p.seed));
if (rand(p.seed) < 0.5) { p.pos.x = -p.pos.x; }
p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand(p.seed))) / 255;
}
`
],
"Bindings": []
};
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));
}`;
// Examples which only require canvaskit
ckLoaded.then((CK) => {
CanvasKit = CK;
ParticlesAPI1(CanvasKit);
RTShaderAPI1(CanvasKit);
ColorSupport(CanvasKit);
});
// Examples requiring external resources.
// Set bounds to fix the 4:3 resolution of the legos
Promise.all([ckLoaded, loadLegoJSON]).then(([ck, jsonstr]) => {
SkottieExample(ck, 'sk_legos', jsonstr, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300});
});
// Re-size to fit
let fullBounds = {fLeft: 0, fTop: 0, fRight: 500, fBottom: 500};
Promise.all([ckLoaded, loadDrinksJSON]).then(([ck, jsonstr]) => {
SkottieExample(ck, 'sk_drinks', jsonstr, fullBounds);
});
Promise.all([ckLoaded, loadConfettiJSON]).then(([ck, jsonstr]) => {
SkottieExample(ck, 'sk_party', jsonstr, fullBounds);
});
Promise.all([ckLoaded, loadOnboardingJSON]).then(([ck, jsonstr]) => {
SkottieExample(ck, 'sk_onboarding', jsonstr, fullBounds);
});
Promise.all([ckLoaded, loadMultiframeJSON, loadFlightGif]).then(([ck, jsonstr, gif]) => {
SkottieExample(ck, 'sk_animated_gif', jsonstr, fullBounds, {'image_0.png': gif});
});
Promise.all([ckLoaded, loadFont]).then((results) => {
ParagraphAPI1(...results);
GlyphGame(...results)
});
Promise.all([ckLoaded, loadSkp]).then((results) => {SkpExample(...results)});
Promise.all([ckLoaded, loadBrickTex, loadBrickBump, loadFont]).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);
}
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;
}
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 shader before_map;
in shader after_map;
in shader 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);
}
// Return the inverse of an SkM44. throw an error if it's not invertible
function mustInvert(m) {
const m2 = CanvasKit.SkM44.invert(m);
if (m2 === null) {
throw "Matrix not invertible";
}
return m2;
}
// TODO(nifong): This function is in desperate need of some explanation of what it does
// cam is a object having eye, coa, up, near, far, angle
function saveCamera(canvas, /* rect */ area, /* scalar */ zscale, cam) {
const camera = CanvasKit.SkM44.lookat(cam.eye, cam.coa, cam.up);
const perspective = CanvasKit.SkM44.perspective(cam.near, cam.far, cam.angle);
// 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 Camera3D(canvas, textureImgData, normalImgData, robotoData) {
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 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 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 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;
}
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]
}
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);
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, cam);
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);
}
// Shows a hidden message by rotating all the characters in a kind of way that makes you
// search with your mouse.
function GlyphGame(canvas, robotoData) {
const surface = CanvasKit.MakeCanvasSurface('glyphgame');
if (!surface) {
console.error('Could not make surface');
return;
}
const sizeX = document.getElementById('glyphgame').width;
const sizeY = document.getElementById('glyphgame').height;
const halfDim = Math.min(sizeX, sizeY) / 2;
const margin = 50;
const marginTop = 25;
let rotX = 0; // expected to be updated in interact()
let rotY = 0;
let pointer = [500, 450];
const radPerPixel = 0.005; // radians of subject rotation per pixel distance moved by mouse.
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.02,
'far' : 4,
'angle': camAngle,
};
let lastImage = null;
const fontMgr = CanvasKit.SkFontMgr.FromData([robotoData]);
const paraStyle = new CanvasKit.ParagraphStyle({
textStyle: {
color: CanvasKit.Color(105, 56, 16), // brown
fontFamilies: ['Roboto'],
fontSize: 28,
},
textAlign: CanvasKit.TextAlign.Left,
});
const hStyle = CanvasKit.RectHeightStyle.Max;
const wStyle = CanvasKit.RectWidthStyle.Tight;
const quotes = [
'Some activities superficially familiar to you are merely stupid and should be avoided for your safety, although they are not illegal as such. These include: giving your bank account details to the son of the Nigerian Minister of Finance; buying title to bridges, skyscrapers, spacecraft, planets, or other real assets; murder; selling your identity; and entering into financial contracts with entities running Economics 2.0 or higher.',
// Charles Stross - Accelerando
'If only there were evil people somewhere insidiously committing evil deeds, and it were necessary only to separate them from the rest of us and destroy them. But the line dividing good and evil cuts through the heart of every human being. And who is willing to destroy a piece of his own heart?',
// Aleksandr Solzhenitsyn - The Gulag Archipelago
'There is one metaphor of which the moderns are very fond; they are always saying, “You can’t put the clock back.” The simple and obvious answer is “You can.” A clock, being a piece of human construction, can be restored by the human finger to any figure or hour. In the same way society, being a piece of human construction, can be reconstructed upon any plan that has ever existed.',
// G. K. Chesterton - What's Wrong With The World?
];
// pick one at random
const text = quotes[Math.floor(Math.random()*3)];
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
builder.addText(text);
const paragraph = builder.build();
const font = new CanvasKit.SkFont(null, 18);
// wrap the text to a given width.
paragraph.layout(sizeX - margin*2);
// to rotate every glyph individually, calculate the bounding rect of each one,
// construct an array of rects and paragraphs that would draw each glyph individually.
const letters = Array(text.length);
for (let i = 0; i < text.length; i++) {
const r = paragraph.getRectsForRange(i, i+1, hStyle, wStyle)[0];
// The character is drawn with drawParagraph so we can pass the paraStyle,
// and have our character be the exact size and shape the paragraph expected
// when it wrapped the text. canvas.drawText wouldn't cut it.
const tmpbuilder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
tmpbuilder.addText(text[i]);
const para = tmpbuilder.build();
para.layout(100);
letters[i] = {
'r': r,
'para': para,
};
}
function drawFrame(canvas) {
// persistence of vision effect is done by drawing the past frame as an image,
// then covering with semitransparent background color.
if (lastImage) {
canvas.drawImage(lastImage, 0, 0, null);
canvas.drawColor(CanvasKit.Color(171, 244, 255, 0.1)); // sky blue, almost transparent
} else {
canvas.clear(CanvasKit.Color(171, 244, 255)); // sky blue, opaque
}
canvas.save();
// Set up 3D view enviroment
saveCamera(canvas, CanvasKit.LTRBRect(0, 0, sizeX, sizeY), halfDim, cam);
// Rotate the whole paragraph as a unit.
const paraRotPoint = [halfDim, halfDim, 1];
canvas.concat(CanvasKit.SkM44.multiply(
CanvasKit.SkM44.translated(paraRotPoint),
CanvasKit.SkM44.rotated([0,1,0], rotX),
CanvasKit.SkM44.rotated([1,0,0], rotY * 0.2),
CanvasKit.SkM44.translated(CanvasKit.SkVector.mulScalar(paraRotPoint, -1)),
));
// Rotate every glyph in the paragraph individually.
let i = 0;
for (const letter of letters) {
canvas.save();
let r = letter['r'];
// rotate about the center of the glyph's rect.
rotationPoint = [
margin + r.fLeft + (r.fRight - r.fLeft) / 2,
marginTop + r.fTop + (r.fBottom - r.fTop) / 2,
0
];
distanceFromPointer = CanvasKit.SkVector.dist(pointer, rotationPoint.slice(0, 2));
// Rotate more around the Y-axis depending on the glyph's distance from the pointer.
canvas.concat(CanvasKit.SkM44.multiply(
CanvasKit.SkM44.translated(rotationPoint),
// note that I'm rotating around the x axis first, undoing some of the rotation done to the whole
// paragraph above, where x came second. If I rotated y first, a lot of letters would end up
// upside down, which is a bit too hard to unscramble.
CanvasKit.SkM44.rotated([1,0,0], rotY * -0.6),
CanvasKit.SkM44.rotated([0,1,0], distanceFromPointer * -0.035),
CanvasKit.SkM44.translated(CanvasKit.SkVector.mulScalar(rotationPoint, -1)),
));
canvas.drawParagraph(letter['para'], margin + r.fLeft, marginTop + r.fTop);
i++;
canvas.restore();
}
canvas.restore();
lastImage = surface.makeImageSnapshot();
}
function interact(e) {
pointer = [e.offsetX, e.offsetY]
rotX = (pointer[0] - halfDim) * radPerPixel;
rotY = (pointer[1] - halfDim) * radPerPixel * -1;
surface.requestAnimationFrame(drawFrame);
};
document.getElementById('glyphgame').addEventListener('pointermove', interact);
surface.requestAnimationFrame(drawFrame);
}
function ColorSupport(CanvasKit) {
const surface = CanvasKit.MakeCanvasSurface('colorsupport', CanvasKit.SkColorSpace.ADOBE_RGB);
if (!surface) {
console.error('Could not make surface');
return;
}
const canvas = surface.getCanvas();
// If the surface is correctly initialized with a higher bit depth color type,
// And chrome is compositing it into a buffer with the P3 color space,
// then the inner round rect should be distinct and less saturated than the full red background.
// Even if the monitor it is viewed on cannot accurately represent that color space.
let red = CanvasKit.Color4f(1, 0, 0, 1);
let paint = new CanvasKit.SkPaint();
paint.setColor(red, CanvasKit.SkColorSpace.ADOBE_RGB);
canvas.drawPaint(paint);
paint.setColor(red, CanvasKit.SkColorSpace.DISPLAY_P3);
canvas.drawRoundRect(CanvasKit.LTRBRect(50, 50, 250, 250), 30, 30, paint);
paint.setColor(red, CanvasKit.SkColorSpace.SRGB);
canvas.drawRoundRect(CanvasKit.LTRBRect(100, 100, 200, 200), 30, 30, paint);
surface.flush();
surface.delete();
}
</script>