Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context isolation #6

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 75 additions & 14 deletions .codesandbox/templates/vanilla/src/testcases/simpleTextbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function testCase(canvas: fabric.Canvas) {
splitByGrapheme: true,
width: 200,
top: 20,
backgroundColor: 'yellow',
styles: fabric.util.stylesFromArray(
[
{
Expand All @@ -20,21 +21,81 @@ export function testCase(canvas: fabric.Canvas) {
],
textValue
),
clipPath: new fabric.Circle({
radius: 50,
originX: 'center',
originY: 'center',
scaleX: 2,
inverted: true,
fill: 'blue',
// opacity: 0.4,
}),
});
canvas.add(text);
canvas.centerObjectH(text);
const rect = new fabric.Rect({
fill: 'blue',
width: 100,
height: 50,
left: 0,
top: 100,
paintFirst: 'stroke',
shadow: new fabric.Shadow({
affectStroke: true,
blur: 5,
offsetX: 20,
offsetY: 20,
// nonScaling: true,
color: 'red',
}),
});
canvas.centerObject(text);
canvas.centerObject(rect);
canvas.preserveObjectStacking = true;
const group = new fabric.Group([rect, text], {
subTargetCheck: true,
interactive: true,
clipPath: new fabric.Circle({
radius: 100,
originX: 'center',
originY: 'center',
}),
});
canvas.add(group);

function animate(toState) {
text.animate(
{ scaleX: Math.max(toState, 0.1) * 2 },
{
onChange: () => canvas.renderAll(),
onComplete: () => animate(!toState),
duration: 1000,
easing: toState
? fabric.util.ease.easeInOutQuad
: fabric.util.ease.easeInOutSine,
}
);
fabric.util.animate({
startValue: 1 - Number(toState),
endValue: Number(toState),
onChange: (value) => {
text.clipPath?.set({
scaleX: Math.max(value, 0.1) * 2,
opacity: value,
});
text.set({ dirty: true });
rect.shadow!.offsetX = 20 * (value + 1);
rect.shadow!.blur = 20 * value;
rect.set({ dirty: true });
canvas.renderAll();
},
onComplete: () => animate(!toState),
duration: 150,
easing: toState
? fabric.util.ease.easeInOutQuad
: fabric.util.ease.easeInOutSine,
});
// text.clipPath!.animate(
// {
// scaleX: Math.max(Number(toState), 0.1) * 2,
// opacity: Number(toState),
// },
// {
// onChange: () => canvas.requestRenderAll(),
// onComplete: () => animate(!toState),
// duration: 150,
// easing: toState
// ? fabric.util.ease.easeInOutQuad
// : fabric.util.ease.easeInOutSine,
// }
// );
}
// animate(1);
animate(1);
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- chore(TS): use consistent and improved types for getDefaults and ownDefaults [#9698](https://github.com/fabricjs/fabric.js/pull/9698)
- fix(SVGParser): Don't crash on nested CSS at-rules [#9707](https://github.com/fabricjs/fabric.js/pull/9707)
- perf(): measuring canvas size [#9697](https://github.com/fabricjs/fabric.js/pull/9697)
- WIP fix(): context isolation [#9693](https://github.com/fabricjs/fabric.js/pull/9693)
- chore(TS): Add type for options in toCanvasElement and toDataUrl [#9673](https://github.com/fabricjs/fabric.js/pull/9673)
- ci(): add source map support to node sandbox [#9686](https://github.com/fabricjs/fabric.js/pull/9686)
- fix(Canvas): Correct type mainTouchId initialization [#9684](https://github.com/fabricjs/fabric.js/pull/9684)
Expand Down
235 changes: 235 additions & 0 deletions src/canvas/CanvasProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { getEnv } from '../env';
import { TSize } from '../typedefs';

Check failure on line 2 in src/canvas/CanvasProvider.ts

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`
import { createCanvasElement } from '../util/misc/dom';

type RenderingContextType = '2d' | 'webgl';

type RenderingContextProviderMap = {
'2d': {
ctx: CanvasRenderingContext2D;
options: CanvasRenderingContext2DSettings;
};
webgl: { ctx: WebGLRenderingContext; options: WebGLContextAttributes };
// webgl2: { ctx: WebGL2RenderingContext; options: WebGLContextAttributes };
// bitmaprenderer: {
// ctx: ImageBitmapRenderingContext;
// options: ImageBitmapRenderingContextSettings;
// };
};

type RenderingContext<T extends RenderingContextType = RenderingContextType> =
RenderingContextProviderMap[T]['ctx'];

type RenderingContextOptions<
T extends RenderingContextType = RenderingContextType
> = RenderingContextProviderMap[T]['options'];

type RenderingContextProvider<
T extends RenderingContextType = RenderingContextType
> = (
type: T,
options?: RenderingContextOptions<T>
) => RenderingContext<T> | null;

type StatefulRenderingContext<
T extends RenderingContextType = RenderingContextType
> = RenderingContext<T> & {
__type: T;
__options: RenderingContextOptions<T>;
__locked: boolean;
release(): void;
};

const isEqualOrDefault = <T>(
a: T | undefined,
b: T | undefined,
defaultValue: T
) => a === b || (!a && b === defaultValue) || (a === defaultValue && !b);

class CanvasProvider {
static compare2dOptions(
a: RenderingContextOptions<'2d'> | undefined,
b: RenderingContextOptions<'2d'> | undefined
) {
return (
a === b ||
isEqualOrDefault(a?.alpha, b?.alpha, false) ||
isEqualOrDefault(a?.colorSpace, b?.colorSpace, 'srgb') ||
isEqualOrDefault(a?.desynchronized, b?.desynchronized, false) ||
isEqualOrDefault(a?.willReadFrequently, b?.willReadFrequently, false)
);
}

static compareWebGLOptions(
a: RenderingContextOptions<'webgl'> | undefined,
b: RenderingContextOptions<'webgl'> | undefined
) {
return (
a === b ||
isEqualOrDefault(a?.alpha, b?.alpha, false) ||
isEqualOrDefault(a?.antialias, b?.antialias, false) ||
isEqualOrDefault(a?.depth, b?.depth, false) ||
isEqualOrDefault(a?.desynchronized, b?.depth, false) ||
isEqualOrDefault(
a?.failIfMajorPerformanceCaveat,
b?.failIfMajorPerformanceCaveat,
false
) ||
isEqualOrDefault(a?.powerPreference, b?.powerPreference, 'default') ||
isEqualOrDefault(a?.premultipliedAlpha, b?.premultipliedAlpha, false) ||
isEqualOrDefault(
a?.preserveDrawingBuffer,
b?.preserveDrawingBuffer,
false
) ||
isEqualOrDefault(a?.stencil, b?.stencil, false)
);
}

static compareOptions<T extends RenderingContextType>(
type: T,
a: RenderingContextOptions<T> | undefined,
b: RenderingContextOptions<T> | undefined
) {
switch (type) {
case '2d':
return this.compare2dOptions(a, b);
case 'webgl':
return this.compareWebGLOptions(a, b);
}
}

private builder: RenderingContextProvider = <T extends RenderingContextType>(
type: T,
options?: RenderingContextOptions<T>
) => {
return createCanvasElement().getContext(
type,
options
) as RenderingContext<T>;
};
private stack: StatefulRenderingContext[] = [];
private pruned: StatefulRenderingContext[] = [];
private pruning = false;
private locked: boolean;

public registerBuilder(builder: RenderingContextProvider) {
this.builder = builder;
}

public create<T extends RenderingContextType>(
type: T,
options?: RenderingContextOptions<T>
) {
const value = Object.defineProperties(this.builder(type, options), {
__type: {
value: type,
enumerable: false,
configurable: false,
writable: false,
},
__options: {
value: options,
enumerable: false,
configurable: false,
writable: false,
},
__locked: {
value: false,
enumerable: false,
configurable: false,
writable: true,
},
release: {
value() {
this.__locked = false;
},
enumerable: false,
configurable: false,
writable: false,
},
}) as StatefulRenderingContext<T>;
this.stack.push(value);
return value;
}

public request<T extends RenderingContextType = '2d'>(
{ type = '2d' as T, width, height }: { type?: T } & TSize,
options?: RenderingContextOptions<T>
): StatefulRenderingContext<T> {
const ctx = (this.stack.find(
(item) =>
!item.__locked &&
item.__type === type &&
(this.constructor as typeof CanvasProvider).compareOptions(
type,
item.__options,
options
)
) || this.create(type, options)) as StatefulRenderingContext<T>;
ctx.__locked = true;
this.pruning && this.pruned.push(ctx);
const { canvas } = ctx;
canvas.width = width;
canvas.height = height;
return ctx;
}

/**
* Inform that resources are locked and can't be freed
* {@link dispose} and {@link prune} will have no affect while {@link locked}
*/
lock() {
this.locked = true;
}

/**
* Inform that the instance is idle so that resources can be freed upon request
*/
unlock() {
this.locked = false;
}

/**
* Call this at the beginning of the rendering cycle of the deepest object tree
* Call {@link prune} at the end of the rendering cycle to cleanup unused resources
*/
public beginPruning() {
this.pruning = true;
this.pruned = [];
}

/**
* Dispose of unused resources
* Notice that this method should be called after {@link beginPruning} at the end of the rendering cycle
* Calling this method without calling {@link beginPruning} or during a rendering cycle has no effect
*/
public prune() {
if (this.locked || !this.pruning) {
return;
}
this.stack
.filter((ctx) => !this.pruned.includes(ctx))
.forEach((ctx) => getEnv().dispose(ctx.canvas));
this.stack = this.pruned;
this.pruned = [];
this.pruning = false;
}

/**
* Dispose of all resources
* Notice that this method can be called at anytime
* However calling during a rendering cycle will have no effect
*/
public dispose() {
if (this.locked) {
return;
}
this.stack.map((ctx) => getEnv().dispose(ctx.canvas));
this.stack = [];
this.pruned = [];
this.pruning = false;
}
}

export const canvasProvider = new CanvasProvider();
10 changes: 6 additions & 4 deletions src/canvas/StaticCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type { StaticCanvasOptions } from './StaticCanvasOptions';
import { staticCanvasDefaults } from './StaticCanvasOptions';
import { log, FabricError } from '../util/internals/console';
import { getDevicePixelRatio } from '../env';
import { canvasProvider } from './CanvasProvider';

/**
* Having both options in TCanvasSizeOptions set to true transform the call in a calcOffset
Expand Down Expand Up @@ -567,6 +568,8 @@ export class StaticCanvas<
return;
}

canvasProvider.lock();

const v = this.viewportTransform,
path = this.clipPath;
this.calcViewportBoundaries();
Expand All @@ -587,18 +590,17 @@ export class StaticCanvas<
}
if (path) {
path._set('canvas', this);
// needed to setup a couple of variables
path.shouldCache();
path._transformDone = true;
path.renderCache({ forClipping: true });
this.drawClipPathOnCanvas(ctx, path as TCachedFabricObject);
path.renderInIsolation(ctx, true);
}
this._renderOverlay(ctx);
if (this.controlsAboveOverlay) {
this.drawControls(ctx);
}
this.fire('after:render', { ctx });

canvasProvider.unlock();

if (this.__cleanupTask) {
this.__cleanupTask();
this.__cleanupTask = undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/env/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export type TFabricEnv = {
readonly window: (Window & typeof globalThis) | DOMWindow;
readonly isTouchSupported: boolean;
WebGLProbe: GLProbe;
dispose(element: Element): void;
dispose(element: Element | OffscreenCanvas): void;
copyPasteData: TCopyPasteData;
};
Loading
Loading