Skip to content

Commit

Permalink
feat: support offscreen canvas
Browse files Browse the repository at this point in the history
closes #32
  • Loading branch information
davidenke committed Sep 13, 2024
1 parent c2cfe32 commit 6893b2b
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 41 deletions.
9 changes: 7 additions & 2 deletions src/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ applyProxy({
return;
}

// prepare a clone of the canvas to to adopt its settings
const _canvas = ctx.canvas.cloneNode() as HTMLCanvasElement;
// prepare a clone of the (offscreen) canvas to adopt its settings
let _canvas: HTMLCanvasElement | OffscreenCanvas;
if (ctx.canvas instanceof HTMLCanvasElement) {
_canvas = ctx.canvas.cloneNode() as HTMLCanvasElement;
} else {
_canvas = new OffscreenCanvas(ctx.canvas.width, ctx.canvas.height);
}
const clone = _canvas.getContext('2d')!;
clone.__cloned = true;

Expand Down
22 changes: 15 additions & 7 deletions src/utils/context.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CanvasRenderingContext2DHistory } from './history.utils.js';
import type { Context2DHistory } from './history.utils.js';
import type { Context2D } from './proxy.utils.js';

declare global {
// some utility types
Expand All @@ -9,21 +10,28 @@ declare global {
type Writeable<T> = { -readonly [P in keyof T]: T[P] };

// all callable functions in CanvasRenderingContext2D
type CanvasRenderingContext2DFn = PickByType<
CanvasRenderingContext2D,
type Context2DFn = PickByType<
Context2D,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
Function
>;

// add some properties to the 2d context
// add some properties to the 2d contexts
interface CanvasRenderingContext2D {
__cloned: boolean;
__withoutSideEffects: CanvasRenderingContext2DFn;
__withoutSideEffects: Context2DFn;
}
interface OffscreenCanvasRenderingContext2D {
__cloned: boolean;
__withoutSideEffects: Context2DFn;
}

// add a history to the canvas element
// add a history to the canvas objects
interface HTMLCanvasElement {
__history: CanvasRenderingContext2DHistory;
__history: Context2DHistory;
}
interface OffscreenCanvas {
__history: Context2DHistory;
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/utils/history.utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export type CanvasRenderingContext2DHistoryEntry = {
import type { Context2D } from './proxy.utils';

export type Context2DHistoryEntry = {
type: 'set' | 'apply' | 'draw';
prop: keyof CanvasRenderingContext2D;
value?: unknown;
args?: unknown[];
};

export class CanvasRenderingContext2DHistory extends Set<CanvasRenderingContext2DHistoryEntry> {
applyTo(context: CanvasRenderingContext2D) {
export class Context2DHistory extends Set<Context2DHistoryEntry> {
applyTo(context: Context2D): void {
this.forEach(({ type, prop, value, args }) => {
switch (type) {
case 'set':
Expand All @@ -23,7 +25,7 @@ export class CanvasRenderingContext2DHistory extends Set<CanvasRenderingContext2

lastValueOf(
prop: keyof CanvasRenderingContext2D,
): CanvasRenderingContext2DHistoryEntry | undefined {
): Context2DHistoryEntry | undefined {
return [...this].findLast(entry => entry.prop === prop);
}
}
59 changes: 31 additions & 28 deletions src/utils/proxy.utils.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,51 @@
import { isDrawingFn } from './context.utils.js';
import { CanvasRenderingContext2DHistory } from './history.utils.js';
import { Context2DHistory } from './history.utils.js';

export type Context2D =
| CanvasRenderingContext2D
| OffscreenCanvasRenderingContext2D;

export type Context2DCtor = new () => Context2D;

export type ProxyOptions = {
onDraw: (
context: CanvasRenderingContext2D,
drawFn: string,
args?: unknown[],
) => void;
onDraw: (context: Context2D, drawFn: string, args?: unknown[]) => void;
};

export function applyProxy({ onDraw }: Partial<ProxyOptions> = {}) {
function applyProxyTo(
objectCtor: typeof HTMLCanvasElement | typeof OffscreenCanvas,
contextCtor: Context2DCtor,
{ onDraw }: Partial<ProxyOptions>,
) {
// create a mirror of the 2d context
const mirror = {
__cloned: false,
__withoutSideEffects: {},
} as unknown as CanvasRenderingContext2D;
} as unknown as typeof contextCtor;

// copy all properties from the original context
const properties = Object.getOwnPropertyDescriptors(
CanvasRenderingContext2D.prototype,
);
const properties = Object.getOwnPropertyDescriptors(contextCtor.prototype);
Object.defineProperties(mirror, properties);
Object.keys(properties).forEach(prop => {
// @ts-expect-error - we're doing nasty things here
delete CanvasRenderingContext2D.prototype[prop];
delete contextCtor.prototype[prop];
});

// prepare history on canvas
Object.defineProperty(HTMLCanvasElement.prototype, '__history', {
get(): CanvasRenderingContext2DHistory {
Object.defineProperty(objectCtor.prototype, '__history', {
get(): Context2DHistory {
if (!this.___history) {
this.___history = new CanvasRenderingContext2DHistory();
this.___history = new Context2DHistory();
}
return this.___history;
},
set(value: CanvasRenderingContext2DHistory) {
set(value: Context2DHistory) {
this.___history = value;
},
});

Object.setPrototypeOf(
CanvasRenderingContext2D.prototype,
new Proxy<CanvasRenderingContext2D>(mirror, {
get(
target,
prop: keyof CanvasRenderingContext2D,
receiver: CanvasRenderingContext2D,
) {
contextCtor.prototype,
new Proxy<Context2DCtor>(mirror, {
get(target, prop: keyof Context2D, receiver: Context2D) {
// trap access to the __withoutSideEffects property to provide a proxy
// which exposes all 2d unpatched functions without side effects
if (prop === '__withoutSideEffects') {
Expand Down Expand Up @@ -82,10 +81,10 @@ export function applyProxy({ onDraw }: Partial<ProxyOptions> = {}) {
return Reflect.get(target, prop, receiver);
},
set(
target: CanvasRenderingContext2D,
prop: keyof CanvasRenderingContext2D,
target: Context2DCtor,
prop: keyof Context2DCtor,
value: unknown,
receiver: CanvasRenderingContext2D,
receiver: Context2D,
) {
// update history
receiver.canvas.__history.add({ type: 'set', prop, value });
Expand All @@ -95,9 +94,13 @@ export function applyProxy({ onDraw }: Partial<ProxyOptions> = {}) {
if (prop === 'filter') return true;

// handle property set: a.b = value
// @ts-expect-error - TS2589: type instantiation is excessively deep and possibly infinite
return Reflect.set(target, prop, value, receiver);
},
}),
);
}

export function applyProxy(options: Partial<ProxyOptions> = {}) {
applyProxyTo(HTMLCanvasElement, CanvasRenderingContext2D, options);
applyProxyTo(OffscreenCanvas, OffscreenCanvasRenderingContext2D, options);
}

0 comments on commit 6893b2b

Please sign in to comment.