[canvaskit] Run JS tests in Bazel

This CL makes a copy of all the files in
//modules/canvaskit/tests/ and puts them into
//modules/canvaskit/tests/bazel. They are slightly modified
to run in Bazel (renamed to be more clear what they are
and using EverythingLoaded instead of CanvasKitLoaded).

The original files will be deleted when we no longer test
CanvasKit outside of Bazel.

Suggested Review Order:
 - hello_world.js is now smoke_test.js. That test polls the
   gold_test_env server, but does not create an image.
 - test_reporter.js which is much simplified from the non-Bazel
   version (due to the removal of a bunch of PathKit stuff).
   These JS tests are not the C++ gms. This means that we need
   to capture the PNG ourselves, which we do using the <canvas>
   API toDataURL(). This is base64 encoded, which is conveniently
   the format accepted by the gold_test_env server.
 - karma.bazel.js and assets/BUILD.bazel which make all the
   test assets available under a shortened path /assets.
 - Feel free to skip over the remaining *_test.js, as they are
   basically the same as what is currently checked in, just
   with the modifications above.
 - Any remaining files.

Change-Id: I45fc38da38faf11f21011e7381d390e6bb299df4
Bug: skia:12541
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/513916
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Ben Wagner <bungeman@google.com>
This commit is contained in:
Kevin Lubick 2022-02-28 15:16:36 -05:00
parent 953c92880b
commit 1fc0c1c0f8
19 changed files with 6595 additions and 52 deletions

View File

@ -432,18 +432,31 @@ bool_flag(
)
karma_test(
name = "hello_world",
name = "canvaskit_js_wasms",
srcs = [
":canvaskit_wasm/canvaskit.js",
# We want to make sure the CanvasKit JS is loaded before the loader script
"tests/bazel_canvaskitinit.js",
"tests/bazel/canvaskitinit.js",
"tests/bazel/util.js",
"tests/bazel/test_reporter.js",
# which is loaded before the tests...
"tests/hello_world.js",
"tests/bazel/smoke_test.js",
"tests/bazel/canvas_test.js",
"tests/bazel/canvas2d_test.js",
"tests/bazel/core_test.js",
"tests/bazel/font_test.js",
"tests/bazel/matrix_test.js",
"tests/bazel/paragraph_test.js",
"tests/bazel/path_test.js",
"tests/bazel/rtshader_test.js",
"tests/bazel/skottie_test.js",
],
config_file = "karma.bazel.js",
# The tests need the Gold server to be up and running.
# The tests need the Gold server to be up and running so they can make POST requests to
# exfiltrate the PNGs they create.
env = "//modules/canvaskit/go/gold_test_env:gold_test_env",
static_files = [
":canvaskit_wasm/canvaskit.wasm",
"//modules/canvaskit/tests/assets:test_assets",
],
)

View File

@ -159,7 +159,8 @@ bazel_canvaskit_release:
ls -l build
bazel_test_canvaskit:
bazelisk test :hello_world --compilation_mode opt --spawn_strategy=local --test_output=all
echo "test output in //bazel-testlogs/modules/canvaskit/hello_world_test_with_env/test.outputs/"
ls ../../bazel-testlogs/modules/canvaskit/hello_world_test_with_env/test.outputs/
bazelisk test :canvaskit_js_wasms --compilation_mode opt --spawn_strategy=local \
--test_output=streamed
echo "test output in //bazel-testlogs/modules/canvaskit/canvaskit_js_wasms/test.outputs/"
ls ../../bazel-testlogs/modules/canvaskit/canvaskit_js_wasms/test.outputs/

View File

@ -103,6 +103,7 @@ func beginTestManagementLogic(listener net.Listener) {
// Write the data in the POST to the special Bazel output directory
fileContents, err := base64.StdEncoding.DecodeString(payload.Base64Data)
if err != nil {
fmt.Printf("Invalid base64 data: %s\n", err.Error())
http.Error(w, "Invalid base64 data "+err.Error(), http.StatusBadRequest)
return
}

View File

@ -18,6 +18,8 @@ module.exports = function(config) {
// The tests will make calls to /gold_rpc/whatever and they will be redirected
// to the correct location.
'/gold_rpc/': `http://localhost:${port}/`,
// This makes it more convenient for tests to load the test assets.
'/assets/': '/static/skia/modules/canvaskit/tests/assets/',
},
// possible values: 'dots', 'progress'

View File

@ -0,0 +1,26 @@
filegroup(
name = "test_assets",
srcs = [
"Bungee-Regular.ttf",
"NotoColorEmoji.ttf",
"NotoSerif-BoldItalic.ttf",
"NotoSerif-Regular.ttf",
"Roboto-Regular.otf",
"Roboto-Regular.woff",
"Roboto-Regular.woff2",
"animated_gif.json",
"audio_external.json",
"brickwork-texture.jpg",
"color_wheel.gif",
"color_wheel.webp",
"exif_rotated_heart.jpg",
"flightAnim.gif",
"mandrill_16.png",
"mandrill_512.png",
"mandrill_h1v1.jpg",
"map-shield.json",
"red_line.skp",
"test.ttc",
],
visibility = ["//:__subpackages__"],
)

View File

@ -0,0 +1,853 @@
describe('Canvas 2D emulation', () => {
let container;
beforeEach(async () => {
await EverythingLoaded;
container = document.createElement('div');
container.innerHTML = `
<canvas width=600 height=600 id=test></canvas>
<canvas width=600 height=600 id=report></canvas>`;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
const expectColorCloseTo = (a, b) => {
expect(a.length).toEqual(4);
expect(b.length).toEqual(4);
for (let i=0; i<4; i++) {
expect(a[i]).toBeCloseTo(b[i], 3);
}
}
describe('color strings', () => {
const hex = (s) => {
return parseInt(s, 16);
}
it('parses hex color strings', () => {
const parseColor = CanvasKit.parseColorString;
expectColorCloseTo(parseColor('#FED'),
CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), 1));
expectColorCloseTo(parseColor('#FEDC'),
CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), hex('CC')/255));
expectColorCloseTo(parseColor('#fed'),
CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), 1));
expectColorCloseTo(parseColor('#fedc'),
CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), hex('CC')/255));
});
it('parses rgba color strings', () => {
const parseColor = CanvasKit.parseColorString;
expectColorCloseTo(parseColor('rgba(117, 33, 64, 0.75)'),
CanvasKit.Color(117, 33, 64, 0.75));
expectColorCloseTo(parseColor('rgb(117, 33, 64, 0.75)'),
CanvasKit.Color(117, 33, 64, 0.75));
expectColorCloseTo(parseColor('rgba(117,33,64)'),
CanvasKit.Color(117, 33, 64, 1.0));
expectColorCloseTo(parseColor('rgb(117,33, 64)'),
CanvasKit.Color(117, 33, 64, 1.0));
expectColorCloseTo(parseColor('rgb(117,33, 64, 32%)'),
CanvasKit.Color(117, 33, 64, 0.32));
expectColorCloseTo(parseColor('rgb(117,33, 64, 0.001)'),
CanvasKit.Color(117, 33, 64, 0.001));
expectColorCloseTo(parseColor('rgb(117,33,64,0)'),
CanvasKit.Color(117, 33, 64, 0.0));
});
it('parses named color strings', () => {
// Keep this one as the _testing version, because we don't include the large
// color map by default.
const parseColor = CanvasKit._testing.parseColor;
expectColorCloseTo(parseColor('grey'),
CanvasKit.Color(128, 128, 128, 1.0));
expectColorCloseTo(parseColor('blanchedalmond'),
CanvasKit.Color(255, 235, 205, 1.0));
expectColorCloseTo(parseColor('transparent'),
CanvasKit.Color(0, 0, 0, 0));
});
it('properly produces color strings', () => {
const colorToString = CanvasKit._testing.colorToString;
expect(colorToString(CanvasKit.Color(102, 51, 153, 1.0))).toEqual('#663399');
expect(colorToString(CanvasKit.Color(255, 235, 205, 0.5))).toEqual(
'rgba(255, 235, 205, 0.50000000)');
});
it('can multiply colors by alpha', () => {
const multiplyByAlpha = CanvasKit.multiplyByAlpha;
const testCases = [
{
inColor: CanvasKit.Color(102, 51, 153, 1.0),
inAlpha: 1.0,
outColor: CanvasKit.Color(102, 51, 153, 1.0),
},
{
inColor: CanvasKit.Color(102, 51, 153, 1.0),
inAlpha: 0.8,
outColor: CanvasKit.Color(102, 51, 153, 0.8),
},
{
inColor: CanvasKit.Color(102, 51, 153, 0.8),
inAlpha: 0.7,
outColor: CanvasKit.Color(102, 51, 153, 0.56),
},
{
inColor: CanvasKit.Color(102, 51, 153, 0.8),
inAlpha: 1000,
outColor: CanvasKit.Color(102, 51, 153, 1.0),
},
];
for (const tc of testCases) {
// Print out the test case if the two don't match.
expect(multiplyByAlpha(tc.inColor, tc.inAlpha))
.toEqual(tc.outColor, JSON.stringify(tc));
}
});
}); // end describe('color string parsing')
describe('fonts', () => {
it('can parse font sizes', () => {
const parseFontString = CanvasKit._testing.parseFontString;
const tests = [{
'input': '10px monospace',
'output': {
'style': '',
'variant': '',
'weight': '',
'sizePx': 10,
'family': 'monospace',
}
},
{
'input': '15pt Arial',
'output': {
'style': '',
'variant': '',
'weight': '',
'sizePx': 20,
'family': 'Arial',
}
},
{
'input': '1.5in Arial, san-serif ',
'output': {
'style': '',
'variant': '',
'weight': '',
'sizePx': 144,
'family': 'Arial, san-serif',
}
},
{
'input': '1.5em SuperFont',
'output': {
'style': '',
'variant': '',
'weight': '',
'sizePx': 24,
'family': 'SuperFont',
}
},
];
for (let i = 0; i < tests.length; i++) {
expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
}
});
it('can parse font attributes', () => {
const parseFontString = CanvasKit._testing.parseFontString;
const tests = [{
'input': 'bold 10px monospace',
'output': {
'style': '',
'variant': '',
'weight': 'bold',
'sizePx': 10,
'family': 'monospace',
}
},
{
'input': 'italic bold 10px monospace',
'output': {
'style': 'italic',
'variant': '',
'weight': 'bold',
'sizePx': 10,
'family': 'monospace',
}
},
{
'input': 'italic small-caps bold 10px monospace',
'output': {
'style': 'italic',
'variant': 'small-caps',
'weight': 'bold',
'sizePx': 10,
'family': 'monospace',
}
},
{
'input': 'small-caps bold 10px monospace',
'output': {
'style': '',
'variant': 'small-caps',
'weight': 'bold',
'sizePx': 10,
'family': 'monospace',
}
},
{
'input': 'italic 10px monospace',
'output': {
'style': 'italic',
'variant': '',
'weight': '',
'sizePx': 10,
'family': 'monospace',
}
},
{
'input': 'small-caps 10px monospace',
'output': {
'style': '',
'variant': 'small-caps',
'weight': '',
'sizePx': 10,
'family': 'monospace',
}
},
{
'input': 'normal bold 10px monospace',
'output': {
'style': 'normal',
'variant': '',
'weight': 'bold',
'sizePx': 10,
'family': 'monospace',
}
},
];
for (let i = 0; i < tests.length; i++) {
expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
}
});
});
const multipleCanvasTest = (testname, done, test) => {
const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
skcanvas._config = 'software_canvas';
const realCanvas = document.getElementById('test');
realCanvas._config = 'html_canvas';
realCanvas.width = CANVAS_WIDTH;
realCanvas.height = CANVAS_HEIGHT;
if (!done) {
console.log('debugging canvaskit');
test(realCanvas);
test(skcanvas);
const png = skcanvas.toDataURL();
const img = document.createElement('img');
document.body.appendChild(img);
img.src = png;
debugger;
return;
}
let promises = [];
for (let canvas of [skcanvas, realCanvas]) {
test(canvas);
// canvas has .toDataURL (even though skcanvas is not a real Canvas)
// so this will work.
promises.push(reportCanvas(canvas, testname, canvas._config));
}
Promise.all(promises).then(() => {
skcanvas.dispose();
done();
}).catch(reportError(done));
}
describe('CanvasContext2D API', () => {
multipleCanvasGM('all_line_drawing_operations', (canvas) => {
const ctx = canvas.getContext('2d');
ctx.scale(3.0, 3.0);
ctx.moveTo(20, 5);
ctx.lineTo(30, 20);
ctx.lineTo(40, 10);
ctx.lineTo(50, 20);
ctx.lineTo(60, 0);
ctx.lineTo(20, 5);
ctx.moveTo(20, 80);
ctx.bezierCurveTo(90, 10, 160, 150, 190, 10);
ctx.moveTo(36, 148);
ctx.quadraticCurveTo(66, 188, 120, 136);
ctx.lineTo(36, 148);
ctx.rect(5, 170, 20, 25);
ctx.moveTo(150, 180);
ctx.arcTo(150, 100, 50, 200, 20);
ctx.lineTo(160, 160);
ctx.moveTo(20, 120);
ctx.arc(20, 120, 18, 0, 1.75 * Math.PI);
ctx.lineTo(20, 120);
ctx.moveTo(150, 5);
ctx.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI)
ctx.lineWidth = 2;
ctx.stroke();
// Test edgecases and draw direction
ctx.beginPath();
ctx.arc(50, 100, 10, Math.PI, -Math.PI/2);
ctx.stroke();
ctx.beginPath();
ctx.arc(75, 100, 10, Math.PI, -Math.PI/2, true);
ctx.stroke();
ctx.beginPath();
ctx.arc(100, 100, 10, Math.PI, 100.1 * Math.PI, true);
ctx.stroke();
ctx.beginPath();
ctx.arc(125, 100, 10, Math.PI, 100.1 * Math.PI, false);
ctx.stroke();
ctx.beginPath();
ctx.ellipse(155, 100, 10, 15, Math.PI/8, 100.1 * Math.PI, Math.PI, true);
ctx.stroke();
ctx.beginPath();
ctx.ellipse(180, 100, 10, 15, Math.PI/8, Math.PI, 100.1 * Math.PI, true);
ctx.stroke();
});
multipleCanvasGM('all_matrix_operations', (canvas) => {
const ctx = canvas.getContext('2d');
ctx.rect(10, 10, 20, 20);
ctx.scale(2.0, 4.0);
ctx.rect(30, 10, 20, 20);
ctx.resetTransform();
ctx.rotate(Math.PI / 3);
ctx.rect(50, 10, 20, 20);
ctx.resetTransform();
ctx.translate(30, -2);
ctx.rect(70, 10, 20, 20);
ctx.resetTransform();
ctx.translate(60, 0);
ctx.rotate(Math.PI / 6);
ctx.transform(1.5, 0, 0, 0.5, 0, 0); // effectively scale
ctx.rect(90, 10, 20, 20);
ctx.resetTransform();
ctx.save();
ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
ctx.rect(110, 10, 20, 20);
ctx.lineTo(110, 0);
ctx.restore();
ctx.lineTo(220, 120);
ctx.scale(3.0, 3.0);
ctx.font = '6pt Noto Mono';
ctx.fillText('This text should be huge', 10, 80);
ctx.resetTransform();
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(250, 30);
ctx.lineTo(250, 80);
ctx.scale(3.0, 3.0);
ctx.lineTo(280/3, 90/3);
ctx.closePath();
ctx.strokeStyle = 'black';
ctx.lineWidth = 5;
ctx.stroke();
});
multipleCanvasGM('shadows_and_save_restore', (canvas) => {
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#000';
ctx.fillStyle = '#CCC';
ctx.shadowColor = 'rebeccapurple';
ctx.shadowBlur = 1;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = -8;
ctx.rect(10, 10, 30, 30);
ctx.save();
ctx.strokeStyle = '#C00';
ctx.fillStyle = '#00C';
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
ctx.stroke();
ctx.restore();
ctx.fill();
ctx.beginPath();
ctx.moveTo(36, 148);
ctx.quadraticCurveTo(66, 188, 120, 136);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.shadowColor = '#993366AA';
ctx.shadowOffsetX = 8;
ctx.shadowBlur = 5;
ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
ctx.rect(110, 10, 20, 20);
ctx.lineTo(110, 0);
ctx.resetTransform();
ctx.lineTo(220, 120);
ctx.stroke();
ctx.fillStyle = 'green';
ctx.font = '16pt Noto Mono';
ctx.fillText('This should be shadowed', 20, 80);
ctx.beginPath();
ctx.lineWidth = 6;
ctx.ellipse(10, 290, 30, 30, 0, 0, Math.PI * 2);
ctx.scale(2, 1);
ctx.moveTo(10, 290)
ctx.ellipse(10, 290, 30, 60, 0, 0, Math.PI * 2);
ctx.resetTransform();
ctx.shadowColor = '#993366AA';
ctx.scale(3, 1);
ctx.moveTo(10, 290)
ctx.ellipse(10, 290, 30, 90, 0, 0, Math.PI * 2);
ctx.stroke();
});
multipleCanvasGM('global_dashed_rects', (canvas) => {
const ctx = canvas.getContext('2d');
ctx.scale(1.1, 1.1);
ctx.translate(10, 10);
// Shouldn't impact the fillRect calls
ctx.setLineDash([5, 3]);
ctx.fillStyle = 'rgba(200, 0, 100, 0.81)';
ctx.fillRect(20, 30, 100, 100);
ctx.globalAlpha = 0.81;
ctx.fillStyle = 'rgba(200, 0, 100, 1.0)';
ctx.fillRect(120, 30, 100, 100);
// This shouldn't do anything
ctx.globalAlpha = 0.1;
ctx.fillStyle = 'rgba(200, 0, 100, 0.9)';
ctx.globalAlpha = 0.9;
// Intentional no-op to check ordering
ctx.clearRect(220, 30, 100, 100);
ctx.fillRect(220, 30, 100, 100);
ctx.fillRect(320, 30, 100, 100);
ctx.clearRect(330, 40, 80, 80);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.setLineDash([5, 3]);
ctx.strokeRect(20, 150, 100, 100);
ctx.setLineDash([50, 30]);
ctx.strokeRect(125, 150, 100, 100);
ctx.lineDashOffset = 25;
ctx.strokeRect(230, 150, 100, 100);
ctx.setLineDash([2, 5, 9]);
ctx.strokeRect(335, 150, 100, 100);
ctx.setLineDash([5, 2]);
ctx.moveTo(336, 400);
ctx.quadraticCurveTo(366, 488, 120, 450);
ctx.lineTo(300, 400);
ctx.stroke();
ctx.font = '36pt Noto Mono';
ctx.strokeText('Dashed', 20, 350);
ctx.fillText('Not Dashed', 20, 400);
});
multipleCanvasGM('gradients_clip', (canvas) => {
const ctx = canvas.getContext('2d');
const rgradient = ctx.createRadialGradient(200, 300, 10, 100, 100, 300);
rgradient.addColorStop(0, 'red');
rgradient.addColorStop(.7, 'white');
rgradient.addColorStop(1, 'blue');
ctx.fillStyle = rgradient;
ctx.globalAlpha = 0.7;
ctx.fillRect(0,0,600,600);
ctx.globalAlpha = 0.95;
ctx.beginPath();
ctx.arc(300, 100, 90, 0, Math.PI*1.66);
ctx.closePath();
ctx.strokeStyle = 'yellow';
ctx.lineWidth = 5;
ctx.stroke();
ctx.save();
ctx.clip();
const lgradient = ctx.createLinearGradient(200, 20, 420, 40);
lgradient.addColorStop(0, 'green');
lgradient.addColorStop(.5, 'cyan');
lgradient.addColorStop(1, 'orange');
ctx.fillStyle = lgradient;
ctx.fillRect(200, 30, 200, 300);
ctx.restore();
ctx.fillRect(550, 550, 40, 40);
});
multipleCanvasGM('get_put_imagedata', (canvas) => {
const ctx = canvas.getContext('2d');
// Make a gradient so we see if the pixels copying worked
const grad = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
grad.addColorStop(0, 'yellow');
grad.addColorStop(1, 'red');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
const iData = ctx.getImageData(400, 100, 200, 150);
expect(iData.width).toBe(200);
expect(iData.height).toBe(150);
expect(iData.data.byteLength).toBe(200*150*4);
ctx.putImageData(iData, 10, 10);
ctx.putImageData(iData, 350, 350, 100, 75, 45, 40);
ctx.strokeRect(350, 350, 200, 150);
const box = ctx.createImageData(20, 40);
ctx.putImageData(box, 10, 300);
const biggerBox = ctx.createImageData(iData);
ctx.putImageData(biggerBox, 10, 350);
expect(biggerBox.width).toBe(iData.width);
expect(biggerBox.height).toBe(iData.height);
});
multipleCanvasGM('shadows_with_rotate_skbug_9947', (canvas) => {
const ctx = canvas.getContext('2d');
const angle = 240;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.save();
ctx.translate(80, 80);
ctx.rotate((angle * Math.PI) / 180);
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor = 'rgba(100,100,100,0.5)';
ctx.shadowBlur = 1;
ctx.fillStyle = 'black';
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.rect(-20, -20, 40, 40);
ctx.fill();
ctx.fillRect(30, 30, 40, 40);
ctx.strokeRect(30, -20, 40, 40);
ctx.fillText('text', -20, -30);
ctx.restore();
});
describe('using images', () => {
let skImageData = null;
let htmlImage = null;
const skPromise = fetch('/assets/mandrill_512.png')
.then((response) => response.arrayBuffer())
.then((buffer) => {
skImageData = buffer;
});
const realPromise = fetch('/assets/mandrill_512.png')
.then((response) => response.blob())
.then((blob) => createImageBitmap(blob))
.then((bitmap) => {
htmlImage = bitmap;
});
beforeEach(async () => {
await skPromise;
await realPromise;
});
multipleCanvasGM('draw_patterns', (canvas) => {
const ctx = canvas.getContext('2d');
let img = htmlImage;
if (canvas._config === 'software_canvas') {
img = canvas.decodeImage(skImageData);
}
ctx.fillStyle = '#EEE';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.lineWidth = 20;
ctx.scale(0.2, 0.4);
let pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 1500, 750);
pattern = ctx.createPattern(img, 'repeat-x');
ctx.fillStyle = pattern;
ctx.fillRect(1500, 0, 3000, 750);
ctx.globalAlpha = 0.7
pattern = ctx.createPattern(img, 'repeat-y');
ctx.fillStyle = pattern;
ctx.fillRect(0, 750, 1500, 1500);
ctx.strokeRect(0, 750, 1500, 1500);
pattern = ctx.createPattern(img, 'no-repeat');
ctx.fillStyle = pattern;
pattern.setTransform({a: 1, b: -.1, c:.1, d: 0.5, e: 1800, f:800});
ctx.fillRect(0, 0, 3000, 1500);
});
multipleCanvasGM('draw_image', (canvas) => {
let ctx = canvas.getContext('2d');
let img = htmlImage;
if (canvas._config === 'software_canvas') {
img = canvas.decodeImage(skImageData);
}
ctx.drawImage(img, 30, -200);
ctx.globalAlpha = 0.7
ctx.rotate(.1);
ctx.imageSmoothingQuality = 'medium';
ctx.drawImage(img, 200, 350, 150, 100);
ctx.rotate(-.2);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 100, 150, 400, 350, 10, 400, 150, 100);
});
}); // end describe('using images')
{
const drawPoint = (ctx, x, y, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, 1, 1);
}
const IN = 'purple';
const OUT = 'orange';
const SCALE = 8;
// Check to see if these points are in or out on each of the
// test configurations.
const pts = [[3, 3], [4, 4], [5, 5], [10, 10], [8, 10], [6, 10],
[6.5, 9], [15, 10], [17, 10], [17, 11], [24, 24],
[25, 25], [26, 26], [27, 27]];
const tests = [
{
xOffset: 0,
yOffset: 0,
fillType: 'nonzero',
strokeWidth: 0,
testFn: (ctx, x, y) => ctx.isPointInPath(x * SCALE, y * SCALE, 'nonzero'),
},
{
xOffset: 30,
yOffset: 0,
fillType: 'evenodd',
strokeWidth: 0,
testFn: (ctx, x, y) => ctx.isPointInPath(x * SCALE, y * SCALE, 'evenodd'),
},
{
xOffset: 0,
yOffset: 30,
fillType: null,
strokeWidth: 1,
testFn: (ctx, x, y) => ctx.isPointInStroke(x * SCALE, y * SCALE),
},
{
xOffset: 30,
yOffset: 30,
fillType: null,
strokeWidth: 2,
testFn: (ctx, x, y) => ctx.isPointInStroke(x * SCALE, y * SCALE),
},
];
multipleCanvasGM('points_in_path_stroke', (canvas) => {
const ctx = canvas.getContext('2d');
ctx.font = '20px Noto Mono';
// Draw some visual aids
ctx.fillText('path-nonzero', 60, 30);
ctx.fillText('path-evenodd', 300, 30);
ctx.fillText('stroke-1px-wide', 60, 260);
ctx.fillText('stroke-2px-wide', 300, 260);
ctx.fillText('purple is IN, orange is OUT', 20, 560);
// Scale up to make single pixels easier to see
ctx.scale(SCALE, SCALE);
for (const test of tests) {
ctx.beginPath();
const xOffset = test.xOffset;
const yOffset = test.yOffset;
ctx.fillStyle = '#AAA';
ctx.lineWidth = test.strokeWidth;
ctx.rect(5+xOffset, 5+yOffset, 20, 20);
ctx.arc(15+xOffset, 15+yOffset, 8, 0, Math.PI*2, false);
if (test.fillType) {
ctx.fill(test.fillType);
} else {
ctx.stroke();
}
for (const pt of pts) {
let [x, y] = pt;
x += xOffset;
y += yOffset;
// naively apply transform when querying because the points queried
// ignore the CTM.
if (test.testFn(ctx, x, y)) {
drawPoint(ctx, x, y, IN);
} else {
drawPoint(ctx, x, y, OUT);
}
}
}
});
}
describe('loading custom fonts', () => {
const realFontLoaded = new FontFace('BungeeNonSystem', 'url(/assets/Bungee-Regular.ttf)', {
'family': 'BungeeNonSystem', // Make sure the canvas does not use the system font
'style': 'normal',
'weight': '400',
}).load().then((font) => {
document.fonts.add(font);
});
let fontBuffer = null;
const skFontLoaded = fetch('/assets/Bungee-Regular.ttf').then(
(response) => response.arrayBuffer()).then(
(buffer) => {
fontBuffer = buffer;
});
beforeEach(async () => {
await realFontLoaded;
await skFontLoaded;
});
multipleCanvasGM('custom_font', (canvas) => {
if (canvas.loadFont) {
canvas.loadFont(fontBuffer, {
'family': 'BungeeNonSystem',
'style': 'normal',
'weight': '400',
});
}
const ctx = canvas.getContext('2d');
ctx.font = '20px monospace';
ctx.fillText('20 px monospace', 10, 30);
ctx.font = '2.0em BungeeNonSystem';
ctx.fillText('2.0em Bungee filled', 10, 80);
ctx.strokeText('2.0em Bungee stroked', 10, 130);
const m = ctx.measureText('A phrase in English');
expect(m).toBeTruthy();
expect(m['width']).toBeTruthy();
ctx.font = '40pt monospace';
ctx.strokeText('40pt monospace', 10, 200);
// bold wasn't defined, so should fallback to just the 400 weight
ctx.font = 'bold 45px BungeeNonSystem';
ctx.fillText('45px Bungee filled', 10, 260);
});
}); // describe('loading custom fonts')
it('can read default properties', () => {
const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
const realCanvas = document.getElementById('test');
realCanvas.width = CANVAS_WIDTH;
realCanvas.height = CANVAS_HEIGHT;
const skcontext = skcanvas.getContext('2d');
const realContext = realCanvas.getContext('2d');
// The skia canvas only comes with a monospace font by default
// Set the html canvas to be monospace too.
realContext.font = '10px monospace';
const toTest = ['font', 'lineWidth', 'strokeStyle', 'lineCap',
'lineJoin', 'miterLimit', 'shadowOffsetY',
'shadowBlur', 'shadowColor', 'shadowOffsetX',
'globalAlpha', 'globalCompositeOperation',
'lineDashOffset', 'imageSmoothingEnabled',
'imageFilterQuality'];
// Compare all the default values of the properties of skcanvas
// to the default values on the properties of a real canvas.
for(let attr of toTest) {
expect(skcontext[attr]).toBe(realContext[attr], attr);
}
skcanvas.dispose();
});
}); // end describe('CanvasContext2D API')
describe('Path2D API', () => {
multipleCanvasGM('path2d_line_drawing_operations', (canvas) => {
const ctx = canvas.getContext('2d');
let clock;
let path;
if (canvas.makePath2D) {
clock = canvas.makePath2D('M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z');
path = canvas.makePath2D();
} else {
clock = new Path2D('M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z')
path = new Path2D();
}
path.moveTo(20, 5);
path.lineTo(30, 20);
path.lineTo(40, 10);
path.lineTo(50, 20);
path.lineTo(60, 0);
path.lineTo(20, 5);
path.moveTo(20, 80);
path.bezierCurveTo(90, 10, 160, 150, 190, 10);
path.moveTo(36, 148);
path.quadraticCurveTo(66, 188, 120, 136);
path.lineTo(36, 148);
path.rect(5, 170, 20, 25);
path.moveTo(150, 180);
path.arcTo(150, 100, 50, 200, 20);
path.lineTo(160, 160);
path.moveTo(20, 120);
path.arc(20, 120, 18, 0, 1.75 * Math.PI);
path.lineTo(20, 120);
path.moveTo(150, 5);
path.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI)
ctx.lineWidth = 2;
ctx.scale(3.0, 3.0);
ctx.stroke(path);
ctx.stroke(clock);
});
}); // end describe('Path2D API')
});

View File

@ -0,0 +1,944 @@
describe('Canvas Behavior', () => {
let container;
beforeEach(async () => {
await EverythingLoaded;
container = document.createElement('div');
container.innerHTML = `
<canvas width=600 height=600 id=test></canvas>
<canvas width=600 height=600 id=report></canvas>`;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
gm('canvas_api_example', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setStrokeWidth(2.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Stroke);
canvas.drawLine(3, 10, 30, 15, paint);
const rrect = CanvasKit.RRectXY([5, 35, 45, 80], 15, 10);
canvas.drawRRect(rrect, paint);
canvas.drawOval(CanvasKit.LTRBRect(5, 35, 45, 80), paint);
canvas.drawArc(CanvasKit.LTRBRect(55, 35, 95, 80), 15, 270, true, paint);
const font = new CanvasKit.Font(null, 20);
canvas.drawText('this is ascii text', 5, 100, paint, font);
const blob = CanvasKit.TextBlob.MakeFromText('Unicode chars 💩 é É ص', font);
canvas.drawTextBlob(blob, 5, 130, paint);
font.delete();
blob.delete();
paint.delete();
// See canvas2d for more API tests
});
gm('effect_and_text_example', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
const textPaint = new CanvasKit.Paint();
textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0));
textPaint.setAntiAlias(true);
const textFont = new CanvasKit.Font(null, 30);
const dpe = CanvasKit.PathEffect.MakeDash([15, 5, 5, 10], 1);
paint.setPathEffect(dpe);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
canvas.drawPath(path, paint);
canvas.drawText('This is text', 10, 280, textPaint, textFont);
dpe.delete();
path.delete();
paint.delete();
textFont.delete();
textPaint.delete();
});
gm('patheffects_canvas', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const path = starPath(CanvasKit, 100, 100, 100);
const paint = new CanvasKit.Paint();
const cornerEffect = CanvasKit.PathEffect.MakeCorner(10);
const discreteEffect = CanvasKit.PathEffect.MakeDiscrete(5, 10, 0);
paint.setPathEffect(cornerEffect);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
canvas.drawPath(path, paint);
canvas.translate(200, 0);
paint.setPathEffect(discreteEffect);
canvas.drawPath(path, paint);
cornerEffect.delete();
path.delete();
paint.delete();
});
it('returns the depth of the save state stack', () => {
const canvas = new CanvasKit.Canvas();
expect(canvas.getSaveCount()).toEqual(1);
canvas.save();
canvas.save();
canvas.restore();
canvas.save();
canvas.save();
expect(canvas.getSaveCount()).toEqual(4);
// does nothing, by the SkCanvas API
canvas.restoreToCount(500);
expect(canvas.getSaveCount()).toEqual(4);
canvas.restore();
expect(canvas.getSaveCount()).toEqual(3);
canvas.save();
canvas.restoreToCount(2);
expect(canvas.getSaveCount()).toEqual(2);
});
gm('circle_canvas', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.CYAN);
canvas.clear(CanvasKit.WHITE);
canvas.drawCircle(30, 50, 15, paint);
paint.setStyle(CanvasKit.PaintStyle.Fill);
paint.setColor(CanvasKit.RED);
canvas.drawCircle(130, 80, 60, paint);
canvas.drawCircle(20, 150, 60, paint);
path.delete();
paint.delete();
});
gm('rrect_canvas', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(3.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
canvas.drawRRect(CanvasKit.RRectXY(
CanvasKit.LTRBRect(10, 10, 50, 50), 5, 10), paint);
canvas.drawRRect(CanvasKit.RRectXY(
CanvasKit.LTRBRect(60, 10, 110, 50), 10, 5), paint);
canvas.drawRRect(CanvasKit.RRectXY(
CanvasKit.LTRBRect(10, 60, 210, 260), 0, 30), paint);
canvas.drawRRect(CanvasKit.RRectXY(
CanvasKit.LTRBRect(50, 90, 160, 210), 30, 30), paint);
path.delete();
paint.delete();
});
gm('rrect_8corners_canvas', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(3.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
canvas.drawRRect([10, 10, 210, 210,
// top left corner, going clockwise
10, 30,
30, 10,
50, 75,
120, 120,
], paint);
path.delete();
paint.delete();
});
// As above, except with the array passed in via malloc'd memory.
gm('rrect_8corners_malloc_canvas', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(3.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
const rrect = CanvasKit.Malloc(Float32Array, 12);
rrect.toTypedArray().set([10, 10, 210, 210,
// top left corner, going clockwise
10, 30,
30, 10,
50, 75,
120, 120,
]);
canvas.drawRRect(rrect, paint);
CanvasKit.Free(rrect);
path.delete();
paint.delete();
});
gm('drawDRRect_canvas', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Fill);
paint.setStrokeWidth(3.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
const outer = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 10, 5);
const inner = CanvasKit.RRectXY(CanvasKit.LTRBRect(50, 90, 160, 210), 30, 30);
canvas.drawDRRect(outer, inner, paint);
path.delete();
paint.delete();
});
gm('colorfilters_canvas', (canvas) => {
const paint = new CanvasKit.Paint();
const blue = CanvasKit.ColorFilter.MakeBlend(
CanvasKit.BLUE, CanvasKit.BlendMode.SrcIn);
const red = CanvasKit.ColorFilter.MakeBlend(
CanvasKit.Color(255, 0, 0, 0.8), CanvasKit.BlendMode.SrcOver);
const lerp = CanvasKit.ColorFilter.MakeLerp(0.6, red, blue);
paint.setStyle(CanvasKit.PaintStyle.Fill);
paint.setAntiAlias(true);
canvas.clear(CanvasKit.Color(230, 230, 230));
paint.setColorFilter(blue)
canvas.drawRect(CanvasKit.LTRBRect(10, 10, 60, 60), paint);
paint.setColorFilter(lerp)
canvas.drawRect(CanvasKit.LTRBRect(50, 10, 100, 60), paint);
paint.setColorFilter(red)
canvas.drawRect4f(90, 10, 140, 60, paint);
const r = CanvasKit.ColorMatrix.rotated(0, .707, -.707);
const b = CanvasKit.ColorMatrix.rotated(2, .5, .866);
const s = CanvasKit.ColorMatrix.scaled(0.9, 1.5, 0.8, 0.8);
let cm = CanvasKit.ColorMatrix.concat(r, s);
cm = CanvasKit.ColorMatrix.concat(cm, b);
CanvasKit.ColorMatrix.postTranslate(cm, 20, 0, -10, 0);
const mat = CanvasKit.ColorFilter.MakeMatrix(cm);
const final = CanvasKit.ColorFilter.MakeCompose(mat, lerp);
paint.setColorFilter(final)
canvas.drawRect(CanvasKit.LTRBRect(10, 70, 140, 120), paint);
paint.delete();
blue.delete();
red.delete();
lerp.delete();
final.delete();
});
gm('blendmodes_canvas', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const blendModeNames = Object.keys(CanvasKit.BlendMode).filter((key) => key !== 'values');
const PASTEL_MUSTARD_YELLOW = CanvasKit.Color(248, 213, 85, 1.0);
const PASTEL_SKY_BLUE = CanvasKit.Color(74, 174, 245, 1.0);
const shapePaint = new CanvasKit.Paint();
shapePaint.setColor(PASTEL_MUSTARD_YELLOW);
shapePaint.setAntiAlias(true);
const textPaint = new CanvasKit.Paint();
textPaint.setAntiAlias(true);
const textFont = new CanvasKit.Font(null, 10);
let x = 10;
let y = 20;
for (const blendModeName of blendModeNames) {
// Draw a checkerboard for each blend mode.
// Each checkerboard is labelled with a blendmode's name.
canvas.drawText(blendModeName, x, y - 5, textPaint, textFont);
drawCheckerboard(canvas, x, y, x + 80, y + 80);
// A blue square is drawn on to each checkerboard with yellow circle.
// In each checkerboard the blue square is drawn using a different blendmode.
const blendMode = CanvasKit.BlendMode[blendModeName];
canvas.drawOval(CanvasKit.LTRBRect(x + 5, y + 5, x + 55, y + 55), shapePaint);
drawRectangle(x + 30, y + 30, x + 70, y + 70, PASTEL_SKY_BLUE, blendMode);
x += 90;
if (x > 500) {
x = 10;
y += 110;
}
}
function drawCheckerboard(canvas, x1, y1, x2, y2) {
const CHECKERBOARD_SQUARE_SIZE = 5;
const GREY = CanvasKit.Color(220, 220, 220, 0.5);
// Draw black border and white background for checkerboard
drawRectangle(x1-1, y1-1, x2+1, y2+1, CanvasKit.BLACK);
drawRectangle(x1, y1, x2, y2, CanvasKit.WHITE);
// Draw checkerboard squares
const numberOfColumns = (x2 - x1) / CHECKERBOARD_SQUARE_SIZE;
const numberOfRows = (y2 - y1) / CHECKERBOARD_SQUARE_SIZE
for (let row = 0; row < numberOfRows; row++) {
for (let column = 0; column < numberOfColumns; column++) {
const rowIsEven = row % 2 === 0;
const columnIsEven = column % 2 === 0;
if ((rowIsEven && !columnIsEven) || (!rowIsEven && columnIsEven)) {
drawRectangle(
x1 + CHECKERBOARD_SQUARE_SIZE * row,
y1 + CHECKERBOARD_SQUARE_SIZE * column,
Math.min(x1 + CHECKERBOARD_SQUARE_SIZE * row + CHECKERBOARD_SQUARE_SIZE, x2),
Math.min(y1 + CHECKERBOARD_SQUARE_SIZE * column + CHECKERBOARD_SQUARE_SIZE, y2),
GREY
);
}
}
}
}
function drawRectangle(x1, y1, x2, y2, color, blendMode=CanvasKit.BlendMode.srcOver) {
canvas.save();
canvas.clipRect(CanvasKit.LTRBRect(x1, y1, x2, y2), CanvasKit.ClipOp.Intersect, true);
canvas.drawColor(color, blendMode);
canvas.restore();
}
});
gm('colorfilters_malloc_canvas', (canvas) => {
const paint = new CanvasKit.Paint();
const src = [
0.8, 0.45, 2, 0, 20,
0.53, -0.918, 0.566, 0, 0,
0.53, -0.918, -0.566, 0, -10,
0, 0, 0, 0.8, 0,
]
const colorObj = new CanvasKit.Malloc(Float32Array, 20);
const cm = colorObj.toTypedArray();
for (i in src) {
cm[i] = src[i];
}
// MakeMatrix will free the malloc'd array when it is done with it.
const final = CanvasKit.ColorFilter.MakeMatrix(cm);
paint.setColorFilter(final)
canvas.drawRect(CanvasKit.LTRBRect(10, 70, 140, 120), paint);
CanvasKit.Free(colorObj);
paint.delete();
final.delete();
});
gm('clips_canvas', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.BLUE);
const rrect = CanvasKit.RRectXY(CanvasKit.LTRBRect(300, 300, 500, 500), 40, 40);
canvas.save();
// draw magenta around the outside edge of an rrect.
canvas.clipRRect(rrect, CanvasKit.ClipOp.Difference, true);
canvas.drawColorComponents(250/255, 30/255, 240/255, 0.9, CanvasKit.BlendMode.SrcOver);
canvas.restore();
// draw grey inside of a star pattern, then the blue star on top
canvas.clipPath(path, CanvasKit.ClipOp.Intersect, false);
canvas.drawColorInt(CanvasKit.ColorAsInt(200, 200, 200, 255), CanvasKit.BlendMode.SrcOver);
canvas.drawPath(path, paint);
path.delete();
paint.delete();
});
// inspired by https://fiddle.skia.org/c/feb2a08bb09ede5309678d6a0ab3f981
gm('savelayer_rect_paint_canvas', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const redPaint = new CanvasKit.Paint();
redPaint.setColor(CanvasKit.RED);
const solidBluePaint = new CanvasKit.Paint();
solidBluePaint.setColor(CanvasKit.BLUE);
const thirtyBluePaint = new CanvasKit.Paint();
thirtyBluePaint.setColor(CanvasKit.BLUE);
thirtyBluePaint.setAlphaf(0.3);
const alpha = new CanvasKit.Paint();
alpha.setAlphaf(0.3);
// Draw 4 solid red rectangles on the 0th layer.
canvas.drawRect(CanvasKit.LTRBRect(10, 10, 60, 60), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(150, 10, 200, 60), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(10, 70, 60, 120), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(150, 70, 200, 120), redPaint);
// Draw 2 blue rectangles that overlap. One is solid, the other
// is 30% transparent. We should see purple from the right one,
// the left one overlaps the red because it is opaque.
canvas.drawRect(CanvasKit.LTRBRect(30, 10, 80, 60), solidBluePaint);
canvas.drawRect(CanvasKit.LTRBRect(170, 10, 220, 60), thirtyBluePaint);
// Save a new layer. When the 1st layer gets merged onto the
// 0th layer (i.e. when restore() is called), it will use the provided
// paint to do so. The provided paint is set to have 30% opacity, but
// it could also have things set like blend modes or image filters.
// The rectangle is just a hint, so I've set it to be the area that
// we actually draw in before restore is called. It could also be omitted,
// see the test below.
canvas.saveLayer(alpha, CanvasKit.LTRBRect(10, 10, 220, 180));
// Draw the same blue overlapping rectangles as before. Notice in the
// final output, we have two different shades of purple instead of the
// solid blue overwriting the red. This proves the opacity was applied.
canvas.drawRect(CanvasKit.LTRBRect(30, 70, 80, 120), solidBluePaint);
canvas.drawRect(CanvasKit.LTRBRect(170, 70, 220, 120), thirtyBluePaint);
// We draw two more sets of overlapping red and blue rectangles. Notice
// the solid blue overwrites the red. This proves that the opacity from
// the alpha paint isn't available when the drawing happens - it only
// matters when restore() is called.
canvas.drawRect(CanvasKit.LTRBRect(10, 130, 60, 180), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(30, 130, 80, 180), solidBluePaint);
canvas.drawRect(CanvasKit.LTRBRect(150, 130, 200, 180), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(170, 130, 220, 180), thirtyBluePaint);
canvas.restore();
redPaint.delete();
solidBluePaint.delete();
thirtyBluePaint.delete();
alpha.delete();
});
// identical to the test above, except the save layer only has the paint, not
// the rectangle.
gm('savelayer_paint_canvas', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const redPaint = new CanvasKit.Paint();
redPaint.setColor(CanvasKit.RED);
const solidBluePaint = new CanvasKit.Paint();
solidBluePaint.setColor(CanvasKit.BLUE);
const thirtyBluePaint = new CanvasKit.Paint();
thirtyBluePaint.setColor(CanvasKit.BLUE);
thirtyBluePaint.setAlphaf(0.3);
const alpha = new CanvasKit.Paint();
alpha.setAlphaf(0.3);
// Draw 4 solid red rectangles on the 0th layer.
canvas.drawRect(CanvasKit.LTRBRect(10, 10, 60, 60), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(150, 10, 200, 60), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(10, 70, 60, 120), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(150, 70, 200, 120), redPaint);
// Draw 2 blue rectangles that overlap. One is solid, the other
// is 30% transparent. We should see purple from the right one,
// the left one overlaps the red because it is opaque.
canvas.drawRect(CanvasKit.LTRBRect(30, 10, 80, 60), solidBluePaint);
canvas.drawRect(CanvasKit.LTRBRect(170, 10, 220, 60), thirtyBluePaint);
// Save a new layer. When the 1st layer gets merged onto the
// 0th layer (i.e. when restore() is called), it will use the provided
// paint to do so. The provided paint is set to have 30% opacity, but
// it could also have things set like blend modes or image filters.
canvas.saveLayerPaint(alpha);
// Draw the same blue overlapping rectangles as before. Notice in the
// final output, we have two different shades of purple instead of the
// solid blue overwriting the red. This proves the opacity was applied.
canvas.drawRect(CanvasKit.LTRBRect(30, 70, 80, 120), solidBluePaint);
canvas.drawRect(CanvasKit.LTRBRect(170, 70, 220, 120), thirtyBluePaint);
// We draw two more sets of overlapping red and blue rectangles. Notice
// the solid blue overwrites the red. This proves that the opacity from
// the alpha paint isn't available when the drawing happens - it only
// matters when restore() is called.
canvas.drawRect(CanvasKit.LTRBRect(10, 130, 60, 180), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(30, 130, 80, 180), solidBluePaint);
canvas.drawRect(CanvasKit.LTRBRect(150, 130, 200, 180), redPaint);
canvas.drawRect(CanvasKit.LTRBRect(170, 130, 220, 180), thirtyBluePaint);
canvas.restore();
redPaint.delete();
solidBluePaint.delete();
thirtyBluePaint.delete();
alpha.delete();
});
gm('savelayerrec_canvas', (canvas) => {
// Note: fiddle.skia.org quietly draws a white background before doing
// other things, which is noticed in cases like this where we use saveLayer
// with the rec struct.
canvas.clear(CanvasKit.WHITE);
canvas.scale(8, 8);
const redPaint = new CanvasKit.Paint();
redPaint.setColor(CanvasKit.RED);
redPaint.setAntiAlias(true);
canvas.drawCircle(21, 21, 8, redPaint);
const bluePaint = new CanvasKit.Paint();
bluePaint.setColor(CanvasKit.BLUE);
canvas.drawCircle(31, 21, 8, bluePaint);
const blurIF = CanvasKit.ImageFilter.MakeBlur(8, 0.2, CanvasKit.TileMode.Decal, null);
const count = canvas.saveLayer(null, null, blurIF, 0);
expect(count).toEqual(1);
canvas.scale(1/4, 1/4);
canvas.drawCircle(125, 85, 8, redPaint);
canvas.restore();
blurIF.delete();
redPaint.delete();
bluePaint.delete();
});
gm('drawpoints_canvas', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(10);
paint.setColor(CanvasKit.Color(153, 204, 162, 0.82));
const points = [32, 16, 48, 48, 16, 32];
const caps = [CanvasKit.StrokeCap.Round, CanvasKit.StrokeCap.Square,
CanvasKit.StrokeCap.Butt];
const joins = [CanvasKit.StrokeJoin.Round, CanvasKit.StrokeJoin.Miter,
CanvasKit.StrokeJoin.Bevel];
const modes = [CanvasKit.PointMode.Points, CanvasKit.PointMode.Lines,
CanvasKit.PointMode.Polygon];
for (let i = 0; i < caps.length; i++) {
paint.setStrokeCap(caps[i]);
paint.setStrokeJoin(joins[i]);
for (const m of modes) {
canvas.drawPoints(m, points, paint);
canvas.translate(64, 0);
}
// Try with the malloc approach. Note that the drawPoints
// will free the pointer when done.
const mPointsObj = CanvasKit.Malloc(Float32Array, 3*2);
const mPoints = mPointsObj.toTypedArray();
mPoints.set([32, 16, 48, 48, 16, 32]);
// The obj from Malloc can be passed in instead of the typed array.
canvas.drawPoints(CanvasKit.PointMode.Polygon, mPointsObj, paint);
canvas.translate(-192, 64);
CanvasKit.Free(mPointsObj);
}
paint.delete();
});
gm('drawPoints in different modes', (canvas) => {
canvas.clear(CanvasKit.WHITE);
// From https://bugs.chromium.org/p/skia/issues/detail?id=11012
const boxPaint = new CanvasKit.Paint();
boxPaint.setStyle(CanvasKit.PaintStyle.Stroke);
boxPaint.setStrokeWidth(1);
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5);
paint.setStrokeCap(CanvasKit.StrokeCap.Round);
paint.setColorInt(0xFF0000FF); // Blue
paint.setAntiAlias(true);
const points = Float32Array.of(40, 40, 80, 40, 120, 80, 160, 80);
canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
canvas.drawPoints(CanvasKit.PointMode.Points, points, paint);
canvas.translate(0, 50);
canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
canvas.drawPoints(CanvasKit.PointMode.Lines, points, paint);
canvas.translate(0, 50);
canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
canvas.drawPoints(CanvasKit.PointMode.Polygon, points, paint);
// The control version using drawPath
canvas.translate(0, 50);
canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
const path = new CanvasKit.Path();
path.moveTo(40, 40);
path.lineTo(80, 40);
path.lineTo(120, 80);
path.lineTo(160, 80);
paint.setColorInt(0xFFFF0000); // RED
canvas.drawPath(path, paint);
paint.delete();
path.delete();
boxPaint.delete();
});
gm('drawImageNine_canvas', (canvas, fetchedByteBuffers) => {
const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
expect(img).toBeTruthy();
canvas.clear(CanvasKit.WHITE);
const paint = new CanvasKit.Paint();
canvas.drawImageNine(img, CanvasKit.LTRBiRect(40, 40, 400, 300),
CanvasKit.LTRBRect(5, 5, 300, 650), CanvasKit.FilterMode.Nearest, paint);
paint.delete();
img.delete();
}, '/assets/mandrill_512.png');
// This should be a nice, clear image.
gm('makeImageShaderCubic_canvas', (canvas, fetchedByteBuffers) => {
const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
expect(img).toBeTruthy();
canvas.clear(CanvasKit.WHITE);
const paint = new CanvasKit.Paint();
const shader = img.makeShaderCubic(CanvasKit.TileMode.Decal, CanvasKit.TileMode.Clamp,
1/3 /*B*/, 1/3 /*C*/,
CanvasKit.Matrix.rotated(0.1));
paint.setShader(shader);
canvas.drawPaint(paint);
paint.delete();
shader.delete();
img.delete();
}, '/assets/mandrill_512.png');
// This will look more blocky than the version above.
gm('makeImageShaderOptions_canvas', (canvas, fetchedByteBuffers) => {
const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
expect(img).toBeTruthy();
const imgWithMipMap = img.makeCopyWithDefaultMipmaps();
canvas.clear(CanvasKit.WHITE);
const paint = new CanvasKit.Paint();
const shader = imgWithMipMap.makeShaderOptions(CanvasKit.TileMode.Decal,
CanvasKit.TileMode.Clamp,
CanvasKit.FilterMode.Nearest,
CanvasKit.MipmapMode.Linear,
CanvasKit.Matrix.rotated(0.1));
paint.setShader(shader);
canvas.drawPaint(paint);
paint.delete();
shader.delete();
img.delete();
imgWithMipMap.delete();
}, '/assets/mandrill_512.png');
gm('drawvertices_canvas', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
const points = [0, 0, 250, 0, 100, 100, 0, 250];
// 2d float color array
const colors = [CanvasKit.RED, CanvasKit.BLUE,
CanvasKit.YELLOW, CanvasKit.CYAN];
const vertices = CanvasKit.MakeVertices(CanvasKit.VertexMode.TriangleFan,
points, null /*textureCoordinates*/, colors, false /*isVolatile*/);
const bounds = vertices.bounds();
expect(bounds).toEqual(CanvasKit.LTRBRect(0, 0, 250, 250));
canvas.drawVertices(vertices, CanvasKit.BlendMode.Dst, paint);
vertices.delete();
paint.delete();
});
gm('drawvertices_canvas_flat_floats', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
const points = [0, 0, 250, 0, 100, 100, 0, 250];
// 1d float color array
const colors = Float32Array.of(...CanvasKit.RED, ...CanvasKit.BLUE,
...CanvasKit.YELLOW, ...CanvasKit.CYAN);
const vertices = CanvasKit.MakeVertices(CanvasKit.VertexMode.TriangleFan,
points, null /*textureCoordinates*/, colors, false /*isVolatile*/);
const bounds = vertices.bounds();
expect(bounds).toEqual(CanvasKit.LTRBRect(0, 0, 250, 250));
canvas.drawVertices(vertices, CanvasKit.BlendMode.Dst, paint);
vertices.delete();
paint.delete();
});
gm('drawvertices_texture_canvas', (canvas, fetchedByteBuffers) => {
const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
const points = [
70, 170, 40, 90, 130, 150, 100, 50,
225, 150, 225, 60, 310, 180, 330, 100,
];
const textureCoordinates = [
0, 240, 0, 0, 80, 240, 80, 0,
160, 240, 160, 0, 240, 240, 240, 0,
];
const vertices = CanvasKit.MakeVertices(CanvasKit.VertexMode.TrianglesStrip,
points, textureCoordinates, null /* colors */, false /*isVolatile*/);
const shader = img.makeShaderCubic(CanvasKit.TileMode.Repeat, CanvasKit.TileMode.Mirror,
1/3 /*B*/, 1/3 /*C*/,);
paint.setShader(shader);
canvas.drawVertices(vertices, CanvasKit.BlendMode.Src, paint);
shader.delete();
vertices.delete();
paint.delete();
img.delete();
}, '/assets/brickwork-texture.jpg');
it('can change the 3x3 matrix on the canvas and read it back', () => {
const canvas = new CanvasKit.Canvas();
let matr = canvas.getTotalMatrix();
expect(matr).toEqual(CanvasKit.Matrix.identity());
// This fills the internal _scratch4x4MatrixPtr with garbage (aka sentinel) values to
// make sure the 3x3 matrix properly sets these to 0 when it uses the same buffer.
canvas.save();
const garbageMatrix = new Float32Array(16);
garbageMatrix.fill(-3);
canvas.concat(garbageMatrix);
canvas.restore();
canvas.concat(CanvasKit.Matrix.rotated(Math.PI/4));
const d = new DOMMatrix().translate(20, 10);
canvas.concat(d);
matr = canvas.getTotalMatrix();
const expected = CanvasKit.Matrix.multiply(
CanvasKit.Matrix.rotated(Math.PI/4),
CanvasKit.Matrix.translated(20, 10)
);
expect3x3MatricesToMatch(expected, matr);
// The 3x3 should be expanded into a 4x4, with 0s in the 3rd row and column.
matr = canvas.getLocalToDevice();
expect4x4MatricesToMatch([
0.707106, -0.707106, 0, 7.071067,
0.707106, 0.707106, 0, 21.213203,
0 , 0 , 0, 0 ,
0 , 0 , 0, 1 ], matr);
});
it('can accept a 3x2 matrix', () => {
const canvas = new CanvasKit.Canvas();
let matr = canvas.getTotalMatrix();
expect(matr).toEqual(CanvasKit.Matrix.identity());
// This fills the internal _scratch4x4MatrixPtr with garbage (aka sentinel) values to
// make sure the 3x2 matrix properly sets these to 0 when it uses the same buffer.
canvas.save();
const garbageMatrix = new Float32Array(16);
garbageMatrix.fill(-3);
canvas.concat(garbageMatrix);
canvas.restore();
canvas.concat([1.4, -0.2, 12,
0.2, 1.4, 24]);
matr = canvas.getTotalMatrix();
const expected = [1.4, -0.2, 12,
0.2, 1.4, 24,
0, 0, 1];
expect3x3MatricesToMatch(expected, matr);
// The 3x2 should be expanded into a 4x4, with 0s in the 3rd row and column
// and the perspective filled in.
matr = canvas.getLocalToDevice();
expect4x4MatricesToMatch([
1.4, -0.2, 0, 12,
0.2, 1.4, 0, 24,
0 , 0 , 0, 0,
0 , 0 , 0, 1], matr);
});
it('can change the 4x4 matrix on the canvas and read it back', () => {
const canvas = new CanvasKit.Canvas();
let matr = canvas.getLocalToDevice();
expect(matr).toEqual(CanvasKit.M44.identity());
canvas.concat(CanvasKit.M44.rotated([0, 1, 0], Math.PI/4));
canvas.concat(CanvasKit.M44.rotated([1, 0, 1], Math.PI/8));
const expected = CanvasKit.M44.multiply(
CanvasKit.M44.rotated([0, 1, 0], Math.PI/4),
CanvasKit.M44.rotated([1, 0, 1], Math.PI/8),
);
expect4x4MatricesToMatch(expected, canvas.getLocalToDevice());
// TODO(kjlubick) add test for DOMMatrix
// TODO(nifong) add more involved test for camera-related math.
});
gm('concat_with4x4_canvas', (canvas) => {
const path = starPath(CanvasKit, CANVAS_WIDTH/2, CANVAS_HEIGHT/2);
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
canvas.clear(CanvasKit.WHITE);
// Rotate it a bit on all 3 major axis, centered on the screen.
// To play with rotations, see https://jsfiddle.skia.org/canvaskit/0525300405796aa87c3b84cc0d5748516fca0045d7d6d9c7840710ab771edcd4
const turn = CanvasKit.M44.multiply(
CanvasKit.M44.translated([CANVAS_WIDTH/2, 0, 0]),
CanvasKit.M44.rotated([1, 0, 0], Math.PI/3),
CanvasKit.M44.rotated([0, 1, 0], Math.PI/4),
CanvasKit.M44.rotated([0, 0, 1], Math.PI/16),
CanvasKit.M44.translated([-CANVAS_WIDTH/2, 0, 0]),
);
canvas.concat(turn);
// Draw some stripes to help the eye detect the turn
const stripeWidth = 10;
paint.setColor(CanvasKit.BLACK);
for (let i = 0; i < CANVAS_WIDTH; i += 2*stripeWidth) {
canvas.drawRect(CanvasKit.LTRBRect(i, 0, i + stripeWidth, CANVAS_HEIGHT), paint);
}
paint.setColor(CanvasKit.YELLOW);
canvas.drawPath(path, paint);
paint.delete();
path.delete();
});
gm('particles_canvas', (canvas) => {
const curveParticles = {
'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 particles = CanvasKit.MakeParticles(JSON.stringify(curveParticles));
particles.start(0, true);
particles.setPosition([0, 0]);
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
paint.setColor(CanvasKit.WHITE);
const font = new CanvasKit.Font(null, 12);
canvas.clear(CanvasKit.BLACK);
// Draw a 5x5 set of different times in the particle system
// like a filmstrip of motion of particles.
const LEFT_MARGIN = 90;
const TOP_MARGIN = 100;
for (let row = 0; row < 5; row++) {
for (let column = 0; column < 5; column++) {
canvas.save();
canvas.translate(LEFT_MARGIN + column*100, TOP_MARGIN + row*100);
// Time moves in row-major order in increments of 0.02.
const particleTime = row/10 + column/50;
canvas.drawText('time ' + particleTime.toFixed(2), -30, 20, paint, font);
particles.update(particleTime);
particles.draw(canvas);
canvas.restore();
}
}
});
});
const expect3x3MatricesToMatch = (expected, actual) => {
expect(expected.length).toEqual(9);
expect(actual.length).toEqual(9);
for (let i = 0; i < expected.length; i++) {
expect(expected[i]).toBeCloseTo(actual[i], 5);
}
};
const expect4x4MatricesToMatch = (expected, actual) => {
expect(expected.length).toEqual(16);
expect(actual.length).toEqual(16);
for (let i = 0; i < expected.length; i++) {
expect(expected[i]).toBeCloseTo(actual[i], 5);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,416 @@
describe('Font Behavior', () => {
let container;
let notoSerifFontBuffer = null;
// This font is known to support kerning
const notoSerifFontLoaded = fetch('/assets/NotoSerif-Regular.ttf').then(
(response) => response.arrayBuffer()).then(
(buffer) => {
notoSerifFontBuffer = buffer;
});
let bungeeFontBuffer = null;
// This font has tofu for incorrect null terminators
// see https://bugs.chromium.org/p/skia/issues/detail?id=9314
const bungeeFontLoaded = fetch('/assets/Bungee-Regular.ttf').then(
(response) => response.arrayBuffer()).then(
(buffer) => {
bungeeFontBuffer = buffer;
});
beforeEach(async () => {
await EverythingLoaded;
await notoSerifFontLoaded;
await bungeeFontLoaded;
container = document.createElement('div');
container.innerHTML = `
<canvas width=600 height=600 id=test></canvas>
<canvas width=600 height=600 id=report></canvas>`;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
gm('monospace_text_on_path', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const font = new CanvasKit.Font(null, 24);
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const arc = new CanvasKit.Path();
arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
arc.lineTo(210, 140);
arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
// Only 1 dot should show up in the image, because we run out of path.
const str = 'This téxt should follow the curve across contours...';
const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font);
canvas.drawPath(arc, paint);
canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
textBlob.delete();
arc.delete();
paint.delete();
font.delete();
fontPaint.delete();
});
gm('serif_text_on_path', (canvas) => {
const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const font = new CanvasKit.Font(notoSerif, 24);
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const arc = new CanvasKit.Path();
arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
arc.lineTo(210, 140);
arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
const str = 'This téxt should follow the curve across contours...';
const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font, 60.5);
canvas.drawPath(arc, paint);
canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
textBlob.delete();
arc.delete();
paint.delete();
notoSerif.delete();
font.delete();
fontPaint.delete();
});
// https://bugs.chromium.org/p/skia/issues/detail?id=9314
gm('nullterminators_skbug_9314', (canvas) => {
const bungee = CanvasKit.Typeface.MakeFreeTypeFaceFromData(bungeeFontBuffer);
// yellow, to make sure tofu is plainly visible
canvas.clear(CanvasKit.Color(255, 255, 0, 1));
const font = new CanvasKit.Font(bungee, 24);
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const str = 'This is téxt';
const textBlob = CanvasKit.TextBlob.MakeFromText(str + ' text blob', font);
canvas.drawTextBlob(textBlob, 10, 50, fontPaint);
canvas.drawText(str + ' normal', 10, 100, fontPaint, font);
canvas.drawText('null terminator ->\u0000<- on purpose', 10, 150, fontPaint, font);
textBlob.delete();
bungee.delete();
font.delete();
fontPaint.delete();
});
gm('textblobs_with_glyphs', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const font = new CanvasKit.Font(notoSerif, 24);
const bluePaint = new CanvasKit.Paint();
bluePaint.setColor(CanvasKit.parseColorString('#04083f')); // arbitrary deep blue
bluePaint.setAntiAlias(true);
bluePaint.setStyle(CanvasKit.PaintStyle.Fill);
const redPaint = new CanvasKit.Paint();
redPaint.setColor(CanvasKit.parseColorString('#770b1e')); // arbitrary deep red
const ids = notoSerif.getGlyphIDs('AEGIS ægis');
expect(ids.length).toEqual(10); // one glyph id per glyph
expect(ids[0]).toEqual(36); // spot check this, should be consistent as long as the font is.
const bounds = font.getGlyphBounds(ids, bluePaint);
expect(bounds.length).toEqual(40); // 4 measurements per glyph
expect(bounds[0]).toEqual(0); // again, spot check the measurements for the first glyph.
expect(bounds[1]).toEqual(-17);
expect(bounds[2]).toEqual(17);
expect(bounds[3]).toEqual(0);
const widths = font.getGlyphWidths(ids, bluePaint);
expect(widths.length).toEqual(10); // 1 width per glyph
expect(widths[0]).toEqual(17);
const topBlob = CanvasKit.TextBlob.MakeFromGlyphs(ids, font);
canvas.drawTextBlob(topBlob, 5, 30, bluePaint);
canvas.drawTextBlob(topBlob, 5, 60, redPaint);
topBlob.delete();
const mIDs = CanvasKit.MallocGlyphIDs(ids.length);
const mArr = mIDs.toTypedArray();
mArr.set(ids);
const mXforms = CanvasKit.Malloc(Float32Array, ids.length * 4);
const mXformsArr = mXforms.toTypedArray();
// Draw each glyph rotated slightly and slightly lower than the glyph before it.
let currX = 0;
for (let i = 0; i < ids.length; i++) {
mXformsArr[i * 4] = Math.cos(-Math.PI / 16); // scos
mXformsArr[i * 4 + 1] = Math.sin(-Math.PI / 16); // ssin
mXformsArr[i * 4 + 2] = currX; // tx
mXformsArr[i * 4 + 3] = i*2; // ty
currX += widths[i];
}
const bottomBlob = CanvasKit.TextBlob.MakeFromRSXformGlyphs(mIDs, mXforms, font);
canvas.drawTextBlob(bottomBlob, 5, 110, bluePaint);
canvas.drawTextBlob(bottomBlob, 5, 140, redPaint);
bottomBlob.delete();
CanvasKit.Free(mIDs);
CanvasKit.Free(mXforms);
bluePaint.delete();
redPaint.delete();
notoSerif.delete();
font.delete();
});
it('can make a font mgr with passed in fonts', () => {
// CanvasKit.FontMgr.FromData([bungeeFontBuffer, notoSerifFontBuffer]) also works
const fontMgr = CanvasKit.FontMgr.FromData(bungeeFontBuffer, notoSerifFontBuffer);
expect(fontMgr).toBeTruthy();
expect(fontMgr.countFamilies()).toBe(2);
// in debug mode, let's list them.
if (fontMgr.dumpFamilies) {
fontMgr.dumpFamilies();
}
fontMgr.delete();
});
it('can make a font provider with passed in fonts and aliases', () => {
const fontProvider = CanvasKit.TypefaceFontProvider.Make();
fontProvider.registerFont(bungeeFontBuffer, "My Bungee Alias");
fontProvider.registerFont(notoSerifFontBuffer, "My Noto Serif Alias");
expect(fontProvider).toBeTruthy();
expect(fontProvider.countFamilies()).toBe(2);
// in debug mode, let's list them.
if (fontProvider.dumpFamilies) {
fontProvider.dumpFamilies();
}
fontProvider.delete();
});
gm('various_font_formats', (canvas, fetchedByteBuffers) => {
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const inputs = [{
type: '.ttf font',
buffer: bungeeFontBuffer,
y: 60,
},{
type: '.otf font',
buffer: fetchedByteBuffers[0],
y: 90,
},{
type: '.woff font',
buffer: fetchedByteBuffers[1],
y: 120,
},{
type: '.woff2 font',
buffer: fetchedByteBuffers[2],
y: 150,
}];
const defaultFont = new CanvasKit.Font(null, 24);
canvas.drawText(`The following should be ${inputs.length + 1} lines of text:`, 5, 30, fontPaint, defaultFont);
for (const fontType of inputs) {
// smoke test that the font bytes loaded.
expect(fontType.buffer).toBeTruthy(fontType.type + ' did not load');
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fontType.buffer);
const font = new CanvasKit.Font(typeface, 24);
if (font && typeface) {
canvas.drawText(fontType.type + ' loaded', 5, fontType.y, fontPaint, font);
} else {
canvas.drawText(fontType.type + ' *not* loaded', 5, fontType.y, fontPaint, defaultFont);
}
font && font.delete();
typeface && typeface.delete();
}
// The only ttc font I could find was 14 MB big, so I'm using the smaller test font,
// which doesn't have very many glyphs in it, so we just check that we got a non-zero
// typeface for it. I was able to load NotoSansCJK-Regular.ttc just fine in a
// manual test.
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fetchedByteBuffers[3]);
expect(typeface).toBeTruthy('.ttc font');
if (typeface) {
canvas.drawText('.ttc loaded', 5, 180, fontPaint, defaultFont);
typeface.delete();
} else {
canvas.drawText('.ttc *not* loaded', 5, 180, fontPaint, defaultFont);
}
defaultFont.delete();
fontPaint.delete();
}, '/assets/Roboto-Regular.otf', '/assets/Roboto-Regular.woff', '/assets/Roboto-Regular.woff2', '/assets/test.ttc');
it('can measure text very precisely with proper settings', () => {
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const fontSizes = [257, 100, 11];
// The point of these values is to let us know 1) we can measure to sub-pixel levels
// and 2) that measurements don't drastically change. If these change a little bit,
// just update them with the new values. For super-accurate readings, one could
// run a C++ snippet of code and compare the values, but that is likely unnecessary
// unless we suspect a bug with the bindings.
const expectedSizes = [241.06299, 93.79883, 10.31787];
for (const idx in fontSizes) {
const font = new CanvasKit.Font(typeface, fontSizes[idx]);
font.setHinting(CanvasKit.FontHinting.None);
font.setLinearMetrics(true);
font.setSubpixel(true);
const ids = font.getGlyphIDs('M');
const widths = font.getGlyphWidths(ids);
expect(widths[0]).toBeCloseTo(expectedSizes[idx], 5);
font.delete();
}
typeface.delete();
});
gm('font_edging', (canvas) => {
// Draw a small font scaled up to see the aliasing artifacts.
canvas.scale(8, 8);
canvas.clear(CanvasKit.WHITE);
const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const textPaint = new CanvasKit.Paint();
const annotationFont = new CanvasKit.Font(notoSerif, 6);
canvas.drawText('Default', 5, 5, textPaint, annotationFont);
canvas.drawText('Alias', 5, 25, textPaint, annotationFont);
canvas.drawText('AntiAlias', 5, 45, textPaint, annotationFont);
canvas.drawText('Subpixel', 5, 65, textPaint, annotationFont);
const testFont = new CanvasKit.Font(notoSerif, 20);
canvas.drawText('SEA', 35, 15, textPaint, testFont);
testFont.setEdging(CanvasKit.FontEdging.Alias);
canvas.drawText('SEA', 35, 35, textPaint, testFont);
testFont.setEdging(CanvasKit.FontEdging.AntiAlias);
canvas.drawText('SEA', 35, 55, textPaint, testFont);
testFont.setEdging(CanvasKit.FontEdging.SubpixelAntiAlias);
canvas.drawText('SEA', 35, 75, textPaint, testFont);
textPaint.delete();
annotationFont.delete();
testFont.delete();
notoSerif.delete();
});
it('can get the intercepts of glyphs', () => {
const font = new CanvasKit.Font(null, 100);
const ids = font.getGlyphIDs('I');
expect(ids.length).toEqual(1);
// aim for the middle of the I at 100 point, expecting a hit
let sects = font.getGlyphIntercepts(ids, [0, 0], -60, -40);
expect(sects.length).toEqual(2, "expected one pair of intercepts");
expect(sects[0]).toBeCloseTo(25.39063, 5);
expect(sects[1]).toBeCloseTo(34.52148, 5);
// aim below the baseline where we expect no intercepts
sects = font.getGlyphIntercepts(ids, [0, 0], 20, 30);
expect(sects.length).toEqual(0, "expected no intercepts");
font.delete();
});
it('can use mallocd and normal arrays', () => {
const font = new CanvasKit.Font(null, 100);
const ids = font.getGlyphIDs('I');
expect(ids.length).toEqual(1);
const glyphID = ids[0];
// aim for the middle of the I at 100 point, expecting a hit
const sects = font.getGlyphIntercepts(Array.of(glyphID), Float32Array.of(0, 0), -60, -40);
expect(sects.length).toEqual(2);
expect(sects[0]).toBeLessThan(sects[1]);
// these values were recorded from the first time it was run
expect(sects[0]).toBeCloseTo(25.39063, 5);
expect(sects[1]).toBeCloseTo(34.52148, 5);
const free_list = []; // will free CanvasKit.Malloc objects at the end
// Want to exercise 4 different ways we can receive an array:
// 1. normal array
// 2. typed-array
// 3. CanvasKit.Malloc typeed-array
// 4. CavnasKit.Malloc (raw)
const id_makers = [
(id) => [ id ],
(id) => new Uint16Array([ id ]),
(id) => {
const a = CanvasKit.Malloc(Uint16Array, 1);
free_list.push(a);
const ta = a.toTypedArray();
ta[0] = id;
return ta; // return typed-array
},
(id) => {
const a = CanvasKit.Malloc(Uint16Array, 1);
free_list.push(a);
a.toTypedArray()[0] = id;
return a; // return raw obj
},
];
const pos_makers = [
(x, y) => [ x, y ],
(x, y) => new Float32Array([ x, y ]),
(x, y) => {
const a = CanvasKit.Malloc(Float32Array, 2);
free_list.push(a);
const ta = a.toTypedArray();
ta[0] = x;
ta[1] = y;
return ta; // return typed-array
},
(x, y) => {
const a = CanvasKit.Malloc(Float32Array, 2);
free_list.push(a);
const ta = a.toTypedArray();
ta[0] = x;
ta[1] = y;
return a; // return raw obj
},
];
for (const idm of id_makers) {
for (const posm of pos_makers) {
const s = font.getGlyphIntercepts(idm(glyphID), posm(0, 0), -60, -40);
expect(s.length).toEqual(sects.length);
for (let i = 0; i < s.length; ++i) {
expect(s[i]).toEqual(sects[i]);
}
}
}
free_list.forEach(obj => CanvasKit.Free(obj));
font.delete();
});
});

View File

@ -0,0 +1,210 @@
describe('CanvasKit\'s Matrix Helpers', () => {
beforeEach(async () => {
await EverythingLoaded;
});
const expectArrayCloseTo = (a, b, precision) => {
precision = precision || 14; // digits of precision in base 10
expect(a.length).toEqual(b.length);
for (let i=0; i<a.length; i++) {
expect(a[i]).toBeCloseTo(b[i], precision);
}
};
describe('3x3 matrices', () => {
it('can make a translated 3x3 matrix', () => {
expectArrayCloseTo(
CanvasKit.Matrix.translated(5, -1),
[1, 0, 5,
0, 1, -1,
0, 0, 1]);
});
it('can make a scaled 3x3 matrix', () => {
expectArrayCloseTo(
CanvasKit.Matrix.scaled(2, 3),
[2, 0, 0,
0, 3, 0,
0, 0, 1]);
});
it('can make a rotated 3x3 matrix', () => {
expectArrayCloseTo(
CanvasKit.Matrix.rotated(Math.PI, 9, 9),
[-1, 0, 18,
0, -1, 18,
0, 0, 1]);
});
it('can make a skewed 3x3 matrix', () => {
expectArrayCloseTo(
CanvasKit.Matrix.skewed(4, 3, 2, 1),
[1, 4, -8,
3, 1, -3,
0, 0, 1]);
});
it('can multiply 3x3 matrices', () => {
const a = [
0.1, 0.2, 0.3,
0.0, 0.6, 0.7,
0.9, -0.9, -0.8,
];
const b = [
2.0, 3.0, 4.0,
-3.0, -4.0, -5.0,
7.0, 8.0, 9.0,
];
const expected = [
1.7, 1.9, 2.1,
3.1, 3.2, 3.3,
-1.1, -0.1, 0.9,
];
expectArrayCloseTo(
CanvasKit.Matrix.multiply(a, b),
expected);
});
it('satisfies the inverse rule for 3x3 matrics', () => {
// a matrix times its inverse is the identity matrix.
const a = [
0.1, 0.2, 0.3,
0.0, 0.6, 0.7,
0.9, -0.9, -0.8,
];
const b = CanvasKit.Matrix.invert(a);
expectArrayCloseTo(
CanvasKit.Matrix.multiply(a, b),
CanvasKit.Matrix.identity());
});
it('maps 2D points correctly with a 3x3 matrix', () => {
const a = [
3, 0, -4,
0, 2, 4,
0, 0, 1,
];
const points = [
0, 0,
1, 1,
];
const expected = [
-4, 4,
-1, 6,
];
expectArrayCloseTo(
CanvasKit.Matrix.mapPoints(a, points),
expected);
});
}); // describe 3x3
describe('4x4 matrices', () => {
it('can make a translated 4x4 matrix', () => {
expectArrayCloseTo(
CanvasKit.M44.translated([5, 6, 7]),
[1, 0, 0, 5,
0, 1, 0, 6,
0, 0, 1, 7,
0, 0, 0, 1]);
});
it('can make a scaled 4x4 matrix', () => {
expectArrayCloseTo(
CanvasKit.M44.scaled([5, 6, 7]),
[5, 0, 0, 0,
0, 6, 0, 0,
0, 0, 7, 0,
0, 0, 0, 1]);
});
it('can make a rotated 4x4 matrix', () => {
expectArrayCloseTo(
CanvasKit.M44.rotated([1,1,1], Math.PI),
[-1/3, 2/3, 2/3, 0,
2/3, -1/3, 2/3, 0,
2/3, 2/3, -1/3, 0,
0, 0, 0, 1]);
});
it('can make a 4x4 matrix looking from eye to center', () => {
eye = [1, 0, 0];
center = [1, 0, 1];
up = [0, 1, 0]
expectArrayCloseTo(
CanvasKit.M44.lookat(eye, center, up),
[-1, 0, 0, 1,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1]);
});
it('can make a 4x4 prespective matrix', () => {
expectArrayCloseTo(
CanvasKit.M44.perspective(2, 10, Math.PI/2),
[1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1.5, 5,
0, 0, -1, 1]);
});
it('can multiply 4x4 matrices', () => {
const a = [
0.1, 0.2, 0.3, 0.4,
0.0, 0.6, 0.7, 0.8,
0.9, -0.9, -0.8, -0.7,
-0.6, -0.5, -0.4, -0.3,
];
const b = [
2.0, 3.0, 4.0, 5.0,
-3.0, -4.0, -5.0, -6.0,
7.0, 8.0, 9.0, 10.0,
-4.0, -3.0, -2.0, -1.0,
];
const expected = [
0.1, 0.7, 1.3, 1.9,
-0.1, 0.8, 1.7, 2.6,
1.7, 2.0, 2.3, 2.6,
-1.3, -2.1, -2.9, -3.7,
];
expectArrayCloseTo(
CanvasKit.M44.multiply(a, b),
expected);
});
it('satisfies the identity rule for 4x4 matrices', () => {
const a = [
0.1, 0.2, 0.3, 0.4,
0.0, 0.6, 0.7, 0.8,
0.9, 0.9, -0.8, -0.7,
-0.6, -0.5, -0.4, -0.3,
];
const b = CanvasKit.M44.invert(a)
expectArrayCloseTo(
CanvasKit.M44.multiply(a, b),
CanvasKit.M44.identity());
});
it('can create a camera setup matrix', () => {
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,
};
const mat = CanvasKit.M44.setupCamera(CanvasKit.LTRBRect(0, 0, 200, 200), 200, cam);
// these values came from an invocation of setupCamera visually inspected.
const expected = [
7.595754, 0, -0.5, 0,
0, 7.595754, -0.5, 0,
0, 0, 1.010050, -1324.368418,
0, 0, -0.005, 7.595754];
expectArrayCloseTo(mat, expected, 5);
});
}); // describe 4x4
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,580 @@
describe('Path Behavior', () => {
let container;
beforeEach(async () => {
await EverythingLoaded;
container = document.createElement('div');
container.innerHTML = `
<canvas width=600 height=600 id=test></canvas>
<canvas width=600 height=600 id=report></canvas>`;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
gm('path_api_example', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setStrokeWidth(1.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const path = new CanvasKit.Path();
path.moveTo(20, 5);
path.lineTo(30, 20);
path.lineTo(40, 10);
path.lineTo(50, 20);
path.lineTo(60, 0);
path.lineTo(20, 5);
path.moveTo(20, 80);
path.cubicTo(90, 10, 160, 150, 190, 10);
path.moveTo(36, 148);
path.quadTo(66, 188, 120, 136);
path.lineTo(36, 148);
path.moveTo(150, 180);
path.arcToTangent(150, 100, 50, 200, 20);
path.lineTo(160, 160);
path.moveTo(20, 120);
path.lineTo(20, 120);
path.transform([2, 0, 0,
0, 2, 0,
0, 0, 1 ]);
canvas.drawPath(path, paint);
const rrect = CanvasKit.RRectXY([100, 10, 140, 62], 10, 4);
const rrectPath = new CanvasKit.Path().addRRect(rrect, true);
canvas.drawPath(rrectPath, paint);
rrectPath.delete();
path.delete();
paint.delete();
// See PathKit for more tests, since they share implementation
});
it('can create a path from an SVG string', () => {
//.This is a parallelogram from
// https://upload.wikimedia.org/wikipedia/commons/e/e7/Simple_parallelogram.svg
const path = CanvasKit.Path.MakeFromSVGString(
'M 205,5 L 795,5 L 595,295 L 5,295 L 205,5 z');
const cmds = path.toCmds();
expect(cmds).toBeTruthy();
// 1 move, 4 lines, 1 close
// each element in cmds is an array, with index 0 being the verb, and the rest being args
expect(cmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 205, 5,
CanvasKit.LINE_VERB, 795, 5,
CanvasKit.LINE_VERB, 595, 295,
CanvasKit.LINE_VERB, 5, 295,
CanvasKit.LINE_VERB, 205, 5,
CanvasKit.CLOSE_VERB));
path.delete();
});
it('can create a path by combining two other paths', () => {
// Get the intersection of two overlapping squares and verify that it is the smaller square.
const pathOne = new CanvasKit.Path();
pathOne.addRect([10, 10, 20, 20]);
const pathTwo = new CanvasKit.Path();
pathTwo.addRect([15, 15, 30, 30]);
const path = CanvasKit.Path.MakeFromOp(pathOne, pathTwo, CanvasKit.PathOp.Intersect);
const cmds = path.toCmds();
expect(cmds).toBeTruthy();
expect(cmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 15, 15,
CanvasKit.LINE_VERB, 20, 15,
CanvasKit.LINE_VERB, 20, 20,
CanvasKit.LINE_VERB, 15, 20,
CanvasKit.CLOSE_VERB));
path.delete();
pathOne.delete();
pathTwo.delete();
});
it('can create an SVG string from a path', () => {
const cmds = [CanvasKit.MOVE_VERB, 205, 5,
CanvasKit.LINE_VERB, 795, 5,
CanvasKit.LINE_VERB, 595, 295,
CanvasKit.LINE_VERB, 5, 295,
CanvasKit.LINE_VERB, 205, 5,
CanvasKit.CLOSE_VERB];
const path = CanvasKit.Path.MakeFromCmds(cmds);
const svgStr = path.toSVGString();
// We output it in terse form, which is different than Wikipedia's version
expect(svgStr).toEqual('M205 5L795 5L595 295L5 295L205 5Z');
path.delete();
});
it('can create a path with malloced verbs, points, weights', () => {
const mVerbs = CanvasKit.Malloc(Uint8Array, 6);
const mPoints = CanvasKit.Malloc(Float32Array, 18);
const mWeights = CanvasKit.Malloc(Float32Array, 1);
mVerbs.toTypedArray().set([CanvasKit.MOVE_VERB, CanvasKit.LINE_VERB,
CanvasKit.QUAD_VERB, CanvasKit.CONIC_VERB, CanvasKit.CUBIC_VERB, CanvasKit.CLOSE_VERB
]);
mPoints.toTypedArray().set([
1,2, // moveTo
3,4, // lineTo
5,6,7,8, // quadTo
9,10,11,12, // conicTo
13,14,15,16,17,18, // cubicTo
]);
mWeights.toTypedArray().set([117]);
let path = CanvasKit.Path.MakeFromVerbsPointsWeights(mVerbs, mPoints, mWeights);
let cmds = path.toCmds();
expect(cmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 1, 2,
CanvasKit.LINE_VERB, 3, 4,
CanvasKit.QUAD_VERB, 5, 6, 7, 8,
CanvasKit.CONIC_VERB, 9, 10, 11, 12, 117,
CanvasKit.CUBIC_VERB, 13, 14, 15, 16, 17, 18,
CanvasKit.CLOSE_VERB,
));
path.delete();
// If given insufficient points, it stops early (but doesn't read out of bounds).
path = CanvasKit.Path.MakeFromVerbsPointsWeights(mVerbs, mPoints.subarray(0, 10), mWeights);
cmds = path.toCmds();
expect(cmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 1, 2,
CanvasKit.LINE_VERB, 3, 4,
CanvasKit.QUAD_VERB, 5, 6, 7, 8,
));
path.delete();
CanvasKit.Free(mVerbs);
CanvasKit.Free(mPoints);
CanvasKit.Free(mWeights);
});
it('can create and update a path with verbs and points (no weights)', () => {
const path = CanvasKit.Path.MakeFromVerbsPointsWeights(
[CanvasKit.MOVE_VERB, CanvasKit.LINE_VERB],
[1,2, 3,4]);
let cmds = path.toCmds();
expect(cmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 1, 2,
CanvasKit.LINE_VERB, 3, 4
));
path.addVerbsPointsWeights(
[CanvasKit.QUAD_VERB, CanvasKit.CLOSE_VERB],
[5,6,7,8],
);
cmds = path.toCmds();
expect(cmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 1, 2,
CanvasKit.LINE_VERB, 3, 4,
CanvasKit.QUAD_VERB, 5, 6, 7, 8,
CanvasKit.CLOSE_VERB
));
path.delete();
});
it('can add points to a path in bulk', () => {
const mVerbs = CanvasKit.Malloc(Uint8Array, 6);
const mPoints = CanvasKit.Malloc(Float32Array, 18);
const mWeights = CanvasKit.Malloc(Float32Array, 1);
mVerbs.toTypedArray().set([CanvasKit.MOVE_VERB, CanvasKit.LINE_VERB,
CanvasKit.QUAD_VERB, CanvasKit.CONIC_VERB, CanvasKit.CUBIC_VERB, CanvasKit.CLOSE_VERB
]);
mPoints.toTypedArray().set([
1,2, // moveTo
3,4, // lineTo
5,6,7,8, // quadTo
9,10,11,12, // conicTo
13,14,15,16,17,18, // cubicTo
]);
mWeights.toTypedArray().set([117]);
const path = new CanvasKit.Path();
path.lineTo(77, 88);
path.addVerbsPointsWeights(mVerbs, mPoints, mWeights);
let cmds = path.toCmds();
expect(cmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 0, 0,
CanvasKit.LINE_VERB, 77, 88,
CanvasKit.MOVE_VERB, 1, 2,
CanvasKit.LINE_VERB, 3, 4,
CanvasKit.QUAD_VERB, 5, 6, 7, 8,
CanvasKit.CONIC_VERB, 9, 10, 11, 12, 117,
CanvasKit.CUBIC_VERB, 13, 14, 15, 16, 17, 18,
CanvasKit.CLOSE_VERB,
));
path.rewind();
cmds = path.toCmds();
expect(cmds).toEqual(new Float32Array(0));
path.delete();
CanvasKit.Free(mVerbs);
CanvasKit.Free(mPoints);
CanvasKit.Free(mWeights);
});
it('can retrieve points from a path', () => {
const path = new CanvasKit.Path();
path.addRect([10, 15, 20, 25]);
let pt = path.getPoint(0);
expect(pt[0]).toEqual(10);
expect(pt[1]).toEqual(15);
path.getPoint(2, pt);
expect(pt[0]).toEqual(20);
expect(pt[1]).toEqual(25);
path.getPoint(1000, pt); // off the end returns (0, 0) as per the docs.
expect(pt[0]).toEqual(0);
expect(pt[1]).toEqual(0);
path.delete();
});
gm('offset_path', (canvas) => {
const path = starPath(CanvasKit);
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
canvas.drawPath(path, paint);
path.offset(80, 40);
canvas.drawPath(path, paint);
path.delete();
paint.delete();
});
gm('oval_path', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
const path = new CanvasKit.Path();
path.moveTo(5, 5);
path.lineTo(10, 120);
path.addOval(CanvasKit.LTRBRect(10, 20, 100, 200), false, 3);
path.lineTo(300, 300);
canvas.drawPath(path, paint);
path.delete();
paint.delete();
});
gm('bounds_path', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
const path = new CanvasKit.Path();
// Arbitrary points to make an interesting curve.
path.moveTo(97, 225);
path.cubicTo(20, 400, 404, 75, 243, 271);
canvas.drawPath(path, paint);
const bounds = new Float32Array(4);
path.getBounds(bounds);
paint.setColor(CanvasKit.BLUE);
paint.setStrokeWidth(3.0);
canvas.drawRect(bounds, paint);
path.computeTightBounds(bounds);
paint.setColor(CanvasKit.RED);
paint.setStrokeWidth(3.0);
canvas.drawRect(bounds, paint);
path.delete();
paint.delete();
});
gm('arcto_path', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
paint.setStrokeWidth(5.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.BLACK);
canvas.clear(CanvasKit.WHITE);
const path = new CanvasKit.Path();
// - x1, y1, x2, y2, radius
path.arcToTangent(40, 0, 40, 40, 40);
// - oval (as Rect), startAngle, sweepAngle, forceMoveTo
path.arcToOval(CanvasKit.LTRBRect(90, 10, 120, 200), 30, 300, true);
// - rx, ry, xAxisRotate, useSmallArc, isCCW, x, y
path.moveTo(5, 105);
path.arcToRotated(24, 24, 45, true, false, 82, 156);
canvas.drawPath(path, paint);
path.delete();
paint.delete();
});
gm('path_relative', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setStrokeWidth(1.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const path = new CanvasKit.Path();
path.rMoveTo(20, 5)
.rLineTo(10, 15) // 30, 20
.rLineTo(10, -5); // 40, 10
path.rLineTo(10, 10); // 50, 20
path.rLineTo(10, -20); // 60, 0
path.rLineTo(-40, 5); // 20, 5
path.moveTo(20, 80)
.rCubicTo(70, -70, 140, 70, 170, -70); // 90, 10, 160, 150, 190, 10
path.moveTo(36, 148)
.rQuadTo(30, 40, 84, -12) // 66, 188, 120, 136
.lineTo(36, 148);
path.moveTo(150, 180)
.rArcTo(24, 24, 45, true, false, -68, -24); // 82, 156
path.lineTo(160, 160);
canvas.drawPath(path, paint);
path.delete();
paint.delete();
});
it('can measure the contours of a path', () => {
const path = new CanvasKit.Path();
path.moveTo(10, 10)
.lineTo(40, 50); // should be length 50 because of the 3/4/5 triangle rule
path.moveTo(80, 0)
.lineTo(80, 10)
.lineTo(100, 5)
.lineTo(80, 0);
const meas = new CanvasKit.ContourMeasureIter(path, false, 1);
let cont = meas.next();
expect(cont).toBeTruthy();
expect(cont.length()).toBeCloseTo(50.0, 3);
const pt = cont.getPosTan(28.7); // arbitrary point
expect(pt[0]).toBeCloseTo(27.22, 3); // x
expect(pt[1]).toBeCloseTo(32.96, 3); // y
expect(pt[2]).toBeCloseTo(0.6, 3); // dy
expect(pt[3]).toBeCloseTo(0.8, 3); // dy
pt.set([-1, -1, -1, -1]); // fill with sentinel values.
cont.getPosTan(28.7, pt); // arbitrary point again, passing in an array to copy into.
expect(pt[0]).toBeCloseTo(27.22, 3); // x
expect(pt[1]).toBeCloseTo(32.96, 3); // y
expect(pt[2]).toBeCloseTo(0.6, 3); // dy
expect(pt[3]).toBeCloseTo(0.8, 3); // dy
const subpath = cont.getSegment(20, 40, true); // make sure this doesn't crash
cont.delete();
cont = meas.next();
expect(cont).toBeTruthy();
expect(cont.length()).toBeCloseTo(51.231, 3);
cont.delete();
expect(meas.next()).toBeFalsy();
meas.delete();
path.delete();
});
gm('drawpoly_path', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setStrokeWidth(1.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const points = [5, 5, 30, 20, 55, 5, 55, 50, 30, 30, 5, 50];
const pointsObj = CanvasKit.Malloc(Float32Array, 6 * 2);
const mPoints = pointsObj.toTypedArray();
mPoints.set([105, 105, 130, 120, 155, 105, 155, 150, 130, 130, 105, 150]);
const path = new CanvasKit.Path();
path.addPoly(points, true)
.moveTo(100, 0)
.addPoly(mPoints, true);
canvas.drawPath(path, paint);
CanvasKit.Free(pointsObj);
path.delete();
paint.delete();
});
// Test trim, adding paths to paths, and a bunch of other path methods.
gm('trim_path', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const paint = new CanvasKit.Paint();
paint.setStrokeWidth(1.0);
paint.setAntiAlias(true);
paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const arcpath = new CanvasKit.Path();
arcpath.arc(400, 400, 100, 0, -90, false) // x, y, radius, startAngle, endAngle, ccw
.dash(3, 1, 0)
.conicTo(10, 20, 30, 40, 5)
.rConicTo(60, 70, 80, 90, 5)
.trim(0.2, 1, false);
const path = new CanvasKit.Path();
path.addArc(CanvasKit.LTRBRect(10, 20, 100, 200), 30, 300)
.addRect(CanvasKit.LTRBRect(200, 200, 300, 300)) // test single arg, default cw
.addRect(CanvasKit.LTRBRect(240, 240, 260, 260), true) // test two arg, true means ccw
.addRect([260, 260, 290, 290], true) // test five arg, true means ccw
.addRRect([300, 10, 500, 290, // Rect in LTRB order
60, 60, 60, 60, 60, 60, 60, 60], // all radii are the same
false) // ccw
.addRRect(CanvasKit.RRectXY([350, 60, 450, 240], 20, 80), true) // Rect, rx, ry, ccw
.addPath(arcpath)
.transform(0.54, -0.84, 390.35,
0.84, 0.54, -114.53,
0, 0, 1);
canvas.drawPath(path, paint);
path.delete();
paint.delete();
});
gm('winding_example', (canvas) => {
// Inspired by https://fiddle.skia.org/c/@Path_FillType_a
const path = new CanvasKit.Path();
// Draw overlapping rects on top
path.addRect(CanvasKit.LTRBRect(10, 10, 30, 30), false);
path.addRect(CanvasKit.LTRBRect(20, 20, 40, 40), false);
// Draw overlapping rects on bottom, with different direction lines.
path.addRect(CanvasKit.LTRBRect(10, 60, 30, 80), false);
path.addRect(CanvasKit.LTRBRect(20, 70, 40, 90), true);
expect(path.getFillType()).toEqual(CanvasKit.FillType.Winding);
// Draw the two rectangles on the left side.
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
canvas.drawPath(path, paint);
const clipRect = CanvasKit.LTRBRect(0, 0, 51, 100);
paint.setStyle(CanvasKit.PaintStyle.Fill);
for (const fillType of [CanvasKit.FillType.Winding, CanvasKit.FillType.EvenOdd]) {
canvas.translate(51, 0);
canvas.save();
canvas.clipRect(clipRect, CanvasKit.ClipOp.Intersect, false);
path.setFillType(fillType);
canvas.drawPath(path, paint);
canvas.restore();
}
path.delete();
paint.delete();
});
gm('as_winding', (canvas) => {
const evenOddPath = new CanvasKit.Path();
// Draw overlapping rects
evenOddPath.addRect(CanvasKit.LTRBRect(10, 10, 70, 70), false);
evenOddPath.addRect(CanvasKit.LTRBRect(30, 30, 50, 50), false);
evenOddPath.setFillType(CanvasKit.FillType.EvenOdd);
const evenOddCmds = evenOddPath.toCmds();
expect(evenOddCmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 10, 10,
CanvasKit.LINE_VERB, 70, 10,
CanvasKit.LINE_VERB, 70, 70,
CanvasKit.LINE_VERB, 10, 70,
CanvasKit.CLOSE_VERB,
CanvasKit.MOVE_VERB, 30, 30, // This contour is drawn
CanvasKit.LINE_VERB, 50, 30, // clockwise, as specified.
CanvasKit.LINE_VERB, 50, 50,
CanvasKit.LINE_VERB, 30, 50,
CanvasKit.CLOSE_VERB
));
const windingPath = evenOddPath.makeAsWinding();
expect(windingPath.getFillType()).toBe(CanvasKit.FillType.Winding);
const windingCmds = windingPath.toCmds();
expect(windingCmds).toEqual(Float32Array.of(
CanvasKit.MOVE_VERB, 10, 10,
CanvasKit.LINE_VERB, 70, 10,
CanvasKit.LINE_VERB, 70, 70,
CanvasKit.LINE_VERB, 10, 70,
CanvasKit.CLOSE_VERB,
CanvasKit.MOVE_VERB, 30, 50, // This contour has been
CanvasKit.LINE_VERB, 50, 50, // re-drawn counter-clockwise
CanvasKit.LINE_VERB, 50, 30, // so that it covers the same
CanvasKit.LINE_VERB, 30, 30, // area, but with the winding fill type.
CanvasKit.CLOSE_VERB
));
const paint = new CanvasKit.Paint();
paint.setStyle(CanvasKit.PaintStyle.Fill);
const font = new CanvasKit.Font(null, 20);
canvas.drawText('Original path (even odd)', 5, 20, paint, font);
canvas.translate(0, 50);
canvas.drawPath(evenOddPath, paint);
canvas.translate(300, 0);
canvas.drawPath(windingPath, paint);
canvas.translate(0, -50);
canvas.drawText('makeAsWinding path', 5, 20, paint, font);
evenOddPath.delete();
windingPath.delete();
});
});

View File

@ -0,0 +1,247 @@
describe('Runtime shader effects', () => {
let container;
beforeEach(async () => {
await EverythingLoaded;
container = document.createElement('div');
container.innerHTML = `
<canvas width=600 height=600 id=test></canvas>
<canvas width=600 height=600 id=report></canvas>`;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
const spiralSkSL = `
uniform float rad_scale;
uniform int2 in_center;
uniform float4 in_colors0;
uniform float4 in_colors1;
half4 main(float2 p) {
float2 pp = p - float2(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));
}`;
// TODO(kjlubick) rewrite testRTShader and callers to use gm.
const testRTShader = (name, done, localMatrix) => {
const surface = CanvasKit.MakeCanvasSurface('test');
expect(surface).toBeTruthy('Could not make surface');
if (!surface) {
return;
}
const spiral = CanvasKit.RuntimeEffect.Make(spiralSkSL);
expect(spiral).toBeTruthy('could not compile program');
expect(spiral.getUniformCount() ).toEqual(4);
expect(spiral.getUniformFloatCount()).toEqual(11);
const center = spiral.getUniform(1);
expect(center).toBeTruthy('could not fetch numbered uniform');
expect(center.slot ).toEqual(1);
expect(center.columns ).toEqual(2);
expect(center.rows ).toEqual(1);
expect(center.isInteger).toEqual(true);
const color_0 = spiral.getUniform(2);
expect(color_0).toBeTruthy('could not fetch numbered uniform');
expect(color_0.slot ).toEqual(3);
expect(color_0.columns ).toEqual(4);
expect(color_0.rows ).toEqual(1);
expect(color_0.isInteger).toEqual(false);
expect(spiral.getUniformName(2)).toEqual('in_colors0');
const canvas = surface.getCanvas();
const paint = new CanvasKit.Paint();
canvas.clear(CanvasKit.BLACK); // black should not be visible
const shader = spiral.makeShader([
0.3,
CANVAS_WIDTH/2, CANVAS_HEIGHT/2,
1, 0, 0, 1, // solid red
0, 1, 0, 1], // solid green
localMatrix);
paint.setShader(shader);
canvas.drawRect(CanvasKit.LTRBRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT), paint);
paint.delete();
shader.delete();
spiral.delete();
reportSurface(surface, name, done);
};
it('can compile custom shader code', (done) => {
testRTShader('rtshader_spiral', done);
});
it('can apply a matrix to the shader', (done) => {
testRTShader('rtshader_spiral_translated', done, CanvasKit.Matrix.translated(-200, 100));
});
it('can provide a error handler for compilation errors', () => {
let error = '';
const spiral = CanvasKit.RuntimeEffect.Make(`invalid sksl code, I hope`, (e) => {
error = e;
});
expect(spiral).toBeFalsy();
expect(error).toContain('error');
});
it('can generate a debug trace', () => {
// We don't support debug tracing on GPU, so we always request a software canvas here.
const surface = CanvasKit.MakeSWCanvasSurface('test');
expect(surface).toBeTruthy('Could not make surface');
if (!surface) {
return;
}
const spiral = CanvasKit.RuntimeEffect.Make(spiralSkSL);
expect(spiral).toBeTruthy('could not compile program');
const canvas = surface.getCanvas();
const paint = new CanvasKit.Paint();
const shader = spiral.makeShader([
0.3,
CANVAS_WIDTH/2, CANVAS_HEIGHT/2,
1, 0, 0, 1, // solid red
0, 1, 0, 1]); // solid green
const traced = CanvasKit.RuntimeEffect.MakeTraced(shader, CANVAS_WIDTH/2, CANVAS_HEIGHT/2);
paint.setShader(traced.shader);
canvas.drawRect(CanvasKit.LTRBRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT), paint);
const traceData = traced.debugTrace.writeTrace();
paint.delete();
shader.delete();
spiral.delete();
traced.shader.delete();
traced.debugTrace.delete();
surface.delete();
const parsedTrace = JSON.parse(traceData);
expect(parsedTrace).toBeTruthy('could not parse trace JSON');
expect(parsedTrace.functions).toBeTruthy('debug trace does not include function list');
expect(parsedTrace.slots).toBeTruthy('debug trace does not include slot list');
expect(parsedTrace.trace).toBeTruthy('debug trace does not include trace data');
expect(parsedTrace.nonsense).toBeFalsy('debug trace includes a nonsense key');
expect(parsedTrace.mystery).toBeFalsy('debug trace includes a mystery key');
expect(parsedTrace.source).toEqual([
"",
"uniform float rad_scale;",
"uniform int2 in_center;",
"uniform float4 in_colors0;",
"uniform float4 in_colors1;",
"",
"half4 main(float2 p) {",
" float2 pp = p - float2(in_center);",
" float radius = sqrt(dot(pp, pp));",
" radius = sqrt(radius);",
" float angle = atan(pp.y / pp.x);",
" float t = (angle + 3.1415926/2) / (3.1415926);",
" t += radius * rad_scale;",
" t = fract(t);",
" return half4(mix(in_colors0, in_colors1, t));",
"}"
]);
});
const loadBrick = fetch(
'/assets/brickwork-texture.jpg')
.then((response) => response.arrayBuffer());
const loadMandrill = fetch(
'/assets/mandrill_512.png')
.then((response) => response.arrayBuffer());
const thresholdSkSL = `
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));
}`;
// TODO(kjlubick) rewrite testChildrenShader and callers to use gm.
const testChildrenShader = (name, done, localMatrix) => {
Promise.all([loadBrick, loadMandrill]).then((values) => {
catchException(done, () => {
const [brickData, mandrillData] = values;
const brickImg = CanvasKit.MakeImageFromEncoded(brickData);
expect(brickImg).toBeTruthy('brick image could not be loaded');
const mandrillImg = CanvasKit.MakeImageFromEncoded(mandrillData);
expect(mandrillImg).toBeTruthy('mandrill image could not be loaded');
const thresholdEffect = CanvasKit.RuntimeEffect.Make(thresholdSkSL);
expect(thresholdEffect).toBeTruthy('threshold did not compile');
const spiralEffect = CanvasKit.RuntimeEffect.Make(spiralSkSL);
expect(spiralEffect).toBeTruthy('spiral did not compile');
const brickShader = brickImg.makeShaderCubic(
CanvasKit.TileMode.Decal, CanvasKit.TileMode.Decal,
1/3 /*B*/, 1/3 /*C*/,
CanvasKit.Matrix.scaled(CANVAS_WIDTH/brickImg.width(),
CANVAS_HEIGHT/brickImg.height()));
const mandrillShader = mandrillImg.makeShaderCubic(
CanvasKit.TileMode.Decal, CanvasKit.TileMode.Decal,
1/3 /*B*/, 1/3 /*C*/,
CanvasKit.Matrix.scaled(CANVAS_WIDTH/mandrillImg.width(),
CANVAS_HEIGHT/mandrillImg.height()));
const spiralShader = spiralEffect.makeShader([
0.8,
CANVAS_WIDTH/2, CANVAS_HEIGHT/2,
1, 1, 1, 1,
0, 0, 0, 1]);
const blendShader = thresholdEffect.makeShaderWithChildren(
[0.5, 5],
[brickShader, mandrillShader, spiralShader], localMatrix);
const surface = CanvasKit.MakeCanvasSurface('test');
expect(surface).toBeTruthy('Could not make surface');
const canvas = surface.getCanvas();
const paint = new CanvasKit.Paint();
canvas.clear(CanvasKit.WHITE);
paint.setShader(blendShader);
canvas.drawRect(CanvasKit.LTRBRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT), paint);
brickImg.delete();
mandrillImg.delete();
thresholdEffect.delete();
spiralEffect.delete();
brickShader.delete();
mandrillShader.delete();
spiralShader.delete();
blendShader.delete();
paint.delete();
reportSurface(surface, name, done);
})();
});
}
it('take other shaders as fragment processors', (done) => {
testChildrenShader('rtshader_children', done);
});
it('apply a local matrix to the children-based shader', (done) => {
testChildrenShader('rtshader_children_rotated', done, CanvasKit.Matrix.rotated(Math.PI/12));
});
});

View File

@ -0,0 +1,327 @@
describe('Skottie behavior', () => {
let container;
beforeEach(async () => {
await EverythingLoaded;
container = document.createElement('div');
container.innerHTML = `
<canvas width=600 height=600 id=test></canvas>
<canvas width=600 height=600 id=report></canvas>`;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
const expectArrayCloseTo = (a, b, precision) => {
precision = precision || 14; // digits of precision in base 10
expect(a.length).toEqual(b.length);
for (let i=0; i<a.length; i++) {
expect(a[i]).toBeCloseTo(b[i], precision);
}
};
const imgPromise = fetch('/assets/flightAnim.gif')
.then((response) => response.arrayBuffer());
const jsonPromise = fetch('/assets/animated_gif.json')
.then((response) => response.text());
const washPromise = fetch('/assets/map-shield.json')
.then((response) => response.text());
gm('skottie_animgif', (canvas, promises) => {
if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
console.warn('Skipping test because not compiled with skottie');
return;
}
expect(promises[1]).not.toBe('NOT FOUND');
const animation = CanvasKit.MakeManagedAnimation(promises[1], {
'flightAnim.gif': promises[0],
});
expect(animation).toBeTruthy();
const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);
const size = animation.size();
expectArrayCloseTo(size, Float32Array.of(800, 600), 4);
canvas.clear(CanvasKit.WHITE);
animation.render(canvas, bounds);
// We intentionally make the length of this array 5 and add a sentinel value
// of 999 so we can make sure the bounds are copied into this rect and a new
// one is not allocated.
const damageRect = Float32Array.of(0, 0, 0, 0, 999);
// There was a bug, fixed in https://skia-review.googlesource.com/c/skia/+/241757
// that seeking again and drawing again revealed.
animation.seek(0.5, damageRect);
expectArrayCloseTo(damageRect, Float32Array.of(0, 0, 800, 600, 999), 4);
canvas.clear(CanvasKit.WHITE);
animation.render(canvas, bounds);
animation.delete();
}, imgPromise, jsonPromise);
gm('skottie_setcolor', (canvas, promises) => {
if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
console.warn('Skipping test because not compiled with skottie');
return;
}
expect(promises[0]).not.toBe('NOT FOUND');
const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);
canvas.clear(CanvasKit.WHITE);
const animation = CanvasKit.MakeManagedAnimation(promises[0]);
expect(animation).toBeTruthy();
animation.setColor('$Icon Fill', CanvasKit.RED);
animation.seek(0.5);
animation.render(canvas, bounds);
animation.delete();
}, washPromise);
it('can load audio assets', (done) => {
if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
console.warn('Skipping test because not compiled with skottie');
return;
}
const mockSoundMap = {
map : new Map(),
getPlayer : function(name) {return this.map.get(name)},
setPlayer : function(name, player) {this.map.set(name, player)},
};
function mockPlayer(name) {
this.name = name;
this.wasPlayed = false,
this.seek = function(t) {
this.wasPlayed = true;
}
}
for (let i = 0; i < 20; i++) {
var name = 'audio_' + i;
mockSoundMap.setPlayer(name, new mockPlayer(name));
}
fetch('/assets/audio_external.json')
.then((response) => response.text())
.then((lottie) => {
const animation = CanvasKit.MakeManagedAnimation(lottie, null, null, mockSoundMap);
expect(animation).toBeTruthy();
// 190 frames in sample lottie
for (let t = 0; t < 190; t++) {
animation.seekFrame(t);
}
animation.delete();
for(const player of mockSoundMap.map.values()) {
expect(player.wasPlayed).toBeTrue(player.name + " was not played");
}
done();
});
});
it('can get logs', (done) => {
if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
console.warn('Skipping test because not compiled with skottie');
return;
}
const logger = {
errors: [],
warnings: [],
reset: function() { this.errors = []; this.warnings = []; },
// Logger API
onError: function(err) { this.errors.push(err) },
onWarning: function(wrn) { this.warnings.push(wrn) }
};
{
const json = `{
"v": "5.2.1",
"w": 100,
"h": 100,
"fr": 10,
"ip": 0,
"op": 100,
"layers": [{
"ty": 3,
"nm": "null",
"ind": 0,
"ip": 0
}]
}`;
const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
expect(animation).toBeTruthy();
expect(logger.errors.length).toEqual(0);
expect(logger.warnings.length).toEqual(0);
}
{
const json = `{
"v": "5.2.1",
"w": 100,
"h": 100,
"fr": 10,
"ip": 0,
"op": 100,
"layers": [{
"ty": 2,
"nm": "image",
"ind": 0,
"ip": 0
}]
}`;
const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
expect(animation).toBeTruthy();
expect(logger.errors.length).toEqual(1);
expect(logger.warnings.length).toEqual(0);
// Image layer missing refID
expect(logger.errors[0].includes('missing ref'));
logger.reset();
}
{
const json = `{
"v": "5.2.1",
"w": 100,
"h": 100,
"fr": 10,
"ip": 0,
"op": 100,
"layers": [{
"ty": 1,
"nm": "solid",
"sw": 100,
"sh": 100,
"sc": "#aabbcc",
"ind": 0,
"ip": 0,
"ef": [{
"mn": "FOO"
}]
}]
}`;
const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
expect(animation).toBeTruthy();
expect(logger.errors.length).toEqual(0);
expect(logger.warnings.length).toEqual(1);
// Unsupported effect FOO
expect(logger.warnings[0].includes('FOO'));
logger.reset();
}
done();
});
it('can access dynamic props', () => {
if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
console.warn('Skipping test because not compiled with skottie');
return;
}
const json = `{
"v": "5.2.1",
"w": 100,
"h": 100,
"fr": 10,
"ip": 0,
"op": 100,
"fonts": {
"list": [{
"fName": "test_font",
"fFamily": "test-family",
"fStyle": "TestFontStyle"
}]
},
"layers": [
{
"ty": 4,
"nm": "__shape_layer",
"ind": 0,
"ip": 0,
"shapes": [
{
"ty": "el",
"p": { "a": 0, "k": [ 50, 50 ] },
"s": { "a": 0, "k": [ 50, 50 ] }
},{
"ty": "fl",
"nm": "__shape_fill",
"c": { "a": 0, "k": [ 1, 0, 0] }
},{
"ty": "tr",
"nm": "__shape_opacity",
"o": { "a": 0, "k": 50 }
}
]
},{
"ty": 5,
"nm": "__text_layer",
"ip": 0,
"t": {
"d": {
"k": [{
"t": 0,
"s": {
"f": "test_font",
"s": 100,
"t": "Foo Bar Baz",
"lh": 120,
"ls": 12
}
}]
}
}
}
]
}`;
const animation = CanvasKit.MakeManagedAnimation(json, null, '__');
expect(animation).toBeTruthy();
{
const colors = animation.getColorProps();
expect(colors.length).toEqual(1);
expect(colors[0].key).toEqual('__shape_fill');
expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(255,0,0,255));
const opacities = animation.getOpacityProps();
expect(opacities.length).toEqual(1);
expect(opacities[0].key).toEqual('__shape_opacity');
expect(opacities[0].value).toEqual(50);
const texts = animation.getTextProps();
expect(texts.length).toEqual(1);
expect(texts[0].key).toEqual('__text_layer');
expect(texts[0].value.text).toEqual('Foo Bar Baz');
expect(texts[0].value.size).toEqual(100);
}
expect(animation.setColor('__shape_fill', [0,1,0,1])).toEqual(true);
expect(animation.setOpacity('__shape_opacity', 100)).toEqual(true);
expect(animation.setText('__text_layer', 'baz bar foo', 10)).toEqual(true);
{
const colors = animation.getColorProps();
expect(colors.length).toEqual(1);
expect(colors[0].key).toEqual('__shape_fill');
expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(0,255,0,255));
const opacities = animation.getOpacityProps();
expect(opacities.length).toEqual(1);
expect(opacities[0].key).toEqual('__shape_opacity');
expect(opacities[0].value).toEqual(100);
const texts = animation.getTextProps();
expect(texts.length).toEqual(1);
expect(texts[0].key).toEqual('__text_layer');
expect(texts[0].value.text).toEqual('baz bar foo');
expect(texts[0].value.size).toEqual(10);
}
expect(animation.setColor('INVALID_KEY', [0,1,0,1])).toEqual(false);
expect(animation.setOpacity('INVALID_KEY', 100)).toEqual(false);
expect(animation.setText('INVALID KEY', '', 10)).toEqual(false);
});
});

View File

@ -0,0 +1,19 @@
describe('The test harness', () => {
beforeEach(async () => {
await EverythingLoaded;
});
it('can do assertions', () => {
expect(2+3).toBe(5);
});
it('has access to CanvasKit', () => {
const r = CanvasKit.LTRBRect(1, 2, 3, 4);
expect(r.constructor.name).toEqual('Float32Array');
});
it('can talk to the Gold server', async () => {
const resp = await fetch('/gold_rpc/healthz');
expect(resp.status).toEqual(200);
});
})

View File

@ -0,0 +1,51 @@
const REPORT_URL = '/gold_rpc/report';
const pngPrefx = 'data:image/png;base64,'
function reportCanvas(canvas, testname) {
// toDataURL returns a base64 encoded string with a data prefix. We only
// want the PNG data itself, so we strip that off before submitting it.
const b64 = canvas.toDataURL('image/png')
.substring(pngPrefx.length);
return fetch(REPORT_URL, {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'b64_data': b64,
'name': testname,
})
}).then((resp) => {
expect(resp.status).toEqual(201); // StatusCreated
console.log(`${testname}: ${resp.statusText}`);
});
}
function reportError(done) {
return (e) => {
fail(e);
done();
};
}
// A wrapper to catch and print a stacktrace to the logs.
// Exceptions normally shows up in the browser console,
// but not in the logs that appear on the bots AND a thrown
// exception will normally cause a test to time out.
// This wrapper mitigates both those pain points.
function catchException(done, fn) {
return () => {
try {
fn()
} catch (e) {
console.log('Failed with the following error', e);
expect(e).toBeFalsy();
debugger;
done();
}
// We don't call done with finally because
// that would make the break the asynchronous nature
// of fn().
}
}

View File

@ -0,0 +1,260 @@
// The size of the golden images (DMs)
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 600;
const _commonGM = (it, pause, name, callback, assetsToFetchOrPromisesToWaitOn) => {
const fetchPromises = [];
for (const assetOrPromise of assetsToFetchOrPromisesToWaitOn) {
// https://stackoverflow.com/a/9436948
if (typeof assetOrPromise === 'string' || assetOrPromise instanceof String) {
const newPromise = fetchWithRetries(assetOrPromise)
.then((response) => response.arrayBuffer())
.catch((err) => {
console.error(err);
throw err;
});
fetchPromises.push(newPromise);
} else if (typeof assetOrPromise.then === 'function') {
fetchPromises.push(assetOrPromise);
} else {
throw 'Neither a string nor a promise ' + assetOrPromise;
}
}
it('draws gm '+name, (done) => {
const surface = CanvasKit.MakeCanvasSurface('test');
expect(surface).toBeTruthy('Could not make surface');
if (!surface) {
done();
return;
}
// if fetchPromises is empty, the returned promise will
// resolve right away and just call the callback.
Promise.all(fetchPromises).then((values) => {
try {
// If callback returns a promise, the chained .then
// will wait for it.
return callback(surface.getCanvas(), values, surface);
} catch (e) {
console.log(`gm ${name} failed with error`, e);
expect(e).toBeFalsy();
debugger;
done();
}
}).then(() => {
surface.flush();
if (pause) {
reportSurface(surface, name, null);
console.error('pausing due to pause_gm being invoked');
} else {
reportSurface(surface, name, done);
}
}).catch((e) => {
console.log(`could not load assets for gm ${name}`, e);
debugger;
done();
});
})
};
const fetchWithRetries = (url) => {
const MAX_ATTEMPTS = 3;
const DELAY_AFTER_FAILURE = 1000;
return new Promise((resolve, reject) => {
let attempts = 0;
const attemptFetch = () => {
attempts++;
fetch(url).then((resp) => resolve(resp))
.catch((err) => {
if (attempts < MAX_ATTEMPTS) {
console.warn(`got error in fetching ${url}, retrying`, err);
retryAfterDelay();
} else {
console.error(`got error in fetching ${url} even after ${attempts} attempts`, err);
reject(err);
}
});
};
const retryAfterDelay = () => {
setTimeout(() => {
attemptFetch();
}, DELAY_AFTER_FAILURE);
}
attemptFetch();
});
}
/**
* Takes a name, a callback, and any number of assets or promises. It executes the
* callback (presumably, the test) and reports the resulting surface to Gold.
* @param name {string}
* @param callback {Function}, has two params, the first is a CanvasKit.Canvas
* and the second is an array of results from the passed in assets or promises.
* If a given assetOrPromise was a string, the result will be an ArrayBuffer.
* @param assetsToFetchOrPromisesToWaitOn {string|Promise}. If a string, it will
* be treated as a url to fetch and return an ArrayBuffer with the contents as
* a result in the callback. Otherwise, the promise will be waited on and its
* result will be whatever the promise resolves to.
*/
const gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
_commonGM(it, false, name, callback, assetsToFetchOrPromisesToWaitOn);
};
/**
* fgm is like gm, except only tests declared with fgm, force_gm, or fit will be
* executed. This mimics the behavior of Jasmine.js.
*/
const fgm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
_commonGM(fit, false, name, callback, assetsToFetchOrPromisesToWaitOn);
};
/**
* force_gm is like gm, except only tests declared with fgm, force_gm, or fit will be
* executed. This mimics the behavior of Jasmine.js.
*/
const force_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
fgm(name, callback, assetsToFetchOrPromisesToWaitOn);
};
/**
* skip_gm does nothing. It is a convenient way to skip a test temporarily.
*/
const skip_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
console.log(`Skipping gm ${name}`);
// do nothing, skip the test for now
};
/**
* pause_gm is like fgm, except the test will not finish right away and clear,
* making it ideal for a human to manually inspect the results.
*/
const pause_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
_commonGM(fit, true, name, callback, assetsToFetchOrPromisesToWaitOn);
};
const _commonMultipleCanvasGM = (it, pause, name, callback) => {
it(`draws gm ${name} on both CanvasKit and using Canvas2D`, (done) => {
const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
skcanvas._config = 'software_canvas';
const realCanvas = document.getElementById('test');
realCanvas._config = 'html_canvas';
realCanvas.width = CANVAS_WIDTH;
realCanvas.height = CANVAS_HEIGHT;
if (pause) {
console.log('debugging canvaskit version');
callback(realCanvas);
callback(skcanvas);
const png = skcanvas.toDataURL();
const img = document.createElement('img');
document.body.appendChild(img);
img.src = png;
debugger;
return;
}
const promises = [];
for (const canvas of [skcanvas, realCanvas]) {
callback(canvas);
// canvas has .toDataURL (even though skcanvas is not a real Canvas)
// so this will work.
promises.push(reportCanvas(canvas, name, canvas._config));
}
Promise.all(promises).then(() => {
skcanvas.dispose();
done();
}).catch(reportError(done));
});
};
/**
* Takes a name and a callback. It executes the callback (presumably, the test)
* for both a CanvasKit.Canvas and a native Canvas2D. The result of both will be
* uploaded to Gold.
* @param name {string}
* @param callback {Function}, has one param, either a CanvasKit.Canvas or a native
* Canvas2D object.
*/
const multipleCanvasGM = (name, callback) => {
_commonMultipleCanvasGM(it, false, name, callback);
};
/**
* fmultipleCanvasGM is like multipleCanvasGM, except only tests declared with
* fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This
* mimics the behavior of Jasmine.js.
*/
const fmultipleCanvasGM = (name, callback) => {
_commonMultipleCanvasGM(fit, false, name, callback);
};
/**
* force_multipleCanvasGM is like multipleCanvasGM, except only tests declared
* with fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This
* mimics the behavior of Jasmine.js.
*/
const force_multipleCanvasGM = (name, callback) => {
fmultipleCanvasGM(name, callback);
};
/**
* pause_multipleCanvasGM is like fmultipleCanvasGM, except the test will not
* finish right away and clear, making it ideal for a human to manually inspect the results.
*/
const pause_multipleCanvasGM = (name, callback) => {
_commonMultipleCanvasGM(fit, true, name, callback);
};
/**
* skip_multipleCanvasGM does nothing. It is a convenient way to skip a test temporarily.
*/
const skip_multipleCanvasGM = (name, callback) => {
console.log(`Skipping multiple canvas gm ${name}`);
};
function reportSurface(surface, testname, done) {
// Sometimes, the webgl canvas is blank, but the surface has the pixel
// data. So, we copy it out and draw it to a normal canvas to take a picture.
// To be consistent across CPU and GPU, we just do it for all configurations
// (even though the CPU canvas shows up after flush just fine).
let pixels = surface.getCanvas().readPixels(0, 0, {
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
colorType: CanvasKit.ColorType.RGBA_8888,
alphaType: CanvasKit.AlphaType.Unpremul,
colorSpace: CanvasKit.ColorSpace.SRGB,
});
if (!pixels) {
throw 'Could not get pixels for test '+testname;
}
pixels = new Uint8ClampedArray(pixels.buffer);
const imageData = new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT);
const reportingCanvas = document.getElementById('report');
if (!reportingCanvas) {
throw 'Reporting canvas not found';
}
reportingCanvas.getContext('2d').putImageData(imageData, 0, 0);
if (!done) {
return;
}
reportCanvas(reportingCanvas, testname).then(() => {
surface.delete();
done();
}).catch(reportError(done));
}
function starPath(CanvasKit, X=128, Y=128, R=116) {
const p = new CanvasKit.Path();
p.moveTo(X + R, Y);
for (let i = 1; i < 8; i++) {
let a = 2.6927937 * i;
p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a));
}
p.close();
return p;
}

View File

@ -1,45 +0,0 @@
describe('The test harness', () => {
it('runs the first test', () => {
expect(2+3).toBe(5);
});
it('runs the second test', () => {
expect(null).toBeFalsy();
});
describe('the EverythingLoaded promise', () => {
beforeEach(async () => {
await EverythingLoaded;
});
it('has access to CanvasKit', () => {
const r = CanvasKit.LTRBRect(1, 2, 3, 4);
expect(r.constructor.name).toEqual('Float32Array');
});
it('can talk to Gold once', async () => {
const payload = {
name: 'test_001',
b64_data: btoa('This could have been a PNG_' + (new Date().toLocaleString()))
}
const resp = await fetch('/gold_rpc/report', {
body: JSON.stringify(payload),
method: 'POST',
});
expect(resp.status).toEqual(201); // StatusCreated
});
it('can talk to Gold twice', async () => {
const payload = {
name: 'test_002',
b64_data: btoa('This is some other data ' + (new Date().toLocaleString()))
}
const resp = await fetch('/gold_rpc/report', {
body: JSON.stringify(payload),
method: 'POST',
});
expect(resp.status).toEqual(201); // StatusCreated
});
});
})