@flighthq/displayobject-dom 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/domBackground.d.ts +3 -0
- package/dist/domBackground.d.ts.map +1 -0
- package/dist/domBackground.js +9 -0
- package/dist/domBackground.js.map +1 -0
- package/dist/domBitmap.d.ts +4 -0
- package/dist/domBitmap.d.ts.map +1 -0
- package/dist/domBitmap.js +77 -0
- package/dist/domBitmap.js.map +1 -0
- package/dist/domCSSFilterBinding.d.ts +28 -0
- package/dist/domCSSFilterBinding.d.ts.map +1 -0
- package/dist/domCSSFilterBinding.js +56 -0
- package/dist/domCSSFilterBinding.js.map +1 -0
- package/dist/domCache.d.ts +14 -0
- package/dist/domCache.d.ts.map +1 -0
- package/dist/domCache.js +64 -0
- package/dist/domCache.js.map +1 -0
- package/dist/domClip.d.ts +3 -0
- package/dist/domClip.d.ts.map +1 -0
- package/dist/domClip.js +38 -0
- package/dist/domClip.js.map +1 -0
- package/dist/domClipContours.d.ts +6 -0
- package/dist/domClipContours.d.ts.map +1 -0
- package/dist/domClipContours.js +48 -0
- package/dist/domClipContours.js.map +1 -0
- package/dist/domClipRectangle.d.ts +6 -0
- package/dist/domClipRectangle.d.ts.map +1 -0
- package/dist/domClipRectangle.js +148 -0
- package/dist/domClipRectangle.js.map +1 -0
- package/dist/domDisplayObject.d.ts +5 -0
- package/dist/domDisplayObject.d.ts.map +1 -0
- package/dist/domDisplayObject.js +60 -0
- package/dist/domDisplayObject.js.map +1 -0
- package/dist/domFontSource.d.ts +5 -0
- package/dist/domFontSource.d.ts.map +1 -0
- package/dist/domFontSource.js +18 -0
- package/dist/domFontSource.js.map +1 -0
- package/dist/domMaterials.d.ts +15 -0
- package/dist/domMaterials.d.ts.map +1 -0
- package/dist/domMaterials.js +62 -0
- package/dist/domMaterials.js.map +1 -0
- package/dist/domNativeText.d.ts +5 -0
- package/dist/domNativeText.d.ts.map +1 -0
- package/dist/domNativeText.js +65 -0
- package/dist/domNativeText.js.map +1 -0
- package/dist/domReconcile.d.ts +9 -0
- package/dist/domReconcile.d.ts.map +1 -0
- package/dist/domReconcile.js +69 -0
- package/dist/domReconcile.js.map +1 -0
- package/dist/domRenderState.d.ts +5 -0
- package/dist/domRenderState.d.ts.map +1 -0
- package/dist/domRenderState.js +43 -0
- package/dist/domRenderState.js.map +1 -0
- package/dist/domRenderView.d.ts +4 -0
- package/dist/domRenderView.d.ts.map +1 -0
- package/dist/domRenderView.js +24 -0
- package/dist/domRenderView.js.map +1 -0
- package/dist/domRichText.d.ts +7 -0
- package/dist/domRichText.d.ts.map +1 -0
- package/dist/domRichText.js +183 -0
- package/dist/domRichText.js.map +1 -0
- package/dist/domScale9Mapper.d.ts +3 -0
- package/dist/domScale9Mapper.d.ts.map +1 -0
- package/dist/domScale9Mapper.js +37 -0
- package/dist/domScale9Mapper.js.map +1 -0
- package/dist/domScale9Shape.d.ts +10 -0
- package/dist/domScale9Shape.d.ts.map +1 -0
- package/dist/domScale9Shape.js +59 -0
- package/dist/domScale9Shape.js.map +1 -0
- package/dist/domShape.d.ts +4 -0
- package/dist/domShape.d.ts.map +1 -0
- package/dist/domShape.js +45 -0
- package/dist/domShape.js.map +1 -0
- package/dist/domStyle.d.ts +9 -0
- package/dist/domStyle.d.ts.map +1 -0
- package/dist/domStyle.js +25 -0
- package/dist/domStyle.js.map +1 -0
- package/dist/domSvgFilter.d.ts +27 -0
- package/dist/domSvgFilter.d.ts.map +1 -0
- package/dist/domSvgFilter.js +88 -0
- package/dist/domSvgFilter.js.map +1 -0
- package/dist/domTextHelpers.d.ts +6 -0
- package/dist/domTextHelpers.d.ts.map +1 -0
- package/dist/domTextHelpers.js +8 -0
- package/dist/domTextHelpers.js.map +1 -0
- package/dist/domTextInput.d.ts +4 -0
- package/dist/domTextInput.d.ts.map +1 -0
- package/dist/domTextInput.js +60 -0
- package/dist/domTextInput.js.map +1 -0
- package/dist/domTextLabel.d.ts +4 -0
- package/dist/domTextLabel.d.ts.map +1 -0
- package/dist/domTextLabel.js +74 -0
- package/dist/domTextLabel.js.map +1 -0
- package/dist/domTransform.d.ts +4 -0
- package/dist/domTransform.d.ts.map +1 -0
- package/dist/domTransform.js +16 -0
- package/dist/domTransform.js.map +1 -0
- package/dist/domVideo.d.ts +4 -0
- package/dist/domVideo.d.ts.map +1 -0
- package/dist/domVideo.js +27 -0
- package/dist/domVideo.js.map +1 -0
- package/dist/htmlView.d.ts +4 -0
- package/dist/htmlView.d.ts.map +1 -0
- package/dist/htmlView.js +28 -0
- package/dist/htmlView.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/domBackground.test.ts +35 -0
- package/src/domBitmap.test.ts +117 -0
- package/src/domCSSFilterBinding.test.ts +104 -0
- package/src/domCache.test.ts +93 -0
- package/src/domClip.test.ts +12 -0
- package/src/domClipContours.test.ts +62 -0
- package/src/domClipRectangle.test.ts +81 -0
- package/src/domDisplayObject.test.ts +236 -0
- package/src/domFontSource.test.ts +30 -0
- package/src/domMaterials.test.ts +111 -0
- package/src/domNativeText.test.ts +71 -0
- package/src/domReconcile.test.ts +158 -0
- package/src/domRenderState.test.ts +87 -0
- package/src/domRenderView.test.ts +69 -0
- package/src/domRichText.test.ts +165 -0
- package/src/domScale9Mapper.test.ts +20 -0
- package/src/domScale9Shape.test.ts +76 -0
- package/src/domShape.test.ts +85 -0
- package/src/domStyle.test.ts +130 -0
- package/src/domSvgFilter.test.ts +116 -0
- package/src/domTextHelpers.test.ts +27 -0
- package/src/domTextInput.test.ts +61 -0
- package/src/domTextLabel.test.ts +92 -0
- package/src/domTransform.test.ts +68 -0
- package/src/domVideo.test.ts +30 -0
- package/src/htmlView.test.ts +117 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
3
|
+
import { createRenderCache, RenderCacheKind, useRenderCache } from '@flighthq/render';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
defaultDomRenderCacheRenderer,
|
|
7
|
+
enableDomRenderCache,
|
|
8
|
+
ensureDomRenderCacheTarget,
|
|
9
|
+
getDomRenderCacheTarget,
|
|
10
|
+
releaseDomRenderCache,
|
|
11
|
+
} from './domCache';
|
|
12
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
13
|
+
|
|
14
|
+
function makeState() {
|
|
15
|
+
return createDomRenderState(document.createElement('div'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeCacheNode(source: unknown): any {
|
|
19
|
+
return { source, kind: RenderCacheKind, transform2D: createMatrix(), alpha: 1, blendMode: null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('defaultDomRenderCacheRenderer', () => {
|
|
23
|
+
it('is a no-op when no cache is attached to the source', () => {
|
|
24
|
+
const state = makeState();
|
|
25
|
+
expect(() => defaultDomRenderCacheRenderer.submit(state, makeCacheNode(createDisplayObject()))).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('places the target canvas attached to the source node', () => {
|
|
29
|
+
const state = makeState();
|
|
30
|
+
const obj = createDisplayObject();
|
|
31
|
+
const cache = createRenderCache();
|
|
32
|
+
useRenderCache(state, obj, cache);
|
|
33
|
+
const target = ensureDomRenderCacheTarget(state, cache, 16, 16);
|
|
34
|
+
defaultDomRenderCacheRenderer.submit(state, makeCacheNode(obj));
|
|
35
|
+
expect(target.canvas.style.transform).not.toBe('');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('enableDomRenderCache', () => {
|
|
40
|
+
it('registers the renderer for the render cache kind', () => {
|
|
41
|
+
const state = makeState();
|
|
42
|
+
enableDomRenderCache(state);
|
|
43
|
+
expect(getDomRenderStateRuntime(state).rendererMap.get(RenderCacheKind)).toBe(defaultDomRenderCacheRenderer);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('ensureDomRenderCacheTarget', () => {
|
|
48
|
+
it('creates a target sized to the request', () => {
|
|
49
|
+
const state = makeState();
|
|
50
|
+
const target = ensureDomRenderCacheTarget(state, createRenderCache(), 64, 32);
|
|
51
|
+
expect(target.width).toBe(64);
|
|
52
|
+
expect(target.height).toBe(32);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('reuses and resizes the same target on subsequent calls', () => {
|
|
56
|
+
const state = makeState();
|
|
57
|
+
const cache = createRenderCache();
|
|
58
|
+
const first = ensureDomRenderCacheTarget(state, cache, 64, 32);
|
|
59
|
+
const second = ensureDomRenderCacheTarget(state, cache, 16, 16);
|
|
60
|
+
expect(second).toBe(first);
|
|
61
|
+
expect(second.width).toBe(16);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('keeps targets isolated per state for the same handle', () => {
|
|
65
|
+
const stateA = makeState();
|
|
66
|
+
const stateB = makeState();
|
|
67
|
+
const cache = createRenderCache();
|
|
68
|
+
expect(ensureDomRenderCacheTarget(stateA, cache, 8, 8)).not.toBe(ensureDomRenderCacheTarget(stateB, cache, 8, 8));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('getDomRenderCacheTarget', () => {
|
|
73
|
+
it('returns null before a target is allocated', () => {
|
|
74
|
+
expect(getDomRenderCacheTarget(makeState(), createRenderCache())).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns the allocated target', () => {
|
|
78
|
+
const state = makeState();
|
|
79
|
+
const cache = createRenderCache();
|
|
80
|
+
const target = ensureDomRenderCacheTarget(state, cache, 8, 8);
|
|
81
|
+
expect(getDomRenderCacheTarget(state, cache)).toBe(target);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('releaseDomRenderCache', () => {
|
|
86
|
+
it('drops the target for the cache', () => {
|
|
87
|
+
const state = makeState();
|
|
88
|
+
const cache = createRenderCache();
|
|
89
|
+
ensureDomRenderCacheTarget(state, cache, 8, 8);
|
|
90
|
+
releaseDomRenderCache(state, cache);
|
|
91
|
+
expect(getDomRenderCacheTarget(state, cache)).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { enableDomClipSupport } from './domClip';
|
|
2
|
+
import { createDomRenderState } from './domRenderState';
|
|
3
|
+
|
|
4
|
+
describe('enableDomClipSupport', () => {
|
|
5
|
+
it('sets DOM clip hooks on the render state', () => {
|
|
6
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
7
|
+
|
|
8
|
+
enableDomClipSupport(state);
|
|
9
|
+
|
|
10
|
+
expect(state.displayObjectClipHooks).not.toBeNull();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import type { DomClipContourEntry } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { buildDomContourClipPath, pushDomClipContours } from './domClipContours';
|
|
5
|
+
|
|
6
|
+
const identityMap = (x: number, y: number): readonly [number, number] => [x, y];
|
|
7
|
+
|
|
8
|
+
describe('buildDomContourClipPath', () => {
|
|
9
|
+
it('emits a polygon() for a single contour', () => {
|
|
10
|
+
const entry: DomClipContourEntry = {
|
|
11
|
+
kind: 'contour',
|
|
12
|
+
contours: [[0, 0, 10, 0, 10, 10, 0, 10]],
|
|
13
|
+
winding: 'nonZero',
|
|
14
|
+
};
|
|
15
|
+
expect(buildDomContourClipPath(entry, identityMap)).toBe('polygon(0px 0px, 10px 0px, 10px 10px, 0px 10px)');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('emits a path() with fill-rule for multiple contours', () => {
|
|
19
|
+
const entry: DomClipContourEntry = {
|
|
20
|
+
kind: 'contour',
|
|
21
|
+
contours: [
|
|
22
|
+
[0, 0, 10, 0, 10, 10],
|
|
23
|
+
[2, 2, 4, 2, 4, 4],
|
|
24
|
+
],
|
|
25
|
+
winding: 'evenOdd',
|
|
26
|
+
};
|
|
27
|
+
const result = buildDomContourClipPath(entry, identityMap);
|
|
28
|
+
expect(result.startsWith("path('evenodd', '")).toBe(true);
|
|
29
|
+
expect(result).toContain('M0 0 L10 0 L10 10 Z');
|
|
30
|
+
expect(result).toContain('M2 2 L4 2 L4 4 Z');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('maps points through the provided mapper', () => {
|
|
34
|
+
const entry: DomClipContourEntry = { kind: 'contour', contours: [[0, 0, 2, 0, 2, 2]], winding: 'nonZero' };
|
|
35
|
+
const shift = (x: number, y: number): readonly [number, number] => [x + 5, y + 1];
|
|
36
|
+
expect(buildDomContourClipPath(entry, shift)).toBe('polygon(5px 1px, 7px 1px, 7px 3px)');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('pushDomClipContours', () => {
|
|
41
|
+
it('pushes a contour entry with points transformed to stage space', () => {
|
|
42
|
+
const stack: DomClipContourEntry[] = [];
|
|
43
|
+
const transform = createMatrix();
|
|
44
|
+
transform.tx = 100;
|
|
45
|
+
transform.ty = 50;
|
|
46
|
+
pushDomClipContours(stack, [[0, 0, 10, 0, 10, 10]], 'nonZero', transform);
|
|
47
|
+
expect(stack.length).toBe(1);
|
|
48
|
+
expect(stack[0].kind).toBe('contour');
|
|
49
|
+
expect(stack[0].winding).toBe('nonZero');
|
|
50
|
+
expect(stack[0].contours).toEqual([[100, 50, 110, 50, 110, 60]]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('applies the full affine transform to each point', () => {
|
|
54
|
+
const stack: DomClipContourEntry[] = [];
|
|
55
|
+
const transform = createMatrix();
|
|
56
|
+
transform.a = 2;
|
|
57
|
+
transform.d = 3;
|
|
58
|
+
pushDomClipContours(stack, [[1, 1]], 'evenOdd', transform);
|
|
59
|
+
expect(stack[0].contours).toEqual([[2, 3]]);
|
|
60
|
+
expect(stack[0].winding).toBe('evenOdd');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createMatrix, createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
4
|
+
import type { DomRenderState, DomStageRectangle } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
applyDomClipRectangles,
|
|
8
|
+
createDomStageRectangle,
|
|
9
|
+
pushDomClipRectangle,
|
|
10
|
+
setDomClipHooks,
|
|
11
|
+
} from './domClipRectangle';
|
|
12
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
13
|
+
|
|
14
|
+
function makeState(): DomRenderState {
|
|
15
|
+
return createDomRenderState(document.createElement('div'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('applyDomClipRectangles', () => {
|
|
19
|
+
it('clears clip styles when no rectangles are active', () => {
|
|
20
|
+
const state = makeState();
|
|
21
|
+
const source = createDisplayObject();
|
|
22
|
+
const data = getOrCreateRenderProxy2D(state, source);
|
|
23
|
+
const element = document.createElement('div');
|
|
24
|
+
element.style.clipPath = 'inset(0)';
|
|
25
|
+
getDomRenderStateRuntime(state).domElementMap.set(data, element);
|
|
26
|
+
|
|
27
|
+
applyDomClipRectangles(state, data, []);
|
|
28
|
+
|
|
29
|
+
expect(element.style.clipPath).toBe('');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('sets a local polygon clip path from intersected stage rectangles', () => {
|
|
33
|
+
const state = makeState();
|
|
34
|
+
const source = createDisplayObject();
|
|
35
|
+
const data = getOrCreateRenderProxy2D(state, source);
|
|
36
|
+
const element = document.createElement('div');
|
|
37
|
+
element.style.transform = 'matrix(1,0,0,1,10,20)';
|
|
38
|
+
getDomRenderStateRuntime(state).domElementMap.set(data, element);
|
|
39
|
+
|
|
40
|
+
applyDomClipRectangles(state, data, [
|
|
41
|
+
{ bottom: 100, left: 0, right: 100, top: 0 },
|
|
42
|
+
{ bottom: 90, left: 20, right: 80, top: 30 },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
expect(element.style.clipPath).toBe('polygon(10px 10px, 70px 10px, 70px 70px, 10px 70px)');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('createDomStageRectangle', () => {
|
|
50
|
+
it('transforms a local rectangle into a stage-space AABB', () => {
|
|
51
|
+
const rect = createDomStageRectangle(createRectangle(10, 20, 30, 40), createMatrix(1, 0, 0, 1, 5, 6));
|
|
52
|
+
|
|
53
|
+
expect(rect).toEqual({ bottom: 66, left: 15, right: 45, top: 26 });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('pushDomClipRectangle', () => {
|
|
58
|
+
it('pushes a transformed stage rectangle', () => {
|
|
59
|
+
const rectangles: DomStageRectangle[] = [];
|
|
60
|
+
|
|
61
|
+
pushDomClipRectangle(rectangles, createRectangle(0, 0, 10, 20), createMatrix(1, 0, 0, 1, 2, 3));
|
|
62
|
+
|
|
63
|
+
expect(rectangles).toEqual([{ bottom: 23, left: 2, right: 12, top: 3 }]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('setDomClipHooks', () => {
|
|
68
|
+
it('sets DOM clip hooks on the render state', () => {
|
|
69
|
+
const state = makeState();
|
|
70
|
+
setDomClipHooks(state);
|
|
71
|
+
expect(getDomRenderStateRuntime(state).domClipHooks).not.toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('is idempotent', () => {
|
|
75
|
+
const state = makeState();
|
|
76
|
+
setDomClipHooks(state);
|
|
77
|
+
const first = getDomRenderStateRuntime(state).domClipHooks;
|
|
78
|
+
setDomClipHooks(state);
|
|
79
|
+
expect(getDomRenderStateRuntime(state).domClipHooks).toBe(first);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { createDisplayObject, setDisplayObjectClip } from '@flighthq/displayobject';
|
|
2
|
+
import { createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { addNodeChild, invalidateNodeLocalTransform } from '@flighthq/node';
|
|
4
|
+
import { getOrCreateRenderProxy2D, prepareDisplayObjectRender, registerRenderer } from '@flighthq/render';
|
|
5
|
+
import type { ClipRegion, Rectangle } from '@flighthq/types';
|
|
6
|
+
import { DisplayObjectKind } from '@flighthq/types';
|
|
7
|
+
|
|
8
|
+
import { enableDomClipSupport } from './domClip';
|
|
9
|
+
import { defaultDomDisplayObjectRenderer, drawDomDisplayObject, renderDomDisplayObject } from './domDisplayObject';
|
|
10
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
11
|
+
|
|
12
|
+
function makeRectangleClip(rect: Rectangle): ClipRegion {
|
|
13
|
+
return { contours: null, rect, version: 0, winding: 'nonZero' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeState() {
|
|
17
|
+
const container = document.createElement('div');
|
|
18
|
+
return createDomRenderState(container);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Sets up a node with a mock renderer that registers a specific element without going
|
|
22
|
+
// through registerRenderer (which would change rendererMapId and overwrite other nodes).
|
|
23
|
+
function setupRenderedNode(
|
|
24
|
+
state: ReturnType<typeof makeState>,
|
|
25
|
+
obj: ReturnType<typeof createDisplayObject>,
|
|
26
|
+
el: HTMLElement,
|
|
27
|
+
) {
|
|
28
|
+
const data = getOrCreateRenderProxy2D(state, obj);
|
|
29
|
+
data.visible = true;
|
|
30
|
+
data.alpha = 1;
|
|
31
|
+
data.transform2D.a = 1;
|
|
32
|
+
data.transform2D.d = 1;
|
|
33
|
+
data.renderer = {
|
|
34
|
+
createData: vi.fn(),
|
|
35
|
+
submit: vi.fn().mockImplementation(() => {
|
|
36
|
+
getDomRenderStateRuntime(state).domCurrentElement = el;
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
data.rendererMapId = getDomRenderStateRuntime(state).rendererMapId;
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('defaultDomDisplayObjectRenderer', () => {
|
|
44
|
+
it('exposes a createData and submit function', () => {
|
|
45
|
+
expect(typeof defaultDomDisplayObjectRenderer.createData).toBe('function');
|
|
46
|
+
expect(typeof defaultDomDisplayObjectRenderer.submit).toBe('function');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('drawDomDisplayObject', () => {
|
|
51
|
+
it('does not throw for any render proxy', () => {
|
|
52
|
+
const state = makeState();
|
|
53
|
+
const proxy = getOrCreateRenderProxy2D(state, createDisplayObject());
|
|
54
|
+
expect(() => drawDomDisplayObject(state, proxy)).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('renderDomDisplayObject', () => {
|
|
59
|
+
it('does not throw for a simple visible object', () => {
|
|
60
|
+
const state = makeState();
|
|
61
|
+
const obj = createDisplayObject();
|
|
62
|
+
expect(() => renderDomDisplayObject(state, obj)).not.toThrow();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('removes foreign elements from the container on first render', () => {
|
|
66
|
+
const state = makeState();
|
|
67
|
+
const foreign = document.createElement('span');
|
|
68
|
+
state.element.appendChild(foreign);
|
|
69
|
+
|
|
70
|
+
const obj = createDisplayObject();
|
|
71
|
+
renderDomDisplayObject(state, obj);
|
|
72
|
+
|
|
73
|
+
expect(state.element.contains(foreign)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('skips rendering when the object has zero scale', () => {
|
|
77
|
+
const state = makeState();
|
|
78
|
+
const obj = createDisplayObject();
|
|
79
|
+
obj.scaleX = 0;
|
|
80
|
+
obj.scaleY = 0;
|
|
81
|
+
invalidateNodeLocalTransform(obj);
|
|
82
|
+
|
|
83
|
+
const renderer = { createData: vi.fn(), submit: vi.fn() };
|
|
84
|
+
registerRenderer(state, DisplayObjectKind, renderer);
|
|
85
|
+
prepareDisplayObjectRender(state, obj);
|
|
86
|
+
renderDomDisplayObject(state, obj);
|
|
87
|
+
|
|
88
|
+
expect(renderer.submit).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('skips rendering invisible objects', () => {
|
|
92
|
+
const state = makeState();
|
|
93
|
+
const obj = createDisplayObject();
|
|
94
|
+
obj.visible = false;
|
|
95
|
+
|
|
96
|
+
const renderer = { createData: vi.fn(), submit: vi.fn() };
|
|
97
|
+
registerRenderer(state, DisplayObjectKind, renderer);
|
|
98
|
+
prepareDisplayObjectRender(state, obj);
|
|
99
|
+
renderDomDisplayObject(state, obj);
|
|
100
|
+
|
|
101
|
+
expect(renderer.submit).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('skips objects with zero alpha', () => {
|
|
105
|
+
const state = makeState();
|
|
106
|
+
const obj = createDisplayObject();
|
|
107
|
+
obj.alpha = 0;
|
|
108
|
+
|
|
109
|
+
const renderer = { createData: vi.fn(), submit: vi.fn() };
|
|
110
|
+
registerRenderer(state, DisplayObjectKind, renderer);
|
|
111
|
+
prepareDisplayObjectRender(state, obj);
|
|
112
|
+
renderDomDisplayObject(state, obj);
|
|
113
|
+
|
|
114
|
+
expect(renderer.submit).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('calls draw when the object is visible and has a renderer', () => {
|
|
118
|
+
const state = makeState();
|
|
119
|
+
const obj = createDisplayObject();
|
|
120
|
+
const el = document.createElement('div');
|
|
121
|
+
const data = setupRenderedNode(state, obj, el);
|
|
122
|
+
|
|
123
|
+
renderDomDisplayObject(state, obj);
|
|
124
|
+
|
|
125
|
+
expect(data.renderer!.submit).toHaveBeenCalledOnce();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('traverses children', () => {
|
|
129
|
+
const state = makeState();
|
|
130
|
+
const parent = createDisplayObject();
|
|
131
|
+
const child = createDisplayObject();
|
|
132
|
+
addNodeChild(parent, child);
|
|
133
|
+
|
|
134
|
+
const childEl = document.createElement('div');
|
|
135
|
+
const childData = setupRenderedNode(state, child, childEl);
|
|
136
|
+
|
|
137
|
+
prepareDisplayObjectRender(state, parent);
|
|
138
|
+
renderDomDisplayObject(state, parent);
|
|
139
|
+
|
|
140
|
+
expect(childData.renderer!.submit).toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('skips draw on fully static nodes after first render', () => {
|
|
144
|
+
const state = makeState();
|
|
145
|
+
const obj = createDisplayObject();
|
|
146
|
+
const el = document.createElement('div');
|
|
147
|
+
const data = setupRenderedNode(state, obj, el);
|
|
148
|
+
|
|
149
|
+
// First render: node is new, draw must be called.
|
|
150
|
+
renderDomDisplayObject(state, obj);
|
|
151
|
+
expect(data.renderer!.submit).toHaveBeenCalledTimes(1);
|
|
152
|
+
|
|
153
|
+
// Second render: nothing changed, draw should be skipped.
|
|
154
|
+
renderDomDisplayObject(state, obj);
|
|
155
|
+
expect(data.renderer!.submit).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('places rendered elements in scene-graph order', () => {
|
|
159
|
+
const state = makeState();
|
|
160
|
+
const parent = createDisplayObject();
|
|
161
|
+
const childA = createDisplayObject();
|
|
162
|
+
const childB = createDisplayObject();
|
|
163
|
+
addNodeChild(parent, childA);
|
|
164
|
+
addNodeChild(parent, childB);
|
|
165
|
+
|
|
166
|
+
const elA = document.createElement('div');
|
|
167
|
+
const elB = document.createElement('span');
|
|
168
|
+
setupRenderedNode(state, childA, elA);
|
|
169
|
+
setupRenderedNode(state, childB, elB);
|
|
170
|
+
|
|
171
|
+
prepareDisplayObjectRender(state, parent);
|
|
172
|
+
renderDomDisplayObject(state, parent);
|
|
173
|
+
|
|
174
|
+
const children = Array.from(state.element.children);
|
|
175
|
+
expect(children.indexOf(elA)).toBeLessThan(children.indexOf(elB));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('reconciles DOM when a child is removed', () => {
|
|
179
|
+
const state = makeState();
|
|
180
|
+
const parent = createDisplayObject();
|
|
181
|
+
const childA = createDisplayObject();
|
|
182
|
+
const childB = createDisplayObject();
|
|
183
|
+
addNodeChild(parent, childA);
|
|
184
|
+
addNodeChild(parent, childB);
|
|
185
|
+
|
|
186
|
+
const elA = document.createElement('div');
|
|
187
|
+
const elB = document.createElement('span');
|
|
188
|
+
setupRenderedNode(state, childA, elA);
|
|
189
|
+
const dataB = setupRenderedNode(state, childB, elB);
|
|
190
|
+
|
|
191
|
+
prepareDisplayObjectRender(state, parent);
|
|
192
|
+
renderDomDisplayObject(state, parent);
|
|
193
|
+
expect(state.element.children.length).toBe(2);
|
|
194
|
+
|
|
195
|
+
dataB.visible = false;
|
|
196
|
+
|
|
197
|
+
renderDomDisplayObject(state, parent);
|
|
198
|
+
|
|
199
|
+
expect(state.element.contains(elA)).toBe(true);
|
|
200
|
+
expect(state.element.contains(elB)).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('applies an inherited rectangle clip to child elements', () => {
|
|
204
|
+
const state = makeState();
|
|
205
|
+
enableDomClipSupport(state);
|
|
206
|
+
const parent = createDisplayObject();
|
|
207
|
+
setDisplayObjectClip(parent, makeRectangleClip(createRectangle(10, 20, 30, 40)));
|
|
208
|
+
const child = createDisplayObject();
|
|
209
|
+
addNodeChild(parent, child);
|
|
210
|
+
|
|
211
|
+
prepareDisplayObjectRender(state, parent);
|
|
212
|
+
const el = document.createElement('div');
|
|
213
|
+
setupRenderedNode(state, child, el);
|
|
214
|
+
|
|
215
|
+
renderDomDisplayObject(state, parent);
|
|
216
|
+
|
|
217
|
+
expect(el.style.clipPath).toBe('polygon(10px 20px, 40px 20px, 40px 60px, 10px 60px)');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('applies a rectangle clip set directly on the clipping node to its content', () => {
|
|
221
|
+
const state = makeState();
|
|
222
|
+
enableDomClipSupport(state);
|
|
223
|
+
const parent = createDisplayObject();
|
|
224
|
+
setDisplayObjectClip(parent, makeRectangleClip(createRectangle(5, 6, 20, 30)));
|
|
225
|
+
const content = createDisplayObject();
|
|
226
|
+
addNodeChild(parent, content);
|
|
227
|
+
|
|
228
|
+
const el = document.createElement('div');
|
|
229
|
+
setupRenderedNode(state, content, el);
|
|
230
|
+
|
|
231
|
+
prepareDisplayObjectRender(state, parent);
|
|
232
|
+
renderDomDisplayObject(state, parent);
|
|
233
|
+
|
|
234
|
+
expect(el.style.clipPath).toBe('polygon(5px 6px, 25px 6px, 25px 36px, 5px 36px)');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getDomFontAscentCached, invalidateDomFontResource, setDomFontAscentCached } from './domFontSource';
|
|
2
|
+
|
|
3
|
+
describe('getDomFontAscentCached', () => {
|
|
4
|
+
it('returns undefined for an unknown font key', () => {
|
|
5
|
+
expect(getDomFontAscentCached('12px "UnknownFont-x9z"')).toBeUndefined();
|
|
6
|
+
});
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('invalidateDomFontResource', () => {
|
|
10
|
+
it('removes cache entries that match the font family', () => {
|
|
11
|
+
setDomFontAscentCached("16px 'MyFont'", 14);
|
|
12
|
+
setDomFontAscentCached('16px "MyFont"', 14);
|
|
13
|
+
invalidateDomFontResource({ family: 'MyFont', face: null });
|
|
14
|
+
expect(getDomFontAscentCached("16px 'MyFont'")).toBeUndefined();
|
|
15
|
+
expect(getDomFontAscentCached('16px "MyFont"')).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does not remove cache entries for other families', () => {
|
|
19
|
+
setDomFontAscentCached("16px 'OtherFont'", 13);
|
|
20
|
+
invalidateDomFontResource({ family: 'MyFont', face: null });
|
|
21
|
+
expect(getDomFontAscentCached("16px 'OtherFont'")).toBe(13);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('setDomFontAscentCached', () => {
|
|
26
|
+
it('stores and retrieves an ascent value', () => {
|
|
27
|
+
setDomFontAscentCached('12px "CacheTestFont"', 10);
|
|
28
|
+
expect(getDomFontAscentCached('12px "CacheTestFont"')).toBe(10);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { BlendMode } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { applyDomBlendMode, enableDomBlendModeSupport, getDomBlendModeFidelity } from './domMaterials';
|
|
4
|
+
import { createDomRenderState } from './domRenderState';
|
|
5
|
+
|
|
6
|
+
describe('applyDomBlendMode', () => {
|
|
7
|
+
let el: HTMLElement;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
el = document.createElement('div');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('sets mixBlendMode to "screen" for BlendMode.Add', () => {
|
|
14
|
+
applyDomBlendMode(el, BlendMode.Add);
|
|
15
|
+
expect(el.style.mixBlendMode).toBe('screen');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('sets mixBlendMode to "darken" for BlendMode.Darken', () => {
|
|
19
|
+
applyDomBlendMode(el, BlendMode.Darken);
|
|
20
|
+
expect(el.style.mixBlendMode).toBe('darken');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('sets mixBlendMode to "difference" for BlendMode.Difference', () => {
|
|
24
|
+
applyDomBlendMode(el, BlendMode.Difference);
|
|
25
|
+
expect(el.style.mixBlendMode).toBe('difference');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('sets mixBlendMode to "hard-light" for BlendMode.Hardlight', () => {
|
|
29
|
+
applyDomBlendMode(el, BlendMode.Hardlight);
|
|
30
|
+
expect(el.style.mixBlendMode).toBe('hard-light');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('sets mixBlendMode to "lighten" for BlendMode.Lighten', () => {
|
|
34
|
+
applyDomBlendMode(el, BlendMode.Lighten);
|
|
35
|
+
expect(el.style.mixBlendMode).toBe('lighten');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('sets mixBlendMode to "multiply" for BlendMode.Multiply', () => {
|
|
39
|
+
applyDomBlendMode(el, BlendMode.Multiply);
|
|
40
|
+
expect(el.style.mixBlendMode).toBe('multiply');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('sets mixBlendMode to "overlay" for BlendMode.Overlay', () => {
|
|
44
|
+
applyDomBlendMode(el, BlendMode.Overlay);
|
|
45
|
+
expect(el.style.mixBlendMode).toBe('overlay');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('sets mixBlendMode to "screen" for BlendMode.Screen', () => {
|
|
49
|
+
applyDomBlendMode(el, BlendMode.Screen);
|
|
50
|
+
expect(el.style.mixBlendMode).toBe('screen');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('clears mixBlendMode for null (default)', () => {
|
|
54
|
+
el.style.mixBlendMode = 'multiply';
|
|
55
|
+
applyDomBlendMode(el, null);
|
|
56
|
+
expect(el.style.mixBlendMode).toBe('');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('clears mixBlendMode for BlendMode.Normal', () => {
|
|
60
|
+
el.style.mixBlendMode = 'multiply';
|
|
61
|
+
applyDomBlendMode(el, BlendMode.Normal);
|
|
62
|
+
expect(el.style.mixBlendMode).toBe('');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('enableDomBlendModeSupport', () => {
|
|
67
|
+
it('wires applyBlendMode onto the state', () => {
|
|
68
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
69
|
+
expect(state.applyBlendMode).toBeNull();
|
|
70
|
+
enableDomBlendModeSupport(state);
|
|
71
|
+
expect(state.applyBlendMode).not.toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('causes blend modes to be applied to elements', () => {
|
|
75
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
76
|
+
enableDomBlendModeSupport(state);
|
|
77
|
+
const el = document.createElement('div');
|
|
78
|
+
state.applyBlendMode!(el, BlendMode.Multiply);
|
|
79
|
+
expect(el.style.mixBlendMode).toBe('multiply');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('getDomBlendModeFidelity', () => {
|
|
84
|
+
it('returns "approximate" for BlendMode.Add', () => {
|
|
85
|
+
expect(getDomBlendModeFidelity(BlendMode.Add)).toBe('approximate');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns "exact" for BlendMode.Multiply', () => {
|
|
89
|
+
expect(getDomBlendModeFidelity(BlendMode.Multiply)).toBe('exact');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns "exact" for BlendMode.Normal', () => {
|
|
93
|
+
expect(getDomBlendModeFidelity(BlendMode.Normal)).toBe('exact');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns "unsupported" for BlendMode.Alpha', () => {
|
|
97
|
+
expect(getDomBlendModeFidelity(BlendMode.Alpha)).toBe('unsupported');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns "unsupported" for BlendMode.Erase', () => {
|
|
101
|
+
expect(getDomBlendModeFidelity(BlendMode.Erase)).toBe('unsupported');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns "unsupported" for BlendMode.Invert', () => {
|
|
105
|
+
expect(getDomBlendModeFidelity(BlendMode.Invert)).toBe('unsupported');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns "unsupported" for BlendMode.Subtract', () => {
|
|
109
|
+
expect(getDomBlendModeFidelity(BlendMode.Subtract)).toBe('unsupported');
|
|
110
|
+
});
|
|
111
|
+
});
|