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/runtime/kit.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// paper-motion/kit: the small helpers the moves share (geometry, springs, effect layers, live copies).
|
|
2
|
+
// A move copied into a project with `pamo add` imports them from here, so the copy stays short and editable.
|
|
3
|
+
import { ease, clamp, lerp, progress, spring, split } from './core.js';
|
|
4
|
+
|
|
5
|
+
export const E = (p, curve = ease.inOutCubic) => curve(clamp(p));
|
|
6
|
+
|
|
7
|
+
export const words = (el) => {
|
|
8
|
+
if (!el._pmWords) {
|
|
9
|
+
// keep <br> line breaks: split each text run into words, leave the breaks where they were
|
|
10
|
+
const parts = [...el.childNodes].flatMap((n) => (n.nodeName === 'BR' ? ['\n'] : (n.textContent ?? '').split(/(\s+)/)));
|
|
11
|
+
el.textContent = '';
|
|
12
|
+
el._pmWords = parts.map((w) => {
|
|
13
|
+
if (w === '\n') { el.append(document.createElement('br')); return null; }
|
|
14
|
+
if (!w) return null;
|
|
15
|
+
if (/^\s+$/.test(w)) { el.append(w); return null; }
|
|
16
|
+
const outer = Object.assign(document.createElement('span'), { style: 'display:inline-block;overflow:hidden;vertical-align:bottom;padding-bottom:0.08em;margin-bottom:-0.08em' });
|
|
17
|
+
const inner = Object.assign(document.createElement('span'), { textContent: w, style: 'display:inline-block' });
|
|
18
|
+
outer.append(inner); el.append(outer);
|
|
19
|
+
return inner;
|
|
20
|
+
}).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
return el._pmWords;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const chars = (el) => (el._labChars ??= split(el, 'chars'));
|
|
26
|
+
|
|
27
|
+
export const hash = (a, b) => { const x = Math.sin(a * 12.9898 + b * 78.233) * 43758.5453; return x - Math.floor(x); };
|
|
28
|
+
|
|
29
|
+
// integer edges: half-pixel edges of neighbouring rects leave faint seams
|
|
30
|
+
export const rects = (list) => list.map(([x, y, w, h]) => { const x0 = Math.floor(x), y0 = Math.floor(y), x1 = Math.ceil(x + w), y1 = Math.ceil(y + h); return `M${x0} ${y0}H${x1}V${y1}H${x0}Z`; }).join('');
|
|
31
|
+
|
|
32
|
+
export const clipTo = (el, d) => { const v = d ? `path('${d}')` : 'inset(100%)'; el.style.clipPath = v; el.style.webkitClipPath = v; };
|
|
33
|
+
|
|
34
|
+
export const size = (a, b) => [a.offsetWidth || b.offsetWidth, a.offsetHeight || b.offsetHeight];
|
|
35
|
+
|
|
36
|
+
/** A horizontal-only blur (an SVG filter shared per id), for motion blur on fast pushes. */
|
|
37
|
+
export function hblur(id, std) {
|
|
38
|
+
let f = document.getElementById(id);
|
|
39
|
+
if (!f) {
|
|
40
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
41
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
42
|
+
svg.setAttribute('width', 0); svg.setAttribute('height', 0); svg.style.position = 'absolute';
|
|
43
|
+
svg.innerHTML = `<filter id="${id}" x="-30%" y="0" width="160%" height="100%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="0 0"/></filter>`;
|
|
44
|
+
document.body.append(svg);
|
|
45
|
+
f = svg.querySelector('filter');
|
|
46
|
+
}
|
|
47
|
+
f.firstElementChild.setAttribute('stdDeviation', `${std} 0`);
|
|
48
|
+
return `url(#${id})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const dims = (el) => [el.offsetWidth, el.offsetHeight];
|
|
52
|
+
|
|
53
|
+
/** Children that are content (not effect layers added by these moves). */
|
|
54
|
+
export const items = (el) => [...el.children].filter((c) => !c.classList.contains('pm-fx'));
|
|
55
|
+
|
|
56
|
+
export const show = (a, b, p) => { a.style.opacity = p >= 1 ? 0 : 1; b.style.opacity = p > 0 ? 1 : 0; };
|
|
57
|
+
|
|
58
|
+
/** A spring released at progress `start`, in the transition's seconds. */
|
|
59
|
+
export const sp = (p, start, duration, o = {}) => spring((p - start) * duration, { stiffness: 200, damping: 18, ...o });
|
|
60
|
+
|
|
61
|
+
/** Eases any value onto 1 over the last stretch, so a spring lands exactly at p = 1. */
|
|
62
|
+
export const settle = (v, p, from = 0.85) => lerp(v, 1, progress(p, from, 1));
|
|
63
|
+
|
|
64
|
+
export const blur = (px) => (px > 0.3 ? `blur(${px}px)` : 'none');
|
|
65
|
+
|
|
66
|
+
export const orig = (el, prop) => ((el._pmOrig ??= {})[prop] ??= { inline: el.style[prop], computed: getComputedStyle(el)[prop] });
|
|
67
|
+
|
|
68
|
+
export const rgba = (c) => { const v = (c.match(/[\d.]+/g) ?? ['0', '0', '0', '0']).map(Number); return [v[0], v[1], v[2], v[3] ?? 1]; };
|
|
69
|
+
|
|
70
|
+
export const mixRgba = (c1, c2, q) => { const x = rgba(c1), y = rgba(c2); return `rgba(${[0, 1, 2].map((i) => Math.round(lerp(x[i], y[i], q))).join(',')},${lerp(x[3], y[3], q)})`; };
|
|
71
|
+
|
|
72
|
+
/** Layout rect of `el` relative to `root` (transforms ignored, so it is stable while things move). */
|
|
73
|
+
export function rectIn(el, root) { let x = 0, y = 0, n = el; while (n && n !== root) { x += n.offsetLeft; y += n.offsetTop; n = n.offsetParent; } return { x, y, w: el.offsetWidth, h: el.offsetHeight }; }
|
|
74
|
+
|
|
75
|
+
export const lerpRect = (r1, r2, q) => ({ x: lerp(r1.x, r2.x, q), y: lerp(r1.y, r2.y, q), w: lerp(r1.w, r2.w, q), h: lerp(r1.h, r2.h, q) });
|
|
76
|
+
|
|
77
|
+
/** An effect layer owned by `host`, made once and restyled every frame. */
|
|
78
|
+
export function layer(host, key, css) {
|
|
79
|
+
const k = `_pmFx_${key}`;
|
|
80
|
+
let d = host[k];
|
|
81
|
+
if (!d) { d = host[k] = document.createElement('div'); d.className = 'pm-fx'; host.append(d); }
|
|
82
|
+
d.style.cssText = `position:absolute;pointer-events:none;${css}`;
|
|
83
|
+
return d;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const hide = (host, ...keys) => { for (const k of keys) { const d = host[`_pmFx_${k}`]; if (d) d.style.display = 'none'; } };
|
|
87
|
+
|
|
88
|
+
export const resetAll = (...els) => { for (const e of els) for (const k of ['transform', 'opacity', 'filter', 'clipPath', 'transformOrigin']) e.style[k] = ''; };
|
|
89
|
+
|
|
90
|
+
/** Live copies of `el` drawn into `host` (for moves that cut a component into pieces). */
|
|
91
|
+
export function copy(el) {
|
|
92
|
+
const c = el.cloneNode(true);
|
|
93
|
+
c.querySelectorAll('.pm-fx').forEach((x) => x.remove());
|
|
94
|
+
Object.assign(c.style, { position: 'absolute', left: '0', top: '0', right: 'auto', bottom: 'auto', width: `${el.offsetWidth}px`, height: `${el.offsetHeight}px`, margin: '0', transform: 'none', opacity: '1', clipPath: 'none', filter: 'none', zIndex: 'auto' });
|
|
95
|
+
return c;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const partsOf = (el) => new Map([...el.querySelectorAll('[data-part]')].map((e) => [e.dataset.part, e]));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ease, progress } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** A solid bar grows over the line, then pulls away to the other side, leaving the text behind it. */
|
|
4
|
+
export function barReveal(el, t, { start = 0, duration = 0.8, color = '#fff', curve = ease.inOutQuart } = {}) {
|
|
5
|
+
if (!el._labBar) {
|
|
6
|
+
const content = document.createElement('span');
|
|
7
|
+
content.append(...el.childNodes);
|
|
8
|
+
const bar = document.createElement('span');
|
|
9
|
+
Object.assign(bar.style, { position: 'absolute', inset: '0.06em -0.04em', background: color });
|
|
10
|
+
el.style.position = 'relative'; el.style.display = 'inline-block';
|
|
11
|
+
el.append(content, bar);
|
|
12
|
+
el._labBar = { content, bar };
|
|
13
|
+
}
|
|
14
|
+
const { content, bar } = el._labBar, half = start + duration / 2;
|
|
15
|
+
const a = curve(progress(t, start, half)), b = curve(progress(t, half, start + duration));
|
|
16
|
+
bar.style.transformOrigin = t < half ? '0 50%' : '100% 50%';
|
|
17
|
+
bar.style.transform = `scaleX(${t < half ? a : 1 - b})`;
|
|
18
|
+
content.style.opacity = t >= half ? 1 : 0;
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ease, progress } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** Text comes into focus from a blur. */
|
|
4
|
+
export function blurIn(el, t, { start = 0, duration = 0.8, blur = 14, curve = ease.outCubic } = {}) {
|
|
5
|
+
const q = curve(progress(t, start, start + duration));
|
|
6
|
+
el.style.filter = `blur(${(1 - q) * blur}px)`; el.style.opacity = q;
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ease, clamp } from '../core.js';
|
|
2
|
+
import { rects, clipTo, size } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** B shows through a grid of squares that grow from their centres in a wave from `origin` (0..1, 0..1). */
|
|
5
|
+
export function cells(a, b, p, { cols = 16, rows = 9, origin = [0, 0], spread = 0.6, curve = ease.outCubic } = {}) {
|
|
6
|
+
if (p >= 1) { b.style.clipPath = 'none'; b.style.opacity = 1; return; }
|
|
7
|
+
const [w, h] = size(a, b), cw = w / cols, ch = h / rows, max = Math.hypot(Math.max(origin[0], 1 - origin[0]), Math.max(origin[1], 1 - origin[1]));
|
|
8
|
+
const list = [];
|
|
9
|
+
for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) {
|
|
10
|
+
const d = (spread * Math.hypot((i + 0.5) / cols - origin[0], (j + 0.5) / rows - origin[1])) / max;
|
|
11
|
+
const q = curve(clamp((p - d) / (1 - spread)));
|
|
12
|
+
if (q <= 0) continue;
|
|
13
|
+
const k = Math.min(1, q * 1.08), sw = cw * k, sh = ch * k; // close the last hairline before the curve settles
|
|
14
|
+
list.push([i * cw + (cw - sw) / 2, j * ch + (ch - sh) / 2, sw, sh]);
|
|
15
|
+
}
|
|
16
|
+
clipTo(b, rects(list)); b.style.opacity = 1;
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ease, lerp, progress } from '../core.js';
|
|
2
|
+
import { dims, items, show, blur, rectIn, layer, hide, resetAll } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** The camera dives into a detail of A (`target`); B opens inside it as it fills the slot, a ring breaks outward, B's parts rise into focus. */
|
|
5
|
+
export function diveIn(a, b, p, { duration = 1.1, target = '[data-part="media"]', depth = 16 } = {}) {
|
|
6
|
+
const slot = a.parentElement, t = a.querySelector(target) ?? a, R = rectIn(t, a), cx = R.x + R.w / 2, cy = R.y + R.h / 2;
|
|
7
|
+
if (p <= 0 || p >= 1) { hide(slot, 'diveRing'); resetAll(a, b, ...items(b)); show(a, b, p); return; }
|
|
8
|
+
const [w, h] = dims(a), full = Math.hypot(Math.max(cx, w - cx), Math.max(cy, h - cy));
|
|
9
|
+
const z = ease.inExpo(progress(p, 0.02, 0.5)), sa = 1 + (depth - 1) * z;
|
|
10
|
+
a.style.transformOrigin = `${cx}px ${cy}px`; a.style.transform = `scale(${sa})`;
|
|
11
|
+
a.style.filter = blur(10 * ease.inQuad(progress(p, 0.25, 0.5))); a.style.opacity = 1 - progress(p, 0.46, 0.56);
|
|
12
|
+
const rad = Math.max((Math.min(R.w, R.h) / 2) * 0.8 * sa, lerp(0, full * 1.05, ease.inOutCubic(progress(p, 0.42, 0.78))));
|
|
13
|
+
const bz = ease.outExpo(progress(p, 0.3, 1)), sb = 1.6 - 0.6 * bz;
|
|
14
|
+
b.style.transformOrigin = `${cx}px ${cy}px`; b.style.transform = `scale(${sb})`; b.style.filter = blur(12 * (1 - bz));
|
|
15
|
+
b.style.clipPath = rad >= full ? 'none' : `circle(${rad / sb}px at ${cx}px ${cy}px)`;
|
|
16
|
+
b.style.opacity = p > 0.1 ? 1 : 0; b.style.zIndex = 3; a.style.zIndex = 2;
|
|
17
|
+
items(b).forEach((c, i) => { const q = ease.outExpo(progress(p, 0.55 + i * 0.035, 0.85 + i * 0.035)); c.style.transform = `translateY(${40 * (1 - q)}px)`; c.style.opacity = q; c.style.filter = blur(8 * (1 - q)); });
|
|
18
|
+
const ring = progress(p, 0.4, 0.82), rr = rad + 60 * ease.outCubic(ring);
|
|
19
|
+
layer(slot, 'diveRing', `display:${ring > 0 && ring < 1 ? 'block' : 'none'};left:${cx - rr}px;top:${cy - rr}px;width:${2 * rr}px;height:${2 * rr}px;border-radius:50%;z-index:4;`
|
|
20
|
+
+ `border:${2 + 6 * (1 - ring)}px solid rgba(255,255,255,${0.75 * (1 - ring)})`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ease, progress } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** Every stroke in `svg` (paths, lines, circles, rects, polylines) draws itself, one after another. */
|
|
4
|
+
export function drawOn(svg, t, { start = 0, each = 0.12, duration = 1.2, curve = ease.inOutCubic } = {}) {
|
|
5
|
+
svg._labStrokes ??= [...svg.querySelectorAll('path, line, circle, rect, polyline, polygon')].map((el) => ({ el, len: el.getTotalLength() }));
|
|
6
|
+
svg._labStrokes.forEach(({ el, len }, i) => {
|
|
7
|
+
const q = curve(progress(t, start + i * each, start + i * each + duration));
|
|
8
|
+
el.style.strokeDasharray = `${len} ${len}`;
|
|
9
|
+
el.style.strokeDashoffset = `${len * (1 - q)}`;
|
|
10
|
+
el.style.opacity = q > 0 ? 1 : 0;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { clamp } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** A slow constant push-in over `duration` seconds (default: the whole scene). */
|
|
4
|
+
export function drift(el, t, { amount = 0.04, duration = 10, origin = '50% 50%' } = {}) {
|
|
5
|
+
el.style.transformOrigin = origin;
|
|
6
|
+
el.style.transform = `scale(${1 + amount * clamp(t / duration)})`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ease, clamp } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** An overexposed white flash: A burns out, the cut happens at the peak, B comes back from the white. */
|
|
4
|
+
export function flash(a, b, p, { color = '#fff', bloom = 18, peak = 0.4 } = {}) {
|
|
5
|
+
const parent = a.parentElement;
|
|
6
|
+
let o = parent.querySelector(':scope > .lab-flash');
|
|
7
|
+
if (!o) { o = document.createElement('div'); o.className = 'lab-flash'; Object.assign(o.style, { position: 'absolute', inset: 0, background: color, pointerEvents: 'none', zIndex: 50 }); parent.append(o); }
|
|
8
|
+
const up = ease.inCubic(clamp(p / peak)), down = 1 - ease.outCubic(clamp((p - peak) / (1 - peak)));
|
|
9
|
+
const k = p < peak ? up : down;
|
|
10
|
+
o.style.opacity = p <= 0 || p >= 1 ? 0 : k;
|
|
11
|
+
const cur = p < peak ? a : b;
|
|
12
|
+
a.style.opacity = p < peak ? 1 : 0; b.style.opacity = p < peak ? 0 : 1;
|
|
13
|
+
cur.style.filter = k > 0.02 ? `blur(${bloom * k}px) brightness(${1 + 1.5 * k})` : 'none';
|
|
14
|
+
cur.style.transform = `scale(${1 + 0.04 * k})`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ease, clamp } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** The frame flips over like a card (around Y) and B is on the back; it pulls back mid-flip. */
|
|
4
|
+
export function flip3D(a, b, p, { depth = 0.82, perspective = 2400, curve = ease.inOutCubic } = {}) {
|
|
5
|
+
const q = curve(clamp(p)), pull = 1 - (1 - depth) * Math.sin(Math.PI * q);
|
|
6
|
+
a.parentElement.style.perspective = `${perspective}px`;
|
|
7
|
+
if (q < 0.5) { a.style.transform = `scale(${pull}) rotateY(${-180 * q}deg)`; a.style.opacity = 1; b.style.opacity = 0; }
|
|
8
|
+
else { b.style.transform = `scale(${pull}) rotateY(${180 * (1 - q)}deg)`; b.style.opacity = 1; a.style.opacity = 0; }
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ease, clamp, progress } from '../core.js';
|
|
2
|
+
import { chars } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** Letters flip up into place around their baseline, in 3D. */
|
|
5
|
+
export function flipIn(el, t, { start = 0, each = 0.045, duration = 0.8, angle = -100, curve = ease.outBack } = {}) {
|
|
6
|
+
chars(el).forEach((c, i) => {
|
|
7
|
+
const p = progress(t, start + i * each, start + i * each + duration), q = curve(p);
|
|
8
|
+
c.style.transformOrigin = '50% 100% -0.2em';
|
|
9
|
+
c.style.transform = `perspective(700px) rotateX(${angle * (1 - q)}deg)`;
|
|
10
|
+
c.style.opacity = clamp(p * 3);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ease, clamp, progress } from '../core.js';
|
|
2
|
+
import { items, show, sp, settle, layer, hide, resetAll } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** A lifts toward the camera, flips with a wobble and a sheen across its face, and lands as B with an echo; B's parts pop in after. */
|
|
5
|
+
export function flipLift(a, b, p, { duration = 1.2, lift = 150, perspective = 1800 } = {}) {
|
|
6
|
+
const slot = a.parentElement;
|
|
7
|
+
if (p <= 0 || p >= 1) { hide(slot, 'flEcho'); hide(a, 'sheen'); hide(b, 'sheen'); resetAll(a, b, ...items(b)); a.style.backfaceVisibility = b.style.backfaceVisibility = ''; show(a, b, p); return; }
|
|
8
|
+
slot.style.perspective = `${perspective}px`;
|
|
9
|
+
const L = ease.outCubic(progress(p, 0, 0.22)) * (1 - ease.inOutCubic(progress(p, 0.6, 0.8)));
|
|
10
|
+
const ang = 180 * settle(sp(p, 0.14, duration, { stiffness: 110, damping: 11 }), p);
|
|
11
|
+
const dt = Math.max(0, (p - 0.8) * duration), imp = p > 0.8 ? Math.exp(-dt * 9) * Math.sin(dt * 30) * (1 - progress(p, 0.92, 1)) : 0;
|
|
12
|
+
const tf = (deg) => `translateZ(${lift * L}px) rotateX(${-8 * L}deg) rotateY(${deg}deg) scale(${1 - 0.035 * imp})`;
|
|
13
|
+
a.style.transform = tf(ang); b.style.transform = tf(ang - 180);
|
|
14
|
+
a.style.backfaceVisibility = 'hidden'; b.style.backfaceVisibility = 'hidden'; a.style.opacity = 1; b.style.opacity = 1;
|
|
15
|
+
const turn = Math.abs(Math.sin((ang * Math.PI) / 180));
|
|
16
|
+
for (const [el, off] of [[a, 0], [b, 180]]) {
|
|
17
|
+
const pos = 120 - ((ang - off + 180) / 360) * 140;
|
|
18
|
+
layer(el, 'sheen', `display:block;inset:0;z-index:9;border-radius:inherit;background:linear-gradient(105deg,transparent 38%,rgba(255,255,255,.28) 50%,transparent 62%);background-size:300% 100%;background-position:${pos}% 0;opacity:${0.25 + 0.75 * turn}`);
|
|
19
|
+
}
|
|
20
|
+
a.style.filter = `brightness(${1 - 0.45 * turn})`; b.style.filter = `brightness(${1 - 0.45 * turn})`;
|
|
21
|
+
const e = progress(p, 0.8, 1);
|
|
22
|
+
layer(slot, 'flEcho', `display:${e > 0 && e < 1 ? 'block' : 'none'};inset:0;z-index:0;border-radius:14px;border:1.5px solid rgba(255,255,255,${0.55 * (1 - e)});transform:scale(${1 + 0.14 * ease.outCubic(e)})`);
|
|
23
|
+
items(b).forEach((c, i) => { const q = settle(sp(p, 0.3 + i * 0.035, duration, { stiffness: 240, damping: 14 }), p); c.style.transform = `translateY(${26 * (1 - q)}px) scale(${0.92 + 0.08 * q})`; c.style.opacity = clamp(0.25 + q); });
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { clamp, rng } from '../core.js';
|
|
2
|
+
import { rects, clipTo, size } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** A few frames of torn bands, jitter and RGB split; the bands show more of B until B holds. */
|
|
5
|
+
export function glitch(a, b, p, { seed = 1, steps = 22, rgb = 16, shift = 140 } = {}) {
|
|
6
|
+
const fringe = (el, s) => { for (const c of el.children) c.style.filter = s > 0.3 ? `drop-shadow(${s}px 0 0 rgba(255,30,70,.9)) drop-shadow(${-s}px 0 0 rgba(0,220,255,.9))` : 'none'; };
|
|
7
|
+
if (p <= 0) { clipTo(b, ''); return; }
|
|
8
|
+
if (p >= 1) { b.style.clipPath = 'none'; b.style.opacity = 1; fringe(a, 0); fringe(b, 0); return; }
|
|
9
|
+
const r = rng(seed * 997 + Math.floor(p * steps)).next, amt = Math.sin(Math.PI * p), cover = clamp((p - 0.1) / 0.75);
|
|
10
|
+
const [w, h] = size(a, b), list = [];
|
|
11
|
+
for (let y = 0; y < h;) { const bh = h * (0.015 + 0.11 * r()); if (r() < cover) list.push([0, y, w, bh + 1]); y += bh; }
|
|
12
|
+
clipTo(b, rects(list)); b.style.opacity = 1;
|
|
13
|
+
b.style.transform = `translateX(${(r() - 0.5) * shift * amt}px)`;
|
|
14
|
+
a.style.transform = `translateX(${(r() - 0.5) * shift * 0.6 * amt}px)`;
|
|
15
|
+
fringe(a, rgb * amt * r()); fringe(b, rgb * amt);
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ease, clamp } from '../core.js';
|
|
2
|
+
import { size } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** B opens from a circle growing out of (x, y); A sinks back a little. */
|
|
5
|
+
export function iris(a, b, p, { x = '50%', y = '50%', curve = ease.inOutCubic } = {}) {
|
|
6
|
+
const q = curve(clamp(p)), [w, h] = size(a, b);
|
|
7
|
+
const r = q * Math.hypot(w, h) * 0.55;
|
|
8
|
+
const v = q >= 1 ? 'none' : `circle(${r}px at ${x} ${y})`;
|
|
9
|
+
b.style.clipPath = v; b.style.webkitClipPath = v; b.style.opacity = 1;
|
|
10
|
+
b.style.transform = `scale(${1.1 - 0.1 * q})`;
|
|
11
|
+
a.style.transform = `scale(${1 - 0.06 * q})`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ease, clamp, lerp, progress } from '../core.js';
|
|
2
|
+
import { show, sp, settle, blur, orig, mixRgba, rectIn, lerpRect, layer, hide, resetAll, partsOf } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Magic move: parts with the same `data-part` in A and B fly from A's place and size to B's, each on its own spring,
|
|
6
|
+
* a few frames apart; text parts scale evenly and cross-blur; parts only in A leave, parts only in B pop in.
|
|
7
|
+
* The box behind (background, border, corners) reshapes from A's to B's.
|
|
8
|
+
*/
|
|
9
|
+
export function magicMove(a, b, p, { duration = 1.1, stagger = 0.045, stiffness = 190, damping = 17 } = {}) {
|
|
10
|
+
const slot = a.parentElement, PA = partsOf(a), PB = partsOf(b);
|
|
11
|
+
const bgA = orig(a, 'backgroundColor'), bgB = orig(b, 'backgroundColor'), bcA = orig(a, 'borderTopColor'), bcB = orig(b, 'borderTopColor');
|
|
12
|
+
const parts = [...PA.values(), ...PB.values()];
|
|
13
|
+
if (p <= 0 || p >= 1) {
|
|
14
|
+
hide(slot, 'mmBox'); resetAll(...parts);
|
|
15
|
+
a.style.backgroundColor = bgA.inline; b.style.backgroundColor = bgB.inline; a.style.borderColor = bcA.inline; b.style.borderColor = bcB.inline;
|
|
16
|
+
a.style.overflow = ''; b.style.overflow = ''; show(a, b, p); return;
|
|
17
|
+
}
|
|
18
|
+
const RA = rectIn(a, slot), RB = rectIn(b, slot), cq = settle(sp(p, 0, duration, { stiffness: stiffness * 0.65, damping: damping * 0.9 }), p), R = lerpRect(RA, RB, cq);
|
|
19
|
+
const k = progress(p, 0.14, 0.5, ease.inOutCubic); // colour, border and corners change while the content swaps
|
|
20
|
+
const bw = lerp(parseFloat(orig(a, 'borderTopWidth').computed) || 0, parseFloat(orig(b, 'borderTopWidth').computed) || 0, k);
|
|
21
|
+
const rad = lerp(parseFloat(orig(a, 'borderTopLeftRadius').computed) || 0, parseFloat(orig(b, 'borderTopLeftRadius').computed) || 0, k);
|
|
22
|
+
layer(slot, 'mmBox', `left:${R.x}px;top:${R.y}px;width:${R.w}px;height:${R.h}px;box-sizing:border-box;z-index:1;background:${mixRgba(bgA.computed, bgB.computed, k)};border:${bw}px solid ${mixRgba(bcA.computed, bcB.computed, k)};border-radius:${rad}px`);
|
|
23
|
+
a.style.backgroundColor = 'transparent'; b.style.backgroundColor = 'transparent'; a.style.borderColor = 'transparent'; b.style.borderColor = 'transparent';
|
|
24
|
+
a.style.overflow = 'visible'; b.style.overflow = 'visible';
|
|
25
|
+
a.style.opacity = 1; b.style.opacity = 1; a.style.zIndex = 2; b.style.zIndex = 3;
|
|
26
|
+
[...new Set([...PA.keys(), ...PB.keys()])].forEach((key, i) => {
|
|
27
|
+
const ea = PA.get(key), eb = PB.get(key), t0 = 0.04 + i * stagger;
|
|
28
|
+
if (ea && eb) {
|
|
29
|
+
const q = settle(sp(p, t0, duration, { stiffness, damping }), p), ra = rectIn(ea, slot), rb = rectIn(eb, slot), r = lerpRect(ra, rb, q);
|
|
30
|
+
const text = !ea.children.length && !eb.children.length;
|
|
31
|
+
const place = (e, e0) => {
|
|
32
|
+
const sx = r.w / e0.w, sy = r.h / e0.h;
|
|
33
|
+
e.style.transformOrigin = '50% 50%';
|
|
34
|
+
e.style.transform = `translate(${r.x + r.w / 2 - (e0.x + e0.w / 2)}px, ${r.y + r.h / 2 - (e0.y + e0.h / 2)}px) scale(${text ? sy : sx}, ${sy})`;
|
|
35
|
+
};
|
|
36
|
+
place(ea, ra); place(eb, rb);
|
|
37
|
+
// boxes stretch when their shapes differ, so they hand over sooner than text does
|
|
38
|
+
const fa = text ? progress(p, t0 + 0.08, t0 + 0.36) : progress(p, t0 + 0.02, t0 + 0.18), fb = text ? progress(p, t0 + 0.16, t0 + 0.46) : progress(p, t0 + 0.08, t0 + 0.34);
|
|
39
|
+
ea.style.opacity = 1 - fa; ea.style.filter = blur(fa < 1 ? 9 * fa : 0);
|
|
40
|
+
eb.style.opacity = fb; eb.style.filter = blur(9 * (1 - fb));
|
|
41
|
+
} else if (ea) {
|
|
42
|
+
const l = progress(p, t0, t0 + 0.3, ease.inCubic);
|
|
43
|
+
ea.style.transform = `translateY(${-26 * l}px) scale(${1 - 0.1 * l})`; ea.style.opacity = 1 - l; ea.style.filter = blur(10 * l);
|
|
44
|
+
} else {
|
|
45
|
+
const q = settle(sp(p, 0.35 + t0, duration, { stiffness: 260, damping: 15 }), p);
|
|
46
|
+
eb.style.transform = `scale(${0.7 + 0.3 * q})`; eb.style.opacity = clamp(q * 1.5); eb.style.filter = blur(8 * clamp(1 - q));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ease } from '../core.js';
|
|
2
|
+
import { E } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** B is revealed from an edge (`from`: bottom, top, left, right) behind a hard mask. */
|
|
5
|
+
export function maskReveal(a, b, p, { from = 'bottom', curve = ease.outExpo } = {}) {
|
|
6
|
+
const r = (1 - E(p, curve)) * 100;
|
|
7
|
+
const inset = { bottom: `${r}% 0 0 0`, top: `0 0 ${r}% 0`, left: `0 ${r}% 0 0`, right: `0 0 0 ${r}%` }[from];
|
|
8
|
+
b.style.clipPath = `inset(${inset})`; b.style.opacity = 1;
|
|
9
|
+
if (a) a.style.opacity = p >= 1 ? 0 : 1;
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ease, clamp, lerp, progress } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** Counts from `from` to `to` on rolling digit wheels; a wheel only turns when the one to its right rolls over. */
|
|
4
|
+
export function odometer(el, t, { from = 0, to = 1000, start = 0, duration = 2, curve = ease.outExpo, separator = ',' } = {}) {
|
|
5
|
+
const v = lerp(from, to, curve(progress(t, start, start + duration)));
|
|
6
|
+
if (!el._labOdo) {
|
|
7
|
+
const nd = String(Math.round(Math.max(from, to))).length;
|
|
8
|
+
el.textContent = '';
|
|
9
|
+
const probe = document.createElement('span'); el.append(probe);
|
|
10
|
+
const w = Math.max(...'0123456789'.split('').map((d) => { probe.textContent = d; return probe.getBoundingClientRect().width; }));
|
|
11
|
+
probe.remove();
|
|
12
|
+
el._labOdo = [];
|
|
13
|
+
for (let i = 0; i < nd; i++) {
|
|
14
|
+
const k = nd - 1 - i;
|
|
15
|
+
const col = document.createElement('span');
|
|
16
|
+
Object.assign(col.style, { display: 'inline-block', overflow: 'hidden', height: '1em', lineHeight: '1em', width: `${w}px`, textAlign: 'center', verticalAlign: 'bottom' });
|
|
17
|
+
const strip = document.createElement('span');
|
|
18
|
+
strip.style.display = 'block';
|
|
19
|
+
strip.innerHTML = '01234567890'.split('').map((d) => `<span style="display:block">${d}</span>`).join('');
|
|
20
|
+
col.append(strip); el.append(col);
|
|
21
|
+
el._labOdo.push({ k, col, strip });
|
|
22
|
+
if (separator && k > 0 && k % 3 === 0) { const s = document.createElement('span'); s.textContent = separator; s.style.verticalAlign = 'bottom'; s.style.lineHeight = '1em'; s.dataset.k = k; el.append(s); el._labOdo.push({ k, sep: s }); }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const d of el._labOdo) {
|
|
26
|
+
const on = d.k === 0 || v >= 10 ** d.k ? 1 : 0.16;
|
|
27
|
+
if (d.sep) { d.sep.style.opacity = on; continue; }
|
|
28
|
+
const pos = d.k === 0 ? v % 10 : (Math.floor(v / 10 ** d.k) % 10) + clamp(((v / 10 ** (d.k - 1)) % 10) - 9);
|
|
29
|
+
d.strip.style.transform = `translateY(${-pos}em)`;
|
|
30
|
+
d.col.style.opacity = on;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ease, lerp, progress } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** A 3D camera around a world whose children sit at different translateZ. Ranges are [from, to]. */
|
|
4
|
+
export function orbit(world, t, { start = 0, duration = 5, yaw = [-20, 20], pitch = [8, -6], dolly = [-200, 200], curve = ease.inOutSine } = {}) {
|
|
5
|
+
const q = curve(progress(t, start, start + duration));
|
|
6
|
+
world.style.transformStyle = 'preserve-3d';
|
|
7
|
+
world.style.transform = `translateZ(${lerp(...dolly, q)}px) rotateX(${lerp(...pitch, q)}deg) rotateY(${lerp(...yaw, q)}deg)`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clamp, spring } from '../core.js';
|
|
2
|
+
import { chars } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** Letters pop up from the baseline on a spring. */
|
|
5
|
+
export function popIn(el, t, { start = 0, each = 0.04, stiffness = 260, damping = 13, rise = 0.45 } = {}) {
|
|
6
|
+
chars(el).forEach((c, i) => {
|
|
7
|
+
const dt = t - (start + i * each), q = spring(dt, { stiffness, damping });
|
|
8
|
+
c.style.transformOrigin = '50% 90%';
|
|
9
|
+
c.style.transform = `translateY(${(1 - q) * rise}em) scale(${q})`;
|
|
10
|
+
c.style.opacity = clamp(dt / 0.06);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { spring } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** The frame jumps in by `amount` on each hit and springs back. */
|
|
4
|
+
export function punch(el, t, { hits = [0], amount = 0.09, stiffness = 420, damping = 16, origin = '50% 50%' } = {}) {
|
|
5
|
+
let s = 1;
|
|
6
|
+
for (const at of hits) { const dt = t - at; if (dt >= 0) s += amount * (1 - spring(dt, { stiffness, damping })); }
|
|
7
|
+
el.style.transformOrigin = origin;
|
|
8
|
+
el.style.transform = `scale(${s})`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ease } from '../core.js';
|
|
2
|
+
import { E } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** B pushes A out; `dir` is where A leaves to. */
|
|
5
|
+
export function push(a, b, p, { dir = 'left', curve = ease.inOutQuart } = {}) {
|
|
6
|
+
const q = E(p, curve);
|
|
7
|
+
const horiz = dir === 'left' || dir === 'right';
|
|
8
|
+
const size = horiz ? (a.offsetWidth || b.offsetWidth) : (a.offsetHeight || b.offsetHeight);
|
|
9
|
+
const s = dir === 'left' || dir === 'up' ? -1 : 1;
|
|
10
|
+
const ax = q * size * s, bx = (q - 1) * size * s;
|
|
11
|
+
a.style.transform = horiz ? `translateX(${ax}px)` : `translateY(${ax}px)`;
|
|
12
|
+
b.style.transform = horiz ? `translateX(${bx}px)` : `translateY(${bx}px)`;
|
|
13
|
+
a.style.opacity = 1; b.style.opacity = 1;
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
|
|
2
|
+
/** Red and cyan fringes burst out on each hit and fade. */
|
|
3
|
+
export function rgbSplit(el, t, { hits = [0], amount = 22, decay = 6, base = 0 } = {}) {
|
|
4
|
+
let d = base;
|
|
5
|
+
for (const at of hits) { const dt = t - at; if (dt >= 0) d += amount * Math.exp(-decay * dt); }
|
|
6
|
+
el.style.filter = d > 0.3 ? `drop-shadow(${d}px 0 0 rgba(255,30,70,.95)) drop-shadow(${-d}px 0 0 rgba(0,220,255,.95))` : 'none';
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ease, clamp, lerp } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** The element rolls through `list` like a slot machine, one word every `every` seconds; its width follows. */
|
|
4
|
+
export function roll(el, list, t, { start = 0, every = 0.9, duration = 0.55, curve = ease.inOutQuart, line = 1.3 } = {}) {
|
|
5
|
+
if (!el._labRoll) {
|
|
6
|
+
el.textContent = '';
|
|
7
|
+
Object.assign(el.style, { display: 'inline-block', overflow: 'hidden', verticalAlign: 'bottom', height: `${line}em`, lineHeight: `${line}em` });
|
|
8
|
+
const strip = document.createElement('span');
|
|
9
|
+
strip.style.display = 'block';
|
|
10
|
+
const items = list.map((w) => { const s = document.createElement('span'); s.textContent = w; Object.assign(s.style, { display: 'block', whiteSpace: 'nowrap', width: 'max-content' }); strip.append(s); return s; });
|
|
11
|
+
el.append(strip);
|
|
12
|
+
el._labRoll = { strip, widths: items.map((s) => s.getBoundingClientRect().width) };
|
|
13
|
+
}
|
|
14
|
+
const { strip, widths } = el._labRoll;
|
|
15
|
+
const x = Math.max(0, t - start) / every;
|
|
16
|
+
let k = Math.floor(x), q = curve(clamp(((x - k) * every) / duration));
|
|
17
|
+
if (k >= list.length - 1) { k = list.length - 1; q = 0; }
|
|
18
|
+
strip.style.transform = `translateY(${-(k + q) * line}em)`;
|
|
19
|
+
el.style.width = `${lerp(widths[k], widths[k + 1] ?? widths[k], q)}px`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ease } from '../core.js';
|
|
2
|
+
import { E } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** B draws in from the top under a scan line element (`line`, absolutely positioned inside B's parent). */
|
|
5
|
+
export function scanReveal(a, b, p, { line, curve = ease.linear } = {}) {
|
|
6
|
+
const q = E(p, curve);
|
|
7
|
+
b.style.clipPath = `inset(0 0 ${(1 - q) * 100}% 0)`; b.style.opacity = 1;
|
|
8
|
+
if (line) { line.style.top = `${b.offsetTop + q * b.offsetHeight}px`; line.style.opacity = q > 0 && q < 1 ? 1 : 0; }
|
|
9
|
+
if (a) a.style.opacity = p >= 1 ? 0 : 1;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { rng } from '../core.js';
|
|
2
|
+
import { chars, hash } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** Letters show random glyphs, then lock into the real text from left to right. Widths stay fixed. */
|
|
5
|
+
export function scramble(el, t, { start = 0, duration = 1.4, fps = 30, rate = 2, seed = 1, glyphs = 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789#%&*+=/<>' } = {}) {
|
|
6
|
+
const cs = chars(el);
|
|
7
|
+
if (!el._labText) {
|
|
8
|
+
el._labText = cs.map((c) => c.textContent);
|
|
9
|
+
cs.forEach((c) => { c.style.width = `${c.getBoundingClientRect().width}px`; c.style.textAlign = 'center'; });
|
|
10
|
+
const r = rng(seed); el._labJit = cs.map(() => r.next());
|
|
11
|
+
}
|
|
12
|
+
const n = cs.length, tick = Math.floor((t * fps) / rate);
|
|
13
|
+
cs.forEach((c, i) => {
|
|
14
|
+
const appear = start + (i / n) * duration * 0.35;
|
|
15
|
+
const settle = start + duration * (0.3 + 0.7 * (i / n)) + el._labJit[i] * 0.1;
|
|
16
|
+
if (t < appear) { c.style.opacity = 0; return; }
|
|
17
|
+
if (t >= settle) { c.textContent = el._labText[i]; c.style.opacity = 1; return; }
|
|
18
|
+
c.textContent = glyphs[Math.floor(hash(tick + seed, i) * glyphs.length)];
|
|
19
|
+
c.style.opacity = 0.5;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { noise } from '../core.js';
|
|
2
|
+
|
|
3
|
+
/** A camera shake on each time in `hits`, dying out at `decay`. */
|
|
4
|
+
export function shake(el, t, { hits = [0], amount = 34, rot = 1.4, decay = 7, freq = 24, seed = 1 } = {}) {
|
|
5
|
+
let x = 0, y = 0, r = 0;
|
|
6
|
+
for (const [i, at] of hits.entries()) {
|
|
7
|
+
const dt = t - at;
|
|
8
|
+
if (dt < 0) continue;
|
|
9
|
+
const e = Math.exp(-decay * dt), z = (seed + i) * 3.1;
|
|
10
|
+
x += amount * e * (noise(dt * freq, z, 0) * 2 - 1);
|
|
11
|
+
y += amount * e * (noise(dt * freq, z, 5) * 2 - 1);
|
|
12
|
+
r += rot * e * (noise(dt * freq, z, 9) * 2 - 1);
|
|
13
|
+
}
|
|
14
|
+
el.style.transform = `translate(${x}px, ${y}px) rotate(${r}deg)`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ease, clamp } from '../core.js';
|
|
2
|
+
import { rects, clipTo, size } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** B comes in as `n` horizontal strips, alternating left and right, staggered top to bottom. */
|
|
5
|
+
export function slices(a, b, p, { n = 8, each = 0.05, curve = ease.outExpo } = {}) {
|
|
6
|
+
if (p >= 1) { b.style.clipPath = 'none'; b.style.opacity = 1; return; }
|
|
7
|
+
const [w, h] = size(a, b), sh = h / n, span = 1 - each * (n - 1), list = [];
|
|
8
|
+
for (let i = 0; i < n; i++) {
|
|
9
|
+
const q = curve(clamp((p - i * each) / span));
|
|
10
|
+
if (q <= 0) continue;
|
|
11
|
+
list.push(i % 2 ? [w * (1 - q), i * sh, w * q, sh + 1] : [0, i * sh, w * q, sh + 1]);
|
|
12
|
+
}
|
|
13
|
+
clipTo(b, rects(list)); b.style.opacity = 1;
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { clamp, lerp, progress } from '../core.js';
|
|
2
|
+
import { dims, show, sp, settle, resetAll, copy } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** A vertical reel of cards (copies of A and B between them) scrolls past in 3D and snaps onto B; neighbours dim, shrink and tilt away. */
|
|
5
|
+
export function snapScroll(a, b, p, { duration = 1.2, count = 3, gap = 0.1, perspective = 1600 } = {}) {
|
|
6
|
+
const slot = a.parentElement, [, h] = dims(a);
|
|
7
|
+
slot._pmReel ??= [];
|
|
8
|
+
const reel = slot._pmReel;
|
|
9
|
+
if (p <= 0 || p >= 1) { reel.forEach((g) => { g.style.display = 'none'; }); resetAll(a, b); show(a, b, p); return; }
|
|
10
|
+
slot.style.perspective = `${perspective}px`;
|
|
11
|
+
while (reel.length < count - 1) { const g = document.createElement('div'); g.className = 'pm-fx'; g.style.position = 'absolute'; slot.append(g); reel.push(g); }
|
|
12
|
+
const list = [a];
|
|
13
|
+
for (let j = 1; j < count; j++) { const g = reel[j - 1], src = j % 2 ? b : a; g.replaceChildren(copy(src)); Object.assign(g.style, { display: 'block', left: `${src.offsetLeft}px`, top: `${src.offsetTop}px`, width: `${src.offsetWidth}px`, height: `${src.offsetHeight}px`, zIndex: 1 }); list.push(g); }
|
|
14
|
+
list.push(b);
|
|
15
|
+
const pos = count * settle(sp(p, 0, duration, { stiffness: 55, damping: 10 }), p, 0.8), env = progress(p, 0, 0.15) * (1 - progress(p, 0.85, 1));
|
|
16
|
+
list.forEach((el, j) => {
|
|
17
|
+
const d = j - pos, foc = clamp(1 - Math.abs(d)), near = Math.abs(d) < 2.2;
|
|
18
|
+
el.style.transform = `translateY(${d * h * (1 + gap)}px) rotateX(${-d * 16}deg) scale(${0.8 + 0.2 * foc})`;
|
|
19
|
+
el.style.filter = `brightness(${0.3 + 0.7 * foc})`;
|
|
20
|
+
el.style.opacity = near ? lerp(env, 1, foc) * (1 - clamp(Math.abs(d) - 1.2)) : 0;
|
|
21
|
+
el.style.zIndex = Math.round(10 - Math.abs(d) * 3);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ease, progress } from '../core.js';
|
|
2
|
+
import { dims, items, show, layer, hide, resetAll } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** A squashes, then launches up stretched; B falls in stretched, squashes on impact and wobbles to rest while its content follows through. */
|
|
5
|
+
export function squashDrop(a, b, p, { duration = 1.1, height = 1.3 } = {}) {
|
|
6
|
+
const slot = a.parentElement, [w, h] = dims(a);
|
|
7
|
+
if (p <= 0 || p >= 1) { hide(slot, 'sdL', 'sdR'); resetAll(a, b, ...items(b)); show(a, b, p); return; }
|
|
8
|
+
const sq = ease.outQuad(progress(p, 0, 0.13)) * (1 - progress(p, 0.13, 0.2)), up = ease.inCubic(progress(p, 0.12, 0.38));
|
|
9
|
+
a.style.transformOrigin = '50% 100%';
|
|
10
|
+
a.style.transform = `translateY(${-h * height * up}px) scale(${1 + 0.05 * sq - 0.07 * up}, ${1 - 0.09 * sq + 0.16 * up})`;
|
|
11
|
+
a.style.opacity = p < 0.38 ? 1 : 0;
|
|
12
|
+
const HIT = 0.58, fall = ease.inCubic(progress(p, 0.3, HIT)), dt = Math.max(0, (p - HIT) * duration), end = 1 - progress(p, 0.9, 1);
|
|
13
|
+
const wob = p > HIT ? Math.exp(-dt * 7) * Math.cos(dt * 24) * end : 0, st = p < HIT ? 0.14 * fall : 0;
|
|
14
|
+
b.style.transformOrigin = '50% 100%';
|
|
15
|
+
b.style.transform = `translateY(${-h * height * (1 - fall)}px) scale(${1 - 0.06 * st + 0.09 * wob}, ${1 + st - 0.13 * wob})`;
|
|
16
|
+
b.style.opacity = p > 0.3 ? 1 : 0;
|
|
17
|
+
items(b).forEach((c, i) => { const y = p < HIT ? -24 * fall : -24 * Math.exp(-dt * 8) * Math.cos(dt * 20 + i * 0.4) * end; c.style.transform = `translateY(${y}px)`; });
|
|
18
|
+
const l = progress(p, HIT, HIT + 0.22), on = l > 0 && l < 1 ? 'block' : 'none', len = 120 * (1 - l) + 12, reach = 150 * ease.outCubic(l);
|
|
19
|
+
layer(slot, 'sdL', `display:${on};left:${-reach - len}px;top:${h - 2}px;width:${len}px;height:3px;background:#fff;opacity:${1 - l};z-index:4`);
|
|
20
|
+
layer(slot, 'sdR', `display:${on};left:${w + reach}px;top:${h - 2}px;width:${len}px;height:3px;background:#fff;opacity:${1 - l};z-index:4`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ease, progress } from '../core.js';
|
|
2
|
+
import { dims, show, copy } from '../kit.js';
|
|
3
|
+
|
|
4
|
+
/** A breaks into tiles that lift and swirl outward in a diagonal wave, flipping over in flight, and land as B. */
|
|
5
|
+
export function tileSwirl(a, b, p, { cols = 6, rows = 4, lift = 280, perspective = 1600 } = {}) {
|
|
6
|
+
const slot = a.parentElement, [w, h] = dims(a), cw = w / cols, ch = h / rows;
|
|
7
|
+
slot._pmTiles ??= [];
|
|
8
|
+
const tiles = slot._pmTiles;
|
|
9
|
+
if (p <= 0 || p >= 1) { tiles.forEach((t) => { t.style.display = 'none'; }); show(a, b, p); return; }
|
|
10
|
+
slot.style.perspective = `${perspective}px`;
|
|
11
|
+
while (tiles.length < cols * rows) {
|
|
12
|
+
const t = document.createElement('div'); t.className = 'pm-fx';
|
|
13
|
+
t.innerHTML = '<div class="f"></div><div class="f"></div>';
|
|
14
|
+
slot.append(t); tiles.push(t);
|
|
15
|
+
}
|
|
16
|
+
a.style.opacity = 0; b.style.opacity = 0;
|
|
17
|
+
for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) {
|
|
18
|
+
const t = tiles[j * cols + i], [front, back] = t.children, x = i * cw, y = j * ch;
|
|
19
|
+
const d = (i / (cols - 1)) * 0.32 + (j / (rows - 1)) * 0.12, q = ease.inOutCubic(progress(p, d, d + 0.56)), arc = Math.sin(Math.PI * q);
|
|
20
|
+
const ox = ((i + 0.5) / cols - 0.5) * w * 0.55 * arc, oy = ((j + 0.5) / rows - 0.5) * h * 0.55 * arc - 50 * arc, spin = (((i * 7 + j * 3) % 5) - 2) * 9 * arc;
|
|
21
|
+
t.style.cssText = `position:absolute;left:${x}px;top:${y}px;width:${Math.ceil(cw) + 1}px;height:${Math.ceil(ch) + 1}px;z-index:${5 + Math.round(arc * 10)};transform-style:preserve-3d;`
|
|
22
|
+
+ `transform:translate3d(${ox}px,${oy}px,${lift * arc}px) rotateZ(${spin}deg) rotateY(${180 * q}deg) scale(${1 - 0.18 * arc})`;
|
|
23
|
+
for (const [face, src, flip] of [[front, a, ''], [back, b, 'rotateY(180deg)']]) {
|
|
24
|
+
face.style.cssText = `position:absolute;inset:0;overflow:hidden;backface-visibility:hidden;transform:${flip || 'none'};filter:brightness(${1 - 0.35 * arc})`;
|
|
25
|
+
const c = copy(src); c.style.left = `${-x}px`; c.style.top = `${-y}px`;
|
|
26
|
+
face.replaceChildren(c);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|