1fc0c1c0f8
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>
854 lines
31 KiB
JavaScript
854 lines
31 KiB
JavaScript
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')
|
|
});
|