paper-motion 0.1.0 → 0.2.0
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/README.md +14 -0
- package/bin/pamo.mjs +133 -4
- package/examples/halo-explainer/audio/script.txt +8 -0
- package/examples/halo-explainer/audio/words.json +554 -0
- package/examples/halo-explainer/brand/IDENTITY.md +24 -0
- package/examples/halo-explainer/brand/decisions/2026-11-08-type.md +5 -0
- package/examples/halo-explainer/brand/motion.json +9 -0
- package/examples/halo-explainer/brand/shader-set/set.json +104 -0
- package/examples/halo-explainer/brand/sound.json +33 -0
- package/examples/halo-explainer/brand/tokens.json +97 -0
- package/examples/halo-explainer/brief.md +34 -0
- package/examples/halo-explainer/poster.jpg +0 -0
- package/examples/halo-explainer/scene.html +372 -0
- package/examples/orbit-keynote/audio/script.txt +7 -0
- package/examples/orbit-keynote/audio/words.json +494 -0
- package/examples/orbit-keynote/brand/IDENTITY.md +25 -0
- package/examples/orbit-keynote/brand/decisions/2026-11-08-look.md +6 -0
- package/examples/orbit-keynote/brand/decisions/2026-11-08-voice.md +3 -0
- package/examples/orbit-keynote/brand/motion.json +9 -0
- package/examples/orbit-keynote/brand/shader-set/set.json +45 -0
- package/examples/orbit-keynote/brand/sound.json +45 -0
- package/examples/orbit-keynote/brand/tokens.json +106 -0
- package/examples/orbit-keynote/brief.md +25 -0
- package/examples/orbit-keynote/poster.jpg +0 -0
- package/examples/orbit-keynote/scene.html +323 -0
- package/lib/customize.mjs +162 -0
- package/lib/live-board.js +128 -0
- package/lib/registry.mjs +344 -0
- package/lib/render.mjs +3 -3
- package/lib/server.mjs +1 -0
- package/lib/sfx.mjs +5 -3
- package/package.json +1 -1
- package/runtime/core.js +269 -0
- package/runtime/index.js +3 -260
- package/runtime/kit.js +98 -0
- package/runtime/moves/bar-reveal.js +19 -0
- package/runtime/moves/blur-in.js +7 -0
- package/runtime/moves/cells.js +17 -0
- package/runtime/moves/crossfade.js +6 -0
- package/runtime/moves/cut.js +3 -0
- package/runtime/moves/dive-in.js +21 -0
- package/runtime/moves/draw-on.js +12 -0
- package/runtime/moves/drift.js +7 -0
- package/runtime/moves/flash.js +15 -0
- package/runtime/moves/flip-3d.js +9 -0
- package/runtime/moves/flip-in.js +12 -0
- package/runtime/moves/flip-lift.js +24 -0
- package/runtime/moves/glitch.js +16 -0
- package/runtime/moves/iris.js +12 -0
- package/runtime/moves/magic-move.js +49 -0
- package/runtime/moves/mask-reveal.js +10 -0
- package/runtime/moves/odometer.js +32 -0
- package/runtime/moves/orbit.js +8 -0
- package/runtime/moves/pop-in.js +12 -0
- package/runtime/moves/punch.js +9 -0
- package/runtime/moves/push.js +14 -0
- package/runtime/moves/rgb-split.js +7 -0
- package/runtime/moves/roll.js +20 -0
- package/runtime/moves/scan-reveal.js +10 -0
- package/runtime/moves/scramble.js +21 -0
- package/runtime/moves/shake.js +15 -0
- package/runtime/moves/slices.js +14 -0
- package/runtime/moves/snap-scroll.js +23 -0
- package/runtime/moves/squash-drop.js +21 -0
- package/runtime/moves/tile-swirl.js +29 -0
- package/runtime/moves/track-in.js +7 -0
- package/runtime/moves/type-on.js +41 -0
- package/runtime/moves/unfold.js +20 -0
- package/runtime/moves/whip-pan.js +21 -0
- package/runtime/moves/whip.js +14 -0
- package/runtime/moves/wipe.js +9 -0
- package/runtime/moves/word-rise.js +10 -0
- package/runtime/moves/zoom-through.js +13 -0
- package/runtime/moves.js +66 -146
- package/templates/move-demo/scene.html +4 -3
package/lib/render.mjs
CHANGED
|
@@ -94,12 +94,12 @@ export async function renderSfx({ url, out, from = 0, to }) {
|
|
|
94
94
|
} finally { await browser.close(); }
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async function sfxFromPage(s, { from = 0, to, out }) {
|
|
97
|
+
async function sfxFromPage(s, { from = 0, to, out, normalize = true }) {
|
|
98
98
|
const { cues, audio } = await s.page.evaluate(() => ({ cues: window.__pm.cues ?? [], audio: window.__pm.audio ?? {} }));
|
|
99
99
|
if (!cues.length) return null;
|
|
100
100
|
const end = to ?? s.config.duration;
|
|
101
101
|
const shifted = cues.filter((c) => c.t < end).map((c) => ({ ...c, t: c.t - from }));
|
|
102
|
-
writeWav(out, synthesize(shifted, { duration: end - from, ...audio }));
|
|
102
|
+
writeWav(out, synthesize(shifted, { duration: end - from, ...audio, normalize }));
|
|
103
103
|
return { out, cues: cues.length };
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -131,7 +131,7 @@ export async function renderVideo({ url, out, format = 'mp4', workers, dpr = 1,
|
|
|
131
131
|
|
|
132
132
|
let sfxWav = null;
|
|
133
133
|
if (sfx && !fmt.gif && !fmt.seq) {
|
|
134
|
-
const r = await sfxFromPage(first, { from: f0 / fps, to: f1 / fps, out: path.join(tmp, 'sfx.wav') });
|
|
134
|
+
const r = await sfxFromPage(first, { from: f0 / fps, to: f1 / fps, out: path.join(tmp, 'sfx.wav'), normalize: !audio }); // with an external track, effects keep their own gains
|
|
135
135
|
sfxWav = r?.out ?? null;
|
|
136
136
|
}
|
|
137
137
|
|
package/lib/server.mjs
CHANGED
|
@@ -21,6 +21,7 @@ const IMPORT_MAP = {
|
|
|
21
21
|
imports: {
|
|
22
22
|
'paper-motion': '/__pm/runtime/index.js',
|
|
23
23
|
'paper-motion/moves': '/__pm/runtime/moves.js',
|
|
24
|
+
'paper-motion/kit': '/__pm/runtime/kit.js',
|
|
24
25
|
'paper-motion/brand': '/__pm/runtime/brand.js',
|
|
25
26
|
'@paper-design/shaders': '/__pm/node_modules/@paper-design/shaders/dist/index.js',
|
|
26
27
|
'@paper-design/shaders-react': '/__pm/node_modules/@paper-design/shaders-react/dist/index.js',
|
package/lib/sfx.mjs
CHANGED
|
@@ -226,10 +226,12 @@ export const SFX_TYPES = Object.keys(VOICES);
|
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
228
|
* @param cues [{ t, type, pan?, seed?, ...voiceOpts }]
|
|
229
|
-
* @param options { duration, lowpass=2000, highpass=30, gain=1, peak=0.8 }
|
|
229
|
+
* @param options { duration, lowpass=2000, highpass=30, gain=1, peak=0.8, normalize=true }
|
|
230
|
+
* normalize: true scales the mix up or down to `peak`; false keeps the cues' own gains and only
|
|
231
|
+
* limits overs (used when the scene brings its own audio, so effects sit where their gains put them)
|
|
230
232
|
* @returns { left: Float32Array, right: Float32Array, sampleRate }
|
|
231
233
|
*/
|
|
232
|
-
export function synthesize(cues, { duration, lowpass = 2000, highpass = 30, gain = 1, peak = 0.8 } = {}) {
|
|
234
|
+
export function synthesize(cues, { duration, lowpass = 2000, highpass = 30, gain = 1, peak = 0.8, normalize = true } = {}) {
|
|
233
235
|
const n = Math.ceil(duration * SR);
|
|
234
236
|
const L = new Float32Array(n), R = new Float32Array(n);
|
|
235
237
|
// Voices marked `bright` (keys) go through their own, gentler low-pass: a keystroke's click lives at 1–4 kHz.
|
|
@@ -260,7 +262,7 @@ export function synthesize(cues, { duration, lowpass = 2000, highpass = 30, gain
|
|
|
260
262
|
});
|
|
261
263
|
let max = 1e-9;
|
|
262
264
|
for (let i = 0; i < n; i++) max = Math.max(max, Math.abs(L[i]), Math.abs(R[i]));
|
|
263
|
-
const k = (peak / max) * gain;
|
|
265
|
+
const k = (normalize ? peak / max : Math.min(1, peak / max)) * gain;
|
|
264
266
|
const fadeOut = Math.min(n, Math.round(0.05 * SR));
|
|
265
267
|
for (let i = 0; i < n; i++) {
|
|
266
268
|
const f = i > n - fadeOut ? (n - i) / fadeOut : 1;
|
package/package.json
CHANGED
package/runtime/core.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// paper-motion core: the pure helpers (math, easing, springs, keyframes, noise, color, text split, shapes).
|
|
2
|
+
// Browser-safe on its own: no shaders, no location, no scene. The moves import from here, so they run anywhere
|
|
3
|
+
// (the render engine, the site's live player). index.js re-exports all of it as 'paper-motion'.
|
|
4
|
+
|
|
5
|
+
const resolveEl = (el) => (typeof el === 'string' ? document.querySelector(el) : el);
|
|
6
|
+
|
|
7
|
+
/* Math, timing, easing */
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
|
|
10
|
+
export const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
|
|
11
|
+
export const lerp = (a, b, p) => a + (b - a) * p;
|
|
12
|
+
export const invlerp = (a, b, v) => (b === a ? 0 : (v - a) / (b - a));
|
|
13
|
+
export const remap = (v, a, b, c, d, doClamp = true) => lerp(c, d, doClamp ? clamp(invlerp(a, b, v)) : invlerp(a, b, v));
|
|
14
|
+
export const smoothstep = (e0, e1, x) => { const p = clamp((x - e0) / (e1 - e0)); return p * p * (3 - 2 * p); };
|
|
15
|
+
export const fract = (x) => x - Math.floor(x);
|
|
16
|
+
export const TAU = Math.PI * 2;
|
|
17
|
+
|
|
18
|
+
/** 0..1 progress of t through [start, end], eased. */
|
|
19
|
+
export const progress = (t, start, end, easing = ease.linear) => easing(clamp(invlerp(start, end, t)));
|
|
20
|
+
|
|
21
|
+
/** Progress for item i of a staggered group. */
|
|
22
|
+
export const stagger = (t, i, { start = 0, each = 0.08, duration = 0.8, easing } = {}) =>
|
|
23
|
+
progress(t, start + i * each, start + i * each + duration, easing);
|
|
24
|
+
|
|
25
|
+
/** 0..1 phase of t in a cycle of `period` seconds. */
|
|
26
|
+
export const phase = (t, period) => fract(t / period);
|
|
27
|
+
/** Sine oscillator in -1..1. Use period = duration / n for perfect loops. */
|
|
28
|
+
export const osc = (t, period, offset = 0) => Math.sin(TAU * (t / period + offset));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Keyframes: keys(t, [[0, 0], [2, 1, ease.outExpo], [5, '#ff0044']])
|
|
32
|
+
* Values: numbers, arrays of numbers, or hex colors (mixed in OKLab).
|
|
33
|
+
* The easing on a key applies to the segment arriving at that key.
|
|
34
|
+
*/
|
|
35
|
+
export function keys(t, frames) {
|
|
36
|
+
if (t <= frames[0][0]) return frames[0][1];
|
|
37
|
+
for (let i = 1; i < frames.length; i++) {
|
|
38
|
+
const [t1, v1, e = ease.inOutCubic] = frames[i];
|
|
39
|
+
const [t0, v0] = frames[i - 1];
|
|
40
|
+
if (t <= t1) return mix(v0, v1, e(invlerp(t0, t1, t)));
|
|
41
|
+
}
|
|
42
|
+
return frames[frames.length - 1][1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function mix(a, b, p) {
|
|
46
|
+
if (typeof a === 'number') return lerp(a, b, p);
|
|
47
|
+
if (Array.isArray(a)) return a.map((v, i) => mix(v, b[i], p));
|
|
48
|
+
if (typeof a === 'string') return mixColor(a, b, p);
|
|
49
|
+
return p < 0.5 ? a : b;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const c1 = 1.70158, c3 = c1 + 1;
|
|
53
|
+
export const ease = {
|
|
54
|
+
linear: (x) => x,
|
|
55
|
+
inSine: (x) => 1 - Math.cos((x * Math.PI) / 2),
|
|
56
|
+
outSine: (x) => Math.sin((x * Math.PI) / 2),
|
|
57
|
+
inOutSine: (x) => -(Math.cos(Math.PI * x) - 1) / 2,
|
|
58
|
+
inQuad: (x) => x * x,
|
|
59
|
+
outQuad: (x) => 1 - (1 - x) ** 2,
|
|
60
|
+
inOutQuad: (x) => (x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2),
|
|
61
|
+
inCubic: (x) => x ** 3,
|
|
62
|
+
outCubic: (x) => 1 - (1 - x) ** 3,
|
|
63
|
+
inOutCubic: (x) => (x < 0.5 ? 4 * x ** 3 : 1 - (-2 * x + 2) ** 3 / 2),
|
|
64
|
+
inQuart: (x) => x ** 4,
|
|
65
|
+
outQuart: (x) => 1 - (1 - x) ** 4,
|
|
66
|
+
inOutQuart: (x) => (x < 0.5 ? 8 * x ** 4 : 1 - (-2 * x + 2) ** 4 / 2),
|
|
67
|
+
inQuint: (x) => x ** 5,
|
|
68
|
+
outQuint: (x) => 1 - (1 - x) ** 5,
|
|
69
|
+
inOutQuint: (x) => (x < 0.5 ? 16 * x ** 5 : 1 - (-2 * x + 2) ** 5 / 2),
|
|
70
|
+
inExpo: (x) => (x === 0 ? 0 : 2 ** (10 * x - 10)),
|
|
71
|
+
outExpo: (x) => (x === 1 ? 1 : 1 - 2 ** (-10 * x)),
|
|
72
|
+
inOutExpo: (x) => (x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? 2 ** (20 * x - 10) / 2 : (2 - 2 ** (-20 * x + 10)) / 2),
|
|
73
|
+
inCirc: (x) => 1 - Math.sqrt(1 - x * x),
|
|
74
|
+
outCirc: (x) => Math.sqrt(1 - (x - 1) ** 2),
|
|
75
|
+
inOutCirc: (x) => (x < 0.5 ? (1 - Math.sqrt(1 - (2 * x) ** 2)) / 2 : (Math.sqrt(1 - (-2 * x + 2) ** 2) + 1) / 2),
|
|
76
|
+
outBack: (x) => 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2,
|
|
77
|
+
inOutBack: (x) => { const c2 = c1 * 1.525; return x < 0.5 ? ((2 * x) ** 2 * ((c2 + 1) * 2 * x - c2)) / 2 : ((2 * x - 2) ** 2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; },
|
|
78
|
+
/** CSS-style cubic-bezier(x1, y1, x2, y2). */
|
|
79
|
+
bezier(x1, y1, x2, y2) {
|
|
80
|
+
const A = (a, b) => 1 - 3 * b + 3 * a, B = (a, b) => 3 * b - 6 * a, C = (a) => 3 * a;
|
|
81
|
+
const calc = (t, a, b) => ((A(a, b) * t + B(a, b)) * t + C(a)) * t;
|
|
82
|
+
const slope = (t, a, b) => 3 * A(a, b) * t * t + 2 * B(a, b) * t + C(a);
|
|
83
|
+
return (x) => {
|
|
84
|
+
if (x <= 0 || x >= 1) return x;
|
|
85
|
+
let t = x;
|
|
86
|
+
for (let i = 0; i < 8; i++) { const s = slope(t, x1, x2); if (Math.abs(s) < 1e-6) break; t -= (calc(t, x1, x2) - x) / s; }
|
|
87
|
+
return calc(clamp(t), y1, y2);
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// After Effects-style curves (graph editor feel)
|
|
93
|
+
ease.easy = ease.bezier(0.33, 0, 0.67, 1); // AE "Easy Ease" (33% influence)
|
|
94
|
+
ease.smooth = ease.bezier(0.65, 0, 0.35, 1); // stronger in/out, motion-design default
|
|
95
|
+
ease.snappy = ease.bezier(0.16, 1, 0.3, 1); // fast start, long soft landing (≈ expo out)
|
|
96
|
+
ease.anticipate = ease.bezier(0.36, -0.35, 0.3, 1); // tiny pull-back before moving
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Physical spring, analytic (deterministic). Returns 0→1 with overshoot for
|
|
100
|
+
* seconds `dt` since the spring was released (negative dt → 0).
|
|
101
|
+
* Lower damping = more bounce. stiffness 170 / damping 26 is a crisp UI spring.
|
|
102
|
+
*/
|
|
103
|
+
export function spring(dt, { stiffness = 170, damping = 20, mass = 1 } = {}) {
|
|
104
|
+
if (dt <= 0) return 0;
|
|
105
|
+
const w0 = Math.sqrt(stiffness / mass), zeta = damping / (2 * Math.sqrt(stiffness * mass));
|
|
106
|
+
if (zeta < 1) {
|
|
107
|
+
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
108
|
+
return 1 - Math.exp(-zeta * w0 * dt) * (Math.cos(wd * dt) + ((zeta * w0) / wd) * Math.sin(wd * dt));
|
|
109
|
+
}
|
|
110
|
+
return 1 - Math.exp(-w0 * dt) * (1 + w0 * dt);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ------------------------------------------------------------------ */
|
|
114
|
+
/* Randomness and noise (seeded, deterministic) */
|
|
115
|
+
/* ------------------------------------------------------------------ */
|
|
116
|
+
|
|
117
|
+
export function rng(seed = 1) {
|
|
118
|
+
let a = typeof seed === 'string' ? [...seed].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619), 2166136261) : seed >>> 0;
|
|
119
|
+
const next = () => {
|
|
120
|
+
a |= 0; a = (a + 0x6d2b79f5) | 0;
|
|
121
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
122
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
123
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
next,
|
|
127
|
+
range: (lo, hi) => lo + (hi - lo) * next(),
|
|
128
|
+
int: (lo, hi) => Math.floor(lo + (hi - lo + 1) * next()),
|
|
129
|
+
pick: (arr) => arr[Math.floor(next() * arr.length)],
|
|
130
|
+
sign: () => (next() < 0.5 ? -1 : 1),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hash3 = (x, y, z) => {
|
|
135
|
+
let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(z, 2147483647);
|
|
136
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
137
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967295;
|
|
138
|
+
};
|
|
139
|
+
/** Smooth 3D value noise in 0..1. noise(x), noise(x, y), noise(x, y, z). */
|
|
140
|
+
export function noise(x, y = 0, z = 0) {
|
|
141
|
+
const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
|
|
142
|
+
const xf = x - xi, yf = y - yi, zf = z - zi;
|
|
143
|
+
const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf), w = zf * zf * (3 - 2 * zf);
|
|
144
|
+
const n = (a, b, c) => hash3(xi + a, yi + b, zi + c);
|
|
145
|
+
const x00 = lerp(n(0, 0, 0), n(1, 0, 0), u), x10 = lerp(n(0, 1, 0), n(1, 1, 0), u);
|
|
146
|
+
const x01 = lerp(n(0, 0, 1), n(1, 0, 1), u), x11 = lerp(n(0, 1, 1), n(1, 1, 1), u);
|
|
147
|
+
return lerp(lerp(x00, x10, v), lerp(x01, x11, v), w);
|
|
148
|
+
}
|
|
149
|
+
/** Fractal noise (octaves of value noise), 0..1. */
|
|
150
|
+
export function fbm(x, y = 0, z = 0, octaves = 4) {
|
|
151
|
+
let s = 0, amp = 0.5, f = 1, norm = 0;
|
|
152
|
+
for (let i = 0; i < octaves; i++) { s += amp * noise(x * f, y * f, z * f); norm += amp; amp *= 0.5; f *= 2; }
|
|
153
|
+
return s / norm;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ------------------------------------------------------------------ */
|
|
157
|
+
/* Color (OKLab mixing) */
|
|
158
|
+
/* ------------------------------------------------------------------ */
|
|
159
|
+
|
|
160
|
+
function hexToRgb(hex) {
|
|
161
|
+
let h = hex.replace('#', '');
|
|
162
|
+
if (h.length <= 4) h = [...h].map((c) => c + c).join('');
|
|
163
|
+
const n = parseInt(h.slice(0, 6), 16);
|
|
164
|
+
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
|
165
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, a];
|
|
166
|
+
}
|
|
167
|
+
const toLin = (c) => { c /= 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
|
|
168
|
+
const toSrgb = (c) => Math.round(255 * clamp(c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055));
|
|
169
|
+
function rgbToOklab([r, g, b]) {
|
|
170
|
+
r = toLin(r); g = toLin(g); b = toLin(b);
|
|
171
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
172
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
173
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
174
|
+
return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
|
|
175
|
+
}
|
|
176
|
+
function oklabToRgb([L, A, B]) {
|
|
177
|
+
const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;
|
|
178
|
+
const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;
|
|
179
|
+
const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;
|
|
180
|
+
return [toSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), toSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), toSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)];
|
|
181
|
+
}
|
|
182
|
+
/** Perceptual color mix of two hex colors -> hex. */
|
|
183
|
+
export function mixColor(a, b, p) {
|
|
184
|
+
const A = hexToRgb(a), B = hexToRgb(b);
|
|
185
|
+
const [r, g, bl] = oklabToRgb(mix(rgbToOklab(A), rgbToOklab(B), p));
|
|
186
|
+
const al = lerp(A[3], B[3], p);
|
|
187
|
+
const hx = (n) => n.toString(16).padStart(2, '0');
|
|
188
|
+
return `#${hx(r)}${hx(g)}${hx(bl)}${al < 1 ? hx(Math.round(al * 255)) : ''}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ------------------------------------------------------------------ */
|
|
192
|
+
/* Typography */
|
|
193
|
+
/* ------------------------------------------------------------------ */
|
|
194
|
+
|
|
195
|
+
/** Wrap each word or char of an element in spans for staggered motion. Returns the spans. */
|
|
196
|
+
export function split(el, by = 'chars') {
|
|
197
|
+
el = resolveEl(el);
|
|
198
|
+
const text = el.textContent;
|
|
199
|
+
el.textContent = '';
|
|
200
|
+
el.setAttribute('aria-label', text);
|
|
201
|
+
const out = [];
|
|
202
|
+
const words = text.split(/(\s+)/);
|
|
203
|
+
for (const w of words) {
|
|
204
|
+
if (/^\s+$/.test(w)) { el.appendChild(document.createTextNode(w)); continue; }
|
|
205
|
+
const ws = document.createElement('span');
|
|
206
|
+
ws.style.display = 'inline-block';
|
|
207
|
+
ws.style.whiteSpace = 'pre';
|
|
208
|
+
if (by === 'words') { ws.textContent = w; out.push(ws); }
|
|
209
|
+
else for (const ch of w) { const c = document.createElement('span'); c.style.display = 'inline-block'; c.textContent = ch; ws.appendChild(c); out.push(c); }
|
|
210
|
+
el.appendChild(ws);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ------------------------------------------------------------------ */
|
|
216
|
+
/* Shapes */
|
|
217
|
+
/* ------------------------------------------------------------------ */
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* SVG path `d` for a hand-cut rounded rectangle: the "organic" shape language.
|
|
221
|
+
* Deterministic for a given seed, so a shape keeps its edges across frames and renders.
|
|
222
|
+
* @param {number} w,h size in px
|
|
223
|
+
* @param {object} [o]
|
|
224
|
+
* @param {number} [o.radius] corner radius in px (default 24)
|
|
225
|
+
* @param {number} [o.wobble] edge irregularity in px (default 0.6% of the short side; keep it low)
|
|
226
|
+
* @param {number} [o.seed] changes the irregularity, not its amount
|
|
227
|
+
*/
|
|
228
|
+
export function organicPath(w, h, { radius = 24, wobble, seed = 1 } = {}) {
|
|
229
|
+
const amp = wobble ?? Math.min(w, h) * 0.014;
|
|
230
|
+
// draw on a box inset by the wobble, so the shape never leaves w×h (and never gets clipped flat)
|
|
231
|
+
const W0 = w, H0 = h; w -= amp * 2; h -= amp * 2;
|
|
232
|
+
const r = Math.min(radius, w / 2, h / 2);
|
|
233
|
+
const per = 2 * (w + h - 4 * r) + 2 * Math.PI * r;
|
|
234
|
+
const n = Math.max(16, Math.round(per / 40));
|
|
235
|
+
const pt = (d) => {
|
|
236
|
+
// walk the rounded-rect perimeter clockwise from the top-left corner's end
|
|
237
|
+
const segs = [[w - 2 * r, 'top'], [Math.PI * r / 2, 'tr'], [h - 2 * r, 'right'], [Math.PI * r / 2, 'br'], [w - 2 * r, 'bottom'], [Math.PI * r / 2, 'bl'], [h - 2 * r, 'left'], [Math.PI * r / 2, 'tl']];
|
|
238
|
+
for (const [len, k] of segs) {
|
|
239
|
+
if (d > len) { d -= len; continue; }
|
|
240
|
+
const a = r ? d / r : 0;
|
|
241
|
+
switch (k) {
|
|
242
|
+
case 'top': return [r + d, 0, 0, -1];
|
|
243
|
+
case 'tr': return [w - r + r * Math.sin(a), r - r * Math.cos(a), Math.sin(a), -Math.cos(a)];
|
|
244
|
+
case 'right': return [w, r + d, 1, 0];
|
|
245
|
+
case 'br': return [w - r + r * Math.cos(a), h - r + r * Math.sin(a), Math.cos(a), Math.sin(a)];
|
|
246
|
+
case 'bottom': return [w - r - d, h, 0, 1];
|
|
247
|
+
case 'bl': return [r - r * Math.sin(a), h - r + r * Math.cos(a), -Math.sin(a), Math.cos(a)];
|
|
248
|
+
case 'left': return [0, h - r - d, -1, 0];
|
|
249
|
+
default: return [r - r * Math.cos(a), r - r * Math.sin(a), -Math.cos(a), -Math.sin(a)];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return [r, 0, 0, -1];
|
|
253
|
+
};
|
|
254
|
+
const P = Array.from({ length: n }, (_, i) => {
|
|
255
|
+
const [x, y, nx, ny] = pt((i / n) * per);
|
|
256
|
+
const o = amp * (noise(i * 0.55, seed * 7.13) * 2 - 1);
|
|
257
|
+
return [x + nx * o + amp, y + ny * o + amp];
|
|
258
|
+
});
|
|
259
|
+
void W0; void H0;
|
|
260
|
+
// closed Catmull-Rom through the points, as cubic Béziers
|
|
261
|
+
let d = `M${P[0][0].toFixed(2)},${P[0][1].toFixed(2)}`;
|
|
262
|
+
for (let i = 0; i < n; i++) {
|
|
263
|
+
const p0 = P[(i - 1 + n) % n], p1 = P[i], p2 = P[(i + 1) % n], p3 = P[(i + 2) % n];
|
|
264
|
+
const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
|
|
265
|
+
const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
|
|
266
|
+
d += `C${c1[0].toFixed(2)},${c1[1].toFixed(2)} ${c2[0].toFixed(2)},${c2[1].toFixed(2)} ${p2[0].toFixed(2)},${p2[1].toFixed(2)}`;
|
|
267
|
+
}
|
|
268
|
+
return d + 'Z';
|
|
269
|
+
}
|
package/runtime/index.js
CHANGED
|
@@ -323,269 +323,12 @@ function child(parent) {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
/* ------------------------------------------------------------------ */
|
|
326
|
-
/* Math, timing, easing */
|
|
327
326
|
/* ------------------------------------------------------------------ */
|
|
328
|
-
|
|
329
|
-
export const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
|
|
330
|
-
export const lerp = (a, b, p) => a + (b - a) * p;
|
|
331
|
-
export const invlerp = (a, b, v) => (b === a ? 0 : (v - a) / (b - a));
|
|
332
|
-
export const remap = (v, a, b, c, d, doClamp = true) => lerp(c, d, doClamp ? clamp(invlerp(a, b, v)) : invlerp(a, b, v));
|
|
333
|
-
export const smoothstep = (e0, e1, x) => { const p = clamp((x - e0) / (e1 - e0)); return p * p * (3 - 2 * p); };
|
|
334
|
-
export const fract = (x) => x - Math.floor(x);
|
|
335
|
-
export const TAU = Math.PI * 2;
|
|
336
|
-
|
|
337
|
-
/** 0..1 progress of t through [start, end], eased. */
|
|
338
|
-
export const progress = (t, start, end, easing = ease.linear) => easing(clamp(invlerp(start, end, t)));
|
|
339
|
-
|
|
340
|
-
/** Progress for item i of a staggered group. */
|
|
341
|
-
export const stagger = (t, i, { start = 0, each = 0.08, duration = 0.8, easing } = {}) =>
|
|
342
|
-
progress(t, start + i * each, start + i * each + duration, easing);
|
|
343
|
-
|
|
344
|
-
/** 0..1 phase of t in a cycle of `period` seconds. */
|
|
345
|
-
export const phase = (t, period) => fract(t / period);
|
|
346
|
-
/** Sine oscillator in -1..1. Use period = duration / n for perfect loops. */
|
|
347
|
-
export const osc = (t, period, offset = 0) => Math.sin(TAU * (t / period + offset));
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Keyframes: keys(t, [[0, 0], [2, 1, ease.outExpo], [5, '#ff0044']])
|
|
351
|
-
* Values: numbers, arrays of numbers, or hex colors (mixed in OKLab).
|
|
352
|
-
* The easing on a key applies to the segment arriving at that key.
|
|
353
|
-
*/
|
|
354
|
-
export function keys(t, frames) {
|
|
355
|
-
if (t <= frames[0][0]) return frames[0][1];
|
|
356
|
-
for (let i = 1; i < frames.length; i++) {
|
|
357
|
-
const [t1, v1, e = ease.inOutCubic] = frames[i];
|
|
358
|
-
const [t0, v0] = frames[i - 1];
|
|
359
|
-
if (t <= t1) return mix(v0, v1, e(invlerp(t0, t1, t)));
|
|
360
|
-
}
|
|
361
|
-
return frames[frames.length - 1][1];
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function mix(a, b, p) {
|
|
365
|
-
if (typeof a === 'number') return lerp(a, b, p);
|
|
366
|
-
if (Array.isArray(a)) return a.map((v, i) => mix(v, b[i], p));
|
|
367
|
-
if (typeof a === 'string') return mixColor(a, b, p);
|
|
368
|
-
return p < 0.5 ? a : b;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const c1 = 1.70158, c3 = c1 + 1;
|
|
372
|
-
export const ease = {
|
|
373
|
-
linear: (x) => x,
|
|
374
|
-
inSine: (x) => 1 - Math.cos((x * Math.PI) / 2),
|
|
375
|
-
outSine: (x) => Math.sin((x * Math.PI) / 2),
|
|
376
|
-
inOutSine: (x) => -(Math.cos(Math.PI * x) - 1) / 2,
|
|
377
|
-
inQuad: (x) => x * x,
|
|
378
|
-
outQuad: (x) => 1 - (1 - x) ** 2,
|
|
379
|
-
inOutQuad: (x) => (x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2),
|
|
380
|
-
inCubic: (x) => x ** 3,
|
|
381
|
-
outCubic: (x) => 1 - (1 - x) ** 3,
|
|
382
|
-
inOutCubic: (x) => (x < 0.5 ? 4 * x ** 3 : 1 - (-2 * x + 2) ** 3 / 2),
|
|
383
|
-
inQuart: (x) => x ** 4,
|
|
384
|
-
outQuart: (x) => 1 - (1 - x) ** 4,
|
|
385
|
-
inOutQuart: (x) => (x < 0.5 ? 8 * x ** 4 : 1 - (-2 * x + 2) ** 4 / 2),
|
|
386
|
-
inQuint: (x) => x ** 5,
|
|
387
|
-
outQuint: (x) => 1 - (1 - x) ** 5,
|
|
388
|
-
inOutQuint: (x) => (x < 0.5 ? 16 * x ** 5 : 1 - (-2 * x + 2) ** 5 / 2),
|
|
389
|
-
inExpo: (x) => (x === 0 ? 0 : 2 ** (10 * x - 10)),
|
|
390
|
-
outExpo: (x) => (x === 1 ? 1 : 1 - 2 ** (-10 * x)),
|
|
391
|
-
inOutExpo: (x) => (x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? 2 ** (20 * x - 10) / 2 : (2 - 2 ** (-20 * x + 10)) / 2),
|
|
392
|
-
inCirc: (x) => 1 - Math.sqrt(1 - x * x),
|
|
393
|
-
outCirc: (x) => Math.sqrt(1 - (x - 1) ** 2),
|
|
394
|
-
inOutCirc: (x) => (x < 0.5 ? (1 - Math.sqrt(1 - (2 * x) ** 2)) / 2 : (Math.sqrt(1 - (-2 * x + 2) ** 2) + 1) / 2),
|
|
395
|
-
outBack: (x) => 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2,
|
|
396
|
-
inOutBack: (x) => { const c2 = c1 * 1.525; return x < 0.5 ? ((2 * x) ** 2 * ((c2 + 1) * 2 * x - c2)) / 2 : ((2 * x - 2) ** 2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; },
|
|
397
|
-
/** CSS-style cubic-bezier(x1, y1, x2, y2). */
|
|
398
|
-
bezier(x1, y1, x2, y2) {
|
|
399
|
-
const A = (a, b) => 1 - 3 * b + 3 * a, B = (a, b) => 3 * b - 6 * a, C = (a) => 3 * a;
|
|
400
|
-
const calc = (t, a, b) => ((A(a, b) * t + B(a, b)) * t + C(a)) * t;
|
|
401
|
-
const slope = (t, a, b) => 3 * A(a, b) * t * t + 2 * B(a, b) * t + C(a);
|
|
402
|
-
return (x) => {
|
|
403
|
-
if (x <= 0 || x >= 1) return x;
|
|
404
|
-
let t = x;
|
|
405
|
-
for (let i = 0; i < 8; i++) { const s = slope(t, x1, x2); if (Math.abs(s) < 1e-6) break; t -= (calc(t, x1, x2) - x) / s; }
|
|
406
|
-
return calc(clamp(t), y1, y2);
|
|
407
|
-
};
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
// After Effects-style curves (graph editor feel)
|
|
412
|
-
ease.easy = ease.bezier(0.33, 0, 0.67, 1); // AE "Easy Ease" (33% influence)
|
|
413
|
-
ease.smooth = ease.bezier(0.65, 0, 0.35, 1); // stronger in/out, motion-design default
|
|
414
|
-
ease.snappy = ease.bezier(0.16, 1, 0.3, 1); // fast start, long soft landing (≈ expo out)
|
|
415
|
-
ease.anticipate = ease.bezier(0.36, -0.35, 0.3, 1); // tiny pull-back before moving
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Physical spring, analytic (deterministic). Returns 0→1 with overshoot for
|
|
419
|
-
* seconds `dt` since the spring was released (negative dt → 0).
|
|
420
|
-
* Lower damping = more bounce. stiffness 170 / damping 26 is a crisp UI spring.
|
|
421
|
-
*/
|
|
422
|
-
export function spring(dt, { stiffness = 170, damping = 20, mass = 1 } = {}) {
|
|
423
|
-
if (dt <= 0) return 0;
|
|
424
|
-
const w0 = Math.sqrt(stiffness / mass), zeta = damping / (2 * Math.sqrt(stiffness * mass));
|
|
425
|
-
if (zeta < 1) {
|
|
426
|
-
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
427
|
-
return 1 - Math.exp(-zeta * w0 * dt) * (Math.cos(wd * dt) + ((zeta * w0) / wd) * Math.sin(wd * dt));
|
|
428
|
-
}
|
|
429
|
-
return 1 - Math.exp(-w0 * dt) * (1 + w0 * dt);
|
|
430
|
-
}
|
|
431
|
-
|
|
327
|
+
/* Helpers (core.js: math, easing, springs, noise, color, text, shapes) */
|
|
432
328
|
/* ------------------------------------------------------------------ */
|
|
433
|
-
/* Randomness and noise (seeded, deterministic) */
|
|
434
|
-
/* ------------------------------------------------------------------ */
|
|
435
|
-
|
|
436
|
-
export function rng(seed = 1) {
|
|
437
|
-
let a = typeof seed === 'string' ? [...seed].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619), 2166136261) : seed >>> 0;
|
|
438
|
-
const next = () => {
|
|
439
|
-
a |= 0; a = (a + 0x6d2b79f5) | 0;
|
|
440
|
-
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
441
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
442
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
443
|
-
};
|
|
444
|
-
return {
|
|
445
|
-
next,
|
|
446
|
-
range: (lo, hi) => lo + (hi - lo) * next(),
|
|
447
|
-
int: (lo, hi) => Math.floor(lo + (hi - lo + 1) * next()),
|
|
448
|
-
pick: (arr) => arr[Math.floor(next() * arr.length)],
|
|
449
|
-
sign: () => (next() < 0.5 ? -1 : 1),
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const hash3 = (x, y, z) => {
|
|
454
|
-
let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(z, 2147483647);
|
|
455
|
-
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
456
|
-
return ((h ^ (h >>> 16)) >>> 0) / 4294967295;
|
|
457
|
-
};
|
|
458
|
-
/** Smooth 3D value noise in 0..1. noise(x), noise(x, y), noise(x, y, z). */
|
|
459
|
-
export function noise(x, y = 0, z = 0) {
|
|
460
|
-
const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
|
|
461
|
-
const xf = x - xi, yf = y - yi, zf = z - zi;
|
|
462
|
-
const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf), w = zf * zf * (3 - 2 * zf);
|
|
463
|
-
const n = (a, b, c) => hash3(xi + a, yi + b, zi + c);
|
|
464
|
-
const x00 = lerp(n(0, 0, 0), n(1, 0, 0), u), x10 = lerp(n(0, 1, 0), n(1, 1, 0), u);
|
|
465
|
-
const x01 = lerp(n(0, 0, 1), n(1, 0, 1), u), x11 = lerp(n(0, 1, 1), n(1, 1, 1), u);
|
|
466
|
-
return lerp(lerp(x00, x10, v), lerp(x01, x11, v), w);
|
|
467
|
-
}
|
|
468
|
-
/** Fractal noise (octaves of value noise), 0..1. */
|
|
469
|
-
export function fbm(x, y = 0, z = 0, octaves = 4) {
|
|
470
|
-
let s = 0, amp = 0.5, f = 1, norm = 0;
|
|
471
|
-
for (let i = 0; i < octaves; i++) { s += amp * noise(x * f, y * f, z * f); norm += amp; amp *= 0.5; f *= 2; }
|
|
472
|
-
return s / norm;
|
|
473
|
-
}
|
|
474
329
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
/* ------------------------------------------------------------------ */
|
|
478
|
-
|
|
479
|
-
function hexToRgb(hex) {
|
|
480
|
-
let h = hex.replace('#', '');
|
|
481
|
-
if (h.length <= 4) h = [...h].map((c) => c + c).join('');
|
|
482
|
-
const n = parseInt(h.slice(0, 6), 16);
|
|
483
|
-
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
|
484
|
-
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, a];
|
|
485
|
-
}
|
|
486
|
-
const toLin = (c) => { c /= 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
|
|
487
|
-
const toSrgb = (c) => Math.round(255 * clamp(c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055));
|
|
488
|
-
function rgbToOklab([r, g, b]) {
|
|
489
|
-
r = toLin(r); g = toLin(g); b = toLin(b);
|
|
490
|
-
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
491
|
-
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
492
|
-
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
493
|
-
return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
|
|
494
|
-
}
|
|
495
|
-
function oklabToRgb([L, A, B]) {
|
|
496
|
-
const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;
|
|
497
|
-
const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;
|
|
498
|
-
const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;
|
|
499
|
-
return [toSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), toSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), toSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)];
|
|
500
|
-
}
|
|
501
|
-
/** Perceptual color mix of two hex colors -> hex. */
|
|
502
|
-
export function mixColor(a, b, p) {
|
|
503
|
-
const A = hexToRgb(a), B = hexToRgb(b);
|
|
504
|
-
const [r, g, bl] = oklabToRgb(mix(rgbToOklab(A), rgbToOklab(B), p));
|
|
505
|
-
const al = lerp(A[3], B[3], p);
|
|
506
|
-
const hx = (n) => n.toString(16).padStart(2, '0');
|
|
507
|
-
return `#${hx(r)}${hx(g)}${hx(bl)}${al < 1 ? hx(Math.round(al * 255)) : ''}`;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/* ------------------------------------------------------------------ */
|
|
511
|
-
/* Typography */
|
|
512
|
-
/* ------------------------------------------------------------------ */
|
|
513
|
-
|
|
514
|
-
/** Wrap each word or char of an element in spans for staggered motion. Returns the spans. */
|
|
515
|
-
export function split(el, by = 'chars') {
|
|
516
|
-
el = resolveEl(el);
|
|
517
|
-
const text = el.textContent;
|
|
518
|
-
el.textContent = '';
|
|
519
|
-
el.setAttribute('aria-label', text);
|
|
520
|
-
const out = [];
|
|
521
|
-
const words = text.split(/(\s+)/);
|
|
522
|
-
for (const w of words) {
|
|
523
|
-
if (/^\s+$/.test(w)) { el.appendChild(document.createTextNode(w)); continue; }
|
|
524
|
-
const ws = document.createElement('span');
|
|
525
|
-
ws.style.display = 'inline-block';
|
|
526
|
-
ws.style.whiteSpace = 'pre';
|
|
527
|
-
if (by === 'words') { ws.textContent = w; out.push(ws); }
|
|
528
|
-
else for (const ch of w) { const c = document.createElement('span'); c.style.display = 'inline-block'; c.textContent = ch; ws.appendChild(c); out.push(c); }
|
|
529
|
-
el.appendChild(ws);
|
|
530
|
-
}
|
|
531
|
-
return out;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/* ------------------------------------------------------------------ */
|
|
535
|
-
/* Shapes */
|
|
536
|
-
/* ------------------------------------------------------------------ */
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* SVG path `d` for a hand-cut rounded rectangle: the "organic" shape language.
|
|
540
|
-
* Deterministic for a given seed, so a shape keeps its edges across frames and renders.
|
|
541
|
-
* @param {number} w,h size in px
|
|
542
|
-
* @param {object} [o]
|
|
543
|
-
* @param {number} [o.radius] corner radius in px (default 24)
|
|
544
|
-
* @param {number} [o.wobble] edge irregularity in px (default 0.6% of the short side; keep it low)
|
|
545
|
-
* @param {number} [o.seed] changes the irregularity, not its amount
|
|
546
|
-
*/
|
|
547
|
-
export function organicPath(w, h, { radius = 24, wobble, seed = 1 } = {}) {
|
|
548
|
-
const amp = wobble ?? Math.min(w, h) * 0.014;
|
|
549
|
-
// draw on a box inset by the wobble, so the shape never leaves w×h (and never gets clipped flat)
|
|
550
|
-
const W0 = w, H0 = h; w -= amp * 2; h -= amp * 2;
|
|
551
|
-
const r = Math.min(radius, w / 2, h / 2);
|
|
552
|
-
const per = 2 * (w + h - 4 * r) + 2 * Math.PI * r;
|
|
553
|
-
const n = Math.max(16, Math.round(per / 40));
|
|
554
|
-
const pt = (d) => {
|
|
555
|
-
// walk the rounded-rect perimeter clockwise from the top-left corner's end
|
|
556
|
-
const segs = [[w - 2 * r, 'top'], [Math.PI * r / 2, 'tr'], [h - 2 * r, 'right'], [Math.PI * r / 2, 'br'], [w - 2 * r, 'bottom'], [Math.PI * r / 2, 'bl'], [h - 2 * r, 'left'], [Math.PI * r / 2, 'tl']];
|
|
557
|
-
for (const [len, k] of segs) {
|
|
558
|
-
if (d > len) { d -= len; continue; }
|
|
559
|
-
const a = r ? d / r : 0;
|
|
560
|
-
switch (k) {
|
|
561
|
-
case 'top': return [r + d, 0, 0, -1];
|
|
562
|
-
case 'tr': return [w - r + r * Math.sin(a), r - r * Math.cos(a), Math.sin(a), -Math.cos(a)];
|
|
563
|
-
case 'right': return [w, r + d, 1, 0];
|
|
564
|
-
case 'br': return [w - r + r * Math.cos(a), h - r + r * Math.sin(a), Math.cos(a), Math.sin(a)];
|
|
565
|
-
case 'bottom': return [w - r - d, h, 0, 1];
|
|
566
|
-
case 'bl': return [r - r * Math.sin(a), h - r + r * Math.cos(a), -Math.sin(a), Math.cos(a)];
|
|
567
|
-
case 'left': return [0, h - r - d, -1, 0];
|
|
568
|
-
default: return [r - r * Math.cos(a), r - r * Math.sin(a), -Math.cos(a), -Math.sin(a)];
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return [r, 0, 0, -1];
|
|
572
|
-
};
|
|
573
|
-
const P = Array.from({ length: n }, (_, i) => {
|
|
574
|
-
const [x, y, nx, ny] = pt((i / n) * per);
|
|
575
|
-
const o = amp * (noise(i * 0.55, seed * 7.13) * 2 - 1);
|
|
576
|
-
return [x + nx * o + amp, y + ny * o + amp];
|
|
577
|
-
});
|
|
578
|
-
void W0; void H0;
|
|
579
|
-
// closed Catmull-Rom through the points, as cubic Béziers
|
|
580
|
-
let d = `M${P[0][0].toFixed(2)},${P[0][1].toFixed(2)}`;
|
|
581
|
-
for (let i = 0; i < n; i++) {
|
|
582
|
-
const p0 = P[(i - 1 + n) % n], p1 = P[i], p2 = P[(i + 1) % n], p3 = P[(i + 2) % n];
|
|
583
|
-
const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
|
|
584
|
-
const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
|
|
585
|
-
d += `C${c1[0].toFixed(2)},${c1[1].toFixed(2)} ${c2[0].toFixed(2)},${c2[1].toFixed(2)} ${p2[0].toFixed(2)},${p2[1].toFixed(2)}`;
|
|
586
|
-
}
|
|
587
|
-
return d + 'Z';
|
|
588
|
-
}
|
|
330
|
+
export * from './core.js';
|
|
331
|
+
import { clamp, lerp, invlerp, progress, ease, keys, mix, mixColor, spring, rng, noise, split } from './core.js';
|
|
589
332
|
|
|
590
333
|
/* ------------------------------------------------------------------ */
|
|
591
334
|
/* Live shaders (web pages, not scenes) */
|