Skip to content

Commit 443f62e

Browse files
delima02William Chou
authored andcommitted
Implement Canvas API (#380)
* Merge master * Revert "Merge master" This reverts commit ca5b033. * CanvasRenderingContext2D and OffscreenCanvas * Add tests for CanvasRenderingContext2D * Remove unnecessary comments * Revert allowing noImplicitAny for tests * Remove JS files from src/test/tsconfig * Remove unnecessary excluded directory from test/tsconfig * Only test what should be in second and third tests for clearRect * Fix implementation and tests for beginPath and closePath * Change static context to most recent OffscreenCanvas instance in tests * Add meta-programming to reduce the number of lines while delegating calls * Add missing tests for putImageData * Simplify ImageBitmap creation logic for tests * Await for same promise instance needed by test, instead of new one * Remove unnecessary check from third test for each method tested * Fix test purposes by moving assertion from third test to second test for each fn * Add missing tests for createPattern, drawImage, and putImageData * Remove object from delegate function definition arguments * Have HTMLCanvasElement actually own the context * Add transfer call on OffscreenCanvas upgrade and canvas demo page * Use exact version numbers for sinon dependency * Improve Canvas API demo * Remove OffscreenCanvas message event listener when not needed * Switch Context2DImpl and Context2D naming * Use single quotes instead of double quotes, when possible * Change more double quotes to singles for consistency * Fix indentation in offscreen-canvas.ts * Fix indentation in offscreen-canvas.ts * Format context tests file * Format files that did not run through prettier before * Empty calls array after all pending methods have been called * Add tests for HTMLCanvasElement.getContext * Check for correct canvas instance being used to retrieve context async * Add one more example to canvas demo * Fix Document creation for tests after merge conflicts * Fix OffscreenCanvas transfer call after merge conflicts * Merge * More polyfill methods and remove console logs * Add .test to test files * Meta-program polyfill methods. * Make houses bigger in Canvas demo. * Delete SampleContext.ts since no longer needed. * Create polyfill context in getContext method. * Have CommandExecutor take only Uint16Array's. * Remove unnecessary argcount check for setters. * Add color to example houses to test methods with strings * Revert changes in mutator.ts. * Only request an OffscreenCanvas if supported. * Revert changes in MutationTransfer.ts. * Pass in offscreen polyfill method calls using a different mechanism * Revert install.ts. * Rename OFFSCREEN_CONTEXT_CALL to OFFSCREEN_POLYFILL * Remove context creation from polyfill constructor. * Remove unused TransferrableKeys.extra. * Remove unnecessary call to callQueuedCalls and console logs. * Improve debug print function for offscreen polyfill processor * Refactor ofscreen processor to avoid bugs when converting typedarrays. * Rename canvas test files to include .test * Fix some method signatures for CanvasRenderingContext2D. * Add +1 to string arg index to avoid negatives. * Fix implementation for beginPath * getLineDash and setLineDash (tested locally) * clip() and fill() * setTransform and remove extra defs of lineDash methods. * Add optional argument for fillText and strokeText * Add optional argument to ellipse() and arc() * Refactor polyfill and call transfer. * Use [...arguments] to delegate all CanvasRenderingContext2D calls. * Throw for unsupported method signatures. * NIT and some refactoring * Remove duplication and args from methods that don't take any * Fix float32Needed bug * Tweak lineDash methods * Fix polyfill CommandExectutor's print() impl. * Tweak canvas demo * Improve canvas demo * Bump up bundle size * Fix circular dependency by templating. * Add missing templating in context used for testing * Fix memory leak + remove unnecessary if. * Remove unnecessary comments * Make 'transferables' an optional argument in messageToWorker * Indicate OffscreenCanvas not available in TS with comment * Rename DOMTypes and group all canvas files in folder * Add getters for CanvasRenderingContext2D * Replace fake 'describe' blocks with comments * Missing getters and tests for getters. * Rename "spy" to something more appropriate in testing helper fn. * Remove unneeded 'workerContext' from offscreen call processor. * Tests for offscreen call processor. * Increase bundle size. * Increase bundle size * Have 'sandbox' be passed through test context. * Rename some variables. * Make 'getOffscreenCanvasAsync' private and owned by CanvasRenderingContext2D. * Split CanvasRenderingContext2D's delegate function into three different ones * Rename test OffscreenCanvas class to FakeOffscreenCanvas * Rename CanvasRenderingContext2DImplementation to CanvasRenderingContext2DShim * Create test stubs inline. * Remove global dependency from Canvas API * Fix demo. * Increase bundle size.
1 parent d1c7bee commit 443f62e

26 files changed

+4801
-9
lines changed

demo/canvas/canvas.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const existingCanvasBtn = document.getElementById('existingCanvasBtn');
18+
const newCanvasBtn = document.getElementById('newCanvasBtn');
19+
const doubleCanvasBtn = document.getElementById('doubleCanvasBtn');
20+
21+
const myCanvas = document.getElementById('myCanvas');
22+
const myCtx = myCanvas.getContext('2d');
23+
24+
function draw(e) {
25+
myCtx.lineTo(e.offsetX, e.offsetY);
26+
myCtx.stroke();
27+
}
28+
29+
myCanvas.addEventListener('mousedown', e => {
30+
myCtx.strokeStyle = 'red';
31+
myCtx.lineWidth = 2;
32+
myCtx.setLineDash([10, 10]);
33+
myCtx.beginPath();
34+
myCtx.moveTo(e.offsetX, e.offsetY);
35+
myCanvas.addEventListener('mousemove', draw);
36+
});
37+
38+
myCanvas.addEventListener('mouseup', e => {
39+
myCanvas.removeEventListener('mousemove', draw);
40+
});
41+
42+
existingCanvasBtn.addEventListener('click', async () => {
43+
// Scenario #1:
44+
// Canvas is already on the page
45+
myCtx.fillStyle = 'blue';
46+
myCtx.strokeStyle = 'blue';
47+
myCtx.lineWidth = 5;
48+
myCtx.setLineDash([1, 0]);
49+
myCtx.beginPath();
50+
myCtx.strokeRect(212.5, 222.5, 75, 55);
51+
myCtx.fillRect(240, 247.5, 20, 30);
52+
myCtx.moveTo(200, 222.5);
53+
myCtx.lineTo(250, 182.5);
54+
myCtx.lineTo(300, 222.5);
55+
myCtx.closePath();
56+
myCtx.stroke();
57+
58+
});
59+
60+
newCanvasBtn.addEventListener('click', async () => {
61+
// Scenario #2:
62+
// Create a canvas element using document.createElement()
63+
const canvas = document.createElement('canvas');
64+
const newCanvasDiv = document.getElementById('newCanvasDiv');
65+
canvas.width = 250;
66+
canvas.height = 250;
67+
newCanvasDiv.appendChild(canvas);
68+
const ctx = canvas.getContext('2d');
69+
70+
ctx.lineWidth = 5;
71+
ctx.fillStyle = 'orange';
72+
ctx.strokeStyle = 'orange';
73+
ctx.strokeRect(87.5, 97.5, 75, 55);
74+
ctx.fillRect(115, 122.5, 20, 30);
75+
ctx.moveTo(75, 97.5);
76+
ctx.lineTo(125, 57.5);
77+
ctx.lineTo(175, 97.5);
78+
79+
ctx.closePath();
80+
ctx.stroke();
81+
});
82+
83+
doubleCanvasBtn.addEventListener('click', async () => {
84+
// Scenario #3:
85+
// Two different canvas elements created at the same time.
86+
// This scenario is needed to make sure async logic works correctly.
87+
const canvasOne = document.createElement('canvas');
88+
const canvasTwo = document.createElement('canvas');
89+
90+
canvasOne.width = 250;
91+
canvasOne.height = 250;
92+
canvasTwo.width = 250;
93+
canvasTwo.height = 250;
94+
95+
const newCanvasDiv = document.getElementById('newCanvasDiv');
96+
97+
newCanvasDiv.appendChild(canvasOne);
98+
newCanvasDiv.appendChild(canvasTwo);
99+
100+
101+
const ctxOne = canvasOne.getContext('2d');
102+
const ctxTwo = canvasTwo.getContext('2d');
103+
104+
ctxOne.lineWidth = 5;
105+
ctxOne.fillStyle = 'red';
106+
ctxOne.strokeStyle = 'red';
107+
ctxOne.strokeRect(87.5, 97.5, 75, 55);
108+
ctxOne.fillRect(115, 122.5, 20, 30);
109+
ctxOne.moveTo(75, 97.5);
110+
ctxOne.lineTo(125, 57.5);
111+
ctxOne.lineTo(175, 97.5);
112+
ctxOne.closePath();
113+
ctxOne.stroke();
114+
115+
ctxTwo.lineWidth = 5;
116+
ctxTwo.fillStyle = 'green';
117+
ctxTwo.strokeStyle = 'green';
118+
ctxTwo.strokeRect(87.5, 97.5, 75, 55);
119+
ctxTwo.fillRect(115, 122.5, 20, 30);
120+
ctxTwo.moveTo(75, 97.5);
121+
ctxTwo.lineTo(125, 57.5);
122+
ctxTwo.lineTo(175, 97.5);
123+
124+
ctxTwo.closePath();
125+
ctxTwo.stroke();
126+
});

demo/canvas/index.html

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!DOCTYPE html>
2+
<html style="background: #e6e9e9;">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Hello World</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link href="/demo.css" rel="stylesheet">
8+
<script src="/dist/main.mjs" type="module"></script>
9+
<script src="/dist/main.js" nomodule defer></script>
10+
<!-- This comment block is intended to make it easier to test both the script module and nomodule path -->
11+
<!-- Comment either block to enable module/nomodule or disable it. -->
12+
<!-- <script src="/dist/main.js" defer></script> -->
13+
</head>
14+
<body style="background: #e6e9e9;">
15+
<div src="canvas.js" id="upgrade-me">
16+
<div id="canvasButtonsDiv">
17+
18+
<button id="existingCanvasBtn">Draw on already existing canvas</button>
19+
<button id="newCanvasBtn">Create new canvas and draw on it</button>
20+
<button id="doubleCanvasBtn">Create two new canvases and draw on them</button>
21+
</div>
22+
<div>
23+
<canvas width="500" height="500" id="myCanvas"></canvas>
24+
<div id="newCanvasDiv"></div>
25+
</div>
26+
</div>
27+
<script type="module">
28+
import {upgradeElement} from '/dist/main.mjs';
29+
upgradeElement(document.getElementById('upgrade-me'), '/dist/worker/worker.mjs');
30+
</script>
31+
<script nomodule async=false defer>
32+
document.addEventListener('DOMContentLoaded', function() {
33+
MainThread.upgradeElement(document.getElementById('upgrade-me'), '/dist/worker.js');
34+
}, false);
35+
</script>
36+
<!-- This comment block is intended to make it easier to test both the script module and nomodule path -->
37+
<!-- Comment either block to enable module/nomodule or disable it. -->
38+
<!-- <script async=false defer>
39+
document.addEventListener('DOMContentLoaded', function() {
40+
MainThread.upgradeElement(document.getElementById('upgrade-me'), './dist/worker.js');
41+
}, false);
42+
</script> -->
43+
</body>
44+
</html>

demo/demo.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,19 @@ li {
3232
code {
3333
white-space: pre-wrap;
3434
}
35+
36+
canvas {
37+
background: #fff;
38+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
39+
}
40+
41+
#myCanvas {
42+
float: left;
43+
margin-right: 5px;
44+
}
45+
46+
#canvasButtonsDiv {
47+
display: flex;
48+
align-items: center;
49+
justify-content: center;
50+
}

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ <h3>Basic</h3>
1919
<li><a href='prime-numbers/'>Prime Numbers</a></li>
2020
<li><a href='svg/'>SVG Rendering</a></li>
2121
<li><a href='comments/'>Comment Rendering</a></li>
22+
<li><a href='canvas/'>Canvas</a></li>
2223
</ul>
2324

2425
<h3>Frameworks</h3>

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
"release": "np",
3939
"prepublishOnly": "npm run build"
4040
},
41+
"dependencies": {
42+
"@types/sinon": "7.0.10"
43+
},
4144
"devDependencies": {
4245
"@ampproject/rollup-plugin-closure-compiler": "0.9.0",
4346
"@babel/cli": "7.4.4",
@@ -69,6 +72,7 @@
6972
"rollup-plugin-copy": "2.0.1",
7073
"rollup-plugin-replace": "2.2.0",
7174
"rollup-plugin-terser": "4.0.4",
75+
"sinon": "7.1.1",
7276
"sirv": "0.4.0",
7377
"tslint": "5.16.0",
7478
"typescript": "3.4.5"
@@ -89,12 +93,12 @@
8993
{
9094
"path": "./dist/worker/worker.mjs",
9195
"compression": "brotli",
92-
"maxSize": "8.4 kB"
96+
"maxSize": "10.3 kB"
9397
},
9498
{
9599
"path": "./dist/worker/worker.js",
96100
"compression": "brotli",
97-
"maxSize": "10.0 kB"
101+
"maxSize": "11.8 kB"
98102
},
99103
{
100104
"path": "./dist/main.mjs",
@@ -104,7 +108,7 @@
104108
{
105109
"path": "./dist/main.js",
106110
"compression": "brotli",
107-
"maxSize": "3.1 kB"
111+
"maxSize": "3.3 kB"
108112
}
109113
],
110114
"esm": {

src/main-thread/commands/interface.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
export interface CommandExecutor {
1817
execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number;
1918
print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { WorkerContext } from '../worker';
2+
import { TransferrableKeys } from '../../transfer/TransferrableKeys';
3+
import { MessageType } from '../../transfer/Messages';
4+
import { CommandExecutor } from './interface';
5+
import { OffscreenCanvasMutationIndex } from '../../transfer/TransferrableMutation';
6+
7+
export function OffscreenCanvasProcessor(workerContext: WorkerContext): CommandExecutor {
8+
return {
9+
execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number {
10+
if (target) {
11+
const canvas = target as HTMLCanvasElement;
12+
const offscreen = canvas.transferControlToOffscreen();
13+
workerContext.messageToWorker(
14+
{
15+
[TransferrableKeys.type]: MessageType.OFFSCREEN_CANVAS_INSTANCE,
16+
[TransferrableKeys.target]: [target._index_],
17+
[TransferrableKeys.data]: offscreen, // Object, an OffscreenCanvas
18+
},
19+
[offscreen],
20+
);
21+
} else {
22+
console.error(`getNode() yields null – ${target}`);
23+
}
24+
25+
return startPosition + OffscreenCanvasMutationIndex.End;
26+
},
27+
print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object {
28+
29+
return {
30+
type: 'OFFSCREEN_CANVAS_INSTANCE',
31+
target,
32+
};
33+
},
34+
};
35+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { CommandExecutor } from './interface';
2+
import { OffscreenContextPolyfillMutationIndex } from '../../transfer/TransferrableMutation';
3+
import { NumericBoolean } from '../../utils';
4+
import { Strings } from '../strings';
5+
6+
export function OffscreenPolyfillCallProcessor(strings: Strings): CommandExecutor {
7+
return {
8+
execute(mutations: Uint16Array, startPosition: number, target: RenderableElement): number {
9+
const float32Needed = mutations[startPosition + OffscreenContextPolyfillMutationIndex.Float32Needed] === NumericBoolean.TRUE;
10+
const argCount = mutations[startPosition + OffscreenContextPolyfillMutationIndex.ArgumentCount];
11+
const methodCalled = strings.get(mutations[startPosition + OffscreenContextPolyfillMutationIndex.MethodCalled]);
12+
const isSetter = mutations[startPosition + OffscreenContextPolyfillMutationIndex.IsSetter] === NumericBoolean.TRUE;
13+
const stringArgIndex = mutations[startPosition + OffscreenContextPolyfillMutationIndex.StringArgIndex];
14+
15+
const argsStart = startPosition + OffscreenContextPolyfillMutationIndex.Args;
16+
let argsTypedArray: Uint16Array | Float32Array;
17+
let argEnd = argCount;
18+
19+
if (float32Needed) {
20+
argEnd *= 2;
21+
argsTypedArray = new Float32Array(mutations.slice(argsStart, argsStart + argEnd).buffer);
22+
} else {
23+
argsTypedArray = mutations.slice(argsStart, argsStart + argEnd);
24+
}
25+
26+
const mainContext = (target as HTMLCanvasElement).getContext('2d');
27+
let args = [] as any[];
28+
29+
if (argCount > 0) {
30+
argsTypedArray.forEach((arg: any, i: number) => {
31+
if (stringArgIndex - 1 === i) {
32+
args.push(strings.get(arg));
33+
} else {
34+
args.push(arg);
35+
}
36+
});
37+
38+
// setLineDash has a single argument: number[]
39+
// values from the array argument are transferred independently, so we must do this
40+
if (methodCalled === 'setLineDash') {
41+
args = [args];
42+
}
43+
}
44+
45+
if (isSetter) {
46+
(mainContext as any)[methodCalled] = args[0];
47+
} else {
48+
(mainContext as any)[methodCalled](...args);
49+
}
50+
51+
return startPosition + OffscreenContextPolyfillMutationIndex.End + argEnd;
52+
},
53+
print(mutations: Uint16Array, startPosition: number, target?: RenderableElement | null): Object {
54+
const float32Needed = mutations[startPosition + OffscreenContextPolyfillMutationIndex.Float32Needed] === NumericBoolean.TRUE;
55+
const argCount = mutations[startPosition + OffscreenContextPolyfillMutationIndex.ArgumentCount];
56+
const methodCalled = strings.get(mutations[startPosition + OffscreenContextPolyfillMutationIndex.MethodCalled]);
57+
const isSetter = mutations[startPosition + OffscreenContextPolyfillMutationIndex.IsSetter] === NumericBoolean.TRUE;
58+
const stringArgIndex = mutations[startPosition + OffscreenContextPolyfillMutationIndex.StringArgIndex];
59+
60+
const argsStart = startPosition + OffscreenContextPolyfillMutationIndex.Args;
61+
let argsTypedArray: Uint16Array | Float32Array;
62+
let argEnd = argCount;
63+
64+
if (float32Needed) {
65+
argEnd *= 2;
66+
argsTypedArray = new Float32Array(mutations.slice(argsStart, argsStart + argEnd).buffer);
67+
} else {
68+
argsTypedArray = mutations.slice(argsStart, argsStart + argEnd);
69+
}
70+
71+
let args = [] as any[];
72+
73+
if (argCount > 0) {
74+
argsTypedArray.forEach((arg: any, i: number) => {
75+
if (stringArgIndex - 1 === i) {
76+
args.push(strings.get(arg));
77+
} else {
78+
args.push(arg);
79+
}
80+
});
81+
82+
// setLineDash has a single argument: number[]
83+
// values from the array argument are transferred independently, so we must do this
84+
if (methodCalled === 'setLineDash') {
85+
args = [args];
86+
}
87+
}
88+
89+
return {
90+
type: 'OFFSCREEN_POLYFILL',
91+
target,
92+
Float32Needed: float32Needed,
93+
ArgumentCount: argCount,
94+
MethodCalled: methodCalled,
95+
IsSetter: isSetter,
96+
StringArgIndex: stringArgIndex,
97+
Args: args,
98+
End: startPosition + OffscreenContextPolyfillMutationIndex.End + argEnd,
99+
};
100+
},
101+
};
102+
}

src/main-thread/install.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function install(fetchPromise: Promise<[string, string]>, baseElement: HT
5959
if (!ALLOWABLE_MESSAGE_TYPES.includes(data[TransferrableKeys.type])) {
6060
return;
6161
}
62+
6263
mutatorContext.mutate(
6364
(data as MutationFromWorker)[TransferrableKeys.phase],
6465
(data as MutationFromWorker)[TransferrableKeys.nodes],

0 commit comments

Comments
 (0)