-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshimmer-keyframes.ts
More file actions
104 lines (96 loc) · 3.39 KB
/
shimmer-keyframes.ts
File metadata and controls
104 lines (96 loc) · 3.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* @file SVG keyframe batcher for the shimmer engine. Pre-renders N frames of
* the engine and emits per-character keyTimes / values arrays suitable for
* SVG SMIL `<animate>` elements. The output is deduplicated (consecutive
* identical colors collapse to a single keyframe) and closed with a `t=1`
* anchor so the animation loops cleanly. Use with `calcMode="discrete"` to
* reproduce the engine's output exactly — no SMIL interpolation between
* frames, each frame holds until the next changes.
*
* @example
* ;```ts
* import { configToSpec } from '@socketsecurity/lib/effects/shimmer'
* import { toShimmerKeyframes } from '@socketsecurity/lib/effects/shimmer-keyframes'
* const spec = configToSpec({ color: RAINBOW_GRADIENT, dir: 'ltr' }, 10)
* const tracks = toShimmerKeyframes(spec, 10, 60)
* // Emit one <animate> per char in your SVG:
* // <animate attributeName="fill" calcMode="discrete"
* // keyTimes={tracks[i].times.join(';')}
* // values={tracks[i].values.join(';')}
* // dur="3s" repeatCount="indefinite" />
* ```
*/
import { frameColors, type ShimmerSpec } from './shimmer'
// === Types ===
/**
* Keyframe track for a single character. `times[i]` is in [0, 1] and pairs with
* `values[i]` (an `rgb(R,G,B)` string). Use directly as SMIL `<animate>`
* attributes:
*
* ```jsx
* ;<animate
* keyTimes={track.times.join(';')}
* values={track.values.join(';')}
* calcMode="discrete"
* />
* ```
*/
export type Keyframes = {
/**
* Normalized timestamps in [0, 1], monotonically non-decreasing.
*/
readonly times: readonly number[]
/**
* `rgb(R,G,B)` color strings, paired by index with `times`.
*/
readonly values: readonly string[]
}
// === API ===
/**
* Render N frames of a shimmer spec into per-character keyframe tracks.
*
* Output is one {@link Keyframes} object per char. Consecutive identical colors
* collapse — only the first occurrence emits a keyframe, the rest are implicit
* (SMIL holds the previous value). A final keyframe at `t=1` closes the loop
* with the same value as `t=0` so playback wraps cleanly.
*
* Use with `calcMode="discrete"` to reproduce the engine's per-frame output
* exactly (no SMIL interpolation between frames).
*
* @param spec Functional shimmer specification.
* @param textLength Number of chars to colorize.
* @param frames Total frame count to bake into the loop.
*
* @returns One Keyframes track per char index, in order
*/
export function toShimmerKeyframes(
spec: ShimmerSpec,
textLength: number,
frames: number,
): Keyframes[] {
const tracks: Array<{ times: number[]; values: string[] }> = []
for (let i = 0; i < textLength; i++) {
tracks.push({ times: [], values: [] })
}
for (let f = 0; f < frames; f++) {
const t = f / frames
const colors = frameColors(spec, textLength, f)
for (let i = 0; i < textLength; i++) {
const c = colors[i]!
const v = `rgb(${c[0]},${c[1]},${c[2]})`
const track = tracks[i]!
if (track.values[track.values.length - 1] !== v) {
track.times.push(t)
track.values.push(v)
}
}
}
// Anchor t=1 to the same value as frame 0 so the loop wraps without a
// visible glitch at the seam.
for (let i = 0; i < textLength; i++) {
const track = tracks[i]!
track.times.push(1)
track.values.push(track.values[0] ?? 'rgb(0,0,0)')
}
return tracks
}