Skip to content

Commit ad16091

Browse files
committed
Add canvas scale/translate transform support
1 parent 1e19137 commit ad16091

File tree

4 files changed

+223
-23
lines changed

4 files changed

+223
-23
lines changed

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "fauxdom-with-canvas",
33
"description": "A fast and lightweight HTML5 parser and DOM with built-in canvas",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"author": "Flaki <[email protected]>",
66
"contributors": [
77
"Joe Stenger <[email protected]>",
@@ -50,10 +50,13 @@
5050
"prepare": "npm exec tsc && rollup -c --silent"
5151
},
5252
"devDependencies": {
53+
"@rollup/plugin-terser": "^0.4.4",
54+
"@rollup/plugin-wasm": "^6.2.2",
5355
"compressing": "^1.5.0",
5456
"jest": "^29.3.0",
5557
"rollup": "^2.79.1",
5658
"rollup-plugin-strip-code": "^0.2.7",
59+
"squoosh": "https://github.com/GoogleChromeLabs/squoosh/archive/refs/tags/v1.12.0.tar.gz",
5760
"typescript": "^5.3.3"
5861
},
5962
"jest": {
@@ -70,8 +73,5 @@
7073
"<rootDir>/debug",
7174
"<rootDir>/src"
7275
]
73-
},
74-
"dependencies": {
75-
"@rollup/plugin-terser": "^0.4.4"
7676
}
7777
}

rollup.config.js

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import terser from "@rollup/plugin-terser";
22
import stripCode from "rollup-plugin-strip-code";
3+
import { wasm } from "@rollup/plugin-wasm";
34
import {spawn} from "child_process";
45
import {zip} from "compressing";
56
import * as fs from "fs";
@@ -9,6 +10,8 @@ import * as pkg from "./package.json";
910

1011
let DEBUG = true;
1112

13+
let EXTERNALS = [ "node:fs/promises" ];
14+
1215
spawn( process.execPath, ["./scripts/entities.js"] );
1316

1417
export default args =>
@@ -27,24 +30,28 @@ export default args =>
2730
start_comment: "@START_BROWSER_ONLY",
2831
end_comment: "@END_BROWSER_ONLY"
2932
} ),
30-
modulePlugins = [debugStripper, browserStripper],
31-
iifePlugins = [debugStripper, unitTestStripper],
33+
wasmPlugin = wasm({
34+
sync: [ "node_modules/squoosh/codecs/resize/pkg/squoosh_resize_bg.wasm" ]
35+
}),
36+
modulePlugins = [debugStripper, browserStripper, wasmPlugin],
37+
iifePlugins = [debugStripper, unitTestStripper, wasmPlugin],
3238
output = [
3339
{
3440
onwarn,
3541
input: "src/document.js",
3642
plugins: modulePlugins,
43+
external: EXTERNALS,
3744
output: [
3845
module( "esm" ),
39-
module( "cjs" )
46+
//module( "cjs" )
4047
]
4148
},
42-
{
43-
onwarn,
44-
input: "src/document.js",
45-
plugins: iifePlugins,
46-
output: module( "iife" )
47-
}
49+
// {
50+
// onwarn,
51+
// input: "src/document.js",
52+
// plugins: iifePlugins,
53+
// output: module( "iife" )
54+
// }
4855
];
4956

5057
if ( !DEBUG )
@@ -57,8 +64,9 @@ export default args =>
5764
output.push( {
5865
onwarn,
5966
input: "src/document.js",
60-
plugins: [browserStripper],
61-
output: module( "cjs", "tests." )
67+
plugins: [browserStripper, wasmPlugin],
68+
external: EXTERNALS,
69+
output: module( "esm", "tests." )
6270
} );
6371
iifePlugins.push( terser( {compress: false, mangle: false, output: {beautify: true}, safari10: true} ) );
6472
}

src/js-canvas/RenderingContext.ts

+112-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { HTMLCanvasElement } from "./HTMLCanvasElement.js";
44

55
import { CANVAS_DATA } from "./HTMLCanvasElement.js";
66
import { ImageData } from "./ImageData.js";
7+
import { resizeImage } from "./WasmResize.js"
78

89
// Partial types via https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts
910
export type RenderingContext = CanvasRenderingContext2D | ImageBitmapRenderingContext
@@ -37,19 +38,52 @@ interface RGBAColor {
3738
a?: number
3839
}
3940

40-
const FILL_STYLE: unique symbol = Symbol("fill-style");
41+
interface Context2DState {
42+
fillStyle: string
43+
scaleX: number
44+
scaleY: number
45+
translateX: number
46+
translateY: number
47+
}
48+
49+
const STATE: unique symbol = Symbol("context2d-state");
4150

4251
export class CanvasRenderingContext2D implements CanvasRect, CanvasDrawImage, CanvasImageData {
4352
readonly canvas: HTMLCanvasElement;
4453

45-
private [FILL_STYLE]: string;
54+
private [STATE]: Context2DState;
55+
56+
reset() {
57+
this[STATE] = {
58+
fillStyle: "#000",
59+
scaleX: 1,
60+
scaleY: 1,
61+
translateX: 0,
62+
translateY: 0,
63+
};
64+
}
4665

4766
get fillStyle(): string {
48-
return this[FILL_STYLE];
67+
return this[STATE].fillStyle;
4968
}
5069
set fillStyle(newStyle: string) {
5170
console.log(`${this}→fillStyle = ${newStyle}`);
52-
this[FILL_STYLE] = newStyle;
71+
this[STATE].fillStyle = newStyle;
72+
}
73+
74+
get transformActive(): boolean {
75+
const active = this[STATE].scaleX !== 1 || this[STATE].scaleY !== 1 || this[STATE].translateX !== 0 || this[STATE].translateY !== 0;
76+
77+
if (active) {
78+
const activeTransforms = [];
79+
if (this[STATE].scaleX !== 1) activeTransforms.push(`scaleX: ${this[STATE].scaleX}`);
80+
if (this[STATE].scaleY !== 1) activeTransforms.push(`scaleY: ${this[STATE].scaleY}`);
81+
if (this[STATE].translateX !== 0) activeTransforms.push(`translateX: ${this[STATE].translateX}`);
82+
if (this[STATE].translateY !== 0) activeTransforms.push(`translateY: ${this[STATE].translateY}`);
83+
console.log(`${this}: context has active matrix transforms: ${activeTransforms.join(', ')}`);
84+
}
85+
86+
return active;
5387
}
5488

5589
// CanvasRect
@@ -58,6 +92,10 @@ export class CanvasRenderingContext2D implements CanvasRect, CanvasDrawImage, Ca
5892
}
5993

6094
fillRect(x: number, y: number, w: number, h: number): void {
95+
if (this[STATE].scaleX !== 1 || this[STATE].scaleY !== 1 || this[STATE].translateX !== 0 || this[STATE].translateY !== 0) {
96+
console.log(`Warning: ${this}→fillRect( ${Array.from(arguments).join(', ')} ) canvas transform matrix not supported: ${Object.values(this[STATE]).map(([k,v]) => k+': '+v).join(', ')}`);
97+
}
98+
6199
const { r, g, b, a } = this.fillStyleRGBA;
62100
const alpha = a*255|0;
63101

@@ -98,7 +136,7 @@ export class CanvasRenderingContext2D implements CanvasRect, CanvasDrawImage, Ca
98136
this.canvas = parentCanvas;
99137

100138
// defaults
101-
this.fillStyle = "#000";
139+
this.reset();
102140
}
103141

104142
// CanvasDrawImage
@@ -109,29 +147,61 @@ export class CanvasRenderingContext2D implements CanvasRect, CanvasDrawImage, Ca
109147
if (image instanceof globalThis.HTMLCanvasElement) {
110148
w1 = w1 ?? image.width;
111149
h1 = h1 ?? image.height;
150+
x2 = x2 ?? 0;
151+
y2 = y2 ?? 0;
112152

113153
if (w1 !== w2 || h1 !== h2) {
114154
console.log(`${this} Not implemented: image scaling in drawImage( <${image.constructor.name}> ${Array.from(arguments).join(', ')} )`);
115155
return;
116156
}
117157

118-
const srcImage = image.getContext("2d").getImageData(x1, y1, w1, h1);
158+
let srcImage = image.getContext("2d").getImageData(x1, y1, w1, h1);
159+
160+
// Scaling/translation needed
161+
if (this.transformActive) {
162+
// This is slightly inaccurate but we don't do subpixel drawing
163+
const targetWidth = this[STATE].scaleX * w1 |0;
164+
const targetHeight = this[STATE].scaleY * h1 |0;
165+
166+
x2 = x2 + this[STATE].translateX |0;
167+
y2 = y2 + this[STATE].translateY |0;
168+
169+
srcImage = resizeImage(srcImage, targetWidth, targetHeight);
170+
w1 = srcImage.width;
171+
h1 = srcImage.height;
172+
173+
console.log(`${this}→drawImage(): source image resized to: ${w1}x${h1} (${srcImage.data.length/4} pixels)`);
174+
console.log(`${this}→drawImage(): drawing to translated coordinates: ( ${x2}, ${y2} )`);
175+
}
176+
119177
const srcPixels = srcImage.data;
120178
const dstPixels = this.canvas[CANVAS_DATA];
179+
const canvasW = this.canvas.width;
180+
const canvasH = this.canvas.height;
121181
const rows = h1;
122182
const cols = w1;
123183

184+
let ntp = 0;
185+
let oob = 0;
124186
for (let row = 0; row < rows; ++row) {
125187
for (let col = 0; col < cols; ++col) {
188+
// Index of the destination canvas pixel should be within bounds
189+
const di = ((y2 + row) * canvasW + x2 + col) * 4;
190+
191+
if (di < 0 || di >= dstPixels.length) {
192+
++oob;
193+
continue;
194+
}
195+
126196
// source pixel
127197
const si = ((y1 + row) * srcImage.width + x1 + col) * 4;
128198
const sr = srcPixels[ si ];
129199
const sg = srcPixels[ si+1 ];
130200
const sb = srcPixels[ si+2 ];
131201
const sa = srcPixels[ si+3 ];
202+
if (sa > 0) ++ntp;
132203

133204
// destination pixel
134-
const di = ((y2 + row) * srcImage.width + x2 + col) * 4;
135205
const dr = dstPixels[ di ];
136206
const dg = dstPixels[ di+1 ];
137207
const db = dstPixels[ di+2 ];
@@ -148,6 +218,8 @@ export class CanvasRenderingContext2D implements CanvasRect, CanvasDrawImage, Ca
148218
}
149219
}
150220
console.log(`${this}→drawImage( <${image.constructor.name}> ${Array.from(arguments).join(', ')} )`);
221+
console.log(`${this}→drawImage(): number of non-transparent source pixels drawn: ${ntp} (${ntp/(srcPixels.length/4)*100|0}%)`);
222+
console.log(`${this}→drawImage(): skipped drawing of ${oob} out-of-bounds pixels on the canvas`);
151223
return;
152224
}
153225

@@ -233,14 +305,46 @@ export class CanvasRenderingContext2D implements CanvasRect, CanvasDrawImage, Ca
233305
setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
234306
setTransform(transform?: DOMMatrix2DInit): void;
235307
setTransform(matrixOrA?: any, b?, c?, d?, e?, f?) {
236-
console.log(`${this} Not implemented: context2d.setTransform( ${Array.from(arguments).join(', ')} )`);
308+
// Expand calls using a DOMMatrix2D object
309+
if (typeof matrixOrA === 'object') {
310+
if ('a' in matrixOrA || 'b' in matrixOrA || 'c' in matrixOrA || 'd' in matrixOrA || 'e' in matrixOrA || 'f' in matrixOrA ||
311+
'm11' in matrixOrA || 'm12' in matrixOrA || 'm21' in matrixOrA || 'm22' in matrixOrA || 'm31' in matrixOrA || 'm32' in matrixOrA) {
312+
return this.setTransform(
313+
matrixOrA.a ?? matrixOrA.m11, matrixOrA.b ?? matrixOrA.m12, matrixOrA.c ?? matrixOrA.m21,
314+
matrixOrA.dx ?? matrixOrA.m22, matrixOrA.e ?? matrixOrA.m31, matrixOrA.f ?? matrixOrA.m32
315+
);
316+
}
317+
} else {
318+
const a = matrixOrA;
319+
320+
if ( b !== 0 || c !== 0) {
321+
console.log(`${this} Not implemented: context2d.setTransform( ${Array.from(arguments).join(', ')} ) skew/rotate transforms`);
322+
}
323+
324+
this.scale(a,d);
325+
this.translate(e,f);
326+
327+
console.log(`${this}→setTransform( ${Array.from(arguments).join(', ')} )`);
328+
}
329+
}
330+
scale(xScale: number, yScale: number) {
331+
this[STATE].scaleX = xScale;
332+
this[STATE].scaleY = yScale;
333+
}
334+
translate(x: number, y: number) {
335+
this[STATE].translateX = x;
336+
this[STATE].translateY = y;
237337
}
238338

239339
// Stringifies the context object with its canvas & unique ID to ease debugging
240340
get [Symbol.toStringTag]() {
241341
return `${this.canvas[Symbol.toStringTag]}::context2d`;
242342
}
243343

344+
private setPixel(x,y,r,g,b,a) {
345+
346+
}
347+
244348
// https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
245349
private get fillStyleRGBA(): RGBAColor {
246350
let c;

src/js-canvas/WasmResize.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// @ts-nocheck
2+
// Based on squoosh_resize_bg.js at v0.12.0
3+
// import * as wasm from './squoosh_resize_bg.wasm';
4+
5+
import wasmInit from '../../node_modules/squoosh/codecs/resize/pkg/squoosh_resize_bg.wasm';
6+
// import { readFile } from 'node:fs/promises';
7+
// const wasmFile = await WebAssembly.compile(
8+
// await readFile(new URL('../../node_modules/squoosh/codecs/resize/pkg/squoosh_resize_bg.wasm', import.meta.url)),
9+
// );
10+
// const wasmInstance = await WebAssembly.instantiate(wasmFile, {});
11+
// const wasm = wasmInstance.exports;
12+
const wasmInstance = wasmInit({});
13+
const wasm = wasmInstance.exports;
14+
console.log('Wasm init:', wasmInstance, wasm);
15+
16+
let cachegetUint8Memory0 = null;
17+
function getUint8Memory0() {
18+
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
19+
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
20+
}
21+
return cachegetUint8Memory0;
22+
}
23+
24+
let WASM_VECTOR_LEN = 0;
25+
26+
function passArray8ToWasm0(arg, malloc) {
27+
const ptr = malloc(arg.length * 1);
28+
getUint8Memory0().set(arg, ptr / 1);
29+
WASM_VECTOR_LEN = arg.length;
30+
return ptr;
31+
}
32+
33+
let cachegetInt32Memory0 = null;
34+
function getInt32Memory0() {
35+
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
36+
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
37+
}
38+
return cachegetInt32Memory0;
39+
}
40+
41+
function getArrayU8FromWasm0(ptr, len) {
42+
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
43+
}
44+
/**
45+
* @param {Uint8Array} input_image
46+
* @param {number} input_width
47+
* @param {number} input_height
48+
* @param {number} output_width
49+
* @param {number} output_height
50+
* @param {number} typ_idx
51+
* @param {boolean} premultiply
52+
* @param {boolean} color_space_conversion
53+
* @returns {Uint8Array}
54+
*/
55+
export function resize(input_image, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion) {
56+
var ptr0 = passArray8ToWasm0(input_image, wasm.__wbindgen_malloc);
57+
var len0 = WASM_VECTOR_LEN;
58+
wasm.resize(8, ptr0, len0, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion);
59+
var r0 = getInt32Memory0()[8 / 4 + 0];
60+
var r1 = getInt32Memory0()[8 / 4 + 1];
61+
var v1 = getArrayU8FromWasm0(r0, r1).slice();
62+
wasm.__wbindgen_free(r0, r1 * 1);
63+
return v1;
64+
}
65+
66+
export function resizeImage(image: ImageData, output_width: number, output_height: number): ImageData {
67+
const input_image = image.data
68+
const input_width = image.width
69+
const input_height = image.height
70+
71+
// https://github.com/GoogleChromeLabs/squoosh/blob/dev/codecs/resize/src/lib.rs
72+
// 0 => Type::Triangle,
73+
// 1 => Type::Catrom,
74+
// 2 => Type::Mitchell,
75+
// 3 => Type::Lanczos3,
76+
const typ_idx = 3
77+
78+
const premultiply = true
79+
const color_space_conversion = false
80+
81+
const output_image = resize(input_image, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion);
82+
83+
return {
84+
width: output_width|0,
85+
height: output_height|0,
86+
data: output_image
87+
}
88+
}

0 commit comments

Comments
 (0)