Skip to content

Commit

Permalink
feat(offscreen): fix typing errors
Browse files Browse the repository at this point in the history
  • Loading branch information
DeltaZN committed Dec 2, 2023
1 parent 5cfaecf commit b638f74
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 49 deletions.
149 changes: 143 additions & 6 deletions src/chart/canvas/offscreen/canvas-offscreen-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CanvasModel } from '../../model/canvas.model';
import {
ARC,
BEGIN_PATH,
Expand Down Expand Up @@ -67,7 +68,7 @@ export const strsToSync: Array<string | number> = [];
* In this case we store the string in the global pool (strSync) and write the id (number) of the string to the buffer.
* After that, before the draw command we need to perform synchronization of this pool between main thread and worker thread.
*/
export class CanvasOffscreenContext2D implements Partial<CanvasRenderingContext2D> {
export class CanvasOffscreenContext2D implements CanvasRenderingContext2D {
public commands: Float64Array;
public buffer: SharedArrayBuffer;
/**
Expand All @@ -87,7 +88,7 @@ export class CanvasOffscreenContext2D implements Partial<CanvasRenderingContext2

private __font: string = '12px Arial';

constructor(public idx: number) {
constructor(public canvas: HTMLCanvasElement) {
this.buffer = new SharedArrayBuffer(8 * 100000);
this.commands = new Float64Array(this.buffer);
this.commit();
Expand Down Expand Up @@ -121,17 +122,15 @@ export class CanvasOffscreenContext2D implements Partial<CanvasRenderingContext2
this.commands[this.counter++] = val;
}

set fillStyle(val: string | CanvasGradient | CanvasPattern | undefined) {
set fillStyle(val: string) {
this.commands[this.counter++] = FILL_STYLE;
this.commands[this.counter++] = -1;
// @ts-ignore
this.commands[this.counter++] = this.getStrPtr(val);
}

set strokeStyle(val: string | CanvasGradient | CanvasPattern | undefined) {
set strokeStyle(val: string) {
this.commands[this.counter++] = STROKE_STYLE;
this.commands[this.counter++] = -1;
// @ts-ignore
this.commands[this.counter++] = this.getStrPtr(val);
}

Expand Down Expand Up @@ -331,4 +330,142 @@ export class CanvasOffscreenContext2D implements Partial<CanvasRenderingContext2
this.commands[this.counter++] = x;
this.commands[this.counter++] = y;
}

//#region unimplemented
globalAlpha = 0;
globalCompositeOperation = 'color' as const;
imageSmoothingEnabled = false;
imageSmoothingQuality = 'high' as const;
lineDashOffset = 0;
lineJoin = 'round' as const;
miterLimit = 0;
shadowBlur = 0;
shadowColor = '';
shadowOffsetX = 0;
shadowOffsetY = 0;
direction = 'inherit' as const;
fontKerning = 'auto' as const;
filter = '';
textAlign = 'center' as const;
textBaseline = 'alphabetic' as const;
getContextAttributes(): CanvasRenderingContext2DSettings {
throw new Error('Method not implemented.');
}
drawImage(image: CanvasImageSource, dx: number, dy: number): void;
drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
drawImage(
image: CanvasImageSource,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number,
): void;
drawImage(
image: unknown,
sx: unknown,
sy: unknown,
sw?: unknown,
sh?: unknown,
dx?: unknown,
dy?: unknown,
dw?: unknown,
dh?: unknown,
): void;
drawImage(): void {
throw new Error('Method not implemented.');
}
isPointInPath(x: number, y: number, fillRule?: CanvasFillRule | undefined): boolean;
isPointInPath(path: Path2D, x: number, y: number, fillRule?: CanvasFillRule | undefined): boolean;
isPointInPath(path: unknown, x: unknown, y?: unknown, fillRule?: unknown): boolean;
isPointInPath(): boolean {
throw new Error('Method not implemented.');
}
isPointInStroke(x: number, y: number): boolean;
isPointInStroke(path: Path2D, x: number, y: number): boolean;
isPointInStroke(path: unknown, x: unknown, y?: unknown): boolean;
isPointInStroke(): boolean {
throw new Error('Method not implemented.');
}
createConicGradient(startAngle: number, x: number, y: number): CanvasGradient;
createConicGradient(): CanvasGradient {
throw new Error('Method not implemented.');
}
createPattern(image: CanvasImageSource, repetition: string | null): CanvasPattern | null;
createPattern(): CanvasPattern | null {
throw new Error('Method not implemented.');
}
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
createRadialGradient(): CanvasGradient {
throw new Error('Method not implemented.');
}
createImageData(sw: number, sh: number, settings?: ImageDataSettings | undefined): ImageData;
createImageData(imagedata: ImageData): ImageData;
createImageData(sw: unknown, sh?: unknown, settings?: unknown): ImageData;
createImageData(): ImageData {
throw new Error('Method not implemented.');
}
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
arcTo(): void {
throw new Error('Method not implemented.');
}
ellipse(
x: number,
y: number,
radiusX: number,
radiusY: number,
rotation: number,
startAngle: number,
endAngle: number,
counterclockwise?: boolean | undefined,
): void;
ellipse(): void {
throw new Error('Method not implemented.');
}
roundRect(
x: number,
y: number,
w: number,
h: number,
radii?: number | DOMPointInit | (number | DOMPointInit)[] | undefined,
): void;
roundRect(): void {
throw new Error('Method not implemented.');
}
getLineDash(): number[] {
throw new Error('Method not implemented.');
}
getTransform(): DOMMatrix {
throw new Error('Method not implemented.');
}
resetTransform(): void {
throw new Error('Method not implemented.');
}
rotate(): void {
throw new Error('Method not implemented.');
}
setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
setTransform(transform?: DOMMatrix2DInit | undefined): void;
setTransform(): void {
throw new Error('Method not implemented.');
}
transform(): void {
throw new Error('Method not implemented.');
}
translate(): void {
throw new Error('Method not implemented.');
}
drawFocusIfNeeded(element: Element): void;
drawFocusIfNeeded(path: Path2D, element: Element): void;
drawFocusIfNeeded(): void {
throw new Error('Method not implemented.');
}
//#endregion
}

export function isOffscreenCanvasModel(model: CanvasModel): model is CanvasModel<CanvasOffscreenContext2D> {
return model.options.offscreen ?? false;
}
12 changes: 4 additions & 8 deletions src/chart/canvas/offscreen/init-offscreen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { wrap, transfer, Remote } from 'comlink';
import { Remote, transfer, wrap } from 'comlink';
import { CanvasModel } from '../../model/canvas.model';
import { isOffscreenCanvasModel } from './canvas-offscreen-wrapper';
import { OffscreenWorker } from './offscreen-worker';
import { CanvasOffscreenContext2D } from './canvas-offscreen-wrapper';
// const OffscreenWorker = wrap<any>(new Worker(new URL('./offscreen-worker.js', import.meta.url)));
const OffscreenWorkerClass = wrap<typeof OffscreenWorker>(new Worker(new URL('http://localhost:3000/worker.js')));
// create global worker instance, so every chart will use the same worker
Expand All @@ -16,18 +16,14 @@ export const initOffscreenWorker = async (canvases: CanvasModel[]): Promise<Remo
const startOffset = canvasesIdxOffset;
canvasesIdxOffset += 10;
for (let i = 0; i < canvases.length; i++) {
if (!canvases[i].options.offscreen) {
const canvas = canvases[i];
if (!isOffscreenCanvasModel(canvas)) {
continue;
}
// @ts-ignore
// eslint-disable-next-line no-restricted-syntax
const canvas = canvases[i] as CanvasModel<CanvasOffscreenContext2D>;
const offscreen = canvas.canvas.transferControlToOffscreen();
const idx = startOffset + i;
canvas.idx = idx;
canvas.ctx.idx = idx;
await worker.addCanvas(idx, canvas.options, transfer(offscreen, [offscreen]), canvas.ctx.buffer);
}

return worker;
};
26 changes: 13 additions & 13 deletions src/chart/canvas/offscreen/offscreen-worker.d.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
export declare class OffscreenWorker {
constructor(): void;

/**
* Adds offscreen canvas to the worker
*/
/**
* Adds offscreen canvas to the worker
*/
addCanvas(
idx: number,
options: CanvasRenderingContext2DSettings,
canvas: OffscreenCanvas,
sharedMemory: SharedArrayBuffer,
): void;

/**
* Syncs an array of strings and their ids between main thread and worker thread
* the format of data in this array is [str1, id1, str2, id2, ...].
* For detailed explanation @see CanvasOffscreenContext2D class
*/
syncStrings(strings: Array<string | number>): void;
/**
* Syncs an array of strings and their ids between main thread and worker thread
* the format of data in this array is [str1, id1, str2, id2, ...].
* For detailed explanation @see CanvasOffscreenContext2D class
*/
syncStrings(strings: Array<string | number>): void;

/**
* Executes canvas commands for the given canvas ids
*/
executeCanvasCommands(canvasIds: number[]): void;
/**
* Executes canvas commands for the given canvas ids
*/
executeCanvasCommands(canvasIds: number[]): void;
}
25 changes: 16 additions & 9 deletions src/chart/drawers/drawing-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Remote } from 'comlink';
import { strsToSync } from '../canvas/offscreen/canvas-offscreen-wrapper';
import {
CanvasOffscreenContext2D,
isOffscreenCanvasModel,
strsToSync,
} from '../canvas/offscreen/canvas-offscreen-wrapper';
import { initOffscreenWorker } from '../canvas/offscreen/init-offscreen';
import { OffscreenWorker } from '../canvas/offscreen/offscreen-worker';
import EventBus from '../events/event-bus';
Expand Down Expand Up @@ -59,9 +63,11 @@ export class DrawingManager {
private animFrameId = `draw_${uuid()}`;
private readyDraw = false;
private worker?: Remote<OffscreenWorker>;
private offscreenCanvases: CanvasModel<CanvasOffscreenContext2D>[] = [];

constructor(eventBus: EventBus, private chartResizeHandler: ChartResizeHandler, private canvases: CanvasModel[]) {
constructor(eventBus: EventBus, private chartResizeHandler: ChartResizeHandler, canvases: CanvasModel[]) {
initOffscreenWorker(canvases).then(worker => {
this.offscreenCanvases = canvases.filter(isOffscreenCanvasModel);
this.worker = worker;
this.readyDraw = true;
eventBus.fireDraw();
Expand Down Expand Up @@ -91,27 +97,26 @@ export class DrawingManager {
}
this.forceDraw();
this.drawHitTestCanvas();
await this.drawInWorker();
await this.drawOffscreen();
this.readyDraw = true;
this.canvasIdsList = [];
});
}
});
}

private async drawInWorker() {
private async drawOffscreen() {
if (this.worker === undefined) {
return;
}
// @ts-ignore
// commit method exists only in offscreen context class and it adds END_OF_FILE marker to the buffer
// commit method exists only in offscreen context class and adds END_OF_FILE marker to the buffer
// so worker knows where is the end of commands
this.canvases.forEach(canvas => canvas.ctx.commit?.());
this.offscreenCanvases.forEach(canvas => canvas.ctx.commit());
if (strsToSync.length) {
await this.worker.syncStrings(strsToSync);
strsToSync.length = 0;
}
await this.worker.executeCanvasCommands(this.canvases.map(canvas => canvas.idx));
await this.worker.executeCanvasCommands(this.offscreenCanvases.map(canvas => canvas.idx));
}

/**
Expand All @@ -120,9 +125,11 @@ export class DrawingManager {
* If all canvases update in separate animation frames - we see visual lag. Instead we should do all updates and then redraw.
* @doc-tags tricky,canvas,resize
*/
public redrawCanvasesImmediate() {
public async redrawCanvasesImmediate() {
this.chartResizeHandler.fireUpdates();
this.forceDraw();
await this.drawOffscreen();
this.readyDraw = true;
}

drawLastBar() {
Expand Down
29 changes: 19 additions & 10 deletions src/chart/model/canvas.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { CanvasOffscreenContext2D } from '../canvas/offscreen/canvas-offscreen-wrapper';
import { CanvasOffscreenContext2D, isOffscreenCanvasModel } from '../canvas/offscreen/canvas-offscreen-wrapper';
import { BarType, FullChartConfig } from '../chart.config';
import EventBus from '../events/event-bus';
import { PickedDOMRect } from '../inputhandlers/chart-resize.handler';
Expand Down Expand Up @@ -31,6 +31,7 @@ export class CanvasModel<T extends CanvasRenderingContext2D = CanvasRenderingCon
public readonly _canvasId: string;
type: CanvasBarType = CANDLE_TYPE;
constructor(
ctx: T,
private eventBus: EventBus,
public canvas: HTMLCanvasElement,
canvasModels: CanvasModel[],
Expand All @@ -41,11 +42,6 @@ export class CanvasModel<T extends CanvasRenderingContext2D = CanvasRenderingCon
this.parent = findHeightParent(canvas);

this._canvasId = canvas.getAttribute('data-element') ?? '';
const ctx = options.offscreen ? new CanvasOffscreenContext2D(this.idx) : canvas.getContext('2d', options);
if (ctx === null) {
throw new Error("Couldn't get 2d context????");
}
// @ts-ignore
this.context = ctx;
this.updateCanvasWidthHeight(canvas, this.getChartResizerElement().getBoundingClientRect());
}
Expand All @@ -59,10 +55,8 @@ export class CanvasModel<T extends CanvasRenderingContext2D = CanvasRenderingCon
this.canvas.style.height = height + 'px';
this.canvas.style.width = width + 'px';
const dpi = window.devicePixelRatio;
if (this.options.offscreen) {
// @ts-ignore
if (isOffscreenCanvasModel(this)) {
this.ctx.width = width * dpi;
// @ts-ignore
this.ctx.height = height * dpi;
} else {
this.canvas.width = width * dpi;
Expand Down Expand Up @@ -225,11 +219,26 @@ export function createCanvasModel(
resizer?: HTMLElement,
options?: CanvasModelOptions,
): CanvasModel {
const canvasModel = new CanvasModel(eventBus, canvas, canvasModels, resizer, options);
const canvasModel = new CanvasModel(
getCanvasContext(canvas, options),
eventBus,
canvas,
canvasModels,
resizer,
options,
);
initCanvasWithConfig(canvasModel, config);
return canvasModel;
}

export const getCanvasContext = (canvas: HTMLCanvasElement, options?: CanvasModelOptions): CanvasRenderingContext2D => {
const ctx = options?.offscreen ? new CanvasOffscreenContext2D(canvas) : canvas.getContext('2d', options);
if (ctx === null) {
throw new Error("Couldn't get 2d context. Canvas is not supported.");
}
return ctx;
};

/**
* Initializes a canvas with a given configuration.
* @param {CanvasModel} canvasModel - The canvas model to be initialized.
Expand Down
Loading

0 comments on commit b638f74

Please sign in to comment.