@flighthq/scene2d-canvas 0.2.1-next.840.857425c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canvasBackground.d.ts +3 -0
- package/dist/canvasBackground.d.ts.map +1 -0
- package/dist/canvasBackground.js +19 -0
- package/dist/canvasBackground.js.map +1 -0
- package/dist/canvasBitmap.d.ts +4 -0
- package/dist/canvasBitmap.d.ts.map +1 -0
- package/dist/canvasBitmap.js +36 -0
- package/dist/canvasBitmap.js.map +1 -0
- package/dist/canvasCSSFilterBinding.d.ts +24 -0
- package/dist/canvasCSSFilterBinding.d.ts.map +1 -0
- package/dist/canvasCSSFilterBinding.js +43 -0
- package/dist/canvasCSSFilterBinding.js.map +1 -0
- package/dist/canvasCache.d.ts +33 -0
- package/dist/canvasCache.d.ts.map +1 -0
- package/dist/canvasCache.js +141 -0
- package/dist/canvasCache.js.map +1 -0
- package/dist/canvasClip.d.ts +3 -0
- package/dist/canvasClip.d.ts.map +1 -0
- package/dist/canvasClip.js +38 -0
- package/dist/canvasClip.js.map +1 -0
- package/dist/canvasClipRectangle.d.ts +5 -0
- package/dist/canvasClipRectangle.d.ts.map +1 -0
- package/dist/canvasClipRectangle.js +31 -0
- package/dist/canvasClipRectangle.js.map +1 -0
- package/dist/canvasElement.d.ts +2 -0
- package/dist/canvasElement.d.ts.map +1 -0
- package/dist/canvasElement.js +9 -0
- package/dist/canvasElement.js.map +1 -0
- package/dist/canvasFillPattern.d.ts +4 -0
- package/dist/canvasFillPattern.d.ts.map +1 -0
- package/dist/canvasFillPattern.js +108 -0
- package/dist/canvasFillPattern.js.map +1 -0
- package/dist/canvasImageSource.d.ts +4 -0
- package/dist/canvasImageSource.d.ts.map +1 -0
- package/dist/canvasImageSource.js +45 -0
- package/dist/canvasImageSource.js.map +1 -0
- package/dist/canvasMaterialRegistry.d.ts +6 -0
- package/dist/canvasMaterialRegistry.d.ts.map +1 -0
- package/dist/canvasMaterialRegistry.js +42 -0
- package/dist/canvasMaterialRegistry.js.map +1 -0
- package/dist/canvasMaterials.d.ts +5 -0
- package/dist/canvasMaterials.d.ts.map +1 -0
- package/dist/canvasMaterials.js +39 -0
- package/dist/canvasMaterials.js.map +1 -0
- package/dist/canvasNode2D.d.ts +5 -0
- package/dist/canvasNode2D.d.ts.map +1 -0
- package/dist/canvasNode2D.js +46 -0
- package/dist/canvasNode2D.js.map +1 -0
- package/dist/canvasParticleEmitter2D.d.ts +4 -0
- package/dist/canvasParticleEmitter2D.d.ts.map +1 -0
- package/dist/canvasParticleEmitter2D.js +64 -0
- package/dist/canvasParticleEmitter2D.js.map +1 -0
- package/dist/canvasQuadBatch.d.ts +4 -0
- package/dist/canvasQuadBatch.d.ts.map +1 -0
- package/dist/canvasQuadBatch.js +64 -0
- package/dist/canvasQuadBatch.js.map +1 -0
- package/dist/canvasRegistration.d.ts +4 -0
- package/dist/canvasRegistration.d.ts.map +1 -0
- package/dist/canvasRegistration.js +39 -0
- package/dist/canvasRegistration.js.map +1 -0
- package/dist/canvasRenderState.d.ts +5 -0
- package/dist/canvasRenderState.d.ts.map +1 -0
- package/dist/canvasRenderState.js +44 -0
- package/dist/canvasRenderState.js.map +1 -0
- package/dist/canvasRenderTarget.d.ts +26 -0
- package/dist/canvasRenderTarget.d.ts.map +1 -0
- package/dist/canvasRenderTarget.js +80 -0
- package/dist/canvasRenderTarget.js.map +1 -0
- package/dist/canvasRichText.d.ts +6 -0
- package/dist/canvasRichText.d.ts.map +1 -0
- package/dist/canvasRichText.js +128 -0
- package/dist/canvasRichText.js.map +1 -0
- package/dist/canvasScale9Mapper.d.ts +10 -0
- package/dist/canvasScale9Mapper.d.ts.map +1 -0
- package/dist/canvasScale9Mapper.js +148 -0
- package/dist/canvasScale9Mapper.js.map +1 -0
- package/dist/canvasScale9Shape.d.ts +5 -0
- package/dist/canvasScale9Shape.d.ts.map +1 -0
- package/dist/canvasScale9Shape.js +156 -0
- package/dist/canvasScale9Shape.js.map +1 -0
- package/dist/canvasShape.d.ts +5 -0
- package/dist/canvasShape.d.ts.map +1 -0
- package/dist/canvasShape.js +79 -0
- package/dist/canvasShape.js.map +1 -0
- package/dist/canvasShapeCommands.d.ts +19 -0
- package/dist/canvasShapeCommands.d.ts.map +1 -0
- package/dist/canvasShapeCommands.js +352 -0
- package/dist/canvasShapeCommands.js.map +1 -0
- package/dist/canvasShapeRegistry.d.ts +5 -0
- package/dist/canvasShapeRegistry.d.ts.map +1 -0
- package/dist/canvasShapeRegistry.js +17 -0
- package/dist/canvasShapeRegistry.js.map +1 -0
- package/dist/canvasSprite.d.ts +5 -0
- package/dist/canvasSprite.d.ts.map +1 -0
- package/dist/canvasSprite.js +61 -0
- package/dist/canvasSprite.js.map +1 -0
- package/dist/canvasTextInput.d.ts +4 -0
- package/dist/canvasTextInput.d.ts.map +1 -0
- package/dist/canvasTextInput.js +67 -0
- package/dist/canvasTextInput.js.map +1 -0
- package/dist/canvasTextLabel.d.ts +4 -0
- package/dist/canvasTextLabel.d.ts.map +1 -0
- package/dist/canvasTextLabel.js +56 -0
- package/dist/canvasTextLabel.js.map +1 -0
- package/dist/canvasTextMeasure.d.ts +3 -0
- package/dist/canvasTextMeasure.d.ts.map +1 -0
- package/dist/canvasTextMeasure.js +15 -0
- package/dist/canvasTextMeasure.js.map +1 -0
- package/dist/canvasTilemap.d.ts +4 -0
- package/dist/canvasTilemap.d.ts.map +1 -0
- package/dist/canvasTilemap.js +50 -0
- package/dist/canvasTilemap.js.map +1 -0
- package/dist/canvasTransform.d.ts +3 -0
- package/dist/canvasTransform.d.ts.map +1 -0
- package/dist/canvasTransform.js +9 -0
- package/dist/canvasTransform.js.map +1 -0
- package/dist/canvasVideo.d.ts +4 -0
- package/dist/canvasVideo.d.ts.map +1 -0
- package/dist/canvasVideo.js +26 -0
- package/dist/canvasVideo.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/canvasBackground.test.ts +65 -0
- package/src/canvasBitmap.test.ts +92 -0
- package/src/canvasCSSFilterBinding.test.ts +86 -0
- package/src/canvasCache.test.ts +189 -0
- package/src/canvasClip.test.ts +12 -0
- package/src/canvasClipRectangle.test.ts +101 -0
- package/src/canvasElement.test.ts +21 -0
- package/src/canvasFillPattern.test.ts +96 -0
- package/src/canvasImageSource.test.ts +79 -0
- package/src/canvasMaterialRegistry.test.ts +69 -0
- package/src/canvasMaterials.test.ts +85 -0
- package/src/canvasNode2D.test.ts +113 -0
- package/src/canvasParticleEmitter2D.test.ts +105 -0
- package/src/canvasQuadBatch.test.ts +119 -0
- package/src/canvasRegistration.test.ts +57 -0
- package/src/canvasRenderState.test.ts +186 -0
- package/src/canvasRenderTarget.test.ts +162 -0
- package/src/canvasRichText.test.ts +93 -0
- package/src/canvasScale9Mapper.test.ts +53 -0
- package/src/canvasScale9Shape.test.ts +89 -0
- package/src/canvasShape.test.ts +183 -0
- package/src/canvasShapeCommands.test.ts +448 -0
- package/src/canvasShapeRegistry.test.ts +42 -0
- package/src/canvasSprite.test.ts +106 -0
- package/src/canvasTextInput.test.ts +67 -0
- package/src/canvasTextLabel.test.ts +32 -0
- package/src/canvasTextMeasure.test.ts +9 -0
- package/src/canvasTilemap.test.ts +69 -0
- package/src/canvasTransform.test.ts +42 -0
- package/src/canvasVideo.test.ts +43 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
2
|
+
import type { Node2D } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
enableCanvasCssFilter,
|
|
6
|
+
getCanvasCssFilter,
|
|
7
|
+
resolveCanvasCssFilter,
|
|
8
|
+
setCanvasCssFilter,
|
|
9
|
+
} from './canvasCSSFilterBinding';
|
|
10
|
+
import { createCanvasRenderState } from './canvasRenderState';
|
|
11
|
+
|
|
12
|
+
function makeState() {
|
|
13
|
+
const canvas = document.createElement('canvas');
|
|
14
|
+
canvas.width = 100;
|
|
15
|
+
canvas.height = 100;
|
|
16
|
+
return createCanvasRenderState(canvas);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('enableCanvasCssFilter', () => {
|
|
20
|
+
it('installs the CSS filter resolver', () => {
|
|
21
|
+
const state = makeState();
|
|
22
|
+
expect(state.canvasCssFilterResolver).toBeNull();
|
|
23
|
+
enableCanvasCssFilter(state);
|
|
24
|
+
expect(state.canvasCssFilterResolver).toBe(resolveCanvasCssFilter);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getCanvasCssFilter', () => {
|
|
29
|
+
it('returns the filter bound to a render node', () => {
|
|
30
|
+
const state = makeState();
|
|
31
|
+
const node = {} as Node2D;
|
|
32
|
+
setCanvasCssFilter(state, node, 'blur(4px)');
|
|
33
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
34
|
+
expect(getCanvasCssFilter(renderProxy)).toBe('blur(4px)');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns undefined for a render node with no binding', () => {
|
|
38
|
+
const state = makeState();
|
|
39
|
+
const renderProxy = getOrCreateRenderProxy2D(state, {} as Node2D);
|
|
40
|
+
expect(getCanvasCssFilter(renderProxy)).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('resolveCanvasCssFilter', () => {
|
|
45
|
+
it('returns null when no filter is set', () => {
|
|
46
|
+
const state = makeState();
|
|
47
|
+
const renderProxy = getOrCreateRenderProxy2D(state, {} as Node2D);
|
|
48
|
+
expect(resolveCanvasCssFilter(state, renderProxy)).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns the bound filter', () => {
|
|
52
|
+
const state = makeState();
|
|
53
|
+
const node = {} as Node2D;
|
|
54
|
+
setCanvasCssFilter(state, node, 'grayscale(1)');
|
|
55
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
56
|
+
expect(resolveCanvasCssFilter(state, renderProxy)).toBe('grayscale(1)');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('setCanvasCssFilter', () => {
|
|
61
|
+
it('stores a filter keyed by the per-state render node', () => {
|
|
62
|
+
const state = makeState();
|
|
63
|
+
const node = {} as Node2D;
|
|
64
|
+
setCanvasCssFilter(state, node, 'blur(2px)');
|
|
65
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
66
|
+
expect(getCanvasCssFilter(renderProxy)).toBe('blur(2px)');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('clears the binding when passed null', () => {
|
|
70
|
+
const state = makeState();
|
|
71
|
+
const node = {} as Node2D;
|
|
72
|
+
setCanvasCssFilter(state, node, 'blur(2px)');
|
|
73
|
+
setCanvasCssFilter(state, node, null);
|
|
74
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
75
|
+
expect(getCanvasCssFilter(renderProxy)).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not leak a binding across render states', () => {
|
|
79
|
+
const a = makeState();
|
|
80
|
+
const b = makeState();
|
|
81
|
+
const node = {} as Node2D;
|
|
82
|
+
setCanvasCssFilter(a, node, 'blur(2px)');
|
|
83
|
+
expect(getCanvasCssFilter(getOrCreateRenderProxy2D(b, node))).toBeUndefined();
|
|
84
|
+
expect(getCanvasCssFilter(getOrCreateRenderProxy2D(a, node))).toBe('blur(2px)');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import { createRenderCache, getRenderProxy2D, RenderCacheKind, useRenderCache } from '@flighthq/render';
|
|
3
|
+
import { createDisplayObject } from '@flighthq/scene2d';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createCanvasCacheState,
|
|
7
|
+
defaultCanvasRenderCacheRenderer,
|
|
8
|
+
destroyCanvasRenderCacheTarget,
|
|
9
|
+
enableCanvasRenderCache,
|
|
10
|
+
ensureCanvasRenderCacheTarget,
|
|
11
|
+
getCanvasRenderCacheTarget,
|
|
12
|
+
refreshCanvasRenderCache,
|
|
13
|
+
releaseCanvasRenderCache,
|
|
14
|
+
} from './canvasCache';
|
|
15
|
+
import { createCanvasRenderState, getCanvasRenderStateRuntime } from './canvasRenderState';
|
|
16
|
+
|
|
17
|
+
function makeCanvasState(options = {}) {
|
|
18
|
+
const canvas = document.createElement('canvas');
|
|
19
|
+
canvas.width = 200;
|
|
20
|
+
canvas.height = 200;
|
|
21
|
+
return createCanvasRenderState(canvas, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeCacheNode(source: unknown): any {
|
|
25
|
+
return { source, kind: RenderCacheKind, transform2D: createMatrix(), alpha: 1, blendMode: null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('createCanvasCacheState', () => {
|
|
29
|
+
it('copies the screen state renderers', () => {
|
|
30
|
+
const screen = makeCanvasState();
|
|
31
|
+
enableCanvasRenderCache(screen);
|
|
32
|
+
const cacheState = createCanvasCacheState(screen);
|
|
33
|
+
expect(getCanvasRenderStateRuntime(cacheState).rendererMap.get(RenderCacheKind)).toBe(
|
|
34
|
+
defaultCanvasRenderCacheRenderer,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('propagates pixel ratio and scene graph sync policy without sharing node maps', () => {
|
|
39
|
+
const screen = makeCanvasState({ pixelRatio: 3, sceneGraphSyncPolicy: 'refreshDerivedState' });
|
|
40
|
+
const cacheState = createCanvasCacheState(screen);
|
|
41
|
+
expect(cacheState.pixelRatio).toBe(3);
|
|
42
|
+
expect(cacheState.sceneGraphSyncPolicy).toBe('refreshDerivedState');
|
|
43
|
+
expect(getCanvasRenderStateRuntime(cacheState).renderProxyMap).not.toBe(
|
|
44
|
+
getCanvasRenderStateRuntime(screen).renderProxyMap,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('defaultCanvasRenderCacheRenderer', () => {
|
|
50
|
+
it('does nothing when no cache is attached to the source', () => {
|
|
51
|
+
const state = makeCanvasState();
|
|
52
|
+
const spy = vi.spyOn(state.context, 'drawImage');
|
|
53
|
+
defaultCanvasRenderCacheRenderer.submit(state, makeCacheNode(createDisplayObject()));
|
|
54
|
+
expect(spy).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('draws the cache target attached to the source node', () => {
|
|
58
|
+
const state = makeCanvasState();
|
|
59
|
+
const obj = createDisplayObject();
|
|
60
|
+
const cache = createRenderCache();
|
|
61
|
+
useRenderCache(state, obj, cache);
|
|
62
|
+
const target = ensureCanvasRenderCacheTarget(state, cache, 16, 16);
|
|
63
|
+
const spy = vi.spyOn(state.context, 'drawImage');
|
|
64
|
+
defaultCanvasRenderCacheRenderer.submit(state, makeCacheNode(obj));
|
|
65
|
+
expect(spy).toHaveBeenCalledWith(target.canvas, 0, 0);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('destroyCanvasRenderCacheTarget', () => {
|
|
70
|
+
it('collapses the canvas to zero size and removes the target', () => {
|
|
71
|
+
const state = makeCanvasState();
|
|
72
|
+
const cache = createRenderCache();
|
|
73
|
+
const target = ensureCanvasRenderCacheTarget(state, cache, 8, 8);
|
|
74
|
+
destroyCanvasRenderCacheTarget(state, cache);
|
|
75
|
+
expect(target.canvas.width).toBe(0);
|
|
76
|
+
expect(target.canvas.height).toBe(0);
|
|
77
|
+
expect(target.width).toBe(0);
|
|
78
|
+
expect(target.height).toBe(0);
|
|
79
|
+
expect(getCanvasRenderCacheTarget(state, cache)).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('is a no-op when the target does not exist', () => {
|
|
83
|
+
const state = makeCanvasState();
|
|
84
|
+
const cache = createRenderCache();
|
|
85
|
+
expect(() => destroyCanvasRenderCacheTarget(state, cache)).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('enableCanvasRenderCache', () => {
|
|
90
|
+
it('registers the renderer for the render cache kind', () => {
|
|
91
|
+
const state = makeCanvasState();
|
|
92
|
+
enableCanvasRenderCache(state);
|
|
93
|
+
expect(getCanvasRenderStateRuntime(state).rendererMap.get(RenderCacheKind)).toBe(defaultCanvasRenderCacheRenderer);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('ensureCanvasRenderCacheTarget', () => {
|
|
98
|
+
it('creates a target sized to the request', () => {
|
|
99
|
+
const state = makeCanvasState();
|
|
100
|
+
const cache = createRenderCache();
|
|
101
|
+
const target = ensureCanvasRenderCacheTarget(state, cache, 64, 32);
|
|
102
|
+
expect(target.width).toBe(64);
|
|
103
|
+
expect(target.height).toBe(32);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('reuses and resizes the same target on subsequent calls', () => {
|
|
107
|
+
const state = makeCanvasState();
|
|
108
|
+
const cache = createRenderCache();
|
|
109
|
+
const first = ensureCanvasRenderCacheTarget(state, cache, 64, 32);
|
|
110
|
+
const second = ensureCanvasRenderCacheTarget(state, cache, 16, 16);
|
|
111
|
+
expect(second).toBe(first);
|
|
112
|
+
expect(second.width).toBe(16);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('keeps targets isolated per state for the same handle', () => {
|
|
116
|
+
const stateA = makeCanvasState();
|
|
117
|
+
const stateB = makeCanvasState();
|
|
118
|
+
const cache = createRenderCache();
|
|
119
|
+
const targetA = ensureCanvasRenderCacheTarget(stateA, cache, 8, 8);
|
|
120
|
+
const targetB = ensureCanvasRenderCacheTarget(stateB, cache, 8, 8);
|
|
121
|
+
expect(targetA).not.toBe(targetB);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('getCanvasRenderCacheTarget', () => {
|
|
126
|
+
it('returns null before a target is allocated', () => {
|
|
127
|
+
const state = makeCanvasState();
|
|
128
|
+
expect(getCanvasRenderCacheTarget(state, createRenderCache())).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns the allocated target', () => {
|
|
132
|
+
const state = makeCanvasState();
|
|
133
|
+
const cache = createRenderCache();
|
|
134
|
+
const target = ensureCanvasRenderCacheTarget(state, cache, 8, 8);
|
|
135
|
+
expect(getCanvasRenderCacheTarget(state, cache)).toBe(target);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('refreshCanvasRenderCache', () => {
|
|
140
|
+
it('bakes on the first call and allocates the target on the screen state', () => {
|
|
141
|
+
const screen = makeCanvasState();
|
|
142
|
+
const cacheState = createCanvasCacheState(screen);
|
|
143
|
+
const cache = createRenderCache();
|
|
144
|
+
const obj = createDisplayObject();
|
|
145
|
+
const rebaked = refreshCanvasRenderCache(cacheState, cache, obj, { padding: 5 });
|
|
146
|
+
expect(rebaked).toBe(true);
|
|
147
|
+
const target = getCanvasRenderCacheTarget(screen, cache);
|
|
148
|
+
expect(target).not.toBeNull();
|
|
149
|
+
expect(target!.width).toBe(10);
|
|
150
|
+
expect(target!.height).toBe(10);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('always rebakes under the refreshDerivedState policy', () => {
|
|
154
|
+
const screen = makeCanvasState({ sceneGraphSyncPolicy: 'refreshDerivedState' });
|
|
155
|
+
const cacheState = createCanvasCacheState(screen);
|
|
156
|
+
const cache = createRenderCache();
|
|
157
|
+
const obj = createDisplayObject();
|
|
158
|
+
refreshCanvasRenderCache(cacheState, cache, obj, { padding: 5 });
|
|
159
|
+
expect(refreshCanvasRenderCache(cacheState, cache, obj, { padding: 5 })).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('skips the bake under requiresInvalidation when nothing changed', () => {
|
|
163
|
+
const screen = makeCanvasState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
164
|
+
const cacheState = createCanvasCacheState(screen);
|
|
165
|
+
const cache = createRenderCache();
|
|
166
|
+
const obj = createDisplayObject();
|
|
167
|
+
refreshCanvasRenderCache(cacheState, cache, obj, { padding: 5 });
|
|
168
|
+
expect(refreshCanvasRenderCache(cacheState, cache, obj, { padding: 5 })).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('does not create render nodes on the screen state', () => {
|
|
172
|
+
const screen = makeCanvasState();
|
|
173
|
+
const cacheState = createCanvasCacheState(screen);
|
|
174
|
+
const cache = createRenderCache();
|
|
175
|
+
const obj = createDisplayObject();
|
|
176
|
+
refreshCanvasRenderCache(cacheState, cache, obj);
|
|
177
|
+
expect(getRenderProxy2D(screen, obj)).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('releaseCanvasRenderCache', () => {
|
|
182
|
+
it('drops the target for the cache', () => {
|
|
183
|
+
const state = makeCanvasState();
|
|
184
|
+
const cache = createRenderCache();
|
|
185
|
+
ensureCanvasRenderCacheTarget(state, cache, 8, 8);
|
|
186
|
+
releaseCanvasRenderCache(state, cache);
|
|
187
|
+
expect(getCanvasRenderCacheTarget(state, cache)).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { enableCanvasClip } from './canvasClip';
|
|
2
|
+
import { createCanvasRenderState } from './canvasRenderState';
|
|
3
|
+
|
|
4
|
+
describe('enableCanvasClip', () => {
|
|
5
|
+
it('sets the display object clip hooks on the render state', () => {
|
|
6
|
+
const state = createCanvasRenderState(document.createElement('canvas'));
|
|
7
|
+
|
|
8
|
+
enableCanvasClip(state);
|
|
9
|
+
|
|
10
|
+
expect(state.displayObjectClipHooks).not.toBeNull();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createMatrix, createRectangle } from '@flighthq/geometry';
|
|
2
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
3
|
+
import { createDisplayObject } from '@flighthq/scene2d';
|
|
4
|
+
import type { CanvasRenderState, Node2D, Matrix, Rectangle, RenderProxy2D } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { popCanvasClipRectangle, pushCanvasClipContours, pushCanvasClipRectangle } from './canvasClipRectangle';
|
|
7
|
+
import { createCanvasRenderState } from './canvasRenderState';
|
|
8
|
+
|
|
9
|
+
describe('Clip Rectangle Functions', () => {
|
|
10
|
+
let canvas: HTMLCanvasElement;
|
|
11
|
+
let state: CanvasRenderState;
|
|
12
|
+
let rect: Rectangle;
|
|
13
|
+
let transform2D: Matrix;
|
|
14
|
+
let source: Node2D;
|
|
15
|
+
let data: RenderProxy2D;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
canvas = document.createElement('canvas');
|
|
19
|
+
state = createCanvasRenderState(canvas);
|
|
20
|
+
rect = createRectangle(10, 10, 100, 100);
|
|
21
|
+
transform2D = createMatrix();
|
|
22
|
+
source = createDisplayObject();
|
|
23
|
+
source.clip = { contours: null, rect, winding: 'nonZero', version: 0 };
|
|
24
|
+
data = getOrCreateRenderProxy2D(state, source);
|
|
25
|
+
data.transform2D = transform2D;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should call context.restore() when popClipRectangle is called', () => {
|
|
29
|
+
const restoreSpy = vi.spyOn(state.context, 'restore');
|
|
30
|
+
popCanvasClipRectangle(state);
|
|
31
|
+
expect(restoreSpy).toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should call context.save(), setTransform, and context.clip() when pushClipRectangle is called', () => {
|
|
35
|
+
const saveSpy = vi.spyOn(state.context, 'save');
|
|
36
|
+
const beginPathSpy = vi.spyOn(state.context, 'beginPath');
|
|
37
|
+
const rectSpy = vi.spyOn(state.context, 'rect');
|
|
38
|
+
const clipSpy = vi.spyOn(state.context, 'clip');
|
|
39
|
+
|
|
40
|
+
pushCanvasClipRectangle(state, rect, transform2D);
|
|
41
|
+
|
|
42
|
+
expect(saveSpy).toHaveBeenCalled();
|
|
43
|
+
expect(beginPathSpy).toHaveBeenCalled();
|
|
44
|
+
expect(rectSpy).toHaveBeenCalledWith(rect.x, rect.y, rect.width, rect.height);
|
|
45
|
+
expect(clipSpy).toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('popCanvasClipRectangle', () => {
|
|
50
|
+
it('calls context.restore()', () => {
|
|
51
|
+
const c = document.createElement('canvas');
|
|
52
|
+
const state = createCanvasRenderState(c);
|
|
53
|
+
const spy = vi.spyOn(state.context, 'restore');
|
|
54
|
+
popCanvasClipRectangle(state);
|
|
55
|
+
expect(spy).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('pushCanvasClipContours', () => {
|
|
60
|
+
it('saves context, traces each contour as a path, and clips', () => {
|
|
61
|
+
const c = document.createElement('canvas');
|
|
62
|
+
const state = createCanvasRenderState(c);
|
|
63
|
+
const t = createMatrix();
|
|
64
|
+
const saveSpy = vi.spyOn(state.context, 'save');
|
|
65
|
+
const beginPathSpy = vi.spyOn(state.context, 'beginPath');
|
|
66
|
+
const moveToSpy = vi.spyOn(state.context, 'moveTo');
|
|
67
|
+
const lineToSpy = vi.spyOn(state.context, 'lineTo');
|
|
68
|
+
const clipSpy = vi.spyOn(state.context, 'clip');
|
|
69
|
+
|
|
70
|
+
// Two triangles.
|
|
71
|
+
pushCanvasClipContours(
|
|
72
|
+
state,
|
|
73
|
+
[
|
|
74
|
+
[0, 0, 10, 0, 10, 10],
|
|
75
|
+
[20, 20, 30, 20, 30, 30],
|
|
76
|
+
],
|
|
77
|
+
'nonZero',
|
|
78
|
+
t,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(saveSpy).toHaveBeenCalled();
|
|
82
|
+
expect(beginPathSpy).toHaveBeenCalled();
|
|
83
|
+
expect(moveToSpy).toHaveBeenCalledTimes(2); // one per contour
|
|
84
|
+
expect(lineToSpy).toHaveBeenCalledTimes(4); // two segments per triangle after the moveTo
|
|
85
|
+
expect(clipSpy).toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('pushCanvasClipRectangle', () => {
|
|
90
|
+
it('saves context, clips to rect, and restores', () => {
|
|
91
|
+
const c = document.createElement('canvas');
|
|
92
|
+
const state = createCanvasRenderState(c);
|
|
93
|
+
const r = createRectangle(0, 0, 50, 50);
|
|
94
|
+
const t = createMatrix();
|
|
95
|
+
const saveSpy = vi.spyOn(state.context, 'save');
|
|
96
|
+
const clipSpy = vi.spyOn(state.context, 'clip');
|
|
97
|
+
pushCanvasClipRectangle(state, r, t);
|
|
98
|
+
expect(saveSpy).toHaveBeenCalled();
|
|
99
|
+
expect(clipSpy).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createCanvasElement } from './canvasElement';
|
|
2
|
+
|
|
3
|
+
describe('createCanvasElement', () => {
|
|
4
|
+
it('sets pixel dimensions equal to logical size with default pixelRatio', () => {
|
|
5
|
+
const canvas = createCanvasElement(100, 200);
|
|
6
|
+
expect(canvas.width).toBe(100);
|
|
7
|
+
expect(canvas.height).toBe(200);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('scales pixel dimensions by pixelRatio', () => {
|
|
11
|
+
const canvas = createCanvasElement(100, 200, 2);
|
|
12
|
+
expect(canvas.width).toBe(200);
|
|
13
|
+
expect(canvas.height).toBe(400);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('sets CSS style dimensions to the logical size', () => {
|
|
17
|
+
const canvas = createCanvasElement(100, 200, 2);
|
|
18
|
+
expect(canvas.style.width).toBe('100px');
|
|
19
|
+
expect(canvas.style.height).toBe('200px');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createBitmapPattern, createGradientPattern } from './canvasFillPattern';
|
|
2
|
+
|
|
3
|
+
function makeContext(): CanvasRenderingContext2D {
|
|
4
|
+
const canvas = document.createElement('canvas');
|
|
5
|
+
canvas.width = 200;
|
|
6
|
+
canvas.height = 200;
|
|
7
|
+
return canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function makeBitmap(w = 64, h = 64) {
|
|
11
|
+
const canvas = document.createElement('canvas');
|
|
12
|
+
canvas.width = w;
|
|
13
|
+
canvas.height = h;
|
|
14
|
+
return { source: canvas as CanvasImageSource, width: w, height: h };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('createBitmapPattern', () => {
|
|
18
|
+
it('returns null when bitmap.source is null', () => {
|
|
19
|
+
const context = makeContext();
|
|
20
|
+
expect(createBitmapPattern(context, { source: null, width: 64, height: 64 } as never, true)).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns a CanvasPattern when src is valid', () => {
|
|
24
|
+
const context = makeContext();
|
|
25
|
+
const result = createBitmapPattern(context, makeBitmap() as never, true);
|
|
26
|
+
expect(result).not.toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('uses repeat repetition when repeat is true', () => {
|
|
30
|
+
const context = makeContext();
|
|
31
|
+
const spy = vi.spyOn(context, 'createPattern');
|
|
32
|
+
createBitmapPattern(context, makeBitmap() as never, true);
|
|
33
|
+
expect(spy).toHaveBeenCalledWith(expect.anything(), 'repeat');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('uses no-repeat when repeat is false', () => {
|
|
37
|
+
const context = makeContext();
|
|
38
|
+
const spy = vi.spyOn(context, 'createPattern');
|
|
39
|
+
createBitmapPattern(context, makeBitmap() as never, false);
|
|
40
|
+
expect(spy).toHaveBeenCalledWith(expect.anything(), 'no-repeat');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('sets imageSmoothingEnabled to true when smooth is true', () => {
|
|
44
|
+
const context = makeContext();
|
|
45
|
+
context.imageSmoothingEnabled = false;
|
|
46
|
+
createBitmapPattern(context, makeBitmap() as never, true, true);
|
|
47
|
+
expect(context.imageSmoothingEnabled).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('sets imageSmoothingEnabled to false when smooth is false', () => {
|
|
51
|
+
const context = makeContext();
|
|
52
|
+
context.imageSmoothingEnabled = true;
|
|
53
|
+
createBitmapPattern(context, makeBitmap() as never, true, false);
|
|
54
|
+
expect(context.imageSmoothingEnabled).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('createGradientPattern', () => {
|
|
59
|
+
it('returns a CanvasGradient for linear type', () => {
|
|
60
|
+
const context = makeContext();
|
|
61
|
+
const result = createGradientPattern(
|
|
62
|
+
context,
|
|
63
|
+
'linear',
|
|
64
|
+
[0xff0000, 0x0000ff],
|
|
65
|
+
[1, 1],
|
|
66
|
+
[0, 255],
|
|
67
|
+
null,
|
|
68
|
+
'pad',
|
|
69
|
+
'rgb',
|
|
70
|
+
0,
|
|
71
|
+
);
|
|
72
|
+
expect(result).toBeInstanceOf(CanvasGradient);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns a CanvasGradient for radial type', () => {
|
|
76
|
+
const context = makeContext();
|
|
77
|
+
const spy = vi.spyOn(context, 'createRadialGradient');
|
|
78
|
+
createGradientPattern(context, 'radial', [0xff0000, 0x0000ff], [1, 1], [0, 255], null, 'pad', 'rgb', 0);
|
|
79
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('calls createLinearGradient for linear type with pad spread', () => {
|
|
83
|
+
const context = makeContext();
|
|
84
|
+
const spy = vi.spyOn(context, 'createLinearGradient');
|
|
85
|
+
createGradientPattern(context, 'linear', [0xff0000], [1], [128], null, 'pad', 'rgb', 0);
|
|
86
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('uses focal point for radial gradient', () => {
|
|
90
|
+
const context = makeContext();
|
|
91
|
+
const spy = vi.spyOn(context, 'createRadialGradient');
|
|
92
|
+
createGradientPattern(context, 'radial', [0xff0000], [1], [128], null, 'pad', 'rgb', 0.5);
|
|
93
|
+
const [fx] = spy.mock.calls[0];
|
|
94
|
+
expect(fx).not.toBe(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createImageResource, invalidateImageResource } from '@flighthq/image';
|
|
2
|
+
import type { ImageResource } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { explainCanvasImageSource, resolveCanvasImageSource } from './canvasImageSource';
|
|
5
|
+
import { createCanvasRenderState } from './canvasRenderState';
|
|
6
|
+
|
|
7
|
+
function makeState() {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.width = 200;
|
|
10
|
+
canvas.height = 200;
|
|
11
|
+
return createCanvasRenderState(canvas);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeDataOnlyResource(width = 4, height = 4): ImageResource {
|
|
15
|
+
const resource = createImageResource();
|
|
16
|
+
resource.width = width;
|
|
17
|
+
resource.height = height;
|
|
18
|
+
resource.data = new Uint8ClampedArray(width * height * 4).fill(255);
|
|
19
|
+
return resource;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('explainCanvasImageSource', () => {
|
|
23
|
+
it('reports element for a host-element-backed resource', () => {
|
|
24
|
+
const resource = createImageResource(document.createElement('img'));
|
|
25
|
+
expect(explainCanvasImageSource(resource)).toBe('element');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reports data for a data-only resource', () => {
|
|
29
|
+
expect(explainCanvasImageSource(makeDataOnlyResource())).toBe('data');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('reports none for a resource with neither representation', () => {
|
|
33
|
+
expect(explainCanvasImageSource(createImageResource())).toBe('none');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('resolveCanvasImageSource', () => {
|
|
38
|
+
it('returns the host source element directly, with no cache entry', () => {
|
|
39
|
+
const state = makeState();
|
|
40
|
+
const img = document.createElement('img');
|
|
41
|
+
const resource = createImageResource(img);
|
|
42
|
+
expect(resolveCanvasImageSource(state, resource)).toBe(img);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('materializes a canvas element from data-only pixels', () => {
|
|
46
|
+
const state = makeState();
|
|
47
|
+
const resolved = resolveCanvasImageSource(state, makeDataOnlyResource());
|
|
48
|
+
expect(resolved).toBeInstanceOf(HTMLCanvasElement);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('caches the materialized element across resolves of the same resource', () => {
|
|
52
|
+
const state = makeState();
|
|
53
|
+
const resource = makeDataOnlyResource();
|
|
54
|
+
const first = resolveCanvasImageSource(state, resource);
|
|
55
|
+
const second = resolveCanvasImageSource(state, resource);
|
|
56
|
+
expect(second).toBe(first);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('re-materializes after the resource version bumps', () => {
|
|
60
|
+
const state = makeState();
|
|
61
|
+
const resource = makeDataOnlyResource();
|
|
62
|
+
const first = resolveCanvasImageSource(state, resource);
|
|
63
|
+
invalidateImageResource(resource);
|
|
64
|
+
const second = resolveCanvasImageSource(state, resource);
|
|
65
|
+
expect(second).not.toBe(first);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('caches independently per render state', () => {
|
|
69
|
+
const stateA = makeState();
|
|
70
|
+
const stateB = makeState();
|
|
71
|
+
const resource = makeDataOnlyResource();
|
|
72
|
+
expect(resolveCanvasImageSource(stateA, resource)).not.toBe(resolveCanvasImageSource(stateB, resource));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns null when the resource carries neither pixels form', () => {
|
|
76
|
+
const state = makeState();
|
|
77
|
+
expect(resolveCanvasImageSource(state, createImageResource())).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CanvasMaterialRenderer, Material } from '@flighthq/types';
|
|
2
|
+
import { DefaultMaterialKind } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
applyCanvasMaterial,
|
|
6
|
+
getCanvasMaterialRenderer,
|
|
7
|
+
registerCanvasMaterialRenderer,
|
|
8
|
+
resolveCanvasMaterialRenderer,
|
|
9
|
+
} from './canvasMaterialRegistry';
|
|
10
|
+
import { createCanvasRenderState } from './canvasRenderState';
|
|
11
|
+
|
|
12
|
+
const TestKind = 'TestMaterial';
|
|
13
|
+
const testRenderer: CanvasMaterialRenderer = { getState: () => ({ composite: 'lighter' }) };
|
|
14
|
+
|
|
15
|
+
function makeState() {
|
|
16
|
+
const canvas = document.createElement('canvas');
|
|
17
|
+
canvas.width = 64;
|
|
18
|
+
canvas.height = 64;
|
|
19
|
+
return createCanvasRenderState(canvas);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeMaterial(kind: string = TestKind): Material {
|
|
23
|
+
return { kind } as Material;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('applyCanvasMaterial', () => {
|
|
27
|
+
it('returns false for a null material', () => {
|
|
28
|
+
expect(applyCanvasMaterial(makeState(), null)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns false when no renderer is registered', () => {
|
|
32
|
+
expect(applyCanvasMaterial(makeState(), makeMaterial())).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('applies registered draw state and reports that it saved', () => {
|
|
36
|
+
const state = makeState();
|
|
37
|
+
registerCanvasMaterialRenderer(state, TestKind, testRenderer);
|
|
38
|
+
expect(applyCanvasMaterial(state, makeMaterial())).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getCanvasMaterialRenderer', () => {
|
|
43
|
+
it('registers and retrieves a renderer by kind', () => {
|
|
44
|
+
const state = makeState();
|
|
45
|
+
expect(getCanvasMaterialRenderer(state, TestKind)).toBeNull();
|
|
46
|
+
registerCanvasMaterialRenderer(state, TestKind, testRenderer);
|
|
47
|
+
expect(getCanvasMaterialRenderer(state, TestKind)).toBe(testRenderer);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('registerCanvasMaterialRenderer', () => {
|
|
52
|
+
it('makes a renderer resolvable for its material kind', () => {
|
|
53
|
+
const state = makeState();
|
|
54
|
+
registerCanvasMaterialRenderer(state, TestKind, testRenderer);
|
|
55
|
+
expect(resolveCanvasMaterialRenderer(state, makeMaterial())).toBe(testRenderer);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('resolveCanvasMaterialRenderer', () => {
|
|
60
|
+
it('returns null when nothing is registered', () => {
|
|
61
|
+
expect(resolveCanvasMaterialRenderer(makeState(), makeMaterial())).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('falls back to the registered default for an unregistered kind', () => {
|
|
65
|
+
const state = makeState();
|
|
66
|
+
registerCanvasMaterialRenderer(state, DefaultMaterialKind, testRenderer);
|
|
67
|
+
expect(resolveCanvasMaterialRenderer(state, makeMaterial('Other'))).toBe(testRenderer);
|
|
68
|
+
});
|
|
69
|
+
});
|