@flighthq/shape 0.3.0-next.906.07cea63 → 0.3.1-next.209.43ef1bc
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/compactStrokePath.d.ts +3 -0
- package/dist/compactStrokePath.d.ts.map +1 -0
- package/dist/compactStrokePath.js +462 -0
- package/dist/compactStrokePath.js.map +1 -0
- package/dist/contract.d.ts +7 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +7 -2
- package/dist/contract.js.map +1 -1
- package/dist/explainMorphShapeGradientEndpoints.d.ts +5 -0
- package/dist/explainMorphShapeGradientEndpoints.d.ts.map +1 -0
- package/dist/explainMorphShapeGradientEndpoints.js +43 -0
- package/dist/explainMorphShapeGradientEndpoints.js.map +1 -0
- package/dist/explainShapeTessellation.d.ts +3 -0
- package/dist/explainShapeTessellation.d.ts.map +1 -0
- package/dist/explainShapeTessellation.js +20 -0
- package/dist/explainShapeTessellation.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/morphShape.d.ts +8 -0
- package/dist/morphShape.d.ts.map +1 -0
- package/dist/morphShape.js +74 -0
- package/dist/morphShape.js.map +1 -0
- package/dist/morphShapeAnimation.d.ts +5 -0
- package/dist/morphShapeAnimation.d.ts.map +1 -0
- package/dist/morphShapeAnimation.js +27 -0
- package/dist/morphShapeAnimation.js.map +1 -0
- package/dist/morphShapePaint.d.ts +9 -0
- package/dist/morphShapePaint.d.ts.map +1 -0
- package/dist/morphShapePaint.js +183 -0
- package/dist/morphShapePaint.js.map +1 -0
- package/dist/scale9ShapeCommands.d.ts +3 -0
- package/dist/scale9ShapeCommands.d.ts.map +1 -0
- package/dist/scale9ShapeCommands.js +119 -0
- package/dist/scale9ShapeCommands.js.map +1 -0
- package/dist/shape.d.ts.map +1 -1
- package/dist/shape.js +16 -3
- package/dist/shape.js.map +1 -1
- package/dist/shapeCommands.d.ts +3 -3
- package/dist/shapeCommands.d.ts.map +1 -1
- package/dist/shapeCommands.js +8 -8
- package/dist/shapeCommands.js.map +1 -1
- package/dist/shapeFill.d.ts.map +1 -1
- package/dist/shapeFill.js +8 -14
- package/dist/shapeFill.js.map +1 -1
- package/dist/shapeStroke.d.ts +2 -2
- package/dist/shapeStroke.d.ts.map +1 -1
- package/dist/shapeStroke.js +13 -77
- package/dist/shapeStroke.js.map +1 -1
- package/dist/shapeStrokeOutline.d.ts +3 -0
- package/dist/shapeStrokeOutline.d.ts.map +1 -0
- package/dist/shapeStrokeOutline.js +164 -0
- package/dist/shapeStrokeOutline.js.map +1 -0
- package/package.json +10 -7
- package/src/compactStrokePath.test.ts +31 -0
- package/src/explainMorphShapeGradientEndpoints.test.ts +47 -0
- package/src/explainShapeTessellation.test.ts +65 -0
- package/src/morphShape.test.ts +207 -0
- package/src/morphShapeAnimation.test.ts +147 -0
- package/src/morphShapePaint.test.ts +236 -0
- package/src/scale9ShapeCommands.test.ts +64 -0
- package/src/shape.test.ts +35 -0
- package/src/shapeCommands.test.ts +31 -31
- package/src/shapeFill.test.ts +9 -3
- package/src/shapeStroke.test.ts +35 -24
- package/src/shapeStrokeOutline.test.ts +31 -0
- package/dist/shapeHitTestBuiltins.d.ts +0 -2
- package/dist/shapeHitTestBuiltins.d.ts.map +0 -1
- package/dist/shapeHitTestBuiltins.js +0 -85
- package/dist/shapeHitTestBuiltins.js.map +0 -1
- package/dist/shapeHitTestRegistry.d.ts +0 -4
- package/dist/shapeHitTestRegistry.d.ts.map +0 -1
- package/dist/shapeHitTestRegistry.js +0 -13
- package/dist/shapeHitTestRegistry.js.map +0 -1
- package/src/shapeHitTestBuiltins.test.ts +0 -77
- package/src/shapeHitTestRegistry.test.ts +0 -45
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAnimationBlendTree,
|
|
3
|
+
createAnimationBlendTreeInput,
|
|
4
|
+
createAnimationChannel,
|
|
5
|
+
createAnimationClip,
|
|
6
|
+
createAnimationPlayer,
|
|
7
|
+
createAnimationTrack,
|
|
8
|
+
sampleAnimationBlendTree,
|
|
9
|
+
} from '@flighthq/animation/contract';
|
|
10
|
+
import { getNodeLocalContentRevision } from '@flighthq/node/contract';
|
|
11
|
+
import { appendPathLineTo, appendPathMoveTo, createPath, createPathMorph } from '@flighthq/path/contract';
|
|
12
|
+
import type { MorphShape, MorphShapeAnimationTarget } from '@flighthq/types/contract';
|
|
13
|
+
|
|
14
|
+
import { appendMorphShapePath, createMorphShape } from './morphShape';
|
|
15
|
+
import {
|
|
16
|
+
applyAnimationClipToMorphShape,
|
|
17
|
+
applyMorphShapeAnimationSample,
|
|
18
|
+
createMorphShapeAnimationTarget,
|
|
19
|
+
} from './morphShapeAnimation';
|
|
20
|
+
import { appendMorphShapeBeginFill } from './morphShapePaint';
|
|
21
|
+
|
|
22
|
+
describe('applyAnimationClipToMorphShape', () => {
|
|
23
|
+
it('samples scalar progress while preserving easing overshoot', () => {
|
|
24
|
+
const shape = createTestMorphShape();
|
|
25
|
+
const target = createMorphShapeAnimationTarget(shape);
|
|
26
|
+
const clip = createAnimationClip([
|
|
27
|
+
createAnimationChannel(
|
|
28
|
+
createAnimationTrack({ easing: (progress) => progress * 2, times: [0, 1], values: [0, 1] }),
|
|
29
|
+
target,
|
|
30
|
+
),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
applyAnimationClipToMorphShape(clip, 0.75);
|
|
34
|
+
|
|
35
|
+
expect(shape.data.progress).toBe(1.5);
|
|
36
|
+
expect(shape.data.path.data.slice(0, 2)).toStrictEqual([30, 45]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('applies one clip sample atomically to compound geometry and paint', () => {
|
|
40
|
+
const shape = createTestMorphShape();
|
|
41
|
+
const secondMorph = createTestMorph(100);
|
|
42
|
+
appendMorphShapeBeginFill(shape, { color: 0xff0000 }, { color: 0x0000ff });
|
|
43
|
+
appendMorphShapePath(shape);
|
|
44
|
+
const secondPath = appendMorphShapePath(shape, secondMorph);
|
|
45
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
46
|
+
const clip = createAnimationClip([
|
|
47
|
+
createAnimationChannel(
|
|
48
|
+
createAnimationTrack({ times: [0, 1], values: [0, 1] }),
|
|
49
|
+
createMorphShapeAnimationTarget(shape),
|
|
50
|
+
),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
applyAnimationClipToMorphShape(clip, 0.5);
|
|
54
|
+
|
|
55
|
+
expect(shape.data.path.data.slice(0, 2)).toStrictEqual([10, 15]);
|
|
56
|
+
expect(secondPath.data.slice(0, 2)).toStrictEqual([110, 115]);
|
|
57
|
+
expect(shape.data.commands[2]).toBe(0x800080);
|
|
58
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('ignores foreign channels in a composed clip', () => {
|
|
62
|
+
const shape = createTestMorphShape();
|
|
63
|
+
const clip = createAnimationClip([
|
|
64
|
+
createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 100] }), { node: shape, path: 'X' }),
|
|
65
|
+
createAnimationChannel(
|
|
66
|
+
createAnimationTrack({ times: [0, 1], values: [0, 1] }),
|
|
67
|
+
createMorphShapeAnimationTarget(shape),
|
|
68
|
+
),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
applyAnimationClipToMorphShape(clip, 0.25);
|
|
72
|
+
|
|
73
|
+
expect(shape.x).toBe(0);
|
|
74
|
+
expect(shape.data.progress).toBe(0.25);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('applyMorphShapeAnimationSample', () => {
|
|
79
|
+
it('mutates stable path buffers and invalidates retained content', () => {
|
|
80
|
+
const shape = createTestMorphShape();
|
|
81
|
+
const commands = shape.data.path.commands;
|
|
82
|
+
const data = shape.data.path.data;
|
|
83
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
84
|
+
const channel = createAnimationChannel(
|
|
85
|
+
createAnimationTrack({ times: [0, 1], values: [0, 1] }),
|
|
86
|
+
createMorphShapeAnimationTarget(shape),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(applyMorphShapeAnimationSample([0.5], channel)).toBe(true);
|
|
90
|
+
expect(shape.data.progress).toBe(0.5);
|
|
91
|
+
expect(shape.data.path.commands).toBe(commands);
|
|
92
|
+
expect(shape.data.path.data).toBe(data);
|
|
93
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('serves as the sink for composed animation samples', () => {
|
|
97
|
+
const shape = createTestMorphShape();
|
|
98
|
+
const target = createMorphShapeAnimationTarget(shape);
|
|
99
|
+
const a = createAnimationPlayer(
|
|
100
|
+
createAnimationClip([createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 1] }), target)]),
|
|
101
|
+
{ time: 0.5 },
|
|
102
|
+
);
|
|
103
|
+
const b = createAnimationPlayer(
|
|
104
|
+
createAnimationClip([createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 0.2] }), target)]),
|
|
105
|
+
{ time: 0.5 },
|
|
106
|
+
);
|
|
107
|
+
const tree = createAnimationBlendTree([createAnimationBlendTreeInput(a, 1), createAnimationBlendTreeInput(b, 1)]);
|
|
108
|
+
|
|
109
|
+
sampleAnimationBlendTree([0], tree, applyMorphShapeAnimationSample);
|
|
110
|
+
|
|
111
|
+
expect(shape.data.progress).toBeCloseTo(0.3);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns false for foreign targets without changing the shape', () => {
|
|
115
|
+
const shape = createTestMorphShape();
|
|
116
|
+
const channel = createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 1] }), {
|
|
117
|
+
shape: { kind: 'acme.NotMorphShape' },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(applyMorphShapeAnimationSample([1], channel)).toBe(false);
|
|
121
|
+
expect(shape.data.progress).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('createMorphShapeAnimationTarget', () => {
|
|
126
|
+
it('allocates a reusable target descriptor for one MorphShape', () => {
|
|
127
|
+
const shape = createTestMorphShape();
|
|
128
|
+
|
|
129
|
+
const target: MorphShapeAnimationTarget = createMorphShapeAnimationTarget(shape);
|
|
130
|
+
|
|
131
|
+
expect(target).toStrictEqual({ shape });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
function createTestMorphShape(): MorphShape {
|
|
136
|
+
return createMorphShape(createTestMorph());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createTestMorph(offset = 0) {
|
|
140
|
+
const start = createPath();
|
|
141
|
+
appendPathMoveTo(start, offset, offset);
|
|
142
|
+
appendPathLineTo(start, offset + 10, offset);
|
|
143
|
+
const end = createPath();
|
|
144
|
+
appendPathMoveTo(end, offset + 20, offset + 30);
|
|
145
|
+
appendPathLineTo(end, offset + 40, offset + 30);
|
|
146
|
+
return createPathMorph(start, end)!;
|
|
147
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry/contract';
|
|
2
|
+
import { getNodeLocalContentRevision } from '@flighthq/node/contract';
|
|
3
|
+
import { appendPathLineTo, appendPathMoveTo, createPath, createPathMorph } from '@flighthq/path/contract';
|
|
4
|
+
|
|
5
|
+
import { createMorphShape, setMorphShapeProgress } from './morphShape';
|
|
6
|
+
import {
|
|
7
|
+
appendMorphShapeBeginFill,
|
|
8
|
+
appendMorphShapeBeginGradientFill,
|
|
9
|
+
appendMorphShapeBeginTextureFill,
|
|
10
|
+
appendMorphShapeLineGradientStyle,
|
|
11
|
+
appendMorphShapeLineStyle,
|
|
12
|
+
appendMorphShapeLineTextureStyle,
|
|
13
|
+
sampleMorphShapePaintBindings,
|
|
14
|
+
} from './morphShapePaint';
|
|
15
|
+
import { clearShapeCommands } from './shape';
|
|
16
|
+
|
|
17
|
+
const fakeTexture = { id: 1 } as never;
|
|
18
|
+
|
|
19
|
+
describe('appendMorphShapeBeginFill', () => {
|
|
20
|
+
it('retains one command while sampling packed RGB and alpha at the current progress', () => {
|
|
21
|
+
const shape = createTestMorphShape(0.5);
|
|
22
|
+
|
|
23
|
+
appendMorphShapeBeginFill(shape, { alpha: 0.25, color: 0xff0000 }, { alpha: 0.75, color: 0x0000ff });
|
|
24
|
+
|
|
25
|
+
expect(shape.data.commands).toStrictEqual(['beginFill', 2, 0x800080, 0.5]);
|
|
26
|
+
expect(shape.data.paintBindings).toHaveLength(1);
|
|
27
|
+
const commands = shape.data.commands;
|
|
28
|
+
setMorphShapeProgress(shape, 1);
|
|
29
|
+
expect(shape.data.commands).toBe(commands);
|
|
30
|
+
expect(shape.data.commands).toStrictEqual(['beginFill', 2, 0x0000ff, 0.75]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('appendMorphShapeBeginGradientFill', () => {
|
|
35
|
+
it('samples stops, matrices, and focal ratio into stable retained values', () => {
|
|
36
|
+
const shape = createTestMorphShape();
|
|
37
|
+
const startMatrix = createMatrix(1, 2, 3, 4, 5, 6);
|
|
38
|
+
const endMatrix = createMatrix(3, 4, 5, 6, 7, 8);
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
appendMorphShapeBeginGradientFill(
|
|
42
|
+
shape,
|
|
43
|
+
'radial',
|
|
44
|
+
{
|
|
45
|
+
alphas: [0.25, 0.5],
|
|
46
|
+
colors: [0x000000, 0xff0000],
|
|
47
|
+
focalPointRatio: -0.5,
|
|
48
|
+
matrix: startMatrix,
|
|
49
|
+
ratios: [0, 100],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
alphas: [0.75, 1],
|
|
53
|
+
colors: [0xffffff, 0x0000ff],
|
|
54
|
+
focalPointRatio: 0.5,
|
|
55
|
+
matrix: endMatrix,
|
|
56
|
+
ratios: [100, 200],
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
).toBe(true);
|
|
60
|
+
const colors = shape.data.commands[3];
|
|
61
|
+
const alphas = shape.data.commands[4];
|
|
62
|
+
const ratios = shape.data.commands[5];
|
|
63
|
+
const matrix = shape.data.commands[6];
|
|
64
|
+
|
|
65
|
+
setMorphShapeProgress(shape, 0.5);
|
|
66
|
+
|
|
67
|
+
expect(shape.data.commands[3]).toBe(colors);
|
|
68
|
+
expect(shape.data.commands[4]).toBe(alphas);
|
|
69
|
+
expect(shape.data.commands[5]).toBe(ratios);
|
|
70
|
+
expect(shape.data.commands[6]).toBe(matrix);
|
|
71
|
+
expect(colors).toStrictEqual([0x808080, 0x800080]);
|
|
72
|
+
expect(alphas).toStrictEqual([0.5, 0.75]);
|
|
73
|
+
expect(ratios).toStrictEqual([50, 150]);
|
|
74
|
+
expect(matrix).toMatchObject({ a: 2, b: 3, c: 4, d: 5, tx: 6, ty: 7 });
|
|
75
|
+
expect(shape.data.commands[9]).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects incompatible gradient stop topology without mutating the shape', () => {
|
|
79
|
+
const shape = createTestMorphShape();
|
|
80
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
81
|
+
|
|
82
|
+
expect(
|
|
83
|
+
appendMorphShapeBeginGradientFill(
|
|
84
|
+
shape,
|
|
85
|
+
'linear',
|
|
86
|
+
{ alphas: [1], colors: [0], ratios: [0] },
|
|
87
|
+
{ alphas: [1, 1], colors: [0, 1], ratios: [0, 255] },
|
|
88
|
+
),
|
|
89
|
+
).toBe(false);
|
|
90
|
+
expect(shape.data.commands).toStrictEqual([]);
|
|
91
|
+
expect(shape.data.paintBindings).toStrictEqual([]);
|
|
92
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('treats a missing endpoint matrix as identity while retaining one live matrix', () => {
|
|
96
|
+
const shape = createTestMorphShape();
|
|
97
|
+
expect(
|
|
98
|
+
appendMorphShapeBeginGradientFill(
|
|
99
|
+
shape,
|
|
100
|
+
'linear',
|
|
101
|
+
{ alphas: [1], colors: [0], ratios: [0] },
|
|
102
|
+
{ alphas: [1], colors: [0xffffff], matrix: createMatrix(3, 0, 0, 5, 20, 40), ratios: [255] },
|
|
103
|
+
),
|
|
104
|
+
).toBe(true);
|
|
105
|
+
const matrix = shape.data.commands[6];
|
|
106
|
+
|
|
107
|
+
setMorphShapeProgress(shape, 0.5);
|
|
108
|
+
|
|
109
|
+
expect(shape.data.commands[6]).toBe(matrix);
|
|
110
|
+
expect(matrix).toMatchObject({ a: 2, b: 0, c: 0, d: 3, tx: 10, ty: 20 });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('appendMorphShapeBeginTextureFill', () => {
|
|
115
|
+
it('preserves an ordinary null matrix when neither endpoint needs placement morphing', () => {
|
|
116
|
+
const shape = createTestMorphShape();
|
|
117
|
+
|
|
118
|
+
appendMorphShapeBeginTextureFill(shape, fakeTexture);
|
|
119
|
+
|
|
120
|
+
expect(shape.data.commands).toStrictEqual(['beginTextureFill', 2, fakeTexture, null]);
|
|
121
|
+
expect(shape.data.paintBindings).toStrictEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('morphs the placement matrix while retaining the texture and matrix identities', () => {
|
|
125
|
+
const shape = createTestMorphShape();
|
|
126
|
+
appendMorphShapeBeginTextureFill(shape, fakeTexture, null, createMatrix(2, 0, 0, 4, 10, 20));
|
|
127
|
+
const matrix = shape.data.commands[3];
|
|
128
|
+
|
|
129
|
+
setMorphShapeProgress(shape, 0.5);
|
|
130
|
+
|
|
131
|
+
expect(shape.data.commands[2]).toBe(fakeTexture);
|
|
132
|
+
expect(shape.data.commands[3]).toBe(matrix);
|
|
133
|
+
expect(matrix).toMatchObject({ a: 1.5, b: 0, c: 0, d: 2.5, tx: 5, ty: 10 });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('appendMorphShapeLineGradientStyle', () => {
|
|
138
|
+
it('uses the same stable gradient sampler for a stroke paint', () => {
|
|
139
|
+
const shape = createTestMorphShape();
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
appendMorphShapeLineGradientStyle(
|
|
143
|
+
shape,
|
|
144
|
+
'linear',
|
|
145
|
+
{ alphas: [1], colors: [0xff0000], ratios: [0] },
|
|
146
|
+
{ alphas: [0.5], colors: [0x0000ff], ratios: [255] },
|
|
147
|
+
),
|
|
148
|
+
).toBe(true);
|
|
149
|
+
setMorphShapeProgress(shape, 1);
|
|
150
|
+
|
|
151
|
+
expect(shape.data.commands[0]).toBe('lineGradientStyle');
|
|
152
|
+
expect(shape.data.commands[3]).toStrictEqual([0x0000ff]);
|
|
153
|
+
expect(shape.data.commands[4]).toStrictEqual([0.5]);
|
|
154
|
+
expect(shape.data.commands[5]).toStrictEqual([255]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('appendMorphShapeLineStyle', () => {
|
|
159
|
+
it('samples stroke width and color while retaining structural stroke options', () => {
|
|
160
|
+
const shape = createTestMorphShape(0.5);
|
|
161
|
+
|
|
162
|
+
appendMorphShapeLineStyle(
|
|
163
|
+
shape,
|
|
164
|
+
{ alpha: 0.25, color: 0xff0000, thickness: 2 },
|
|
165
|
+
{ alpha: 0.75, color: 0x0000ff, thickness: 10 },
|
|
166
|
+
true,
|
|
167
|
+
'horizontal',
|
|
168
|
+
'round',
|
|
169
|
+
'bevel',
|
|
170
|
+
5,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(shape.data.commands).toStrictEqual([
|
|
174
|
+
'lineStyle',
|
|
175
|
+
8,
|
|
176
|
+
6,
|
|
177
|
+
0x800080,
|
|
178
|
+
0.5,
|
|
179
|
+
true,
|
|
180
|
+
'horizontal',
|
|
181
|
+
'round',
|
|
182
|
+
'bevel',
|
|
183
|
+
5,
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('appendMorphShapeLineTextureStyle', () => {
|
|
189
|
+
it('uses the same stable matrix sampler for a stroke texture', () => {
|
|
190
|
+
const shape = createTestMorphShape();
|
|
191
|
+
appendMorphShapeLineTextureStyle(shape, fakeTexture, createMatrix(), createMatrix(2, 0, 0, 2, 10, 20));
|
|
192
|
+
const matrix = shape.data.commands[3];
|
|
193
|
+
|
|
194
|
+
setMorphShapeProgress(shape, 1);
|
|
195
|
+
|
|
196
|
+
expect(shape.data.commands[0]).toBe('lineTextureStyle');
|
|
197
|
+
expect(shape.data.commands[3]).toBe(matrix);
|
|
198
|
+
expect(matrix).toMatchObject({ a: 2, d: 2, tx: 10, ty: 20 });
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('clearShapeCommands with MorphShape paint', () => {
|
|
203
|
+
it('removes stale paint bindings with the ordinary Shape command stream', () => {
|
|
204
|
+
const shape = createTestMorphShape();
|
|
205
|
+
appendMorphShapeBeginFill(shape, { color: 0 }, { color: 0xffffff });
|
|
206
|
+
|
|
207
|
+
clearShapeCommands(shape);
|
|
208
|
+
setMorphShapeProgress(shape, 1);
|
|
209
|
+
|
|
210
|
+
expect(shape.data.commands).toStrictEqual([]);
|
|
211
|
+
expect(shape.data.paintBindings).toStrictEqual([]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('sampleMorphShapePaintBindings', () => {
|
|
216
|
+
it('can resample prepared paint bindings without reallocating their command values', () => {
|
|
217
|
+
const shape = createTestMorphShape();
|
|
218
|
+
appendMorphShapeBeginFill(shape, { color: 0 }, { color: 0xffffff });
|
|
219
|
+
const commands = shape.data.commands;
|
|
220
|
+
|
|
221
|
+
sampleMorphShapePaintBindings(shape.data, 0.25);
|
|
222
|
+
|
|
223
|
+
expect(shape.data.commands).toBe(commands);
|
|
224
|
+
expect(shape.data.commands[2]).toBe(0x404040);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
function createTestMorphShape(progress = 0) {
|
|
229
|
+
const start = createPath();
|
|
230
|
+
appendPathMoveTo(start, 0, 0);
|
|
231
|
+
appendPathLineTo(start, 10, 0);
|
|
232
|
+
const end = createPath();
|
|
233
|
+
appendPathMoveTo(end, 20, 30);
|
|
234
|
+
appendPathLineTo(end, 40, 30);
|
|
235
|
+
return createMorphShape(createPathMorph(start, end)!, { data: { progress } });
|
|
236
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ShapeCommandToken } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import { createScale9Shape } from './scale9Shape';
|
|
4
|
+
import { mapScale9ShapeCommands } from './scale9ShapeCommands';
|
|
5
|
+
import { appendShapeBeginFill, appendShapeRectangle } from './shapeCommands';
|
|
6
|
+
|
|
7
|
+
const grid = { x: 10, y: 10, width: 80, height: 80 };
|
|
8
|
+
|
|
9
|
+
describe('mapScale9ShapeCommands', () => {
|
|
10
|
+
const out: ShapeCommandToken[] = [];
|
|
11
|
+
|
|
12
|
+
it('passes style commands through unchanged', () => {
|
|
13
|
+
const mapper = { mapX: (x: number) => x, mapY: (y: number) => y };
|
|
14
|
+
mapScale9ShapeCommands(out, ['beginFill', 2, 0xff0000, 1], mapper);
|
|
15
|
+
expect(out).toEqual(['beginFill', 2, 0xff0000, 1]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('remaps moveTo coordinates', () => {
|
|
19
|
+
const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 3 };
|
|
20
|
+
mapScale9ShapeCommands(out, ['moveTo', 2, 10, 20], mapper);
|
|
21
|
+
expect(out).toEqual(['moveTo', 2, 20, 60]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('remaps lineTo coordinates', () => {
|
|
25
|
+
const mapper = { mapX: (x: number) => x + 5, mapY: (y: number) => y + 10 };
|
|
26
|
+
mapScale9ShapeCommands(out, ['lineTo', 2, 100, 50], mapper);
|
|
27
|
+
expect(out).toEqual(['lineTo', 2, 105, 60]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('remaps drawRectangle corners and recomputes size', () => {
|
|
31
|
+
const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 2 };
|
|
32
|
+
mapScale9ShapeCommands(out, ['drawRectangle', 4, 10, 20, 50, 30], mapper);
|
|
33
|
+
// x=10→20, y=20→40, x+w=60→120 (w=100), y+h=50→100 (h=60)
|
|
34
|
+
expect(out).toEqual(['drawRectangle', 4, 20, 40, 100, 60]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('remaps drawRoundRectangle corners but leaves ellipse radii unchanged', () => {
|
|
38
|
+
const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 2 };
|
|
39
|
+
mapScale9ShapeCommands(out, ['drawRoundRectangle', 6, 10, 20, 50, 30, 8, 8], mapper);
|
|
40
|
+
expect(out).toEqual(['drawRoundRectangle', 6, 20, 40, 100, 60, 8, 8]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('remaps drawCircle center but leaves radius unchanged', () => {
|
|
44
|
+
const mapper = { mapX: (x: number) => x + 5, mapY: (y: number) => y + 10 };
|
|
45
|
+
mapScale9ShapeCommands(out, ['drawCircle', 3, 50, 50, 25], mapper);
|
|
46
|
+
expect(out).toEqual(['drawCircle', 3, 55, 60, 25]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('remaps drawEllipse corners and recomputes size', () => {
|
|
50
|
+
const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 2 };
|
|
51
|
+
mapScale9ShapeCommands(out, ['drawEllipse', 4, 0, 0, 100, 50], mapper);
|
|
52
|
+
expect(out).toEqual(['drawEllipse', 4, 0, 0, 200, 100]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns a buffer with the same element count as the input', () => {
|
|
56
|
+
const shape = createScale9Shape(grid);
|
|
57
|
+
appendShapeBeginFill(shape, 0xff0000);
|
|
58
|
+
appendShapeRectangle(shape, 0, 0, 100, 100);
|
|
59
|
+
// A mapper is any pair of coordinate functions; building one from a grid is a renderer's job, and
|
|
60
|
+
// the rewrite under test does not care where the mapping came from.
|
|
61
|
+
mapScale9ShapeCommands(out, shape.data.commands, { mapX: (x) => x * 2, mapY: (y) => y * 2 });
|
|
62
|
+
expect(out).toHaveLength(shape.data.commands.length);
|
|
63
|
+
});
|
|
64
|
+
});
|
package/src/shape.test.ts
CHANGED
|
@@ -4,9 +4,12 @@ import {
|
|
|
4
4
|
getNodeLocalContentRevision,
|
|
5
5
|
getNodeLocalTransformRevision,
|
|
6
6
|
} from '@flighthq/node/contract';
|
|
7
|
+
import { createPath, createPathMorph } from '@flighthq/path/contract';
|
|
7
8
|
import type { ShapeCommandToken } from '@flighthq/types/contract';
|
|
8
9
|
import { ShapeKind } from '@flighthq/types/contract';
|
|
9
10
|
|
|
11
|
+
import { createMorphShape, setMorphShapeProgress } from './morphShape';
|
|
12
|
+
import { appendMorphShapeBeginFill } from './morphShapePaint';
|
|
10
13
|
import {
|
|
11
14
|
clearShapeCommands,
|
|
12
15
|
computeShapeLocalBoundsRectangle,
|
|
@@ -169,6 +172,19 @@ describe('computeShapeLocalBoundsRectangle', () => {
|
|
|
169
172
|
});
|
|
170
173
|
|
|
171
174
|
describe('copyShapeCommands', () => {
|
|
175
|
+
it('discards target paint bindings when replacing a MorphShape command stream', () => {
|
|
176
|
+
const path = createPath();
|
|
177
|
+
const target = createMorphShape(createPathMorph(path, path)!);
|
|
178
|
+
appendMorphShapeBeginFill(target, { color: 0 }, { color: 0xffffff });
|
|
179
|
+
const source = createShape({ data: { commands: ['beginFill', 2, 0x123456, 1] } });
|
|
180
|
+
|
|
181
|
+
copyShapeCommands(target, source);
|
|
182
|
+
setMorphShapeProgress(target, 1);
|
|
183
|
+
|
|
184
|
+
expect(target.data.commands).toStrictEqual(['beginFill', 2, 0x123456, 1]);
|
|
185
|
+
expect(target.data.paintBindings).toStrictEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
172
188
|
it('copies commands from source to target', () => {
|
|
173
189
|
const source = createShape();
|
|
174
190
|
source.data.commands.push('endFill', 0);
|
|
@@ -178,6 +194,25 @@ describe('copyShapeCommands', () => {
|
|
|
178
194
|
expect(target.data.commands).toEqual(['endFill', 0]);
|
|
179
195
|
});
|
|
180
196
|
|
|
197
|
+
it('copies a command stream longer than the engine argument limit', () => {
|
|
198
|
+
// Imported vector artwork reaches this length routinely, and a spread into push would pass one
|
|
199
|
+
// argument per command — so this is the size at which copying, not drawing, is what fails.
|
|
200
|
+
const source = createShape();
|
|
201
|
+
for (let i = 0; i < 400_000; i++) source.data.commands.push('lineTo', 2, i, i);
|
|
202
|
+
const target = createShape();
|
|
203
|
+
|
|
204
|
+
expect(() => copyShapeCommands(target, source)).not.toThrow();
|
|
205
|
+
expect(target.data.commands).toHaveLength(source.data.commands.length);
|
|
206
|
+
expect(target.data.commands[1_599_998]).toBe(399_999);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('leaves the command stream intact when target and source are the same shape', () => {
|
|
210
|
+
const shape = createShape();
|
|
211
|
+
shape.data.commands.push('endFill', 0);
|
|
212
|
+
copyShapeCommands(shape, shape);
|
|
213
|
+
expect(shape.data.commands).toEqual(['endFill', 0]);
|
|
214
|
+
});
|
|
215
|
+
|
|
181
216
|
it('replaces existing target commands and bumps the content revision', () => {
|
|
182
217
|
const source = createShape();
|
|
183
218
|
source.data.commands.push('endFill', 0);
|
|
@@ -4,7 +4,7 @@ import { createShape } from './shape';
|
|
|
4
4
|
import {
|
|
5
5
|
appendShapeArc,
|
|
6
6
|
appendShapeArcTo,
|
|
7
|
-
|
|
7
|
+
appendShapeBeginTextureFill,
|
|
8
8
|
appendShapeBeginFill,
|
|
9
9
|
appendShapeBeginGradientFill,
|
|
10
10
|
appendShapeCircle,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
appendShapeDrawTriangles,
|
|
14
14
|
appendShapeEllipse,
|
|
15
15
|
appendShapeEndFill,
|
|
16
|
-
|
|
16
|
+
appendShapeLineTextureStyle,
|
|
17
17
|
appendShapeLineGradientStyle,
|
|
18
18
|
appendShapeLineStyle,
|
|
19
19
|
appendShapeLineTo,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
PathCommand,
|
|
28
28
|
} from './shapeCommands';
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const fakeTexture = { id: 1 } as never;
|
|
31
31
|
const fakeMatrix = { id: 2, a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 } as never;
|
|
32
32
|
|
|
33
33
|
describe('appendShapeArc', () => {
|
|
@@ -124,20 +124,6 @@ describe('appendShapeArcTo', () => {
|
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
describe('appendShapeBeginBitmapFill', () => {
|
|
128
|
-
it('pushes a beginBitmapFill command with bitmap, matrix, repeat, smooth', () => {
|
|
129
|
-
const shape = createShape();
|
|
130
|
-
appendShapeBeginBitmapFill(shape, fakeImageSource, fakeMatrix, false, true);
|
|
131
|
-
expect(shape.data.commands).toEqual(['beginBitmapFill', 4, fakeImageSource, fakeMatrix, false, true]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('defaults matrix to null, repeat to true, smooth to false', () => {
|
|
135
|
-
const shape = createShape();
|
|
136
|
-
appendShapeBeginBitmapFill(shape, fakeImageSource);
|
|
137
|
-
expect(shape.data.commands).toEqual(['beginBitmapFill', 4, fakeImageSource, null, true, false]);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
127
|
describe('appendShapeBeginFill', () => {
|
|
142
128
|
it('pushes a beginFill command with color and alpha', () => {
|
|
143
129
|
const shape = createShape();
|
|
@@ -198,6 +184,20 @@ describe('appendShapeBeginGradientFill', () => {
|
|
|
198
184
|
});
|
|
199
185
|
});
|
|
200
186
|
|
|
187
|
+
describe('appendShapeBeginTextureFill', () => {
|
|
188
|
+
it('pushes a beginTextureFill command with texture and matrix', () => {
|
|
189
|
+
const shape = createShape();
|
|
190
|
+
appendShapeBeginTextureFill(shape, fakeTexture, fakeMatrix);
|
|
191
|
+
expect(shape.data.commands).toEqual(['beginTextureFill', 2, fakeTexture, fakeMatrix]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('defaults matrix to null', () => {
|
|
195
|
+
const shape = createShape();
|
|
196
|
+
appendShapeBeginTextureFill(shape, fakeTexture);
|
|
197
|
+
expect(shape.data.commands).toEqual(['beginTextureFill', 2, fakeTexture, null]);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
201
|
describe('appendShapeCircle', () => {
|
|
202
202
|
it('pushes a drawCircle command with position and radius', () => {
|
|
203
203
|
const shape = createShape();
|
|
@@ -293,20 +293,6 @@ describe('appendShapeEndFill', () => {
|
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
-
describe('appendShapeLineBitmapStyle', () => {
|
|
297
|
-
it('pushes a lineBitmapStyle command with bitmap, matrix, repeat, smooth', () => {
|
|
298
|
-
const shape = createShape();
|
|
299
|
-
appendShapeLineBitmapStyle(shape, fakeImageSource, fakeMatrix, false, true);
|
|
300
|
-
expect(shape.data.commands).toEqual(['lineBitmapStyle', 4, fakeImageSource, fakeMatrix, false, true]);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('defaults matrix to null, repeat to true, smooth to false', () => {
|
|
304
|
-
const shape = createShape();
|
|
305
|
-
appendShapeLineBitmapStyle(shape, fakeImageSource);
|
|
306
|
-
expect(shape.data.commands).toEqual(['lineBitmapStyle', 4, fakeImageSource, null, true, false]);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
296
|
describe('appendShapeLineGradientStyle', () => {
|
|
311
297
|
it('pushes a lineGradientStyle command with all fields', () => {
|
|
312
298
|
const shape = createShape();
|
|
@@ -340,6 +326,20 @@ describe('appendShapeLineStyle', () => {
|
|
|
340
326
|
});
|
|
341
327
|
});
|
|
342
328
|
|
|
329
|
+
describe('appendShapeLineTextureStyle', () => {
|
|
330
|
+
it('pushes a lineTextureStyle command with texture and matrix', () => {
|
|
331
|
+
const shape = createShape();
|
|
332
|
+
appendShapeLineTextureStyle(shape, fakeTexture, fakeMatrix);
|
|
333
|
+
expect(shape.data.commands).toEqual(['lineTextureStyle', 2, fakeTexture, fakeMatrix]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('defaults matrix to null', () => {
|
|
337
|
+
const shape = createShape();
|
|
338
|
+
appendShapeLineTextureStyle(shape, fakeTexture);
|
|
339
|
+
expect(shape.data.commands).toEqual(['lineTextureStyle', 2, fakeTexture, null]);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
343
|
describe('appendShapeLineTo', () => {
|
|
344
344
|
it('pushes a lineTo command with position', () => {
|
|
345
345
|
const shape = createShape();
|
package/src/shapeFill.test.ts
CHANGED
|
@@ -151,14 +151,17 @@ describe('getShapeFillRegions', () => {
|
|
|
151
151
|
expect(getShapeFillRegions(shape.data.commands)).toBeNull();
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
it('
|
|
154
|
+
it('resolves solid fills independently of a solid stroke', () => {
|
|
155
155
|
const shape = createShape();
|
|
156
156
|
appendShapeLineStyle(shape, 2, 0x000000);
|
|
157
157
|
appendShapeBeginFill(shape, 0xff0000);
|
|
158
158
|
appendShapeRectangle(shape, 0, 0, 10, 10);
|
|
159
159
|
appendShapeEndFill(shape);
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
const regions = getShapeFillRegions(shape.data.commands);
|
|
162
|
+
expect(regions).not.toBeNull();
|
|
163
|
+
expect(regions).toHaveLength(1);
|
|
164
|
+
expect(regions![0].color).toBe(0xff0000);
|
|
162
165
|
});
|
|
163
166
|
});
|
|
164
167
|
|
|
@@ -171,9 +174,12 @@ describe('hasNonSolidShapeFill', () => {
|
|
|
171
174
|
expect(hasNonSolidShapeFill(shape.data.commands)).toBe(false);
|
|
172
175
|
});
|
|
173
176
|
|
|
174
|
-
it('is true
|
|
177
|
+
it('is false for a solid stroke and true for a bitmap or gradient style', () => {
|
|
175
178
|
const shape = createShape();
|
|
176
179
|
appendShapeLineStyle(shape, 1, 0);
|
|
180
|
+
expect(hasNonSolidShapeFill(shape.data.commands)).toBe(false);
|
|
181
|
+
|
|
182
|
+
appendShapeBeginGradientFill(shape, 'linear', [0xff0000, 0x0000ff], [1, 1], [0, 255]);
|
|
177
183
|
expect(hasNonSolidShapeFill(shape.data.commands)).toBe(true);
|
|
178
184
|
});
|
|
179
185
|
});
|