@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.
- package/package.json +3 -2
- package/src/AbsoluteFill.tsx +50 -0
- package/src/Audio.tsx +294 -0
- package/src/Composition.tsx +378 -0
- package/src/Easing.ts +294 -0
- package/src/ErrorBoundary.tsx +136 -0
- package/src/Folder.tsx +66 -0
- package/src/Freeze.tsx +63 -0
- package/src/IFrame.tsx +100 -0
- package/src/Img.tsx +146 -0
- package/src/Loop.tsx +139 -0
- package/src/Player.tsx +594 -0
- package/src/Sequence.tsx +80 -0
- package/src/Series.tsx +181 -0
- package/src/Text.tsx +376 -0
- package/src/Video.tsx +247 -0
- package/src/__tests__/Easing.test.js +119 -0
- package/src/__tests__/interpolate.test.js +127 -0
- package/src/config.ts +406 -0
- package/src/context.tsx +241 -0
- package/src/delayRender.ts +278 -0
- package/src/getInputProps.ts +217 -0
- package/src/hooks/useDelayRender.ts +117 -0
- package/src/hooks.ts +28 -0
- package/src/index.d.ts +571 -0
- package/src/index.ts +260 -0
- package/src/interpolate.ts +160 -0
- package/src/interpolateColors.ts +368 -0
- package/src/makeTransform.ts +339 -0
- package/src/measureSpring.ts +152 -0
- package/src/noise.ts +308 -0
- package/src/preload.ts +303 -0
- package/src/registerRoot.ts +346 -0
- package/src/shapes/Circle.tsx +37 -0
- package/src/shapes/Ellipse.tsx +39 -0
- package/src/shapes/Line.tsx +37 -0
- package/src/shapes/Path.tsx +56 -0
- package/src/shapes/Polygon.tsx +39 -0
- package/src/shapes/Rect.tsx +43 -0
- package/src/shapes/Svg.tsx +39 -0
- package/src/shapes/index.ts +16 -0
- package/src/shapes/usePathLength.ts +38 -0
- package/src/staticFile.ts +117 -0
- package/src/templates/api.ts +165 -0
- package/src/templates/index.ts +7 -0
- package/src/templates/mockData.ts +271 -0
- package/src/templates/types.ts +126 -0
- package/src/transitions/TransitionSeries.tsx +399 -0
- package/src/transitions/index.ts +109 -0
- package/src/transitions/presets/fade.ts +89 -0
- package/src/transitions/presets/flip.ts +263 -0
- package/src/transitions/presets/slide.ts +154 -0
- package/src/transitions/presets/wipe.ts +195 -0
- package/src/transitions/presets/zoom.ts +183 -0
- package/src/useAudioData.ts +260 -0
- 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;
|