@codellyson/framely 0.1.0 → 0.1.1

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 (56) hide show
  1. package/package.json +3 -2
  2. package/src/AbsoluteFill.tsx +50 -0
  3. package/src/Audio.tsx +294 -0
  4. package/src/Composition.tsx +378 -0
  5. package/src/Easing.ts +294 -0
  6. package/src/ErrorBoundary.tsx +136 -0
  7. package/src/Folder.tsx +66 -0
  8. package/src/Freeze.tsx +63 -0
  9. package/src/IFrame.tsx +100 -0
  10. package/src/Img.tsx +146 -0
  11. package/src/Loop.tsx +139 -0
  12. package/src/Player.tsx +594 -0
  13. package/src/Sequence.tsx +80 -0
  14. package/src/Series.tsx +181 -0
  15. package/src/Text.tsx +376 -0
  16. package/src/Video.tsx +247 -0
  17. package/src/__tests__/Easing.test.js +119 -0
  18. package/src/__tests__/interpolate.test.js +127 -0
  19. package/src/config.ts +406 -0
  20. package/src/context.tsx +241 -0
  21. package/src/delayRender.ts +278 -0
  22. package/src/getInputProps.ts +217 -0
  23. package/src/hooks/useDelayRender.ts +117 -0
  24. package/src/hooks.ts +28 -0
  25. package/src/index.d.ts +571 -0
  26. package/src/index.ts +260 -0
  27. package/src/interpolate.ts +160 -0
  28. package/src/interpolateColors.ts +368 -0
  29. package/src/makeTransform.ts +339 -0
  30. package/src/measureSpring.ts +152 -0
  31. package/src/noise.ts +308 -0
  32. package/src/preload.ts +303 -0
  33. package/src/registerRoot.ts +346 -0
  34. package/src/shapes/Circle.tsx +37 -0
  35. package/src/shapes/Ellipse.tsx +39 -0
  36. package/src/shapes/Line.tsx +37 -0
  37. package/src/shapes/Path.tsx +56 -0
  38. package/src/shapes/Polygon.tsx +39 -0
  39. package/src/shapes/Rect.tsx +43 -0
  40. package/src/shapes/Svg.tsx +39 -0
  41. package/src/shapes/index.ts +16 -0
  42. package/src/shapes/usePathLength.ts +38 -0
  43. package/src/staticFile.ts +117 -0
  44. package/src/templates/api.ts +165 -0
  45. package/src/templates/index.ts +7 -0
  46. package/src/templates/mockData.ts +271 -0
  47. package/src/templates/types.ts +126 -0
  48. package/src/transitions/TransitionSeries.tsx +399 -0
  49. package/src/transitions/index.ts +109 -0
  50. package/src/transitions/presets/fade.ts +89 -0
  51. package/src/transitions/presets/flip.ts +263 -0
  52. package/src/transitions/presets/slide.ts +154 -0
  53. package/src/transitions/presets/wipe.ts +195 -0
  54. package/src/transitions/presets/zoom.ts +183 -0
  55. package/src/useAudioData.ts +260 -0
  56. package/src/useSpring.ts +215 -0
@@ -0,0 +1,368 @@
1
+ import { Easing, EasingFunction } from './Easing';
2
+
3
+ /**
4
+ * RGBA color with channels 0-255 for RGB and 0-1 for alpha.
5
+ */
6
+ export interface RGBAColor {
7
+ r: number;
8
+ g: number;
9
+ b: number;
10
+ a: number;
11
+ }
12
+
13
+ /**
14
+ * Color in the OKLCH perceptual color space.
15
+ */
16
+ export interface OKLCHColor {
17
+ L: number;
18
+ C: number;
19
+ H: number;
20
+ }
21
+
22
+ /**
23
+ * Options for interpolateColors.
24
+ */
25
+ export interface InterpolateColorsOptions {
26
+ easing?: EasingFunction;
27
+ extrapolateLeft?: 'extend' | 'clamp';
28
+ extrapolateRight?: 'extend' | 'clamp';
29
+ colorSpace?: 'rgb' | 'oklch';
30
+ }
31
+
32
+ /**
33
+ * Parse a color string into RGBA components.
34
+ *
35
+ * Supports:
36
+ * - Hex: #RGB, #RRGGBB, #RGBA, #RRGGBBAA
37
+ * - RGB: rgb(r, g, b)
38
+ * - RGBA: rgba(r, g, b, a)
39
+ * - HSL: hsl(h, s%, l%)
40
+ * - HSLA: hsla(h, s%, l%, a)
41
+ * - Named colors (limited set)
42
+ *
43
+ * @param {string} color - Color string to parse
44
+ * @returns {{ r: number, g: number, b: number, a: number }} RGBA values (0-255 for RGB, 0-1 for alpha)
45
+ */
46
+ function parseColor(color: string): RGBAColor {
47
+ if (typeof color !== 'string') {
48
+ throw new Error(`Invalid color: ${color}`);
49
+ }
50
+
51
+ const trimmed = color.trim().toLowerCase();
52
+
53
+ // Named colors (common ones)
54
+ const namedColors: Record<string, RGBAColor> = {
55
+ transparent: { r: 0, g: 0, b: 0, a: 0 },
56
+ black: { r: 0, g: 0, b: 0, a: 1 },
57
+ white: { r: 255, g: 255, b: 255, a: 1 },
58
+ red: { r: 255, g: 0, b: 0, a: 1 },
59
+ green: { r: 0, g: 128, b: 0, a: 1 },
60
+ blue: { r: 0, g: 0, b: 255, a: 1 },
61
+ yellow: { r: 255, g: 255, b: 0, a: 1 },
62
+ cyan: { r: 0, g: 255, b: 255, a: 1 },
63
+ magenta: { r: 255, g: 0, b: 255, a: 1 },
64
+ gray: { r: 128, g: 128, b: 128, a: 1 },
65
+ grey: { r: 128, g: 128, b: 128, a: 1 },
66
+ orange: { r: 255, g: 165, b: 0, a: 1 },
67
+ purple: { r: 128, g: 0, b: 128, a: 1 },
68
+ pink: { r: 255, g: 192, b: 203, a: 1 },
69
+ };
70
+
71
+ if (namedColors[trimmed]) {
72
+ return { ...namedColors[trimmed] };
73
+ }
74
+
75
+ // Hex colors
76
+ if (trimmed.startsWith('#')) {
77
+ const hex = trimmed.slice(1);
78
+ let r: number, g: number, b: number, a: number = 1;
79
+
80
+ if (hex.length === 3) {
81
+ // #RGB
82
+ r = parseInt(hex[0] + hex[0], 16);
83
+ g = parseInt(hex[1] + hex[1], 16);
84
+ b = parseInt(hex[2] + hex[2], 16);
85
+ } else if (hex.length === 4) {
86
+ // #RGBA
87
+ r = parseInt(hex[0] + hex[0], 16);
88
+ g = parseInt(hex[1] + hex[1], 16);
89
+ b = parseInt(hex[2] + hex[2], 16);
90
+ a = parseInt(hex[3] + hex[3], 16) / 255;
91
+ } else if (hex.length === 6) {
92
+ // #RRGGBB
93
+ r = parseInt(hex.slice(0, 2), 16);
94
+ g = parseInt(hex.slice(2, 4), 16);
95
+ b = parseInt(hex.slice(4, 6), 16);
96
+ } else if (hex.length === 8) {
97
+ // #RRGGBBAA
98
+ r = parseInt(hex.slice(0, 2), 16);
99
+ g = parseInt(hex.slice(2, 4), 16);
100
+ b = parseInt(hex.slice(4, 6), 16);
101
+ a = parseInt(hex.slice(6, 8), 16) / 255;
102
+ } else {
103
+ throw new Error(`Invalid hex color: ${color}`);
104
+ }
105
+
106
+ return { r, g, b, a };
107
+ }
108
+
109
+ // RGB/RGBA
110
+ const rgbMatch = trimmed.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/);
111
+ if (rgbMatch) {
112
+ return {
113
+ r: parseInt(rgbMatch[1], 10),
114
+ g: parseInt(rgbMatch[2], 10),
115
+ b: parseInt(rgbMatch[3], 10),
116
+ a: rgbMatch[4] !== undefined ? parseFloat(rgbMatch[4]) : 1,
117
+ };
118
+ }
119
+
120
+ // HSL/HSLA
121
+ const hslMatch = trimmed.match(/^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+))?\s*\)$/);
122
+ if (hslMatch) {
123
+ const h = parseFloat(hslMatch[1]) / 360;
124
+ const s = parseFloat(hslMatch[2]) / 100;
125
+ const l = parseFloat(hslMatch[3]) / 100;
126
+ const a = hslMatch[4] !== undefined ? parseFloat(hslMatch[4]) : 1;
127
+
128
+ // Convert HSL to RGB
129
+ let r: number, g: number, b: number;
130
+ if (s === 0) {
131
+ r = g = b = l;
132
+ } else {
133
+ const hue2rgb = (p: number, q: number, t: number): number => {
134
+ if (t < 0) t += 1;
135
+ if (t > 1) t -= 1;
136
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
137
+ if (t < 1 / 2) return q;
138
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
139
+ return p;
140
+ };
141
+
142
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
143
+ const p = 2 * l - q;
144
+ r = hue2rgb(p, q, h + 1 / 3);
145
+ g = hue2rgb(p, q, h);
146
+ b = hue2rgb(p, q, h - 1 / 3);
147
+ }
148
+
149
+ return {
150
+ r: Math.round(r * 255),
151
+ g: Math.round(g * 255),
152
+ b: Math.round(b * 255),
153
+ a,
154
+ };
155
+ }
156
+
157
+ // OKLCH: oklch(70% 0.15 240) or oklch(70% 0.15 240 / 0.5)
158
+ const oklchMatch = trimmed.match(
159
+ /^oklch\(\s*([\d.]+)%?\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/,
160
+ );
161
+ if (oklchMatch) {
162
+ const L = parseFloat(oklchMatch[1]) / 100;
163
+ const C = parseFloat(oklchMatch[2]);
164
+ const H = parseFloat(oklchMatch[3]);
165
+ const a = oklchMatch[4] !== undefined ? parseFloat(oklchMatch[4]) : 1;
166
+ const rgb = oklchToRgb(L, C, H);
167
+ return { r: rgb.r, g: rgb.g, b: rgb.b, a };
168
+ }
169
+
170
+ throw new Error(`Unable to parse color: ${color}`);
171
+ }
172
+
173
+ // ─── OKLCH ↔ RGB conversion ─────────────────────────────────────────────────
174
+
175
+ /**
176
+ * Convert OKLCH to linear RGB, then to sRGB.
177
+ *
178
+ * OKLCH → OKLab → linear RGB → sRGB
179
+ */
180
+ function oklchToRgb(L: number, C: number, H: number): Omit<RGBAColor, 'a'> {
181
+ // OKLCH → OKLab
182
+ const hRad = (H * Math.PI) / 180;
183
+ const a = C * Math.cos(hRad);
184
+ const b = C * Math.sin(hRad);
185
+
186
+ // OKLab → linear RGB (via LMS intermediate)
187
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
188
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
189
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
190
+
191
+ const l = l_ * l_ * l_;
192
+ const m = m_ * m_ * m_;
193
+ const s = s_ * s_ * s_;
194
+
195
+ const rLin = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
196
+ const gLin = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
197
+ const bLin = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
198
+
199
+ return {
200
+ r: Math.round(Math.max(0, Math.min(255, linearToSrgb(rLin) * 255))),
201
+ g: Math.round(Math.max(0, Math.min(255, linearToSrgb(gLin) * 255))),
202
+ b: Math.round(Math.max(0, Math.min(255, linearToSrgb(bLin) * 255))),
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Convert sRGB to OKLCH.
208
+ */
209
+ function rgbToOklch(r: number, g: number, b: number): OKLCHColor {
210
+ const rLin = srgbToLinear(r / 255);
211
+ const gLin = srgbToLinear(g / 255);
212
+ const bLin = srgbToLinear(b / 255);
213
+
214
+ const l_ = Math.cbrt(0.4122214708 * rLin + 0.5363325363 * gLin + 0.0514459929 * bLin);
215
+ const m_ = Math.cbrt(0.2119034982 * rLin + 0.6806995451 * gLin + 0.1073969566 * bLin);
216
+ const s_ = Math.cbrt(0.0883024619 * rLin + 0.2817188376 * gLin + 0.6299787005 * bLin);
217
+
218
+ const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
219
+ const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
220
+ const bOk = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
221
+
222
+ const C = Math.sqrt(a * a + bOk * bOk);
223
+ let H = (Math.atan2(bOk, a) * 180) / Math.PI;
224
+ if (H < 0) H += 360;
225
+
226
+ return { L, C, H };
227
+ }
228
+
229
+ function linearToSrgb(c: number): number {
230
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
231
+ }
232
+
233
+ function srgbToLinear(c: number): number {
234
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
235
+ }
236
+
237
+ /**
238
+ * Convert RGBA components to a CSS color string.
239
+ *
240
+ * @param {{ r: number, g: number, b: number, a: number }} color
241
+ * @returns {string} CSS color string
242
+ */
243
+ function colorToString(color: RGBAColor): string {
244
+ const r = Math.round(Math.max(0, Math.min(255, color.r)));
245
+ const g = Math.round(Math.max(0, Math.min(255, color.g)));
246
+ const b = Math.round(Math.max(0, Math.min(255, color.b)));
247
+ const a = Math.max(0, Math.min(1, color.a));
248
+
249
+ if (a === 1) {
250
+ return `rgb(${r}, ${g}, ${b})`;
251
+ }
252
+ return `rgba(${r}, ${g}, ${b}, ${a.toFixed(3)})`;
253
+ }
254
+
255
+ /**
256
+ * Interpolate between colors across a range.
257
+ *
258
+ * Works like interpolate() but for colors. Supports any CSS color format.
259
+ *
260
+ * Usage:
261
+ * interpolateColors(frame, [0, 30], ['#ff0000', '#0000ff'])
262
+ * interpolateColors(frame, [0, 30], ['red', 'blue'], { easing: Easing.easeOut })
263
+ * interpolateColors(frame, [0, 30, 60], ['red', 'yellow', 'green']) // multi-color
264
+ *
265
+ * @param {number} value - The input value (usually the current frame)
266
+ * @param {number[]} inputRange - Array of ascending input breakpoints
267
+ * @param {string[]} outputRange - Corresponding color values
268
+ * @param {object} [options]
269
+ * @param {function} [options.easing=Easing.linear] - Easing function
270
+ * @param {'extend'|'clamp'} [options.extrapolateLeft='clamp']
271
+ * @param {'extend'|'clamp'} [options.extrapolateRight='clamp']
272
+ * @param {'rgb'|'oklch'} [options.colorSpace='rgb'] - Interpolation color space
273
+ * @returns {string} Interpolated CSS color string
274
+ */
275
+ export function interpolateColors(
276
+ value: number,
277
+ inputRange: number[],
278
+ outputRange: string[],
279
+ options: InterpolateColorsOptions = {}
280
+ ): string {
281
+ const {
282
+ easing = Easing.linear,
283
+ extrapolateLeft = 'clamp',
284
+ extrapolateRight = 'clamp',
285
+ colorSpace = 'rgb',
286
+ } = options;
287
+
288
+ if (inputRange.length !== outputRange.length) {
289
+ throw new Error('inputRange and outputRange must have the same length');
290
+ }
291
+ if (inputRange.length < 2) {
292
+ throw new Error('inputRange must have at least 2 elements');
293
+ }
294
+
295
+ // Parse all colors upfront
296
+ const parsedColors: RGBAColor[] = outputRange.map(parseColor);
297
+
298
+ // Find which segment we're in
299
+ let segIndex = 0;
300
+ for (let i = 1; i < inputRange.length; i++) {
301
+ if (value >= inputRange[i - 1]) {
302
+ segIndex = i - 1;
303
+ }
304
+ }
305
+
306
+ const inputMin = inputRange[segIndex];
307
+ const inputMax = inputRange[segIndex + 1] ?? inputRange[segIndex];
308
+ const colorMin = parsedColors[segIndex];
309
+ const colorMax = parsedColors[segIndex + 1] ?? parsedColors[segIndex];
310
+
311
+ // Calculate progress within this segment (0 to 1)
312
+ let progress: number;
313
+ if (inputMax === inputMin) {
314
+ progress = 0;
315
+ } else {
316
+ progress = (value - inputMin) / (inputMax - inputMin);
317
+ }
318
+
319
+ // Handle extrapolation
320
+ if (progress < 0) {
321
+ progress = extrapolateLeft === 'clamp' ? 0 : progress;
322
+ }
323
+ if (progress > 1) {
324
+ progress = extrapolateRight === 'clamp' ? 1 : progress;
325
+ }
326
+
327
+ // Apply easing only to the 0-1 range
328
+ const easedProgress: number =
329
+ progress < 0 || progress > 1 ? progress : easing(progress);
330
+
331
+ // Interpolate in the chosen color space
332
+ let interpolated: RGBAColor;
333
+
334
+ if (colorSpace === 'oklch') {
335
+ // Convert to OKLCH, interpolate there, convert back
336
+ const oklch1 = rgbToOklch(colorMin.r, colorMin.g, colorMin.b);
337
+ const oklch2 = rgbToOklch(colorMax.r, colorMax.g, colorMax.b);
338
+
339
+ // Interpolate hue via shortest path
340
+ let hDiff = oklch2.H - oklch1.H;
341
+ if (hDiff > 180) hDiff -= 360;
342
+ if (hDiff < -180) hDiff += 360;
343
+
344
+ const L = oklch1.L + easedProgress * (oklch2.L - oklch1.L);
345
+ const C = oklch1.C + easedProgress * (oklch2.C - oklch1.C);
346
+ let H = oklch1.H + easedProgress * hDiff;
347
+ if (H < 0) H += 360;
348
+ if (H >= 360) H -= 360;
349
+
350
+ const rgb = oklchToRgb(L, C, H);
351
+ interpolated = {
352
+ ...rgb,
353
+ a: colorMin.a + easedProgress * (colorMax.a - colorMin.a),
354
+ };
355
+ } else {
356
+ // RGB interpolation (default)
357
+ interpolated = {
358
+ r: colorMin.r + easedProgress * (colorMax.r - colorMin.r),
359
+ g: colorMin.g + easedProgress * (colorMax.g - colorMin.g),
360
+ b: colorMin.b + easedProgress * (colorMax.b - colorMin.b),
361
+ a: colorMin.a + easedProgress * (colorMax.a - colorMin.a),
362
+ };
363
+ }
364
+
365
+ return colorToString(interpolated);
366
+ }
367
+
368
+ export default interpolateColors;
@@ -0,0 +1,339 @@
1
+ /**
2
+ * makeTransform Utility
3
+ *
4
+ * Build CSS transform strings from an array of transform operations.
5
+ * Helps construct complex transforms in a readable, composable way.
6
+ *
7
+ * Usage:
8
+ * const transform = makeTransform([
9
+ * { translateX: '100px' },
10
+ * { rotate: '45deg' },
11
+ * { scale: 1.5 },
12
+ * ]);
13
+ * // => "translateX(100px) rotate(45deg) scale(1.5)"
14
+ *
15
+ * // Or using the fluent API:
16
+ * const transform = transform()
17
+ * .translateX('100px')
18
+ * .rotate('45deg')
19
+ * .scale(1.5)
20
+ * .toString();
21
+ */
22
+
23
+ /**
24
+ * A single transform operation mapping function names to their values.
25
+ * Values can be numbers, strings, or arrays of numbers/strings (for multi-arg functions).
26
+ */
27
+ type TransformValue = number | string;
28
+ type TransformOperation = Record<string, TransformValue | TransformValue[]>;
29
+
30
+ /**
31
+ * Supported transform operations with their CSS function names.
32
+ */
33
+ const transformFunctions: Record<string, string> = {
34
+ // 2D Transforms
35
+ translateX: 'translateX',
36
+ translateY: 'translateY',
37
+ translate: 'translate',
38
+ scaleX: 'scaleX',
39
+ scaleY: 'scaleY',
40
+ scale: 'scale',
41
+ rotate: 'rotate',
42
+ skewX: 'skewX',
43
+ skewY: 'skewY',
44
+ skew: 'skew',
45
+
46
+ // 3D Transforms
47
+ translateZ: 'translateZ',
48
+ translate3d: 'translate3d',
49
+ scaleZ: 'scaleZ',
50
+ scale3d: 'scale3d',
51
+ rotateX: 'rotateX',
52
+ rotateY: 'rotateY',
53
+ rotateZ: 'rotateZ',
54
+ rotate3d: 'rotate3d',
55
+ perspective: 'perspective',
56
+
57
+ // Matrix
58
+ matrix: 'matrix',
59
+ matrix3d: 'matrix3d',
60
+ };
61
+
62
+ /**
63
+ * Format a value for CSS transform.
64
+ * Numbers are kept as-is for scale, strings are used directly.
65
+ */
66
+ function formatValue(key: string, value: TransformValue | TransformValue[]): string {
67
+ // Handle arrays (for translate, scale, rotate3d, etc.)
68
+ if (Array.isArray(value)) {
69
+ return value.join(', ');
70
+ }
71
+
72
+ // Handle numbers - add units where needed
73
+ if (typeof value === 'number') {
74
+ // Scale values don't need units
75
+ if (key.startsWith('scale') || key === 'matrix' || key === 'matrix3d') {
76
+ return String(value);
77
+ }
78
+ // Rotation needs deg
79
+ if (key.startsWith('rotate') || key.startsWith('skew')) {
80
+ return `${value}deg`;
81
+ }
82
+ // Translation needs px
83
+ if (key.startsWith('translate') || key === 'perspective') {
84
+ return `${value}px`;
85
+ }
86
+ return String(value);
87
+ }
88
+
89
+ return String(value);
90
+ }
91
+
92
+ /**
93
+ * Convert a transform operation object to CSS string.
94
+ */
95
+ function operationToString(operation: TransformOperation): string {
96
+ const entries = Object.entries(operation);
97
+ if (entries.length === 0) return '';
98
+
99
+ return entries
100
+ .map(([key, value]) => {
101
+ const fnName = transformFunctions[key] || key;
102
+ const formattedValue = formatValue(key, value);
103
+ return `${fnName}(${formattedValue})`;
104
+ })
105
+ .join(' ');
106
+ }
107
+
108
+ /**
109
+ * Build a CSS transform string from an array of transform operations.
110
+ *
111
+ * @param {TransformOperation | TransformOperation[]} operations - Array of transform operation objects
112
+ * @returns {string} CSS transform string
113
+ *
114
+ * @example
115
+ * makeTransform([
116
+ * { translateX: 100 },
117
+ * { rotate: 45 },
118
+ * { scale: 1.5 },
119
+ * ])
120
+ * // => "translateX(100px) rotate(45deg) scale(1.5)"
121
+ */
122
+ export function makeTransform(operations: TransformOperation | TransformOperation[]): string {
123
+ if (!Array.isArray(operations)) {
124
+ return operationToString(operations);
125
+ }
126
+
127
+ return operations
128
+ .map(operationToString)
129
+ .filter(Boolean)
130
+ .join(' ');
131
+ }
132
+
133
+ /**
134
+ * Fluent transform builder class.
135
+ */
136
+ class TransformBuilder {
137
+ operations: TransformOperation[];
138
+
139
+ constructor() {
140
+ this.operations = [];
141
+ }
142
+
143
+ // 2D Transforms
144
+ translateX(value: TransformValue): this {
145
+ this.operations.push({ translateX: value });
146
+ return this;
147
+ }
148
+
149
+ translateY(value: TransformValue): this {
150
+ this.operations.push({ translateY: value });
151
+ return this;
152
+ }
153
+
154
+ translate(x: TransformValue, y?: TransformValue): this {
155
+ this.operations.push({ translate: y !== undefined ? [x, y] : x });
156
+ return this;
157
+ }
158
+
159
+ scaleX(value: TransformValue): this {
160
+ this.operations.push({ scaleX: value });
161
+ return this;
162
+ }
163
+
164
+ scaleY(value: TransformValue): this {
165
+ this.operations.push({ scaleY: value });
166
+ return this;
167
+ }
168
+
169
+ scale(x: TransformValue, y?: TransformValue): this {
170
+ this.operations.push({ scale: y !== undefined ? [x, y] : x });
171
+ return this;
172
+ }
173
+
174
+ rotate(value: TransformValue): this {
175
+ this.operations.push({ rotate: value });
176
+ return this;
177
+ }
178
+
179
+ skewX(value: TransformValue): this {
180
+ this.operations.push({ skewX: value });
181
+ return this;
182
+ }
183
+
184
+ skewY(value: TransformValue): this {
185
+ this.operations.push({ skewY: value });
186
+ return this;
187
+ }
188
+
189
+ skew(x: TransformValue, y?: TransformValue): this {
190
+ this.operations.push({ skew: y !== undefined ? [x, y] : x });
191
+ return this;
192
+ }
193
+
194
+ // 3D Transforms
195
+ translateZ(value: TransformValue): this {
196
+ this.operations.push({ translateZ: value });
197
+ return this;
198
+ }
199
+
200
+ translate3d(x: TransformValue, y: TransformValue, z: TransformValue): this {
201
+ this.operations.push({ translate3d: [x, y, z] });
202
+ return this;
203
+ }
204
+
205
+ scaleZ(value: TransformValue): this {
206
+ this.operations.push({ scaleZ: value });
207
+ return this;
208
+ }
209
+
210
+ scale3d(x: TransformValue, y: TransformValue, z: TransformValue): this {
211
+ this.operations.push({ scale3d: [x, y, z] });
212
+ return this;
213
+ }
214
+
215
+ rotateX(value: TransformValue): this {
216
+ this.operations.push({ rotateX: value });
217
+ return this;
218
+ }
219
+
220
+ rotateY(value: TransformValue): this {
221
+ this.operations.push({ rotateY: value });
222
+ return this;
223
+ }
224
+
225
+ rotateZ(value: TransformValue): this {
226
+ this.operations.push({ rotateZ: value });
227
+ return this;
228
+ }
229
+
230
+ rotate3d(x: TransformValue, y: TransformValue, z: TransformValue, angle: TransformValue): this {
231
+ this.operations.push({ rotate3d: [x, y, z, angle] });
232
+ return this;
233
+ }
234
+
235
+ perspective(value: TransformValue): this {
236
+ this.operations.push({ perspective: value });
237
+ return this;
238
+ }
239
+
240
+ // Matrix
241
+ matrix(...values: TransformValue[]): this {
242
+ this.operations.push({ matrix: values });
243
+ return this;
244
+ }
245
+
246
+ matrix3d(...values: TransformValue[]): this {
247
+ this.operations.push({ matrix3d: values });
248
+ return this;
249
+ }
250
+
251
+ // Build the transform string
252
+ toString(): string {
253
+ return makeTransform(this.operations);
254
+ }
255
+
256
+ // Get the operations array
257
+ toArray(): TransformOperation[] {
258
+ return [...this.operations];
259
+ }
260
+
261
+ // Clear all operations
262
+ clear(): this {
263
+ this.operations = [];
264
+ return this;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Create a new transform builder for fluent API usage.
270
+ *
271
+ * @returns {TransformBuilder}
272
+ *
273
+ * @example
274
+ * const transform = transform()
275
+ * .translateX(100)
276
+ * .rotate(45)
277
+ * .scale(1.5)
278
+ * .toString();
279
+ */
280
+ export function transform(): TransformBuilder {
281
+ return new TransformBuilder();
282
+ }
283
+
284
+ /**
285
+ * Interpolate between two transform strings.
286
+ * Note: This is a simplified version that works best with matching operations.
287
+ *
288
+ * @param {number} progress - Interpolation progress (0-1)
289
+ * @param {TransformOperation[]} fromOps - Starting transform operations
290
+ * @param {TransformOperation[]} toOps - Ending transform operations
291
+ * @returns {string} Interpolated transform string
292
+ */
293
+ export function interpolateTransform(
294
+ progress: number,
295
+ fromOps: TransformOperation[],
296
+ toOps: TransformOperation[],
297
+ ): string {
298
+ const result: TransformOperation[] = [];
299
+
300
+ const maxLength = Math.max(fromOps.length, toOps.length);
301
+
302
+ for (let i = 0; i < maxLength; i++) {
303
+ const fromOp: TransformOperation = fromOps[i] || {};
304
+ const toOp: TransformOperation = toOps[i] || {};
305
+
306
+ const interpolatedOp: TransformOperation = {};
307
+
308
+ // Get all keys from both operations
309
+ const keys = new Set([...Object.keys(fromOp), ...Object.keys(toOp)]);
310
+
311
+ keys.forEach((key: string) => {
312
+ const fromValue = fromOp[key];
313
+ const toValue = toOp[key];
314
+
315
+ if (fromValue === undefined && toValue !== undefined) {
316
+ interpolatedOp[key] = toValue;
317
+ } else if (toValue === undefined && fromValue !== undefined) {
318
+ interpolatedOp[key] = fromValue;
319
+ } else if (typeof fromValue === 'number' && typeof toValue === 'number') {
320
+ interpolatedOp[key] = fromValue + (toValue - fromValue) * progress;
321
+ } else if (Array.isArray(fromValue) && Array.isArray(toValue)) {
322
+ interpolatedOp[key] = fromValue.map((v: TransformValue, idx: number) => {
323
+ const from = typeof v === 'number' ? v : parseFloat(v) || 0;
324
+ const to = typeof toValue[idx] === 'number' ? (toValue[idx] as number) : parseFloat(toValue[idx] as string) || 0;
325
+ return from + (to - from) * progress;
326
+ });
327
+ } else {
328
+ // Can't interpolate, use the target value
329
+ interpolatedOp[key] = progress < 0.5 ? fromValue! : toValue!;
330
+ }
331
+ });
332
+
333
+ result.push(interpolatedOp);
334
+ }
335
+
336
+ return makeTransform(result);
337
+ }
338
+
339
+ export default makeTransform;