@cntrl-site/sdk-nextjs 1.9.78 → 1.9.80

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