@flighthq/displayobject-canvas 0.1.0
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 +32 -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/canvasDisplayObject.d.ts +5 -0
- package/dist/canvasDisplayObject.d.ts.map +1 -0
- package/dist/canvasDisplayObject.js +46 -0
- package/dist/canvasDisplayObject.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/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 +33 -0
- package/dist/canvasMaterials.js.map +1 -0
- package/dist/canvasParticleEmitter.d.ts +4 -0
- package/dist/canvasParticleEmitter.d.ts.map +1 -0
- package/dist/canvasParticleEmitter.js +64 -0
- package/dist/canvasParticleEmitter.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 +16 -0
- package/dist/canvasRenderTarget.d.ts.map +1 -0
- package/dist/canvasRenderTarget.js +66 -0
- package/dist/canvasRenderTarget.js.map +1 -0
- package/dist/canvasRichText.d.ts +7 -0
- package/dist/canvasRichText.d.ts.map +1 -0
- package/dist/canvasRichText.js +127 -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 +59 -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 +55 -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 +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -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/canvasDisplayObject.test.ts +117 -0
- package/src/canvasElement.test.ts +21 -0
- package/src/canvasFillPattern.test.ts +96 -0
- package/src/canvasMaterialRegistry.test.ts +69 -0
- package/src/canvasMaterials.test.ts +85 -0
- package/src/canvasParticleEmitter.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 +154 -0
- package/src/canvasRichText.test.ts +89 -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 +90 -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,189 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
3
|
+
import { createRenderCache, getRenderProxy2D, RenderCacheKind, useRenderCache } from '@flighthq/render';
|
|
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 { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix, createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
4
|
+
import type { CanvasRenderState, DisplayObject, 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: DisplayObject;
|
|
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,117 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { addNodeChild } from '@flighthq/node';
|
|
3
|
+
import { getOrCreateRenderProxy2D, prepareDisplayObjectRender, registerRenderer } from '@flighthq/render';
|
|
4
|
+
import { DisplayObjectKind } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { enableCanvasCssFilter, setCanvasCssFilter } from './canvasCSSFilterBinding';
|
|
7
|
+
import {
|
|
8
|
+
defaultCanvasDisplayObjectRenderer,
|
|
9
|
+
drawCanvasDisplayObject,
|
|
10
|
+
renderCanvasDisplayObject,
|
|
11
|
+
} from './canvasDisplayObject';
|
|
12
|
+
import { createCanvasRenderState } from './canvasRenderState';
|
|
13
|
+
|
|
14
|
+
function makeState() {
|
|
15
|
+
const canvas = document.createElement('canvas');
|
|
16
|
+
canvas.width = 200;
|
|
17
|
+
canvas.height = 200;
|
|
18
|
+
const state = createCanvasRenderState(canvas);
|
|
19
|
+
registerRenderer(state, DisplayObjectKind, defaultCanvasDisplayObjectRenderer);
|
|
20
|
+
return state;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('defaultCanvasDisplayObjectRenderer', () => {
|
|
24
|
+
it('has submit, and createData', () => {
|
|
25
|
+
expect(typeof defaultCanvasDisplayObjectRenderer.submit).toBe('function');
|
|
26
|
+
expect(typeof defaultCanvasDisplayObjectRenderer.createData).toBe('function');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('drawCanvasDisplayObject', () => {
|
|
31
|
+
it('does not throw', () => {
|
|
32
|
+
const state = makeState();
|
|
33
|
+
const obj = createDisplayObject();
|
|
34
|
+
const data = getOrCreateRenderProxy2D(state, obj);
|
|
35
|
+
expect(() => drawCanvasDisplayObject(state, data)).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('does not call fillRect (no visual geometry)', () => {
|
|
39
|
+
const state = makeState();
|
|
40
|
+
const obj = createDisplayObject();
|
|
41
|
+
const data = getOrCreateRenderProxy2D(state, obj);
|
|
42
|
+
const spy = vi.spyOn(state.context, 'fillRect');
|
|
43
|
+
|
|
44
|
+
drawCanvasDisplayObject(state, data);
|
|
45
|
+
|
|
46
|
+
expect(spy).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('renderCanvasDisplayObject', () => {
|
|
51
|
+
it('does not throw for a simple visible object', () => {
|
|
52
|
+
const state = makeState();
|
|
53
|
+
const obj = createDisplayObject();
|
|
54
|
+
prepareDisplayObjectRender(state, obj);
|
|
55
|
+
expect(() => renderCanvasDisplayObject(state, obj)).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('calls renderer.submit for a visible object with a renderer', () => {
|
|
59
|
+
const state = makeState();
|
|
60
|
+
const obj = createDisplayObject();
|
|
61
|
+
const renderer = { createData: vi.fn().mockReturnValue(null), submit: vi.fn() };
|
|
62
|
+
registerRenderer(state, DisplayObjectKind, renderer);
|
|
63
|
+
prepareDisplayObjectRender(state, obj);
|
|
64
|
+
|
|
65
|
+
renderCanvasDisplayObject(state, obj);
|
|
66
|
+
|
|
67
|
+
expect(renderer.submit).toHaveBeenCalledOnce();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not call renderer.submit for a hidden object', () => {
|
|
71
|
+
const state = makeState();
|
|
72
|
+
const obj = createDisplayObject();
|
|
73
|
+
obj.visible = false;
|
|
74
|
+
const renderer = { createData: vi.fn().mockReturnValue(null), submit: vi.fn() };
|
|
75
|
+
registerRenderer(state, DisplayObjectKind, renderer);
|
|
76
|
+
prepareDisplayObjectRender(state, obj);
|
|
77
|
+
|
|
78
|
+
renderCanvasDisplayObject(state, obj);
|
|
79
|
+
|
|
80
|
+
expect(renderer.submit).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('traverses and draws children', () => {
|
|
84
|
+
const state = makeState();
|
|
85
|
+
const parent = createDisplayObject();
|
|
86
|
+
const child = createDisplayObject();
|
|
87
|
+
addNodeChild(parent, child);
|
|
88
|
+
const renderer = { createData: vi.fn().mockReturnValue(null), submit: vi.fn() };
|
|
89
|
+
registerRenderer(state, DisplayObjectKind, renderer);
|
|
90
|
+
prepareDisplayObjectRender(state, parent);
|
|
91
|
+
|
|
92
|
+
renderCanvasDisplayObject(state, parent);
|
|
93
|
+
|
|
94
|
+
expect(renderer.submit).toHaveBeenCalledTimes(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('applies a bound canvas filter around the node draw and resets after', () => {
|
|
98
|
+
const state = makeState();
|
|
99
|
+
const obj = createDisplayObject();
|
|
100
|
+
let observed: string | undefined;
|
|
101
|
+
const renderer = {
|
|
102
|
+
createData: vi.fn().mockReturnValue(null),
|
|
103
|
+
submit: vi.fn(() => {
|
|
104
|
+
observed = state.context.filter;
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
registerRenderer(state, DisplayObjectKind, renderer);
|
|
108
|
+
enableCanvasCssFilter(state);
|
|
109
|
+
setCanvasCssFilter(state, obj, 'blur(3px)');
|
|
110
|
+
prepareDisplayObjectRender(state, obj);
|
|
111
|
+
|
|
112
|
+
renderCanvasDisplayObject(state, obj);
|
|
113
|
+
|
|
114
|
+
expect(observed).toBe('blur(3px)');
|
|
115
|
+
expect(state.context.filter).toBe('none');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -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,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
|
+
});
|