paper-motion 0.0.0-stage → 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 +59 -2
- package/bin/pamo.mjs +664 -0
- package/examples/breath/brief.md +11 -0
- package/examples/breath/poster.jpg +0 -0
- package/examples/breath/scene.html +59 -0
- package/examples/chrome-endcard/brief.md +7 -0
- package/examples/chrome-endcard/fonts/GeistMono-Variable.woff2 +0 -0
- package/examples/chrome-endcard/fonts/GeistSerif-Regular.woff2 +0 -0
- package/examples/chrome-endcard/poster.jpg +0 -0
- package/examples/chrome-endcard/scene.html +68 -0
- package/examples/chrome-endcard/wordmark.js +16 -0
- 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/install-loop/brief.md +7 -0
- package/examples/install-loop/fonts/GeistMono-Variable.woff2 +0 -0
- package/examples/install-loop/fonts/GeistSerif-Regular.woff2 +0 -0
- package/examples/install-loop/poster.jpg +0 -0
- package/examples/install-loop/scene.html +46 -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/examples/signal/brief.md +10 -0
- package/examples/signal/poster.jpg +0 -0
- package/examples/signal/scene.html +50 -0
- package/lib/board.mjs +95 -0
- package/lib/browser.mjs +88 -0
- package/lib/catalog.mjs +77 -0
- package/lib/customize.mjs +162 -0
- package/lib/keys.mjs +93 -0
- package/lib/live-board.js +128 -0
- package/lib/modules.mjs +23 -0
- package/lib/react-hooks.mjs +9 -0
- package/lib/registry.mjs +344 -0
- package/lib/render.mjs +193 -0
- package/lib/server.mjs +89 -0
- package/lib/sfx.mjs +287 -0
- package/lib/shaderset.mjs +65 -0
- package/lib/wordmark.mjs +64 -0
- package/package.json +46 -4
- package/runtime/brand.js +71 -0
- package/runtime/core.js +269 -0
- package/runtime/index.js +349 -0
- package/runtime/jsx-shim.js +3 -0
- 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 +94 -0
- package/runtime/player.js +69 -0
- package/runtime/react-shim.js +15 -0
- package/templates/blank/brief.md +39 -0
- package/templates/blank/scene.html +44 -0
- package/templates/brand-sheet/index.html +241 -0
- package/templates/move-demo/brief.md +4 -0
- package/templates/move-demo/scene.html +158 -0
- package/templates/piece/brief.md +39 -0
- package/templates/piece/scene.html +62 -0
- package/templates/shader-set/shot.js +47 -0
- package/templates/still/brief.md +3 -0
- package/templates/still/scene.html +56 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ease, progress } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** Letters settle from wide spacing (`from`, em) to `to`. */
|
|
4
|
+
export function trackIn(el, t, { start = 0, duration = 1.0, from = 0.4, to = 0, curve = ease.outExpo } = {}) {
|
|
5
|
+
const q = curve(progress(t, start, start + duration));
|
|
6
|
+
el.style.letterSpacing = `${from + (to - from) * q}em`; el.style.opacity = Math.min(1, q * 2);
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { clamp, rng } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* When each character of `text` appears, typed by a person: quick bursts inside words, a beat after spaces,
|
|
5
|
+
* longer after punctuation, the odd hesitation. Deterministic per seed. Use the same times for the text
|
|
6
|
+
* (typeOn's `times`) and the sound (keySounds), so every letter lands on its keystroke.
|
|
7
|
+
* @returns {number[]} times in seconds, one per character
|
|
8
|
+
*/
|
|
9
|
+
export function typingTimes(text, { start = 0, cps = 11, seed = 1 } = {}) {
|
|
10
|
+
const r = rng(seed).next, base = 1 / cps, out = [];
|
|
11
|
+
let t = start;
|
|
12
|
+
for (let i = 0; i < text.length; i++) {
|
|
13
|
+
const prev = text[i - 1] ?? '', ch = text[i];
|
|
14
|
+
let k = 0.7 + 0.45 * r(); // inside a word: fast, uneven
|
|
15
|
+
if (prev === ' ') k = 1.35 + 0.5 * r(); // starting a new word
|
|
16
|
+
if (/[.,;:!?]/.test(prev)) k = 2.2 + 1.2 * r(); // after punctuation
|
|
17
|
+
if (/[\/\-_@]/.test(ch) || /[\/\-_@]/.test(prev)) k *= 1.25; // symbols need a reach
|
|
18
|
+
if (r() < 0.04) k *= 2.4; // a hesitation
|
|
19
|
+
t += i === 0 ? 0 : base * k;
|
|
20
|
+
out.push(t);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Schedules one keystroke sound per character at `times` (from typingTimes), plus an optional enter. */
|
|
26
|
+
export function keySounds(s, text, times, { bank, style, gain = 0.16, seed = 1, enter = null, pan = 0.04 } = {}) {
|
|
27
|
+
[...text].forEach((ch, i) => s.sfx(times[i], 'key', { kind: ch === ' ' ? 'space' : 'letter', gain, seed: seed * 1000 + i, pan: ((i % 5) - 2) * pan, ...(bank ? { bank } : {}), ...(style ? { style } : {}) }));
|
|
28
|
+
if (enter !== null) s.sfx(enter, 'key', { kind: 'enter', gain: gain * 1.1, seed: seed * 1000 + 999, ...(bank ? { bank } : {}), ...(style ? { style } : {}) });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Types `text` from `start` at `cps` characters a second, or at explicit `times` (typingTimes); `cursor` (an element) is solid while typing, blinks at `blink` Hz after. */
|
|
32
|
+
export function typeOn(el, text, t, { start = 0, cps = 35, times = null, cursor = null, blink = 2.2 } = {}) {
|
|
33
|
+
if (times) start = times[0] ?? start;
|
|
34
|
+
const n = times ? times.filter((x) => x <= t).length : clamp(Math.floor((t - start) * cps), 0, text.length);
|
|
35
|
+
el.textContent = text.slice(0, n);
|
|
36
|
+
if (cursor) {
|
|
37
|
+
const typing = t >= start && (n < text.length || (times && t - times[times.length - 1] < 0.25));
|
|
38
|
+
cursor.style.opacity = t < start ? 0 : typing || Math.floor((t - start) * blink * 2) % 2 === 0 ? 1 : 0;
|
|
39
|
+
}
|
|
40
|
+
return n;
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ease, clamp, lerp, progress } from '../core.js';
|
|
2
|
+
import { dims, items, show, sp, settle, blur, layer, hide, resetAll } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** A's items leave, A folds into a glowing line, a glint runs along it, and B unfolds out of the line with overshoot as its items rise from it. */
|
|
5
|
+
export function unfold(a, b, p, { duration = 1.2, radius = 14 } = {}) {
|
|
6
|
+
const slot = a.parentElement, [w, h] = dims(a);
|
|
7
|
+
if (p <= 0 || p >= 1) { hide(slot, 'ufLine'); resetAll(a, b, ...items(a), ...items(b)); show(a, b, p); return; }
|
|
8
|
+
items(a).forEach((c, i) => { const l = progress(p, i * 0.03, i * 0.03 + 0.22); c.style.transform = `translateY(${-30 * ease.inCubic(l)}px)`; c.style.opacity = 1 - l; c.style.filter = blur(10 * l); });
|
|
9
|
+
const f = ease.inOutCubic(progress(p, 0.16, 0.42)), ins = (h / 2 - 1.5) * f;
|
|
10
|
+
a.style.clipPath = `inset(${ins}px 0 ${ins}px 0 round ${lerp(radius, 2, f)}px)`;
|
|
11
|
+
a.style.opacity = p < 0.44 ? 1 : 0;
|
|
12
|
+
const lp = progress(p, 0.36, 0.64), breathe = Math.sin(Math.PI * lp), g = ease.inOutCubic(lp) * 120 - 10;
|
|
13
|
+
layer(slot, 'ufLine', `display:${lp > 0 && lp < 1 ? 'block' : 'none'};left:${(w - w * (1 + 0.08 * breathe)) / 2}px;width:${w * (1 + 0.08 * breathe)}px;top:${h / 2 - 1.5}px;height:3px;z-index:4;border-radius:2px;`
|
|
14
|
+
+ `background:linear-gradient(90deg,rgba(255,255,255,.35) ${g - 14}%,#fff ${g}%,rgba(255,255,255,.35) ${g + 14}%);box-shadow:0 0 ${18 * breathe}px rgba(255,255,255,${0.6 * breathe})`);
|
|
15
|
+
const u = settle(sp(p, 0.5, duration, { stiffness: 230, damping: 16 }), p), bi = Math.max(0, (h / 2 - 1.5) * (1 - u));
|
|
16
|
+
b.style.clipPath = `inset(${bi}px 0 ${bi}px 0 round ${lerp(2, radius, clamp(u))}px)`;
|
|
17
|
+
b.style.transform = `scaleY(${1 + Math.max(0, u - 1) * 0.6})`;
|
|
18
|
+
b.style.opacity = p > 0.5 ? 1 : 0;
|
|
19
|
+
items(b).forEach((c, i) => { const q = ease.outExpo(progress(p, 0.6 + i * 0.04, 0.88 + i * 0.03)); c.style.transform = `translateY(${40 * (1 - q)}px)`; c.style.opacity = q; c.style.filter = blur(8 * (1 - q)); });
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ease, clamp, lerp, progress } from '../core.js';
|
|
2
|
+
import { dims, items, show, sp, settle, layer, hide, resetAll } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** A winds up, then whips out tilted and smeared; a light streak crosses at full speed; B whips in, overshoots and settles, its content trailing. */
|
|
5
|
+
export function whipPan(a, b, p, { duration = 0.9, dir = 'left', tilt = 7, travel = 1.35 } = {}) {
|
|
6
|
+
const slot = a.parentElement, [w] = dims(a), s = dir === 'left' ? -1 : 1;
|
|
7
|
+
if (p <= 0 || p >= 1) { hide(slot, 'wpStreak'); resetAll(a, b, ...items(a), ...items(b)); show(a, b, p); return; }
|
|
8
|
+
const wind = ease.outCubic(progress(p, 0, 0.18)) * (1 - ease.inCubic(progress(p, 0.18, 0.3)));
|
|
9
|
+
const out = ease.inExpo(progress(p, 0.16, 0.5));
|
|
10
|
+
a.style.transformOrigin = '50% 70%';
|
|
11
|
+
a.style.transform = `translateX(${-s * 40 * wind + s * w * travel * out}px) rotate(${-s * 1.6 * wind + s * tilt * out}deg) skewX(${-s * 12 * out}deg) scale(${1 - 0.03 * wind})`;
|
|
12
|
+
a.style.opacity = p < 0.5 ? 1 : 0;
|
|
13
|
+
const e = settle(sp(p, 0.42, duration, { stiffness: 250, damping: 19 }), p), r = 1 - e;
|
|
14
|
+
b.style.transformOrigin = '50% 70%';
|
|
15
|
+
b.style.transform = `translateX(${-s * w * travel * r}px) rotate(${-s * tilt * r}deg) skewX(${s * 12 * clamp(r) * clamp(r)}deg)`;
|
|
16
|
+
b.style.opacity = p > 0.42 ? 1 : 0;
|
|
17
|
+
items(b).forEach((c, i) => { const ci = settle(sp(p, 0.47 + i * 0.03, duration, { stiffness: 230, damping: 17 }), p); c.style.transform = `translateX(${-s * 110 * (1 - ci)}px)`; });
|
|
18
|
+
const st = progress(p, 0.28, 0.64), so = Math.sin(Math.PI * st), sw = w * 0.45;
|
|
19
|
+
layer(slot, 'wpStreak', `display:${so > 0.01 ? 'block' : 'none'};left:${lerp(s < 0 ? w : -sw, s < 0 ? -sw : w, ease.inOutCubic(st))}px;top:-15%;width:${sw}px;height:130%;z-index:5;opacity:${0.7 * so};`
|
|
20
|
+
+ `background:linear-gradient(90deg,transparent,rgba(255,255,255,.85),transparent);transform:skewX(${-s * 18}deg);mix-blend-mode:screen;filter:blur(16px)`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ease, clamp } from '../core.js';
|
|
2
|
+
import { size, hblur } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** B pushes A out very fast, with horizontal motion blur that follows the speed. `dir` is where A leaves to. */
|
|
5
|
+
export function whip(a, b, p, { dir = 'left', blur = 70, curve = ease.inOutQuint } = {}) {
|
|
6
|
+
const q = curve(clamp(p)), v = (curve(clamp(p + 0.01)) - curve(clamp(p - 0.01))) / 0.02;
|
|
7
|
+
const [w] = size(a, b), s = dir === 'left' ? -1 : 1;
|
|
8
|
+
a.style.transform = `translateX(${q * w * s}px)`;
|
|
9
|
+
b.style.transform = `translateX(${(q - 1) * w * s}px)`;
|
|
10
|
+
const id = (a.parentElement._pmWhipId ??= `pm-whip-${Math.random().toString(36).slice(2, 8)}`); // one blur filter per stage, so two whips on a page don't share it
|
|
11
|
+
const f = v > 0.15 ? hblur(id, (blur * v) / 5) : 'none';
|
|
12
|
+
a.style.filter = f; b.style.filter = f;
|
|
13
|
+
a.style.opacity = 1; b.style.opacity = 1;
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { E } from '../kit.js';
|
|
2
|
+
|
|
3
|
+
/** B is uncovered by a soft edge at `angle` degrees; A stays underneath. */
|
|
4
|
+
export function wipe(a, b, p, { angle = 100, soft = 30, curve } = {}) {
|
|
5
|
+
const w = E(p, curve) * (100 + soft) - soft;
|
|
6
|
+
const m = `linear-gradient(${angle}deg, #000 ${w}%, transparent ${w + soft}%)`;
|
|
7
|
+
b.style.opacity = 1; b.style.maskImage = m; b.style.webkitMaskImage = m;
|
|
8
|
+
if (a) a.style.opacity = p >= 1 ? 0 : 1;
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ease, progress } from '../core.js';
|
|
2
|
+
import { words } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** Words rise one by one from behind a mask line. */
|
|
5
|
+
export function wordRise(el, t, { start = 0, each = 0.08, duration = 0.5, curve = ease.outExpo } = {}) {
|
|
6
|
+
words(el).forEach((w, i) => {
|
|
7
|
+
const q = curve(progress(t, start + i * each, start + i * each + duration));
|
|
8
|
+
w.style.transform = `translateY(${(1 - q) * 110}%)`;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ease, clamp } from '../core.js';
|
|
2
|
+
import { E } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** The camera dives through A (scaling past the lens) and lands on B. */
|
|
5
|
+
/** `origin` is where the camera dives in (put it on the subject, e.g. '30% 50%'). B lands from slightly larger, so no edge shows. */
|
|
6
|
+
export function zoomThrough(a, b, p, { scale = 6, origin = '50% 50%', curve = ease.inOutCubic } = {}) {
|
|
7
|
+
const q = E(p, curve);
|
|
8
|
+
a.style.transformOrigin = origin;
|
|
9
|
+
a.style.transform = `scale(${1 + (scale - 1) * Math.pow(q, 2)})`;
|
|
10
|
+
a.style.opacity = 1 - clamp((q - 0.55) / 0.3);
|
|
11
|
+
b.style.transform = `scale(${1.12 - 0.12 * q})`;
|
|
12
|
+
b.style.opacity = clamp((q - 0.45) / 0.35);
|
|
13
|
+
}
|
package/runtime/moves.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// paper-motion moves: named, reusable pieces of motion. Each is a pure function of time or progress
|
|
2
|
+
// that sets styles on elements, so it can be called from s.update() and stays frame-exact.
|
|
3
|
+
// import { wipe, typeOn, PACE } from 'paper-motion/moves';
|
|
4
|
+
// Transitions take progress p (0 → 1); text and camera moves take scene time t.
|
|
5
|
+
// Each move lives in its own file under moves/ (the registry copies them into projects with `pamo add`);
|
|
6
|
+
// shared helpers are in kit.js (paper-motion/kit).
|
|
7
|
+
|
|
8
|
+
/** Timing per pace: transition length, hold between beats, stagger between items (seconds). */
|
|
9
|
+
export const PACE = {
|
|
10
|
+
calm: { enter: 1.0, hold: 2.6, stagger: 0.12 },
|
|
11
|
+
medium: { enter: 0.5, hold: 1.6, stagger: 0.08 },
|
|
12
|
+
fast: { enter: 0.25, hold: 0.8, stagger: 0.05 },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Every move by name, with what it does, so skills can list and demo them. */
|
|
16
|
+
export const MOVES = {
|
|
17
|
+
cut: 'hard cut from A to B, on the beat',
|
|
18
|
+
crossfade: 'A dissolves into B',
|
|
19
|
+
wipe: 'a soft diagonal edge uncovers B',
|
|
20
|
+
push: 'B pushes A out of frame',
|
|
21
|
+
maskReveal: 'B is revealed from one edge, behind a hard mask line',
|
|
22
|
+
scanReveal: 'B draws in under a moving scan line',
|
|
23
|
+
zoomThrough: 'the camera dives through A and lands on B',
|
|
24
|
+
typeOn: 'text types itself, terminal style, with a block cursor',
|
|
25
|
+
wordRise: 'words rise one by one from behind a mask line',
|
|
26
|
+
blurIn: 'text comes into focus from a blur',
|
|
27
|
+
trackIn: 'letters settle from wide spacing to their tracking',
|
|
28
|
+
drift: 'a slow, constant push-in that keeps a still frame alive',
|
|
29
|
+
scramble: 'letters cycle through random glyphs and lock into place, left to right',
|
|
30
|
+
popIn: 'letters pop up one by one on a spring, with a little overshoot',
|
|
31
|
+
flipIn: 'letters flip up into place in 3D, one after another',
|
|
32
|
+
roll: 'the last word rolls like a slot machine through a list of words',
|
|
33
|
+
odometer: 'a number counts up with rolling digit wheels, like an odometer',
|
|
34
|
+
barReveal: 'a solid bar sweeps across and leaves the line behind it',
|
|
35
|
+
whip: 'a fast push with horizontal motion blur',
|
|
36
|
+
cells: 'B appears through a grid of squares that grow in a diagonal wave',
|
|
37
|
+
slices: 'B comes in as horizontal strips from alternating sides',
|
|
38
|
+
iris: 'B opens from a circle that grows out of a point',
|
|
39
|
+
glitch: 'a few frames of torn bands and RGB split, then B',
|
|
40
|
+
flip3D: 'the frame flips over like a card and shows B on the back',
|
|
41
|
+
shake: 'a camera shake on each hit that dies out fast',
|
|
42
|
+
punch: 'the frame punches in on each hit and springs back',
|
|
43
|
+
orbit: 'a 3D camera moves around layers set at different depths',
|
|
44
|
+
rgbSplit: 'red and cyan fringes burst out on each hit and fade',
|
|
45
|
+
flash: 'an overexposed white flash hides the cut from A to B',
|
|
46
|
+
drawOn: 'lines and shapes draw themselves stroke by stroke',
|
|
47
|
+
magicMove: 'each part of A flies on its own spring into the matching part of B; text cross-blurs; the box reshapes',
|
|
48
|
+
whipPan: 'A winds up, whips out tilted and smeared under a light streak; B whips in, overshoots, its content trails',
|
|
49
|
+
diveIn: 'the camera dives into a detail of A; B opens inside it, a ring breaks outward, B\'s parts rise into focus',
|
|
50
|
+
flipLift: 'A lifts toward the camera, flips with a wobble and a sheen, lands as B with an echo; B\'s parts pop in',
|
|
51
|
+
squashDrop: 'A squashes, launches up stretched; B falls in, squashes on impact, wobbles; its content follows through',
|
|
52
|
+
snapScroll: 'a vertical reel of cards scrolls past in 3D and snaps onto B; neighbours dim and tilt away',
|
|
53
|
+
unfold: 'A\'s items leave, A folds into a glowing line, B unfolds out of the line with overshoot, items rise from it',
|
|
54
|
+
tileSwirl: 'A breaks into tiles that lift and swirl in a wave, flipping over in flight to land as B',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export { cut } from './moves/cut.js';
|
|
58
|
+
export { crossfade } from './moves/crossfade.js';
|
|
59
|
+
export { wipe } from './moves/wipe.js';
|
|
60
|
+
export { push } from './moves/push.js';
|
|
61
|
+
export { maskReveal } from './moves/mask-reveal.js';
|
|
62
|
+
export { scanReveal } from './moves/scan-reveal.js';
|
|
63
|
+
export { zoomThrough } from './moves/zoom-through.js';
|
|
64
|
+
export { typingTimes, keySounds, typeOn } from './moves/type-on.js';
|
|
65
|
+
export { wordRise } from './moves/word-rise.js';
|
|
66
|
+
export { blurIn } from './moves/blur-in.js';
|
|
67
|
+
export { trackIn } from './moves/track-in.js';
|
|
68
|
+
export { drift } from './moves/drift.js';
|
|
69
|
+
export { scramble } from './moves/scramble.js';
|
|
70
|
+
export { popIn } from './moves/pop-in.js';
|
|
71
|
+
export { flipIn } from './moves/flip-in.js';
|
|
72
|
+
export { roll } from './moves/roll.js';
|
|
73
|
+
export { odometer } from './moves/odometer.js';
|
|
74
|
+
export { barReveal } from './moves/bar-reveal.js';
|
|
75
|
+
export { whip } from './moves/whip.js';
|
|
76
|
+
export { cells } from './moves/cells.js';
|
|
77
|
+
export { slices } from './moves/slices.js';
|
|
78
|
+
export { iris } from './moves/iris.js';
|
|
79
|
+
export { glitch } from './moves/glitch.js';
|
|
80
|
+
export { flip3D } from './moves/flip-3d.js';
|
|
81
|
+
export { flash } from './moves/flash.js';
|
|
82
|
+
export { shake } from './moves/shake.js';
|
|
83
|
+
export { punch } from './moves/punch.js';
|
|
84
|
+
export { orbit } from './moves/orbit.js';
|
|
85
|
+
export { rgbSplit } from './moves/rgb-split.js';
|
|
86
|
+
export { drawOn } from './moves/draw-on.js';
|
|
87
|
+
export { magicMove } from './moves/magic-move.js';
|
|
88
|
+
export { whipPan } from './moves/whip-pan.js';
|
|
89
|
+
export { diveIn } from './moves/dive-in.js';
|
|
90
|
+
export { flipLift } from './moves/flip-lift.js';
|
|
91
|
+
export { squashDrop } from './moves/squash-drop.js';
|
|
92
|
+
export { snapScroll } from './moves/snap-scroll.js';
|
|
93
|
+
export { unfold } from './moves/unfold.js';
|
|
94
|
+
export { tileSwirl } from './moves/tile-swirl.js';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Preview player: real-time playback, scrubbing and frame stepping.
|
|
2
|
+
// Only mounted in preview (never in renders).
|
|
3
|
+
// space play/pause · ←/→ one frame · shift+←/→ one second · home start · L loop · H hide UI
|
|
4
|
+
|
|
5
|
+
export function mountPlayer(scene, startT = 0) {
|
|
6
|
+
const { stage, duration, fps } = scene;
|
|
7
|
+
let t = startT, playing = true, loop = true, last = performance.now(), busy = false;
|
|
8
|
+
|
|
9
|
+
const bar = document.createElement('div');
|
|
10
|
+
bar.innerHTML = `
|
|
11
|
+
<button data-a="play" title="space">❚❚</button>
|
|
12
|
+
<input type="range" min="0" max="${duration}" step="${1 / fps}" value="${t}">
|
|
13
|
+
<span class="pm-time"></span>
|
|
14
|
+
<button data-a="loop" title="L">loop</button>`;
|
|
15
|
+
bar.style.cssText = `position:fixed;left:0;right:0;bottom:0;height:44px;display:flex;gap:12px;align-items:center;
|
|
16
|
+
padding:0 16px;background:#0d0d0dee;color:#ddd;font:12px/1 ui-monospace,Menlo,monospace;z-index:99999;
|
|
17
|
+
border-top:1px solid #222;box-sizing:border-box`;
|
|
18
|
+
const css = document.createElement('style');
|
|
19
|
+
css.textContent = `.pm-bar button{background:#1c1c1c;color:#ddd;border:1px solid #333;border-radius:6px;padding:6px 10px;font:inherit;cursor:pointer}
|
|
20
|
+
.pm-bar button.on{background:#ddd;color:#111}.pm-bar input{flex:1;accent-color:#ddd}.pm-time{min-width:150px;text-align:right}`;
|
|
21
|
+
bar.className = 'pm-bar';
|
|
22
|
+
document.head.appendChild(css);
|
|
23
|
+
document.body.appendChild(bar);
|
|
24
|
+
const [playBtn, , loopBtn] = bar.querySelectorAll('button, input');
|
|
25
|
+
const range = bar.querySelector('input');
|
|
26
|
+
const label = bar.querySelector('.pm-time');
|
|
27
|
+
loopBtn.classList.toggle('on', loop);
|
|
28
|
+
|
|
29
|
+
function fit() {
|
|
30
|
+
const avail = { w: innerWidth, h: innerHeight - (bar.style.display === 'none' ? 0 : 44) };
|
|
31
|
+
const s = Math.min(avail.w / scene.width, avail.h / scene.height, 1);
|
|
32
|
+
stage.style.transform = `translate(${(avail.w - scene.width * s) / 2}px, ${(avail.h - scene.height * s) / 2}px) scale(${s})`;
|
|
33
|
+
}
|
|
34
|
+
addEventListener('resize', fit); fit();
|
|
35
|
+
|
|
36
|
+
async function show(nt) {
|
|
37
|
+
t = Math.max(0, Math.min(duration, nt));
|
|
38
|
+
range.value = t;
|
|
39
|
+
label.textContent = `${t.toFixed(2)}s · f${String(Math.round(t * fps)).padStart(4, '0')} / ${Math.round(duration * fps)}`;
|
|
40
|
+
try { sessionStorage.setItem(`pm:t:${location.pathname}`, String(t)); } catch { /* storage blocked */ }
|
|
41
|
+
busy = true; await scene.seek(t); busy = false;
|
|
42
|
+
}
|
|
43
|
+
function setPlaying(p) { playing = p; playBtn.textContent = p ? '❚❚' : '▶'; last = performance.now(); }
|
|
44
|
+
|
|
45
|
+
function tick(now) {
|
|
46
|
+
if (playing && !busy) {
|
|
47
|
+
let nt = t + (now - last) / 1000;
|
|
48
|
+
if (nt >= duration) { if (loop) nt %= duration; else { nt = duration; setPlaying(false); } }
|
|
49
|
+
show(nt);
|
|
50
|
+
}
|
|
51
|
+
last = now;
|
|
52
|
+
requestAnimationFrame(tick);
|
|
53
|
+
}
|
|
54
|
+
requestAnimationFrame(tick);
|
|
55
|
+
|
|
56
|
+
playBtn.onclick = () => setPlaying(!playing);
|
|
57
|
+
loopBtn.onclick = () => { loop = !loop; loopBtn.classList.toggle('on', loop); };
|
|
58
|
+
range.oninput = () => { setPlaying(false); show(Number(range.value)); };
|
|
59
|
+
addEventListener('keydown', (e) => {
|
|
60
|
+
const step = e.shiftKey ? 1 : 1 / fps;
|
|
61
|
+
if (e.code === 'Space') { e.preventDefault(); setPlaying(!playing); }
|
|
62
|
+
else if (e.key === 'ArrowRight') { setPlaying(false); show(t + step); }
|
|
63
|
+
else if (e.key === 'ArrowLeft') { setPlaying(false); show(t - step); }
|
|
64
|
+
else if (e.key === 'Home') { show(0); }
|
|
65
|
+
else if (e.key.toLowerCase() === 'l') loopBtn.click();
|
|
66
|
+
else if (e.key.toLowerCase() === 'h') { bar.style.display = bar.style.display === 'none' ? 'flex' : 'none'; fit(); }
|
|
67
|
+
});
|
|
68
|
+
show(t);
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Minimal stand-in for React so the official @paper-design/shaders-react
|
|
2
|
+
// components can be called as plain functions. Calling a component returns
|
|
3
|
+
// the props it would hand to <ShaderMount> (fragmentShader, uniforms, ...),
|
|
4
|
+
// which paper-motion then mounts with the vanilla ShaderMount.
|
|
5
|
+
const identity = (fn) => fn;
|
|
6
|
+
export const memo = identity;
|
|
7
|
+
export const forwardRef = identity;
|
|
8
|
+
export const useState = (init) => [typeof init === 'function' ? init() : init, () => {}];
|
|
9
|
+
export const useRef = (v = null) => ({ current: v });
|
|
10
|
+
export const useEffect = () => {};
|
|
11
|
+
export const useLayoutEffect = () => {};
|
|
12
|
+
export const useMemo = (fn) => fn();
|
|
13
|
+
export const useCallback = (fn) => fn;
|
|
14
|
+
export const createElement = (type, props) => ({ type, props: props ?? {} });
|
|
15
|
+
export default { memo, forwardRef, useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback, createElement };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Brief — <title>
|
|
2
|
+
Status: draft | approved by <user> on <date>
|
|
3
|
+
|
|
4
|
+
## Purpose
|
|
5
|
+
Where it lives · who watches · what should happen after watching.
|
|
6
|
+
|
|
7
|
+
## Feeling
|
|
8
|
+
Three words. One sentence of intent.
|
|
9
|
+
|
|
10
|
+
## References
|
|
11
|
+
- <link> — what we take from it: …
|
|
12
|
+
- <image> — what we take from it: …
|
|
13
|
+
|
|
14
|
+
## Anti-references (never)
|
|
15
|
+
- …
|
|
16
|
+
|
|
17
|
+
## Content
|
|
18
|
+
Exact text / none · must be read? · hero moment.
|
|
19
|
+
|
|
20
|
+
## Format
|
|
21
|
+
Aspect · duration · fps · loop · deliverables · safe areas.
|
|
22
|
+
|
|
23
|
+
## Structure and pace
|
|
24
|
+
Continuous / cuts / chapters · pace (cut every N s) · sync (music/VO/none) · opening · ending.
|
|
25
|
+
|
|
26
|
+
## Look
|
|
27
|
+
Palette · background · type (family, weights, sizes) · composition · texture · 2D/3D.
|
|
28
|
+
|
|
29
|
+
## Motion
|
|
30
|
+
Movement character · easing · transitions · camera · motion blur.
|
|
31
|
+
|
|
32
|
+
## Sound
|
|
33
|
+
Music / sfx (character) / VO / silent.
|
|
34
|
+
|
|
35
|
+
## Done when
|
|
36
|
+
The user's own words.
|
|
37
|
+
|
|
38
|
+
## Open questions
|
|
39
|
+
- …
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>untitled</title>
|
|
6
|
+
<style>
|
|
7
|
+
/* Everything lives inside #stage, sized by the scene config. */
|
|
8
|
+
#stage { font-family: ui-sans-serif, system-ui, sans-serif; color: #f2efe9; }
|
|
9
|
+
.layer { position: absolute; inset: 0; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main id="stage">
|
|
14
|
+
<div id="field" class="layer"></div>
|
|
15
|
+
</main>
|
|
16
|
+
|
|
17
|
+
<script type="module">
|
|
18
|
+
import { scene, progress, keys, ease, osc, noise, rng } from 'paper-motion';
|
|
19
|
+
|
|
20
|
+
// The whole piece is a pure function of t (seconds). No Date.now(), no Math.random(),
|
|
21
|
+
// no setTimeout: use rng(seed) / noise() and derive every value from t.
|
|
22
|
+
const s = scene({ width: 1920, height: 1080, fps: 30, duration: 10, background: '#0e0e0e' });
|
|
23
|
+
|
|
24
|
+
// Any Paper shader by name, with the same props as its React component.
|
|
25
|
+
// `paper-motion shaders` lists them all with defaults and presets.
|
|
26
|
+
const field = s.shader('#field', 'GrainGradient', {
|
|
27
|
+
colorBack: '#0e0e0e',
|
|
28
|
+
colors: ['#3b3a36', '#8c7e6a', '#e8e1d4'],
|
|
29
|
+
softness: 0.7, intensity: 0.15, noise: 0.35,
|
|
30
|
+
shape: 'blob', speed: 0.6,
|
|
31
|
+
});
|
|
32
|
+
// For a seamless loop of a diffuse field (mesh gradients, grain, noise), pass
|
|
33
|
+
// { loop: 2 } as 4th arg: it crossfades the last 2s into the first frame.
|
|
34
|
+
// Avoid it for crisp figures (blobs, shapes): they ghost during the crossfade.
|
|
35
|
+
|
|
36
|
+
s.update((t) => {
|
|
37
|
+
// Animate shader params over time, e.g.:
|
|
38
|
+
// field.set({ intensity: keys(t, [[0, 0.1], [5, 0.4, ease.inOutSine], [10, 0.1, ease.inOutSine]]) });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await s.start();
|
|
42
|
+
</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|