@cntrl-site/sdk-nextjs 1.9.0 → 1.9.2

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.
Files changed (130) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +4 -4
  3. package/TODO.md +1 -1
  4. package/cntrl-site-sdk-nextjs-1.8.40.tgz +0 -0
  5. package/eslint.config.mjs +35 -35
  6. package/jest.config.js +6 -6
  7. package/lib/common/useItemFXData.js +7 -7
  8. package/lib/components/Article.js +5 -5
  9. package/lib/components/LayoutStyle.js +4 -4
  10. package/lib/components/Section/Section.js +27 -27
  11. package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +36 -36
  12. package/lib/components/items/ComponentItem/ComponentItem.js +13 -13
  13. package/lib/components/items/CompoundItem/CompoundChild.js +20 -20
  14. package/lib/components/items/CompoundItem/CompoundItem.js +14 -14
  15. package/lib/components/items/CustomItem/CustomItem.js +9 -9
  16. package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +24 -24
  17. package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +27 -27
  18. package/lib/components/items/FileItem/ImageItem.js +52 -52
  19. package/lib/components/items/FileItem/VideoItem.js +76 -76
  20. package/lib/components/items/GroupItem/GroupItem.js +14 -14
  21. package/lib/components/items/Item.js +30 -30
  22. package/lib/components/items/RectangleItem/RectangleItem.js +39 -39
  23. package/lib/components/items/RichTextItem/RichTextItem.js +22 -22
  24. package/lib/utils/RichTextConverter/RichTextConverter.js +21 -21
  25. package/package.json +58 -57
  26. package/src/common/useCurrentLayout.ts +52 -52
  27. package/src/common/useExemplary.ts +9 -9
  28. package/src/common/useItemFXData.ts +47 -47
  29. package/src/common/useKeyframeValue.ts +85 -85
  30. package/src/common/useRegisterResize.ts +16 -16
  31. package/src/components/Article.tsx +73 -73
  32. package/src/components/ArticleWrapper.tsx +21 -21
  33. package/src/components/Head.tsx +56 -56
  34. package/src/components/LayoutStyle.tsx +21 -21
  35. package/src/components/Page.tsx +33 -33
  36. package/src/components/ScrollPlaybackVideo.tsx +56 -56
  37. package/src/components/Section/Section.tsx +127 -127
  38. package/src/components/Section/SectionImage.tsx +48 -48
  39. package/src/components/Section/SectionVideo.tsx +144 -144
  40. package/src/components/Section/useSectionColor.ts +19 -19
  41. package/src/components/Section/useSectionHeightMap.ts +19 -19
  42. package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +145 -145
  43. package/src/components/items/CodeEmbedItem/useCodeEmbedItem.ts +36 -36
  44. package/src/components/items/ComponentItem/ComponentItem.tsx +67 -67
  45. package/src/components/items/ComponentItem/useComponentItem.ts +35 -35
  46. package/src/components/items/CompoundItem/CompoundChild.tsx +150 -150
  47. package/src/components/items/CompoundItem/CompoundItem.tsx +70 -70
  48. package/src/components/items/CompoundItem/useCompoundItem.ts +21 -21
  49. package/src/components/items/CustomItem/CustomItem.tsx +48 -48
  50. package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +206 -206
  51. package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +193 -193
  52. package/src/components/items/EmbedVideoItem/useEmbedVideoItem.ts +46 -46
  53. package/src/components/items/FileItem/ImageItem.tsx +211 -211
  54. package/src/components/items/FileItem/VideoItem.tsx +328 -328
  55. package/src/components/items/FileItem/useFileItem.ts +81 -81
  56. package/src/components/items/GroupItem/GroupItem.tsx +81 -81
  57. package/src/components/items/GroupItem/useGroupItem.ts +34 -34
  58. package/src/components/items/Item.tsx +251 -251
  59. package/src/components/items/LinkWrapper.tsx +39 -39
  60. package/src/components/items/RectangleItem/RectangleItem.tsx +199 -199
  61. package/src/components/items/RectangleItem/useRectangleItem.ts +89 -89
  62. package/src/components/items/RichTextItem/RichTextItem.tsx +107 -107
  63. package/src/components/items/RichTextItem/useRichTextItem.ts +12 -12
  64. package/src/components/items/RichTextWrapper.tsx +15 -15
  65. package/src/components/items/itemsMap.ts +28 -28
  66. package/src/components/items/useDraggable.ts +252 -252
  67. package/src/components/items/useItemAngle.ts +13 -13
  68. package/src/components/items/useItemArea.ts +35 -35
  69. package/src/components/items/useItemPointerEvents.ts +23 -23
  70. package/src/components/items/useItemScale.ts +17 -17
  71. package/src/components/items/useItemTriggers.ts +15 -15
  72. package/src/components/items/useRichTextItemValues.ts +65 -65
  73. package/src/components/items/useSizing.ts +25 -25
  74. package/src/components/items/useStickyItemTop.ts +21 -21
  75. package/src/components/useLayoutContext.ts +7 -7
  76. package/src/index.ts +24 -24
  77. package/src/interactions/CSSPropertyNameMap.ts +37 -37
  78. package/src/interactions/InteractionsRegistry.ts +380 -380
  79. package/src/interactions/ItemInteractionCtrl.ts +78 -78
  80. package/src/interactions/getTransition.ts +27 -27
  81. package/src/interactions/types.ts +36 -36
  82. package/src/interactions/useItemInteractionCtrl.ts +18 -18
  83. package/src/provider/ArticleRectContext.ts +4 -4
  84. package/src/provider/CntrlContext.ts +5 -5
  85. package/src/provider/CntrlProvider.tsx +11 -11
  86. package/src/provider/CntrlSdkContext.ts +83 -83
  87. package/src/provider/CustomItemRegistry.ts +14 -14
  88. package/src/provider/CustomItemTypes.ts +3 -3
  89. package/src/provider/CustomSectionRegistry.ts +25 -25
  90. package/src/provider/InteractionsContext.tsx +59 -59
  91. package/src/provider/Keyframes.ts +11 -11
  92. package/src/provider/KeyframesContext.ts +5 -5
  93. package/src/provider/LayoutContext.ts +3 -3
  94. package/src/provider/WebGLContextManagerContext.tsx +4 -4
  95. package/src/provider/defaultContext.ts +8 -8
  96. package/src/provider/useCntrlContext.ts +8 -8
  97. package/src/styled-jsx.d.ts +1 -1
  98. package/src/utils/Animator/Animator.ts +571 -571
  99. package/src/utils/ArticleRectManager/ArticleRectObserver.ts +93 -93
  100. package/src/utils/ArticleRectManager/useArticleRectObserver.ts +17 -17
  101. package/src/utils/ArticleRectManager/useSectionRegistry.ts +11 -11
  102. package/src/utils/EventEmitter.ts +41 -41
  103. package/src/utils/RichTextConverter/RichTextConverter.tsx +326 -326
  104. package/src/utils/ScaleAnchorMap.ts +13 -13
  105. package/src/utils/StickyManager/StickyManager.ts +23 -23
  106. package/src/utils/Youtube/YouTubeIframeApiLoader.ts +58 -58
  107. package/src/utils/Youtube/YoutubeIframeApi.ts +101 -101
  108. package/src/utils/Youtube/useYouTubeIframeApi.ts +12 -12
  109. package/src/utils/areFillsVisible/areFillsVisible.test.ts +63 -63
  110. package/src/utils/areFillsVisible/areFillsVisible.ts +37 -37
  111. package/src/utils/binSearchInsertAt.ts +33 -33
  112. package/src/utils/castObject.ts +10 -10
  113. package/src/utils/checkOverflowClipSupport.ts +13 -13
  114. package/src/utils/effects/useImageFx.ts +156 -156
  115. package/src/utils/effects/useVideoFx.ts +158 -158
  116. package/src/utils/getAnchoredItemTop.ts +12 -12
  117. package/src/utils/getCompoundBoundaryStyles.ts +60 -60
  118. package/src/utils/getFill.ts +46 -46
  119. package/src/utils/getFontFamilyValue.ts +3 -3
  120. package/src/utils/getInvertedRanges.ts +43 -43
  121. package/src/utils/getItemTopStyle.ts +14 -14
  122. package/src/utils/getStyleFromItemStateAndParams.ts +8 -8
  123. package/src/utils/getValidYoutubeUrl.ts +29 -29
  124. package/src/utils/isItemType.ts +5 -5
  125. package/src/utils/rangeMap.ts +13 -13
  126. package/src/utils/useElementRect.ts +23 -23
  127. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  128. package/.idea/modules.xml +0 -8
  129. package/.idea/sdk-nextjs.iml +0 -12
  130. package/.idea/vcs.xml +0 -6
@@ -1,571 +1,571 @@
1
- import { KeyframeType, KeyframeValueMap } from '@cntrl-site/sdk';
2
- import { CntrlColor } from '@cntrl-site/color';
3
- import { binSearchInsertAt, createInsert } from '../binSearchInsertAt';
4
- import { rangeMap } from '../rangeMap';
5
- import { FillLayer } from '@cntrl-site/sdk/dist/types/article/Item';
6
-
7
- export interface AnimationData<T extends KeyframeType> {
8
- position: number;
9
- value: KeyframeValueMap[T];
10
- type: T;
11
- }
12
-
13
- interface PositionKeyframes<T extends KeyframeType> {
14
- start: AnimationData<T>;
15
- end: AnimationData<T>;
16
- }
17
-
18
- export type KeyframesMap = {
19
- [T in KeyframeType]: AnimationData<T>[];
20
- };
21
-
22
- type ColorPoint = {
23
- id: string;
24
- value: string;
25
- position: number;
26
- };
27
-
28
- const compare = (lhs: { position: number }, rhs: { position: number }) => lhs.position - rhs.position;
29
- const insertBin = createInsert(binSearchInsertAt, compare);
30
-
31
- export class Animator {
32
- private static pushKeyframeToMap<T extends KeyframeType>(keyframe: AnimationData<T>, map: KeyframesMap): void {
33
- insertBin(map[keyframe.type], keyframe);
34
- }
35
-
36
- private keyframesMap: KeyframesMap = createKeyframesMap();
37
- constructor(
38
- private keyframes: AnimationData<KeyframeType>[]
39
- ) {
40
- this.fillKeyframesMap();
41
- }
42
-
43
- getFXParams(
44
- valuesMap: Record<string, number>,
45
- pos: number
46
- ): KeyframeValueMap[KeyframeType.FXParams] {
47
- const keyframes = this.keyframesMap[KeyframeType.FXParams];
48
- if (!keyframes.length) return valuesMap;
49
- if (keyframes.length === 1) {
50
- const [keyframe] = keyframes;
51
- return Object.entries(valuesMap).reduce<Record<string, number>>((acc, [key, fallbackValue]) => {
52
- acc[key] = keyframe.value[key] ?? fallbackValue;
53
- return acc;
54
- }, {});
55
- }
56
- const { start, end } = this.getStartEnd<KeyframeType.FXParams>(pos, keyframes);
57
- return Object.entries(valuesMap).reduce<Record<string, number>>((acc, [key, fallbackValue]) => {
58
- acc[key] = rangeMap(pos, start.position, end.position, start.value[key] ?? fallbackValue, end.value[key] ?? fallbackValue, true);
59
- return acc;
60
- }, {});
61
- }
62
-
63
- getDimensions(
64
- values: KeyframeValueMap[KeyframeType.Dimensions],
65
- pos: number
66
- ): KeyframeValueMap[KeyframeType.Dimensions] {
67
- const keyframes = this.keyframesMap[KeyframeType.Dimensions];
68
- if (!keyframes || !keyframes.length) return values;
69
- if (keyframes.length === 1) {
70
- const [keyframe] = keyframes;
71
- return {
72
- width: keyframe.value.width,
73
- height: keyframe.value.height
74
- };
75
- }
76
- const { start, end } = this.getStartEnd<KeyframeType.Dimensions>(pos, keyframes);
77
- return {
78
- width: rangeMap(pos, start.position, end.position, start.value.width, end.value.width, true),
79
- height: rangeMap(pos, start.position, end.position, start.value.height, end.value.height, true)
80
- };
81
- }
82
-
83
- getPositions(
84
- values: KeyframeValueMap[KeyframeType.Position],
85
- pos: number
86
- ): KeyframeValueMap[KeyframeType.Position] {
87
- const keyframes = this.keyframesMap[KeyframeType.Position];
88
- if (!keyframes || !keyframes.length) return values;
89
- if (keyframes.length === 1) {
90
- const [keyframe] = keyframes;
91
- return {
92
- left: keyframe.value.left,
93
- top: keyframe.value.top
94
- };
95
- }
96
- const { start, end } = this.getStartEnd<KeyframeType.Position>(pos, keyframes);
97
- return {
98
- left: rangeMap(pos, start.position, end.position, start.value.left, end.value.left, true),
99
- top: rangeMap(pos, start.position, end.position, start.value.top, end.value.top, true)
100
- };
101
- }
102
-
103
- getBorderFill(
104
- values: KeyframeValueMap[KeyframeType.BorderFill],
105
- pos: number
106
- ): FillLayer[] {
107
- const keyframes = this.keyframesMap[KeyframeType.BorderFill];
108
- if (!keyframes || !keyframes.length) return values;
109
- if (keyframes.length === 1) {
110
- const [keyframe] = keyframes;
111
- return keyframe.value;
112
- }
113
- const { start, end } = this.getStartEnd<KeyframeType.BorderFill>(pos, keyframes);
114
- return this.getRangeGradient(start, end, pos);
115
- }
116
-
117
- getFill(
118
- values: KeyframeValueMap[KeyframeType.Fill],
119
- pos: number
120
- ): FillLayer[] {
121
- const keyframes = this.keyframesMap[KeyframeType.Fill];
122
- if (!keyframes || !keyframes.length) return values;
123
- if (keyframes.length === 1) {
124
- const [keyframe] = keyframes;
125
- return keyframe.value;
126
- }
127
- const { start, end } = this.getStartEnd<KeyframeType.Fill>(pos, keyframes);
128
- return this.getRangeGradient(start, end, pos);
129
- }
130
-
131
- getRadius(
132
- values: KeyframeValueMap[KeyframeType.BorderRadius],
133
- pos: number
134
- ): KeyframeValueMap[KeyframeType.BorderRadius] {
135
- const keyframes = this.keyframesMap[KeyframeType.BorderRadius];
136
- if (!keyframes || !keyframes.length) return values;
137
- if (keyframes.length === 1) {
138
- const [keyframe] = keyframes;
139
- return {
140
- radius: keyframe.value.radius
141
- };
142
- }
143
- const { start, end } = this.getStartEnd<KeyframeType.BorderRadius>(pos, keyframes);
144
- return {
145
- radius: rangeMap(pos, start.position, end.position, start.value.radius, end.value.radius, true)
146
- };
147
- }
148
-
149
- getBorderWidth(
150
- values: KeyframeValueMap[KeyframeType.BorderWidth],
151
- pos: number
152
- ): KeyframeValueMap[KeyframeType.BorderWidth] {
153
- const keyframes = this.keyframesMap[KeyframeType.BorderWidth];
154
- if (!keyframes || !keyframes.length) return values;
155
- if (keyframes.length === 1) {
156
- const [keyframe] = keyframes;
157
- return {
158
- borderWidth: keyframe.value.borderWidth
159
- };
160
- }
161
- const { start, end } = this.getStartEnd<KeyframeType.BorderWidth>(pos, keyframes);
162
- return {
163
- borderWidth: rangeMap(pos, start.position, end.position, start.value.borderWidth, end.value.borderWidth, true)
164
- };
165
- }
166
-
167
- getRotation(
168
- values: KeyframeValueMap[KeyframeType.Rotation],
169
- pos: number
170
- ): KeyframeValueMap[KeyframeType.Rotation] {
171
- const keyframes = this.keyframesMap[KeyframeType.Rotation];
172
- if (!keyframes || !keyframes.length) return values;
173
- if (keyframes.length === 1) {
174
- const [keyframe] = keyframes;
175
- return {
176
- angle: keyframe.value.angle
177
- };
178
- }
179
- const { start, end } = this.getStartEnd<KeyframeType.Rotation>(pos, keyframes);
180
- return {
181
- angle: rangeMap(pos, start.position, end.position, start.value.angle, end.value.angle, true)
182
- };
183
- }
184
-
185
- getOpacity(
186
- values: KeyframeValueMap[KeyframeType.Opacity],
187
- pos: number
188
- ): KeyframeValueMap[KeyframeType.Opacity] {
189
- const keyframes = this.keyframesMap[KeyframeType.Opacity];
190
- if (!keyframes || !keyframes.length) return values;
191
- if (keyframes.length === 1) {
192
- const [keyframe] = keyframes;
193
- return {
194
- opacity: keyframe.value.opacity
195
- };
196
- }
197
- const { start, end } = this.getStartEnd<KeyframeType.Opacity>(pos, keyframes);
198
- return {
199
- opacity: rangeMap(pos, start.position, end.position, start.value.opacity, end.value.opacity, true)
200
- };
201
- }
202
-
203
- getScale(
204
- values: KeyframeValueMap[KeyframeType.Scale],
205
- pos: number
206
- ): KeyframeValueMap[KeyframeType.Scale] {
207
- const keyframes = this.keyframesMap[KeyframeType.Scale];
208
- if (!keyframes || !keyframes.length) return values;
209
- if (keyframes.length === 1) {
210
- const [keyframe] = keyframes;
211
- return {
212
- scale: keyframe.value.scale
213
- };
214
- }
215
- const { start, end } = this.getStartEnd<KeyframeType.Scale>(pos, keyframes);
216
- return {
217
- scale: rangeMap(pos, start.position, end.position, start.value.scale, end.value.scale, true)
218
- };
219
- }
220
-
221
- getBlur(
222
- values: KeyframeValueMap[KeyframeType.Blur],
223
- pos: number
224
- ): KeyframeValueMap[KeyframeType.Blur] {
225
- const keyframes = this.keyframesMap[KeyframeType.Blur];
226
- if (!keyframes || !keyframes.length) return values;
227
- if (keyframes.length === 1) {
228
- const [keyframe] = keyframes;
229
- return {
230
- blur: keyframe.value.blur
231
- };
232
- }
233
- const { start, end } = this.getStartEnd<KeyframeType.Blur>(pos, keyframes);
234
- return {
235
- blur: rangeMap(pos, start.position, end.position, start.value.blur, end.value.blur, true)
236
- };
237
- }
238
-
239
- getBackdropBlur(
240
- values: KeyframeValueMap[KeyframeType.BackdropBlur],
241
- pos: number
242
- ): KeyframeValueMap[KeyframeType.BackdropBlur] {
243
- const keyframes = this.keyframesMap[KeyframeType.BackdropBlur];
244
- if (!keyframes || !keyframes.length) return values;
245
- if (keyframes.length === 1) {
246
- const [keyframe] = keyframes;
247
- return {
248
- backdropBlur: keyframe.value.backdropBlur
249
- };
250
- }
251
- const { start, end } = this.getStartEnd<KeyframeType.BackdropBlur>(pos, keyframes);
252
- return {
253
- backdropBlur: rangeMap(pos, start.position, end.position, start.value.backdropBlur, end.value.backdropBlur, true)
254
- };
255
- }
256
-
257
- getTextColor(
258
- values: KeyframeValueMap[KeyframeType.TextColor],
259
- pos: number
260
- ): KeyframeValueMap[KeyframeType.TextColor] {
261
- const keyframes = this.keyframesMap[KeyframeType.TextColor];
262
- if (!keyframes || !keyframes.length) return values;
263
- if (keyframes.length === 1) {
264
- const [keyframe] = keyframes;
265
- return {
266
- color: keyframe.value.color
267
- };
268
- }
269
- const { start, end } = this.getStartEnd<KeyframeType.TextColor>(pos, keyframes);
270
- return {
271
- color: this.getRangeColor(start, end, pos)
272
- };
273
- }
274
-
275
- getLetterSpacing(
276
- values: KeyframeValueMap[KeyframeType.LetterSpacing],
277
- pos: number
278
- ): KeyframeValueMap[KeyframeType.LetterSpacing] {
279
- const keyframes = this.keyframesMap[KeyframeType.LetterSpacing];
280
- if (!keyframes || !keyframes.length) return values;
281
- if (keyframes.length === 1) {
282
- const [keyframe] = keyframes;
283
- return {
284
- letterSpacing: keyframe.value.letterSpacing
285
- };
286
- }
287
- const { start, end } = this.getStartEnd<KeyframeType.LetterSpacing>(pos, keyframes);
288
- return {
289
- letterSpacing: rangeMap(
290
- pos,
291
- start.position,
292
- end.position,
293
- start.value.letterSpacing,
294
- end.value.letterSpacing,
295
- true
296
- )
297
- };
298
- }
299
-
300
- getWordSpacing(
301
- values: KeyframeValueMap[KeyframeType.WordSpacing],
302
- pos: number
303
- ): KeyframeValueMap[KeyframeType.WordSpacing] {
304
- const keyframes = this.keyframesMap[KeyframeType.WordSpacing];
305
- if (!keyframes || !keyframes.length) return values;
306
- if (keyframes.length === 1) {
307
- const [keyframe] = keyframes;
308
- return {
309
- wordSpacing: keyframe.value.wordSpacing
310
- };
311
- }
312
- const { start, end } = this.getStartEnd<KeyframeType.WordSpacing>(pos, keyframes);
313
- return {
314
- wordSpacing: rangeMap(
315
- pos,
316
- start.position,
317
- end.position,
318
- start.value.wordSpacing,
319
- end.value.wordSpacing,
320
- true
321
- )
322
- };
323
- }
324
-
325
- getStartEnd<T extends KeyframeType>(position: number, keyframes: AnimationData<T>[]): PositionKeyframes<T> {
326
- const index = binSearchInsertAt(keyframes, { position }, compare);
327
- const end = index === keyframes.length ? index - 1 : index;
328
- const start = end === 0 ? end : end - 1;
329
- return { start: keyframes[start], end: keyframes[end] };
330
- }
331
-
332
- private fillKeyframesMap() {
333
- this.keyframesMap = createKeyframesMap();
334
- for (const keyframe of this.keyframes) {
335
- Animator.pushKeyframeToMap(keyframe, this.keyframesMap);
336
- }
337
- }
338
-
339
- private getRangeColor(
340
- start: AnimationData<KeyframeType.TextColor>,
341
- end: AnimationData<KeyframeType.TextColor>,
342
- position: number
343
- ): string {
344
- const rangeAmount = rangeMap(position, start.position, end.position, 0, 1, true);
345
- const startColor = CntrlColor.parse(start.value.color);
346
- const endColor = CntrlColor.parse(end.value.color);
347
- const mixedColor = startColor.mix(endColor, rangeAmount);
348
- return mixedColor.fmt('oklch');
349
- }
350
-
351
- private getRangeGradient(
352
- start: AnimationData<KeyframeType.Fill | KeyframeType.BorderFill>,
353
- end: AnimationData<KeyframeType.Fill | KeyframeType.BorderFill>,
354
- position: number
355
- ): FillLayer[] {
356
- const rangeAmount = rangeMap(position, start.position, end.position, 0, 1, true);
357
- const allLayerIds = new Set([
358
- ...start.value.map(layer => layer.id),
359
- ...end.value.map(layer => layer.id)
360
- ]);
361
- const result: FillLayer[] = [];
362
- for (const layerId of allLayerIds) {
363
- const startLayers = start.value.find(layer => layer.id === layerId);
364
- const endLayers = end.value.find(layer => layer.id === layerId);
365
- const startLayersArray = Array.isArray(startLayers) ? startLayers : startLayers ? [startLayers] : [];
366
- const endLayersArray = Array.isArray(endLayers) ? endLayers : endLayers ? [endLayers] : [];
367
- if (startLayersArray.length > 0 && endLayersArray.length === 0) {
368
- result.push(...startLayersArray);
369
- continue;
370
- }
371
- if (startLayersArray.length === 0 && endLayersArray.length > 0) {
372
- result.push(...endLayersArray);
373
- continue;
374
- }
375
- if (startLayersArray.length > 0 && endLayersArray.length > 0) {
376
- result.push(...this.interpolateFillLayerArray(startLayersArray, endLayersArray, rangeAmount));
377
- }
378
- }
379
-
380
- return result;
381
- }
382
-
383
- private interpolateFillLayerArray(startLayers: FillLayer[], endLayers: FillLayer[], rangeAmount: number): FillLayer[] {
384
- const startLayerMap = new Map(startLayers.map(layer => [layer.id, layer]));
385
- const endLayerMap = new Map(endLayers.map(layer => [layer.id, layer]));
386
- const allLayerIds = new Set([
387
- ...startLayers.map(layer => layer.id),
388
- ...endLayers.map(layer => layer.id)
389
- ]);
390
-
391
- const interpolatedLayers: FillLayer[] = [];
392
-
393
- for (const layerId of allLayerIds) {
394
- const startLayer = startLayerMap.get(layerId);
395
- const endLayer = endLayerMap.get(layerId);
396
- if (startLayer && !endLayer) {
397
- interpolatedLayers.push(startLayer);
398
- continue;
399
- }
400
- if (!startLayer && endLayer) {
401
- interpolatedLayers.push(endLayer);
402
- continue;
403
- }
404
- if (startLayer && endLayer) {
405
- interpolatedLayers.push(this.interpolateFillLayer(startLayer, endLayer, rangeAmount));
406
- }
407
- }
408
- const result: FillLayer[] = [];
409
- for (const startLayer of startLayers) {
410
- const interpolatedLayer = interpolatedLayers.find(layer => layer.id === startLayer.id);
411
- if (interpolatedLayer) {
412
- result.push(interpolatedLayer);
413
- }
414
- }
415
- for (const endLayer of endLayers) {
416
- if (!startLayerMap.has(endLayer.id)) {
417
- const interpolatedLayer = interpolatedLayers.find(layer => layer.id === endLayer.id);
418
- if (interpolatedLayer) {
419
- result.push(interpolatedLayer);
420
- }
421
- }
422
- }
423
-
424
- return result;
425
- }
426
-
427
- private interpolateFillLayer(startLayer: FillLayer, endLayer: FillLayer, rangeAmount: number): FillLayer {
428
- if (startLayer.type !== endLayer.type) {
429
- return startLayer;
430
- }
431
-
432
- switch (startLayer.type) {
433
- case 'solid': {
434
- const endSolidLayer = endLayer as typeof startLayer;
435
- const startColor = CntrlColor.parse(startLayer.value);
436
- const endColor = CntrlColor.parse(endSolidLayer.value);
437
- const mixedColor = startColor.mix(endColor, rangeAmount);
438
- return {
439
- ...startLayer,
440
- value: mixedColor.fmt('oklch')
441
- };
442
- }
443
-
444
- case 'linear-gradient': {
445
- const endLinearLayer = endLayer as typeof startLayer;
446
- const interpolatedColors = this.interpolateColorStops(startLayer.colors, endLinearLayer.colors, rangeAmount);
447
- const angle = startLayer.angle + (endLinearLayer.angle - startLayer.angle) * rangeAmount;
448
- const startPoint: [number, number] = [
449
- startLayer.start[0] + (endLinearLayer.start[0] - startLayer.start[0]) * rangeAmount,
450
- startLayer.start[1] + (endLinearLayer.start[1] - startLayer.start[1]) * rangeAmount
451
- ];
452
- const endPoint: [number, number] = [
453
- startLayer.end[0] + (endLinearLayer.end[0] - startLayer.end[0]) * rangeAmount,
454
- startLayer.end[1] + (endLinearLayer.end[1] - startLayer.end[1]) * rangeAmount
455
- ];
456
- return {
457
- ...startLayer,
458
- colors: interpolatedColors,
459
- angle,
460
- start: startPoint,
461
- end: endPoint
462
- };
463
- }
464
-
465
- case 'radial-gradient': {
466
- const endRadialLayer = endLayer as typeof startLayer;
467
- const interpolatedColors = this.interpolateColorStops(startLayer.colors, endRadialLayer.colors, rangeAmount);
468
- const angle = startLayer.angle + (endRadialLayer.angle - startLayer.angle) * rangeAmount;
469
- const center: [number, number] = [
470
- startLayer.center[0] + (endRadialLayer.center[0] - startLayer.center[0]) * rangeAmount,
471
- startLayer.center[1] + (endRadialLayer.center[1] - startLayer.center[1]) * rangeAmount
472
- ];
473
- const diameter = startLayer.diameter + (endRadialLayer.diameter - startLayer.diameter) * rangeAmount;
474
- return {
475
- ...startLayer,
476
- colors: interpolatedColors,
477
- angle,
478
- center,
479
- diameter
480
- };
481
- }
482
-
483
- case 'conic-gradient': {
484
- const endConicLayer = endLayer as typeof startLayer;
485
- const interpolatedColors = this.interpolateColorStops(startLayer.colors, endConicLayer.colors, rangeAmount);
486
- const angle = startLayer.angle + (endConicLayer.angle - startLayer.angle) * rangeAmount;
487
- const center: [number, number] = [
488
- startLayer.center[0] + (endConicLayer.center[0] - startLayer.center[0]) * rangeAmount,
489
- startLayer.center[1] + (endConicLayer.center[1] - startLayer.center[1]) * rangeAmount
490
- ];
491
- return {
492
- ...startLayer,
493
- colors: interpolatedColors,
494
- angle,
495
- center
496
- };
497
- }
498
-
499
- case 'image': {
500
- const endImageLayer = endLayer as typeof startLayer;
501
- const opacity = startLayer.opacity + (endImageLayer.opacity - startLayer.opacity) * rangeAmount;
502
- const backgroundSize = startLayer.backgroundSize + (endImageLayer.backgroundSize - startLayer.backgroundSize) * rangeAmount;
503
- return {
504
- ...startLayer,
505
- opacity,
506
- backgroundSize
507
- };
508
- }
509
-
510
- default:
511
- return startLayer;
512
- }
513
- }
514
-
515
- private interpolateColorStops(startColors: ColorPoint[], endColors: ColorPoint[], rangeAmount: number): ColorPoint[] {
516
- const startColorMap = new Map(startColors.map(stop => [stop.id, stop]));
517
- const endColorMap = new Map(endColors.map(stop => [stop.id, stop]));
518
- const allStopIds = new Set([
519
- ...startColors.map(stop => stop.id),
520
- ...endColors.map(stop => stop.id)
521
- ]);
522
-
523
- const interpolatedStops: ColorPoint[] = [];
524
-
525
- for (const stopId of allStopIds) {
526
- const startStop = startColorMap.get(stopId);
527
- const endStop = endColorMap.get(stopId);
528
- if (startStop && !endStop) {
529
- interpolatedStops.push(startStop);
530
- continue;
531
- }
532
- if (!startStop && endStop) {
533
- interpolatedStops.push(endStop);
534
- continue;
535
- }
536
- if (startStop && endStop) {
537
- const startColor = CntrlColor.parse(startStop.value);
538
- const endColor = CntrlColor.parse(endStop.value);
539
- const mixedColor = startColor.mix(endColor, rangeAmount);
540
- const position = startStop.position + (endStop.position - startStop.position) * rangeAmount;
541
-
542
- interpolatedStops.push({
543
- id: stopId,
544
- value: mixedColor.fmt('oklch'),
545
- position
546
- });
547
- }
548
- }
549
- return interpolatedStops.sort((a, b) => a.position - b.position);
550
- }
551
- }
552
-
553
- function createKeyframesMap(): KeyframesMap {
554
- return {
555
- [KeyframeType.Dimensions]: [],
556
- [KeyframeType.Position]: [],
557
- [KeyframeType.BorderWidth]: [],
558
- [KeyframeType.BorderRadius]: [],
559
- [KeyframeType.Rotation]: [],
560
- [KeyframeType.BorderFill]: [],
561
- [KeyframeType.Opacity]: [],
562
- [KeyframeType.Scale]: [],
563
- [KeyframeType.Blur]: [],
564
- [KeyframeType.BackdropBlur]: [],
565
- [KeyframeType.LetterSpacing]: [],
566
- [KeyframeType.WordSpacing]: [],
567
- [KeyframeType.TextColor]: [],
568
- [KeyframeType.FXParams]: [],
569
- [KeyframeType.Fill]: []
570
- };
571
- }
1
+ import { KeyframeType, KeyframeValueMap } from '@cntrl-site/sdk';
2
+ import { CntrlColor } from '@cntrl-site/color';
3
+ import { binSearchInsertAt, createInsert } from '../binSearchInsertAt';
4
+ import { rangeMap } from '../rangeMap';
5
+ import { FillLayer } from '@cntrl-site/sdk/dist/types/article/Item';
6
+
7
+ export interface AnimationData<T extends KeyframeType> {
8
+ position: number;
9
+ value: KeyframeValueMap[T];
10
+ type: T;
11
+ }
12
+
13
+ interface PositionKeyframes<T extends KeyframeType> {
14
+ start: AnimationData<T>;
15
+ end: AnimationData<T>;
16
+ }
17
+
18
+ export type KeyframesMap = {
19
+ [T in KeyframeType]: AnimationData<T>[];
20
+ };
21
+
22
+ type ColorPoint = {
23
+ id: string;
24
+ value: string;
25
+ position: number;
26
+ };
27
+
28
+ const compare = (lhs: { position: number }, rhs: { position: number }) => lhs.position - rhs.position;
29
+ const insertBin = createInsert(binSearchInsertAt, compare);
30
+
31
+ export class Animator {
32
+ private static pushKeyframeToMap<T extends KeyframeType>(keyframe: AnimationData<T>, map: KeyframesMap): void {
33
+ insertBin(map[keyframe.type], keyframe);
34
+ }
35
+
36
+ private keyframesMap: KeyframesMap = createKeyframesMap();
37
+ constructor(
38
+ private keyframes: AnimationData<KeyframeType>[]
39
+ ) {
40
+ this.fillKeyframesMap();
41
+ }
42
+
43
+ getFXParams(
44
+ valuesMap: Record<string, number>,
45
+ pos: number
46
+ ): KeyframeValueMap[KeyframeType.FXParams] {
47
+ const keyframes = this.keyframesMap[KeyframeType.FXParams];
48
+ if (!keyframes.length) return valuesMap;
49
+ if (keyframes.length === 1) {
50
+ const [keyframe] = keyframes;
51
+ return Object.entries(valuesMap).reduce<Record<string, number>>((acc, [key, fallbackValue]) => {
52
+ acc[key] = keyframe.value[key] ?? fallbackValue;
53
+ return acc;
54
+ }, {});
55
+ }
56
+ const { start, end } = this.getStartEnd<KeyframeType.FXParams>(pos, keyframes);
57
+ return Object.entries(valuesMap).reduce<Record<string, number>>((acc, [key, fallbackValue]) => {
58
+ acc[key] = rangeMap(pos, start.position, end.position, start.value[key] ?? fallbackValue, end.value[key] ?? fallbackValue, true);
59
+ return acc;
60
+ }, {});
61
+ }
62
+
63
+ getDimensions(
64
+ values: KeyframeValueMap[KeyframeType.Dimensions],
65
+ pos: number
66
+ ): KeyframeValueMap[KeyframeType.Dimensions] {
67
+ const keyframes = this.keyframesMap[KeyframeType.Dimensions];
68
+ if (!keyframes || !keyframes.length) return values;
69
+ if (keyframes.length === 1) {
70
+ const [keyframe] = keyframes;
71
+ return {
72
+ width: keyframe.value.width,
73
+ height: keyframe.value.height
74
+ };
75
+ }
76
+ const { start, end } = this.getStartEnd<KeyframeType.Dimensions>(pos, keyframes);
77
+ return {
78
+ width: rangeMap(pos, start.position, end.position, start.value.width, end.value.width, true),
79
+ height: rangeMap(pos, start.position, end.position, start.value.height, end.value.height, true)
80
+ };
81
+ }
82
+
83
+ getPositions(
84
+ values: KeyframeValueMap[KeyframeType.Position],
85
+ pos: number
86
+ ): KeyframeValueMap[KeyframeType.Position] {
87
+ const keyframes = this.keyframesMap[KeyframeType.Position];
88
+ if (!keyframes || !keyframes.length) return values;
89
+ if (keyframes.length === 1) {
90
+ const [keyframe] = keyframes;
91
+ return {
92
+ left: keyframe.value.left,
93
+ top: keyframe.value.top
94
+ };
95
+ }
96
+ const { start, end } = this.getStartEnd<KeyframeType.Position>(pos, keyframes);
97
+ return {
98
+ left: rangeMap(pos, start.position, end.position, start.value.left, end.value.left, true),
99
+ top: rangeMap(pos, start.position, end.position, start.value.top, end.value.top, true)
100
+ };
101
+ }
102
+
103
+ getBorderFill(
104
+ values: KeyframeValueMap[KeyframeType.BorderFill],
105
+ pos: number
106
+ ): FillLayer[] {
107
+ const keyframes = this.keyframesMap[KeyframeType.BorderFill];
108
+ if (!keyframes || !keyframes.length) return values;
109
+ if (keyframes.length === 1) {
110
+ const [keyframe] = keyframes;
111
+ return keyframe.value;
112
+ }
113
+ const { start, end } = this.getStartEnd<KeyframeType.BorderFill>(pos, keyframes);
114
+ return this.getRangeGradient(start, end, pos);
115
+ }
116
+
117
+ getFill(
118
+ values: KeyframeValueMap[KeyframeType.Fill],
119
+ pos: number
120
+ ): FillLayer[] {
121
+ const keyframes = this.keyframesMap[KeyframeType.Fill];
122
+ if (!keyframes || !keyframes.length) return values;
123
+ if (keyframes.length === 1) {
124
+ const [keyframe] = keyframes;
125
+ return keyframe.value;
126
+ }
127
+ const { start, end } = this.getStartEnd<KeyframeType.Fill>(pos, keyframes);
128
+ return this.getRangeGradient(start, end, pos);
129
+ }
130
+
131
+ getRadius(
132
+ values: KeyframeValueMap[KeyframeType.BorderRadius],
133
+ pos: number
134
+ ): KeyframeValueMap[KeyframeType.BorderRadius] {
135
+ const keyframes = this.keyframesMap[KeyframeType.BorderRadius];
136
+ if (!keyframes || !keyframes.length) return values;
137
+ if (keyframes.length === 1) {
138
+ const [keyframe] = keyframes;
139
+ return {
140
+ radius: keyframe.value.radius
141
+ };
142
+ }
143
+ const { start, end } = this.getStartEnd<KeyframeType.BorderRadius>(pos, keyframes);
144
+ return {
145
+ radius: rangeMap(pos, start.position, end.position, start.value.radius, end.value.radius, true)
146
+ };
147
+ }
148
+
149
+ getBorderWidth(
150
+ values: KeyframeValueMap[KeyframeType.BorderWidth],
151
+ pos: number
152
+ ): KeyframeValueMap[KeyframeType.BorderWidth] {
153
+ const keyframes = this.keyframesMap[KeyframeType.BorderWidth];
154
+ if (!keyframes || !keyframes.length) return values;
155
+ if (keyframes.length === 1) {
156
+ const [keyframe] = keyframes;
157
+ return {
158
+ borderWidth: keyframe.value.borderWidth
159
+ };
160
+ }
161
+ const { start, end } = this.getStartEnd<KeyframeType.BorderWidth>(pos, keyframes);
162
+ return {
163
+ borderWidth: rangeMap(pos, start.position, end.position, start.value.borderWidth, end.value.borderWidth, true)
164
+ };
165
+ }
166
+
167
+ getRotation(
168
+ values: KeyframeValueMap[KeyframeType.Rotation],
169
+ pos: number
170
+ ): KeyframeValueMap[KeyframeType.Rotation] {
171
+ const keyframes = this.keyframesMap[KeyframeType.Rotation];
172
+ if (!keyframes || !keyframes.length) return values;
173
+ if (keyframes.length === 1) {
174
+ const [keyframe] = keyframes;
175
+ return {
176
+ angle: keyframe.value.angle
177
+ };
178
+ }
179
+ const { start, end } = this.getStartEnd<KeyframeType.Rotation>(pos, keyframes);
180
+ return {
181
+ angle: rangeMap(pos, start.position, end.position, start.value.angle, end.value.angle, true)
182
+ };
183
+ }
184
+
185
+ getOpacity(
186
+ values: KeyframeValueMap[KeyframeType.Opacity],
187
+ pos: number
188
+ ): KeyframeValueMap[KeyframeType.Opacity] {
189
+ const keyframes = this.keyframesMap[KeyframeType.Opacity];
190
+ if (!keyframes || !keyframes.length) return values;
191
+ if (keyframes.length === 1) {
192
+ const [keyframe] = keyframes;
193
+ return {
194
+ opacity: keyframe.value.opacity
195
+ };
196
+ }
197
+ const { start, end } = this.getStartEnd<KeyframeType.Opacity>(pos, keyframes);
198
+ return {
199
+ opacity: rangeMap(pos, start.position, end.position, start.value.opacity, end.value.opacity, true)
200
+ };
201
+ }
202
+
203
+ getScale(
204
+ values: KeyframeValueMap[KeyframeType.Scale],
205
+ pos: number
206
+ ): KeyframeValueMap[KeyframeType.Scale] {
207
+ const keyframes = this.keyframesMap[KeyframeType.Scale];
208
+ if (!keyframes || !keyframes.length) return values;
209
+ if (keyframes.length === 1) {
210
+ const [keyframe] = keyframes;
211
+ return {
212
+ scale: keyframe.value.scale
213
+ };
214
+ }
215
+ const { start, end } = this.getStartEnd<KeyframeType.Scale>(pos, keyframes);
216
+ return {
217
+ scale: rangeMap(pos, start.position, end.position, start.value.scale, end.value.scale, true)
218
+ };
219
+ }
220
+
221
+ getBlur(
222
+ values: KeyframeValueMap[KeyframeType.Blur],
223
+ pos: number
224
+ ): KeyframeValueMap[KeyframeType.Blur] {
225
+ const keyframes = this.keyframesMap[KeyframeType.Blur];
226
+ if (!keyframes || !keyframes.length) return values;
227
+ if (keyframes.length === 1) {
228
+ const [keyframe] = keyframes;
229
+ return {
230
+ blur: keyframe.value.blur
231
+ };
232
+ }
233
+ const { start, end } = this.getStartEnd<KeyframeType.Blur>(pos, keyframes);
234
+ return {
235
+ blur: rangeMap(pos, start.position, end.position, start.value.blur, end.value.blur, true)
236
+ };
237
+ }
238
+
239
+ getBackdropBlur(
240
+ values: KeyframeValueMap[KeyframeType.BackdropBlur],
241
+ pos: number
242
+ ): KeyframeValueMap[KeyframeType.BackdropBlur] {
243
+ const keyframes = this.keyframesMap[KeyframeType.BackdropBlur];
244
+ if (!keyframes || !keyframes.length) return values;
245
+ if (keyframes.length === 1) {
246
+ const [keyframe] = keyframes;
247
+ return {
248
+ backdropBlur: keyframe.value.backdropBlur
249
+ };
250
+ }
251
+ const { start, end } = this.getStartEnd<KeyframeType.BackdropBlur>(pos, keyframes);
252
+ return {
253
+ backdropBlur: rangeMap(pos, start.position, end.position, start.value.backdropBlur, end.value.backdropBlur, true)
254
+ };
255
+ }
256
+
257
+ getTextColor(
258
+ values: KeyframeValueMap[KeyframeType.TextColor],
259
+ pos: number
260
+ ): KeyframeValueMap[KeyframeType.TextColor] {
261
+ const keyframes = this.keyframesMap[KeyframeType.TextColor];
262
+ if (!keyframes || !keyframes.length) return values;
263
+ if (keyframes.length === 1) {
264
+ const [keyframe] = keyframes;
265
+ return {
266
+ color: keyframe.value.color
267
+ };
268
+ }
269
+ const { start, end } = this.getStartEnd<KeyframeType.TextColor>(pos, keyframes);
270
+ return {
271
+ color: this.getRangeColor(start, end, pos)
272
+ };
273
+ }
274
+
275
+ getLetterSpacing(
276
+ values: KeyframeValueMap[KeyframeType.LetterSpacing],
277
+ pos: number
278
+ ): KeyframeValueMap[KeyframeType.LetterSpacing] {
279
+ const keyframes = this.keyframesMap[KeyframeType.LetterSpacing];
280
+ if (!keyframes || !keyframes.length) return values;
281
+ if (keyframes.length === 1) {
282
+ const [keyframe] = keyframes;
283
+ return {
284
+ letterSpacing: keyframe.value.letterSpacing
285
+ };
286
+ }
287
+ const { start, end } = this.getStartEnd<KeyframeType.LetterSpacing>(pos, keyframes);
288
+ return {
289
+ letterSpacing: rangeMap(
290
+ pos,
291
+ start.position,
292
+ end.position,
293
+ start.value.letterSpacing,
294
+ end.value.letterSpacing,
295
+ true
296
+ )
297
+ };
298
+ }
299
+
300
+ getWordSpacing(
301
+ values: KeyframeValueMap[KeyframeType.WordSpacing],
302
+ pos: number
303
+ ): KeyframeValueMap[KeyframeType.WordSpacing] {
304
+ const keyframes = this.keyframesMap[KeyframeType.WordSpacing];
305
+ if (!keyframes || !keyframes.length) return values;
306
+ if (keyframes.length === 1) {
307
+ const [keyframe] = keyframes;
308
+ return {
309
+ wordSpacing: keyframe.value.wordSpacing
310
+ };
311
+ }
312
+ const { start, end } = this.getStartEnd<KeyframeType.WordSpacing>(pos, keyframes);
313
+ return {
314
+ wordSpacing: rangeMap(
315
+ pos,
316
+ start.position,
317
+ end.position,
318
+ start.value.wordSpacing,
319
+ end.value.wordSpacing,
320
+ true
321
+ )
322
+ };
323
+ }
324
+
325
+ getStartEnd<T extends KeyframeType>(position: number, keyframes: AnimationData<T>[]): PositionKeyframes<T> {
326
+ const index = binSearchInsertAt(keyframes, { position }, compare);
327
+ const end = index === keyframes.length ? index - 1 : index;
328
+ const start = end === 0 ? end : end - 1;
329
+ return { start: keyframes[start], end: keyframes[end] };
330
+ }
331
+
332
+ private fillKeyframesMap() {
333
+ this.keyframesMap = createKeyframesMap();
334
+ for (const keyframe of this.keyframes) {
335
+ Animator.pushKeyframeToMap(keyframe, this.keyframesMap);
336
+ }
337
+ }
338
+
339
+ private getRangeColor(
340
+ start: AnimationData<KeyframeType.TextColor>,
341
+ end: AnimationData<KeyframeType.TextColor>,
342
+ position: number
343
+ ): string {
344
+ const rangeAmount = rangeMap(position, start.position, end.position, 0, 1, true);
345
+ const startColor = CntrlColor.parse(start.value.color);
346
+ const endColor = CntrlColor.parse(end.value.color);
347
+ const mixedColor = startColor.mix(endColor, rangeAmount);
348
+ return mixedColor.fmt('oklch');
349
+ }
350
+
351
+ private getRangeGradient(
352
+ start: AnimationData<KeyframeType.Fill | KeyframeType.BorderFill>,
353
+ end: AnimationData<KeyframeType.Fill | KeyframeType.BorderFill>,
354
+ position: number
355
+ ): FillLayer[] {
356
+ const rangeAmount = rangeMap(position, start.position, end.position, 0, 1, true);
357
+ const allLayerIds = new Set([
358
+ ...start.value.map(layer => layer.id),
359
+ ...end.value.map(layer => layer.id)
360
+ ]);
361
+ const result: FillLayer[] = [];
362
+ for (const layerId of allLayerIds) {
363
+ const startLayers = start.value.find(layer => layer.id === layerId);
364
+ const endLayers = end.value.find(layer => layer.id === layerId);
365
+ const startLayersArray = Array.isArray(startLayers) ? startLayers : startLayers ? [startLayers] : [];
366
+ const endLayersArray = Array.isArray(endLayers) ? endLayers : endLayers ? [endLayers] : [];
367
+ if (startLayersArray.length > 0 && endLayersArray.length === 0) {
368
+ result.push(...startLayersArray);
369
+ continue;
370
+ }
371
+ if (startLayersArray.length === 0 && endLayersArray.length > 0) {
372
+ result.push(...endLayersArray);
373
+ continue;
374
+ }
375
+ if (startLayersArray.length > 0 && endLayersArray.length > 0) {
376
+ result.push(...this.interpolateFillLayerArray(startLayersArray, endLayersArray, rangeAmount));
377
+ }
378
+ }
379
+
380
+ return result;
381
+ }
382
+
383
+ private interpolateFillLayerArray(startLayers: FillLayer[], endLayers: FillLayer[], rangeAmount: number): FillLayer[] {
384
+ const startLayerMap = new Map(startLayers.map(layer => [layer.id, layer]));
385
+ const endLayerMap = new Map(endLayers.map(layer => [layer.id, layer]));
386
+ const allLayerIds = new Set([
387
+ ...startLayers.map(layer => layer.id),
388
+ ...endLayers.map(layer => layer.id)
389
+ ]);
390
+
391
+ const interpolatedLayers: FillLayer[] = [];
392
+
393
+ for (const layerId of allLayerIds) {
394
+ const startLayer = startLayerMap.get(layerId);
395
+ const endLayer = endLayerMap.get(layerId);
396
+ if (startLayer && !endLayer) {
397
+ interpolatedLayers.push(startLayer);
398
+ continue;
399
+ }
400
+ if (!startLayer && endLayer) {
401
+ interpolatedLayers.push(endLayer);
402
+ continue;
403
+ }
404
+ if (startLayer && endLayer) {
405
+ interpolatedLayers.push(this.interpolateFillLayer(startLayer, endLayer, rangeAmount));
406
+ }
407
+ }
408
+ const result: FillLayer[] = [];
409
+ for (const startLayer of startLayers) {
410
+ const interpolatedLayer = interpolatedLayers.find(layer => layer.id === startLayer.id);
411
+ if (interpolatedLayer) {
412
+ result.push(interpolatedLayer);
413
+ }
414
+ }
415
+ for (const endLayer of endLayers) {
416
+ if (!startLayerMap.has(endLayer.id)) {
417
+ const interpolatedLayer = interpolatedLayers.find(layer => layer.id === endLayer.id);
418
+ if (interpolatedLayer) {
419
+ result.push(interpolatedLayer);
420
+ }
421
+ }
422
+ }
423
+
424
+ return result;
425
+ }
426
+
427
+ private interpolateFillLayer(startLayer: FillLayer, endLayer: FillLayer, rangeAmount: number): FillLayer {
428
+ if (startLayer.type !== endLayer.type) {
429
+ return startLayer;
430
+ }
431
+
432
+ switch (startLayer.type) {
433
+ case 'solid': {
434
+ const endSolidLayer = endLayer as typeof startLayer;
435
+ const startColor = CntrlColor.parse(startLayer.value);
436
+ const endColor = CntrlColor.parse(endSolidLayer.value);
437
+ const mixedColor = startColor.mix(endColor, rangeAmount);
438
+ return {
439
+ ...startLayer,
440
+ value: mixedColor.fmt('oklch')
441
+ };
442
+ }
443
+
444
+ case 'linear-gradient': {
445
+ const endLinearLayer = endLayer as typeof startLayer;
446
+ const interpolatedColors = this.interpolateColorStops(startLayer.colors, endLinearLayer.colors, rangeAmount);
447
+ const angle = startLayer.angle + (endLinearLayer.angle - startLayer.angle) * rangeAmount;
448
+ const startPoint: [number, number] = [
449
+ startLayer.start[0] + (endLinearLayer.start[0] - startLayer.start[0]) * rangeAmount,
450
+ startLayer.start[1] + (endLinearLayer.start[1] - startLayer.start[1]) * rangeAmount
451
+ ];
452
+ const endPoint: [number, number] = [
453
+ startLayer.end[0] + (endLinearLayer.end[0] - startLayer.end[0]) * rangeAmount,
454
+ startLayer.end[1] + (endLinearLayer.end[1] - startLayer.end[1]) * rangeAmount
455
+ ];
456
+ return {
457
+ ...startLayer,
458
+ colors: interpolatedColors,
459
+ angle,
460
+ start: startPoint,
461
+ end: endPoint
462
+ };
463
+ }
464
+
465
+ case 'radial-gradient': {
466
+ const endRadialLayer = endLayer as typeof startLayer;
467
+ const interpolatedColors = this.interpolateColorStops(startLayer.colors, endRadialLayer.colors, rangeAmount);
468
+ const angle = startLayer.angle + (endRadialLayer.angle - startLayer.angle) * rangeAmount;
469
+ const center: [number, number] = [
470
+ startLayer.center[0] + (endRadialLayer.center[0] - startLayer.center[0]) * rangeAmount,
471
+ startLayer.center[1] + (endRadialLayer.center[1] - startLayer.center[1]) * rangeAmount
472
+ ];
473
+ const diameter = startLayer.diameter + (endRadialLayer.diameter - startLayer.diameter) * rangeAmount;
474
+ return {
475
+ ...startLayer,
476
+ colors: interpolatedColors,
477
+ angle,
478
+ center,
479
+ diameter
480
+ };
481
+ }
482
+
483
+ case 'conic-gradient': {
484
+ const endConicLayer = endLayer as typeof startLayer;
485
+ const interpolatedColors = this.interpolateColorStops(startLayer.colors, endConicLayer.colors, rangeAmount);
486
+ const angle = startLayer.angle + (endConicLayer.angle - startLayer.angle) * rangeAmount;
487
+ const center: [number, number] = [
488
+ startLayer.center[0] + (endConicLayer.center[0] - startLayer.center[0]) * rangeAmount,
489
+ startLayer.center[1] + (endConicLayer.center[1] - startLayer.center[1]) * rangeAmount
490
+ ];
491
+ return {
492
+ ...startLayer,
493
+ colors: interpolatedColors,
494
+ angle,
495
+ center
496
+ };
497
+ }
498
+
499
+ case 'image': {
500
+ const endImageLayer = endLayer as typeof startLayer;
501
+ const opacity = startLayer.opacity + (endImageLayer.opacity - startLayer.opacity) * rangeAmount;
502
+ const backgroundSize = startLayer.backgroundSize + (endImageLayer.backgroundSize - startLayer.backgroundSize) * rangeAmount;
503
+ return {
504
+ ...startLayer,
505
+ opacity,
506
+ backgroundSize
507
+ };
508
+ }
509
+
510
+ default:
511
+ return startLayer;
512
+ }
513
+ }
514
+
515
+ private interpolateColorStops(startColors: ColorPoint[], endColors: ColorPoint[], rangeAmount: number): ColorPoint[] {
516
+ const startColorMap = new Map(startColors.map(stop => [stop.id, stop]));
517
+ const endColorMap = new Map(endColors.map(stop => [stop.id, stop]));
518
+ const allStopIds = new Set([
519
+ ...startColors.map(stop => stop.id),
520
+ ...endColors.map(stop => stop.id)
521
+ ]);
522
+
523
+ const interpolatedStops: ColorPoint[] = [];
524
+
525
+ for (const stopId of allStopIds) {
526
+ const startStop = startColorMap.get(stopId);
527
+ const endStop = endColorMap.get(stopId);
528
+ if (startStop && !endStop) {
529
+ interpolatedStops.push(startStop);
530
+ continue;
531
+ }
532
+ if (!startStop && endStop) {
533
+ interpolatedStops.push(endStop);
534
+ continue;
535
+ }
536
+ if (startStop && endStop) {
537
+ const startColor = CntrlColor.parse(startStop.value);
538
+ const endColor = CntrlColor.parse(endStop.value);
539
+ const mixedColor = startColor.mix(endColor, rangeAmount);
540
+ const position = startStop.position + (endStop.position - startStop.position) * rangeAmount;
541
+
542
+ interpolatedStops.push({
543
+ id: stopId,
544
+ value: mixedColor.fmt('oklch'),
545
+ position
546
+ });
547
+ }
548
+ }
549
+ return interpolatedStops.sort((a, b) => a.position - b.position);
550
+ }
551
+ }
552
+
553
+ function createKeyframesMap(): KeyframesMap {
554
+ return {
555
+ [KeyframeType.Dimensions]: [],
556
+ [KeyframeType.Position]: [],
557
+ [KeyframeType.BorderWidth]: [],
558
+ [KeyframeType.BorderRadius]: [],
559
+ [KeyframeType.Rotation]: [],
560
+ [KeyframeType.BorderFill]: [],
561
+ [KeyframeType.Opacity]: [],
562
+ [KeyframeType.Scale]: [],
563
+ [KeyframeType.Blur]: [],
564
+ [KeyframeType.BackdropBlur]: [],
565
+ [KeyframeType.LetterSpacing]: [],
566
+ [KeyframeType.WordSpacing]: [],
567
+ [KeyframeType.TextColor]: [],
568
+ [KeyframeType.FXParams]: [],
569
+ [KeyframeType.Fill]: []
570
+ };
571
+ }