blob: e01f2b6d646b0a07e82738f7782572c904b97f39 [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;
}
#sk_legos,#sk_drinks,#sk_party,#sk_onboarding, #sk_animated_gif {
width: 300px;
height: 300px;
}
</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>
<canvas id=para2 width=600 height=600 tabindex='-1'></canvas>
<canvas id=para3 width=600 height=600 tabindex='-1'></canvas>
``
<h2> CanvasKit can serialize/deserialize .skp files</h2>
<canvas id=skp width=500 height=500></canvas>
<h2> 3D perspective transformations </h2>
<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="/build/canvaskit.js"></script>
<script type="text/javascript" src="textapi_utils.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) => '/build/'+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 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
},
"Code": [`
void effectSpawn(inout Effect effect) {
effect.rate = 200;
effect.color = float4(1, 0, 0, 1);
}
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;
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));
}`;
// Examples which only require canvaskit
ckLoaded.then((CK) => {
CanvasKit = CK;
ParticlesAPI1(CanvasKit);
RTShaderAPI1(CanvasKit);
ColorSupport(CanvasKit);
SkpExample(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, [-50, 0, 350, 300]);
});
// Re-size to fit
let fullBounds = [0, 0, 500, 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);
ParagraphAPI2(...results);
ParagraphAPI3(...results);
GlyphGame(...results)
});
const rectLeft = 0;
const rectTop = 1;
const rectRight = 2;
const rectBottom = 3;
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 || CanvasKit.LTRBRect(0, 0, size.w, 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);
if (damage[rectRight] > damage[rectLeft] && damage[rectBottom] > damage[rectTop]) {
canvas.clear(CanvasKit.WHITE);
animation.render(canvas, bounds);
}
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
return surface;
}
function ParticlesAPI1(CanvasKit) {
const surface = CanvasKit.MakeCanvasSurface('particles');
if (!surface) {
console.error('Could not make surface');
return;
}
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.FontMgr.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 fontPaint = new CanvasKit.Paint();
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);
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 ParagraphAPI2(CanvasKit, fontData) {
if (!CanvasKit || !fontData) {
return;
}
const surface = CanvasKit.MakeCanvasSurface('para2');
if (!surface) {
console.error('Could not make surface');
return;
}
const mouse = MakeMouse();
const cursor = MakeCursor(CanvasKit);
const canvas = surface.getCanvas();
const text0 = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, " +
"wet hole full of worms and oozy smells. This was a hobbit-hole and " +
"that means good food, a warm hearth, and all the comforts of home.";
const LOC_X = 20,
LOC_Y = 20;
const bgPaint = new CanvasKit.Paint();
bgPaint.setColor([0.965, 0.965, 0.965, 1]);
const editor = MakeEditor(text0, {typeface:null, size:24}, cursor, 400);
editor.applyStyleToRange({size:100}, 0, 1);
editor.applyStyleToRange({italic:true}, 38, 38+6);
editor.applyStyleToRange({color:[1,0,0,1]}, 5, 5+4);
editor.setXY(LOC_X, LOC_Y);
function drawFrame(canvas) {
const lines = editor.getLines();
canvas.clear(CanvasKit.WHITE);
if (mouse.isActive()) {
const pos = mouse.getPos(-LOC_X, -LOC_Y);
const a = lines_pos_to_index(lines, pos[0], pos[1]);
const b = lines_pos_to_index(lines, pos[2], pos[3]);
if (a == b) {
editor.setIndex(a);
} else {
editor.setIndices(a, b);
}
}
canvas.drawRect(editor.bounds(), bgPaint);
editor.draw(canvas);
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
function interact(e) {
const type = e.type;
if (type === 'pointerup') {
mouse.setUp(e.offsetX, e.offsetY);
} else if (type === 'pointermove') {
mouse.setMove(e.offsetX, e.offsetY);
} else if (type === 'pointerdown') {
mouse.setDown(e.offsetX, e.offsetY);
}
};
function keyhandler(e) {
switch (e.key) {
case 'ArrowLeft': editor.moveDX(-1); return;
case 'ArrowRight': editor.moveDX(1); return;
case 'ArrowUp':
e.preventDefault();
editor.moveDY(-1);
return;
case 'ArrowDown':
e.preventDefault();
editor.moveDY(1);
return;
case 'Backspace':
editor.deleteSelection();
return;
case 'Shift':
return;
}
if (e.ctrlKey) {
switch (e.key) {
case 'r': editor.applyStyleToSelection({color:[1,0,0,1]}); return;
case 'g': editor.applyStyleToSelection({color:[0,0.6,0,1]}); return;
case 'u': editor.applyStyleToSelection({color:[0,0,1,1]}); return;
case 'k': editor.applyStyleToSelection({color:[0,0,0,1]}); return;
case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
case 'l': editor.applyStyleToSelection({underline:'toggle'}); return;
case ']': editor.applyStyleToSelection({size_add:1}); return;
case '[': editor.applyStyleToSelection({size_add:-1}); return;
}
}
if (!e.ctrlKey && !e.metaKey) {
e.preventDefault(); // at least needed for 'space'
editor.insert(e.key);
}
}
document.getElementById('para2').addEventListener('pointermove', interact);
document.getElementById('para2').addEventListener('pointerdown', interact);
document.getElementById('para2').addEventListener('pointerup', interact);
document.getElementById('para2').addEventListener('keydown', keyhandler);
return surface;
}
function ParagraphAPI3(CanvasKit, fontData) {
if (!CanvasKit || !fontData) {
return;
}
const surface = CanvasKit.MakeCanvasSurface('para3');
if (!surface) {
console.error('Could not make surface');
return;
}
const fontMgr = CanvasKit.FontMgr.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.');
// The code below works for English-only text assuming
// that each character is a glyph (cluster), spaces are breaks between words,
// new lines are breaks between lines and the entire text is LTR.
// In case the paragraph text was constructed in a set of calls we need the text
const text = builder.getText();
// Pass 1 bidi region as a triple of integers, [start:end), direction
const mallocedBidis = CanvasKit.Malloc(Uint32Array, 3);
mallocedBidis.toTypedArray().set([0, text.length, CanvasKit.TextDirection.LTR.value]);
// Pass the entire text as one word. It's only used for the method
// getWords
const mallocedWords = CanvasKit.Malloc(Uint32Array, 2);
mallocedWords.toTypedArray().set([0, text.length]);
// Pass each character as a separate grapheme
const mallocedGraphemes = CanvasKit.Malloc(Uint32Array, text.length + 1);
const graphemesArr = mallocedGraphemes.toTypedArray();
for (let i = 0; i <= text.length; i++) {
graphemesArr[i] = i;
}
// Pass each space as a "soft" break and each new line as a "hard" break.
const SOFT = 0;
const HARD = 1;
const lineBreaks = [0, SOFT];
for (let i = 0; i < text.length; ++i) {
if (text[i] === ' ') {
lineBreaks.push(i + 1, SOFT);
}
if (text[i] === '\n') {
lineBreaks.push(i + 1, HARD);
}
}
lineBreaks.push(text.length, SOFT);
const mallocedLineBreaks = CanvasKit.Malloc(Uint32Array, lineBreaks.length);
mallocedLineBreaks.toTypedArray().set(lineBreaks);
builder.setBidiRegionsUtf16(mallocedBidis);
builder.setWordsUtf16(mallocedWords);
builder.setGraphemeBreaksUtf16(mallocedGraphemes);
builder.setLineBreaksUtf16(mallocedLineBreaks);
const paragraph = builder.build();
paragraph.layout(600);
let wrapTo = 0;
let X = 100;
let Y = 100;
const fontPaint = new CanvasKit.Paint();
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);
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.RuntimeEffect.Make(spiralSkSL);
const shader = effect.makeShader([
0.5,
150, 150,
0, 1, 0, 1,
1, 0, 0, 1]);
const paint = new CanvasKit.Paint();
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.makeShaderCubic(
CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
1/3, 1/3,
CanvasKit.Matrix.scaled(quadrantSize/dogImg.width(),
quadrantSize/dogImg.height()));
const mandrillShader = mandrillImg.makeShaderCubic(
CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
1/3, 1/3,
CanvasKit.Matrix.scaled(
quadrantSize/mandrillImg.width(),
quadrantSize/mandrillImg.height()));
const surface = CanvasKit.MakeCanvasSurface('rtshader2');
if (!surface) {
console.error('Could not make surface');
return;
}
const prog = `
uniform shader before_map;
uniform shader after_map;
uniform 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);
}
half4 main(float2 xy) {
half4 before = before_map.eval(xy);
half4 after = after_map.eval(xy);
float m = smooth_cutoff(threshold_map.eval(xy).r);
return mix(before, after, half(m));
}`;
const canvas = surface.getCanvas();
const thresholdEffect = CanvasKit.RuntimeEffect.Make(prog);
const spiralEffect = CanvasKit.RuntimeEffect.Make(spiralSkSL);
const draw = (x, y, shader) => {
const paint = new CanvasKit.Paint();
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]);
const blur = CanvasKit.ImageFilter.MakeBlur(0.1, 0.1, CanvasKit.TileMode.Clamp, null);
const paint = new CanvasKit.Paint();
paint.setShader(spiralShader);
paint.setImageFilter(blur);
oCanvas.drawRect(CanvasKit.LTRBRect(0, 0, quadrantSize, quadrantSize), paint);
paint.delete();
blur.delete();
spiralShader.delete();
return offscreenSurface.makeImageSnapshot()
.makeShaderCubic(CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp,
1/3, 1/3);
};
const drawFrame = () => {
surface.requestAnimationFrame(drawFrame);
const thresholdShader = getBlurrySpiralShader(Math.sin(Date.now() / 5000) / 2);
const blendShader = thresholdEffect.makeShaderWithChildren(
[0.5, 10],
[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) {
if (!CanvasKit) {
return;
}
const surface = CanvasKit.MakeSWCanvasSurface('skp');
if (!surface) {
console.error('Could not make surface');
return;
}
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.RED);
const textPaint = new CanvasKit.Paint();
const textFont = new CanvasKit.Font(null, 20);
const pr = new CanvasKit.PictureRecorder();
const skpCanvas = pr.beginRecording(CanvasKit.LTRBRect(0, 0, 200, 200));
skpCanvas.drawRect(CanvasKit.LTRBRect(10, 10, 50, 50), paint);
skpCanvas.drawText('If you see this, CanvasKit loaded!!', 5, 100, textPaint, textFont);
const pic = pr.finishRecordingAsPicture();
const skpData = pic.serialize();
paint.delete();
pr.delete();
const deserialized = CanvasKit.MakePicture(skpData);
function drawFrame(canvas) {
if (deserialized) {
canvas.drawPicture(deserialized);
} else {
canvas.drawText('SKP did not deserialize', 5, 100, textPaint, textFont);
}
}
surface.drawOnce(drawFrame);
textPaint.delete();
textFont.delete();
}
// 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.FontMgr.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.Font(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
canvas.concat(CanvasKit.M44.setupCamera(
CanvasKit.LTRBRect(0, 0, sizeX, sizeY), halfDim, cam));
// Rotate the whole paragraph as a unit.
const paraRotPoint = [halfDim, halfDim, 1];
canvas.concat(CanvasKit.M44.multiply(
CanvasKit.M44.translated(paraRotPoint),
CanvasKit.M44.rotated([0,1,0], rotX),
CanvasKit.M44.rotated([1,0,0], rotY * 0.2),
CanvasKit.M44.translated(CanvasKit.Vector.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[rectLeft] + (r[rectRight] - r[rectLeft]) / 2,
marginTop + r[rectTop] + (r[rectBottom] - r[rectTop]) / 2,
0
];
distanceFromPointer = CanvasKit.Vector.dist(pointer, rotationPoint.slice(0, 2));
// Rotate more around the Y-axis depending on the glyph's distance from the pointer.
canvas.concat(CanvasKit.M44.multiply(
CanvasKit.M44.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.M44.rotated([1,0,0], rotY * -0.6),
CanvasKit.M44.rotated([0,1,0], distanceFromPointer * -0.035),
CanvasKit.M44.translated(CanvasKit.Vector.mulScalar(rotationPoint, -1)),
));
canvas.drawParagraph(letter['para'], margin + r[rectLeft], marginTop + r[rectTop]);
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.ColorSpace.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.Paint();
paint.setColor(red, CanvasKit.ColorSpace.ADOBE_RGB);
canvas.drawPaint(paint);
paint.setColor(red, CanvasKit.ColorSpace.DISPLAY_P3);
canvas.drawRRect(CanvasKit.RRectXY([50, 50, 250, 250], 30, 30), paint);
paint.setColor(red, CanvasKit.ColorSpace.SRGB);
canvas.drawRRect(CanvasKit.RRectXY([100, 100, 200, 200], 30, 30), paint);
surface.flush();
surface.delete();
}
</script>