blob: 1ea4dbcc2b14399ffd402d027334d6280cfc6dea [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>
<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="/node_modules/canvaskit/bin/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) => '/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
},
"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);
});
// 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);
GlyphGame(...results)
});
Promise.all([ckLoaded, loadSkp]).then((results) => {SkpExample(...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 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.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 tf = fontMgr.MakeTypefaceFromData(fontData);
const font = new CanvasKit.Font(tf, 50);
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 fontMgr = CanvasKit.FontMgr.FromData([fontData]);
const paraStyle = new CanvasKit.ParagraphStyle({
textStyle: {
color: CanvasKit.GRAY,
fontFamilies: ['Roboto'],
fontSize: 40,
},
textAlign: CanvasKit.TextAlign.Left,
});
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 WIDTH = 600;
const tf = fontMgr.MakeTypefaceFromData(fontData);
const font = new CanvasKit.Font(tf, 40);
const fm = font.getMetrics();
const fontPaint = new CanvasKit.Paint();
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
fontPaint.setAntiAlias(true);
function mid(a, b) { return (a + b) * 0.5; }
function runs_x_to_index(runs, x) {
for (const r of runs) {
for (let i = 1; i < r.offsets.length; i += 1) {
if (x < r.positions[i*2]) {
if (x <= mid(r.positions[i*2-2], r.positions[i*2])) {
return r.offsets[i-1];
} else {
return r.offsets[i];
}
}
}
}
const r = runs[runs.length-1];
return r.offsets[r.offsets.length-1];
}
function lines_pos_to_index(lines, x, y) {
if (y < lines[0].top) {
return 0;
}
for (const l of lines) {
if (y <= l.bottom) {
return runs_x_to_index(l.runs, x);
}
}
return lines[lines.length - 1].textRange.last + 1;
}
function runs_index_to_run(runs, index) {
for (const r of runs) {
if (index <= r.offsets[r.offsets.length-1]) {
return r;
}
}
return null;
}
function runs_index_to_x(runs, index) {
const r = runs_index_to_run(runs, index);
for (const i in r.offsets) {
if (index == r.offsets[i]) {
return r.positions[i*2];
}
}
return null;
}
function lines_index_to_line_index(lines, index) {
let i = 0;
for (const l of lines) {
if (index <= l.textRange.last) {
return i;
}
i += 1;
}
return lines.length-1;
}
function lines_index_to_line(lines, index) {
return lines[lines_index_to_line_index(lines, index)];
}
function lines_index_to_x(lines, index) {
for (const l of lines) {
if (index <= l.textRange.last) {
return runs_index_to_x(l.runs, index);
}
}
}
function lines_indices_to_path(lines, a, b, fm, width) {
if (a == b) {
return null;
}
if (a > b) { [a, b] = [b, a]; }
const path = new CanvasKit.Path();
const la = lines_index_to_line(lines, a);
const lb = lines_index_to_line(lines, b);
const ax = runs_index_to_x(la.runs, a);
const bx = runs_index_to_x(lb.runs, b);
if (la == lb) {
path.addRect([ax, la.top, bx, la.bottom]);
} else {
path.addRect([ax, la.top, width, la.bottom]);
path.addRect([0, lb.top, bx, lb.bottom]);
if (la.bottom < lb.top) {
path.addRect([0, la.bottom, width, lb.top]); // extra lines inbetween
}
}
return path;
}
const string_del = function(str, start, end) {
return str.slice(0, start) + str.slice(end, str.length);
};
let editor = {
_text: null,
_lines: null,
_cursor: null,
_width: 1e20,
_index: { start: 0, end: 0 },
init: function(text, cursor, width) {
this._text = text;
this._cursor = cursor;
this._width = width;
this._buildLines();
},
getLines: function() { return this._lines; },
setIndex: function(i) {
this._index.start = this._index.end = i;
const l = lines_index_to_line(this._lines, i);
const x = runs_index_to_x(l.runs, i);
this._cursor.place(x, l.top, l.bottom);
},
setIndices: function(a, b) {
if (a > b) { [a, b] = [b, a]; }
this._index.start = a;
this._index.end = b;
this._cursor.setPath(lines_indices_to_path(this._lines, a, b, fm, this._width));
},
moveDX: function(dx) {
let index;
if (this._index.start == this._index.end) {
// just adjust and pin
index = Math.max(Math.min(this._index.start + dx, this._text.length), 0);
} else {
// 'deselect' the region, and turn it into just a single index
index = dx < 0 ? this._index.start : this._index.end;
}
this.setIndex(index);
},
moveDY: function(dy) {
let index = (dy < 0) ? this._index.start : this._index.end;
const i = lines_index_to_line_index(this._lines, index);
if (dy < 0 && i == 0) {
index = 0;
} else if (dy > 0 && i == this._lines.length - 1) {
index = this._text.length;
} else {
const x = runs_index_to_x(this._lines[i].runs, index);
// todo: statefully track "original" x when an up/down sequence started,
// so we can avoid drift.
index = runs_x_to_index(this._lines[i+dy].runs, x);
}
this.setIndex(index);
},
_buildLines: function() {
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
builder.addText(this._text);
const paragraph = builder.build();
paragraph.layout(WIDTH);
const rec = new CanvasKit.PictureRecorder();
const can = rec.beginRecording([0,0,9999,9999]);
can.drawParagraph(paragraph, 0, 0);
rec.delete();
this._lines = paragraph.getShapedLines();
paragraph.delete();
builder.delete();
},
deleteSelection: function() {
let start = this._index.start;
if (start == this._index.end) {
if (start > 0) {
this._text = string_del(this._text, start - 1, start);
start -= 1;
}
} else {
this._text = string_del(this._text, start, this._index.end);
}
this._buildLines();
this.setIndex(start);
},
insert: function(charcode) {
if (this._index.start != this._index.end) {
this.deleteSelection();
}
const index = this._index.start;
this._text = this._text.slice(0, index) + charcode + this._text.slice(index);
this._buildLines();
this.setIndex(index + 1);
},
};
editor.init(text0, cursor, WIDTH);
function drawFrame(canvas) {
const lines = editor.getLines();
canvas.clear(CanvasKit.WHITE);
if (mouse.isActive()) {
const pos = mouse.getPos();
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);
}
}
const bgcolors = [[1,0,0,0.05], [0,1,0,0.05]];
let lineNo = 0;
cursor.draw_before(canvas);
for (const l of lines) {
if (true) { // test line bounds
const bounds = [0, l.top, WIDTH, l.bottom];
fontPaint.setColor(bgcolors[lineNo & 1]);
canvas.drawRect(bounds, fontPaint);
fontPaint.setColor([0,0,0,1]);
}
for (let r of l.runs) {
canvas.drawGlyphs(r.glyphs, r.positions, 0, 0, font, fontPaint);
}
lineNo += 1;
}
cursor.draw_after(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); break;
case 'ArrowRight': editor.moveDX(1); break;
case 'ArrowUp':
e.preventDefault();
editor.moveDY(-1);
break;
case 'ArrowDown':
e.preventDefault();
editor.moveDY(1);
break;
case 'Backspace':
editor.deleteSelection();
break;
case 'Shift':
break;
default:
if (!e.ctrlKey && !e.metaKey) {
editor.insert(e.key);
}
break;
}
}
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 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], true);
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 = sample(before_map, xy);
half4 after = sample(after_map, xy);
float m = smooth_cutoff(sample(threshold_map, 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], 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.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()
.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.MakePicture(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);
}
// 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>