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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"brand": "Orbit",
|
|
3
|
+
"colors": {
|
|
4
|
+
"ground": {
|
|
5
|
+
"hex": "#000000",
|
|
6
|
+
"role": "black: the ground of every frame"
|
|
7
|
+
},
|
|
8
|
+
"surface": {
|
|
9
|
+
"hex": "#0A0A0A",
|
|
10
|
+
"role": "cards and windows"
|
|
11
|
+
},
|
|
12
|
+
"border": {
|
|
13
|
+
"hex": "#2A2A2A",
|
|
14
|
+
"role": "1 px edges"
|
|
15
|
+
},
|
|
16
|
+
"figure": {
|
|
17
|
+
"hex": "#FFFFFF",
|
|
18
|
+
"role": "white: type and lines"
|
|
19
|
+
},
|
|
20
|
+
"muted": {
|
|
21
|
+
"hex": "#8A8A8A",
|
|
22
|
+
"role": "secondary text, other people's events"
|
|
23
|
+
},
|
|
24
|
+
"accent": {
|
|
25
|
+
"hex": "#6E7E9E",
|
|
26
|
+
"role": "slate blue: only inside the two shaders"
|
|
27
|
+
},
|
|
28
|
+
"accentDeep": {
|
|
29
|
+
"hex": "#1C2232",
|
|
30
|
+
"role": "the warp's second blue"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"ramp": [
|
|
34
|
+
"#000000",
|
|
35
|
+
"#1C2232",
|
|
36
|
+
"#6E7E9E"
|
|
37
|
+
],
|
|
38
|
+
"fonts": {
|
|
39
|
+
"display": {
|
|
40
|
+
"family": "Space Grotesk",
|
|
41
|
+
"weights": [
|
|
42
|
+
500,
|
|
43
|
+
700
|
|
44
|
+
],
|
|
45
|
+
"css": "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap",
|
|
46
|
+
"license": "OFL"
|
|
47
|
+
},
|
|
48
|
+
"text": {
|
|
49
|
+
"family": "Space Grotesk",
|
|
50
|
+
"weights": [
|
|
51
|
+
400,
|
|
52
|
+
500
|
|
53
|
+
],
|
|
54
|
+
"css": "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap",
|
|
55
|
+
"license": "OFL"
|
|
56
|
+
},
|
|
57
|
+
"mono": {
|
|
58
|
+
"family": "Space Mono",
|
|
59
|
+
"weights": [
|
|
60
|
+
400,
|
|
61
|
+
700
|
|
62
|
+
],
|
|
63
|
+
"css": "https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap",
|
|
64
|
+
"license": "OFL"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"type": {
|
|
68
|
+
"reference": 1080,
|
|
69
|
+
"display": {
|
|
70
|
+
"font": "display",
|
|
71
|
+
"weight": 700,
|
|
72
|
+
"size": 220,
|
|
73
|
+
"lineHeight": 0.88,
|
|
74
|
+
"tracking": "-0.05em"
|
|
75
|
+
},
|
|
76
|
+
"title": {
|
|
77
|
+
"font": "display",
|
|
78
|
+
"weight": 500,
|
|
79
|
+
"size": 72,
|
|
80
|
+
"lineHeight": 1.05,
|
|
81
|
+
"tracking": "-0.03em"
|
|
82
|
+
},
|
|
83
|
+
"body": {
|
|
84
|
+
"font": "text",
|
|
85
|
+
"weight": 400,
|
|
86
|
+
"size": 28,
|
|
87
|
+
"lineHeight": 1.4,
|
|
88
|
+
"tracking": "0em"
|
|
89
|
+
},
|
|
90
|
+
"caption": {
|
|
91
|
+
"font": "mono",
|
|
92
|
+
"weight": 400,
|
|
93
|
+
"size": 18,
|
|
94
|
+
"lineHeight": 1.4,
|
|
95
|
+
"tracking": "0.04em"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"shape": {
|
|
99
|
+
"radius": 6,
|
|
100
|
+
"pill": 999,
|
|
101
|
+
"stroke": 1
|
|
102
|
+
},
|
|
103
|
+
"wordmark": {
|
|
104
|
+
"text": "Orbit"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Orbit · "Meetings find their own time"
|
|
2
|
+
|
|
3
|
+
A product keynote for an invented app (Orbit, a calendar for teams that schedules meetings by itself), in the smooth After Effects style: one unbroken chain where each element becomes the next, real depth, an inertial camera, kinetic words. 16:9 · 39 s · 60 fps · a voice-over.
|
|
4
|
+
|
|
5
|
+
## The brand
|
|
6
|
+
|
|
7
|
+
`brand/` holds Orbit's brand folder: black and white, Space Grotesk + Space Mono, and one slate blue that lives only inside two Paper shaders (Warp, and Dithering in warp shape), so wherever there is blue, something is moving.
|
|
8
|
+
|
|
9
|
+
## The chain (voice script in `audio/script.txt`; word timings in `audio/words.json`)
|
|
10
|
+
|
|
11
|
+
| t | said | what happens |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| 0–5 s | "Finding a time for six people takes eleven emails." | A chat thread piles up in depth, the camera climbing with it. The pile squashes into one white line; "Eleven emails." rises out of it. |
|
|
14
|
+
| 6–9 s | "Orbit is a calendar that schedules meetings for you." | The line rises and widens into Orbit's text field. "Orbit" rises from it and shrinks into the window's title as the window unfolds; the dithered warp leaks out behind. |
|
|
15
|
+
| 9–14 s | "Tell it who, and how long." | The prompt types itself; the chips spawn from the typed words; a line from the last chip draws the button; its fill spills out as a band. |
|
|
16
|
+
| 14–19 s | "It reads everyone's calendar, finds the gap, and books it." | The band lifts; the chips rise to be the column headers; six calendars draw up from the band in depth. The band becomes a scan line, stops at the free row; the lit cells merge into the booked card. |
|
|
17
|
+
| 20–24 s | "Someone drops out? It moves the meeting, and tells the rest." | One chip falls; the card lifts, the scan resumes, the card lands lower; pings reach the others. |
|
|
18
|
+
| 25–30 s | "No back and forth. No 'does Tuesday work?'." | The window folds back into the field, into the line. "No back and forth." rises from it; the first bubble returns and is squashed. |
|
|
19
|
+
| 32–39 s | "Orbit. Meetings find their own time." | The line curls into a ring filled with the smooth warp; the name and the line land; a "TRY ORBIT" pill. |
|
|
20
|
+
|
|
21
|
+
## Make it yours
|
|
22
|
+
|
|
23
|
+
`pamo new my-video --from orbit-keynote` copies the whole folder (scene, brand and timings).
|
|
24
|
+
|
|
25
|
+
To give it a voice: record `audio/script.txt`, keep or re-align `audio/words.json`, render with `--audio voice.wav`. Without a voice, the piece plays on its crisp sound cues alone.
|
|
Binary file
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Orbit · keynote</title>
|
|
6
|
+
<!-- Orbit product keynote, 39 s, 60 fps. ONE chain, each element becomes the next, the camera follows what leads.
|
|
7
|
+
thread piles → squashes into one line → "Eleven emails." rises from it → the line becomes the text field → "Orbit"
|
|
8
|
+
rises and shrinks into the window title → the window unfolds (the dithered warp leaks out behind) → the prompt types
|
|
9
|
+
→ chips spawn from the words → a line from the last chip draws the button → its fill spills into a band → the band
|
|
10
|
+
lifts and the chips become column headers; six calendars draw up from it in depth → the band becomes a scan line
|
|
11
|
+
that finds the free row → the row lights and merges into the booked card → one person drops: the card lifts, the
|
|
12
|
+
scan resumes, the card lands lower, pings reach the rest → the window folds back into the field, into the line →
|
|
13
|
+
"No back and forth." rises from it; the first bubble returns and is squashed → the line curls into a ring filled
|
|
14
|
+
with the smooth warp → "Orbit. Meetings find their own time." → a pill. Beats are keyed to Eric's words
|
|
15
|
+
(audio/words.json). Render from this folder: pamo render scene.html --fps 60 --motion-blur 4 [--audio voice.wav]. -->
|
|
16
|
+
<style>
|
|
17
|
+
#stage { overflow: hidden; color: #FFF; font-family: 'Space Grotesk', sans-serif; perspective: 1500px; perspective-origin: 50% 50%; }
|
|
18
|
+
.abs { position: absolute; box-sizing: border-box; }
|
|
19
|
+
.sh { position: absolute; inset: 0; }
|
|
20
|
+
#cam { position: absolute; left: 0; top: 0; width: 1px; height: 1px; transform-style: preserve-3d; }
|
|
21
|
+
.layer { position: absolute; left: 0; top: 0; width: 1px; height: 1px; transform-style: preserve-3d; }
|
|
22
|
+
.m { font-family: 'Space Mono', monospace; letter-spacing: 0.04em; white-space: nowrap; }
|
|
23
|
+
.d { font-family: 'Space Grotesk', sans-serif; font-weight: 700; letter-spacing: -0.05em; white-space: nowrap; }
|
|
24
|
+
.ch { display: inline-block; }
|
|
25
|
+
.bub { position: absolute; padding: 14px 22px; border: 1px solid #FFF; font-size: 26px; line-height: 1.2; white-space: nowrap; background: #000; transform-origin: 50% 100%; opacity: 0; }
|
|
26
|
+
.bub.them { border-color: #8A8A8A; color: #8A8A8A; }
|
|
27
|
+
.dots { position: absolute; padding: 14px 18px; border: 1px solid #8A8A8A; background: #000; display: flex; gap: 7px; opacity: 0; }
|
|
28
|
+
.dots i { width: 8px; height: 8px; border-radius: 50%; background: #8A8A8A; display: block; }
|
|
29
|
+
.chip { position: absolute; display: inline-flex; align-items: center; gap: 12px; padding: 10px 22px 10px 11px; border-radius: 999px; border: 1px solid #FFF; font-size: 24px; font-weight: 500; white-space: nowrap; background: #000; opacity: 0; transform-origin: 0 50%; }
|
|
30
|
+
.chip .ok { font-family: 'Space Mono', monospace; font-size: 14px; color: #FFF; opacity: 0; margin-left: -6px; }
|
|
31
|
+
.av { width: 30px; height: 30px; border-radius: 50%; border: 1px solid #FFF; display: inline-flex; align-items: center; justify-content: center; font-family: 'Space Mono', monospace; font-size: 13px; }
|
|
32
|
+
.mask { position: absolute; overflow: hidden; }
|
|
33
|
+
.col { position: absolute; transform-style: preserve-3d; }
|
|
34
|
+
.ev { position: absolute; border: 1px solid #2A2A2A; background: #0A0A0A; opacity: 0; }
|
|
35
|
+
.ping { position: absolute; width: 10px; height: 10px; border-radius: 50%; background: #FFF; margin: -5px 0 0 -5px; opacity: 0; }
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<main id="stage">
|
|
40
|
+
<div id="cam">
|
|
41
|
+
<div id="far" class="layer">
|
|
42
|
+
<div id="planeBox" class="abs" style="left:-100px;top:-120px;width:2120px;height:1340px;overflow:hidden;clip-path:inset(50% 50% 50% 50%)"><div id="plane" class="sh"></div></div>
|
|
43
|
+
</div>
|
|
44
|
+
<div id="mid" class="layer">
|
|
45
|
+
<div id="thread" class="abs" style="left:0;top:0"></div>
|
|
46
|
+
<div id="line" class="abs" style="left:960px;top:840px;width:0;height:2px;border:1px solid #FFF;background:#000"></div>
|
|
47
|
+
<div id="mask1" class="mask" style="left:400px;top:0;width:1200px;height:140px"><div id="eleven" class="d" style="position:absolute;left:0;top:20px;font-size:100px;line-height:1"></div></div>
|
|
48
|
+
<div id="mask2" class="mask" style="left:400px;top:0;width:1260px;height:230px"><div id="orbit" class="d" style="position:absolute;left:60px;top:20px;font-size:150px;line-height:1;transform-origin:0 0">Orbit</div><div id="sub" style="position:absolute;left:4px;top:176px;font-size:40px;font-weight:500;letter-spacing:-0.03em;white-space:nowrap">schedules meetings for you.</div></div>
|
|
49
|
+
<svg class="abs" style="left:0;top:0;overflow:visible" width="2000" height="1200">
|
|
50
|
+
<line id="wtop" x1="360" y1="300" x2="1560" y2="300" stroke="#FFF" stroke-width="2"/>
|
|
51
|
+
<line id="whead" x1="360" y1="356" x2="1560" y2="356" stroke="#FFF" stroke-width="1.5"/>
|
|
52
|
+
<line id="wl" x1="360" y1="300" x2="360" y2="1000" stroke="#FFF" stroke-width="2"/>
|
|
53
|
+
<line id="wr" x1="1560" y1="300" x2="1560" y2="1000" stroke="#FFF" stroke-width="2"/>
|
|
54
|
+
<line id="wbot" x1="360" y1="1000" x2="1560" y2="1000" stroke="#FFF" stroke-width="2"/>
|
|
55
|
+
<path id="conn" d="M 1388 556 V 910 H 660" fill="none" stroke="#FFF" stroke-width="1.5"/>
|
|
56
|
+
<rect id="btn" x="400" y="880" width="260" height="60" fill="none" stroke="#FFF" stroke-width="1.5"/>
|
|
57
|
+
</svg>
|
|
58
|
+
<div id="wfill" class="abs" style="left:361px;top:301px;width:1198px;height:698px;background:#000;opacity:0"></div>
|
|
59
|
+
<div id="wtitle" class="abs m" style="left:384px;top:318px;font-size:18px;opacity:0">ORBIT <span style="color:#8A8A8A">· THIS WEEK</span></div>
|
|
60
|
+
<div id="wcount" class="abs m" style="left:1300px;top:318px;width:236px;text-align:right;font-size:18px;color:#8A8A8A;opacity:0">0 PEOPLE</div>
|
|
61
|
+
<div id="prompt" class="abs" style="left:420px;top:410px;font-size:30px;white-space:pre;opacity:0"></div>
|
|
62
|
+
<div id="cur" class="abs" style="left:420px;top:412px;width:14px;height:34px;background:#FFF;opacity:0"></div>
|
|
63
|
+
<div id="ul" class="abs" style="left:0;top:448px;height:2px;background:#FFF;width:0"></div>
|
|
64
|
+
<div id="btnt" class="abs m" style="left:402px;top:882px;width:256px;height:56px;display:flex;align-items:center;justify-content:center;font-size:17px;opacity:0"></div>
|
|
65
|
+
<div class="abs" style="left:402px;top:882px;width:256px;height:56px;overflow:hidden"><div id="bfill" class="abs" style="left:0;top:0;width:256px;height:56px;clip-path:inset(0 100% 0 0)"><div id="bfill-sh" class="sh"></div></div></div>
|
|
66
|
+
<!-- the band: the spill, then the scan line -->
|
|
67
|
+
<div id="band" class="abs" style="left:402px;top:940px;width:256px;height:0;overflow:hidden"><div id="band-sh" class="sh"></div></div>
|
|
68
|
+
<!-- the calendars -->
|
|
69
|
+
<div id="times" class="abs" style="left:0;top:0"></div>
|
|
70
|
+
<div id="cols" class="abs" style="left:0;top:0"></div>
|
|
71
|
+
<div id="slots" class="abs" style="left:0;top:0"></div>
|
|
72
|
+
<div id="card" class="abs" style="left:400px;top:660px;width:1120px;height:44px;border:1px solid #FFF;overflow:hidden;opacity:0;background:#000"><div id="card-sh" class="sh"></div><div id="cardt" class="abs m" style="left:14px;top:12px;font-size:15px;background:#000;padding:1px 6px"></div><div id="booked" class="abs m" style="right:14px;top:12px;font-size:15px;background:#000;padding:1px 6px;opacity:0">BOOKED</div></div>
|
|
73
|
+
<div id="pings" class="abs" style="left:0;top:0"></div>
|
|
74
|
+
<div id="chips" class="abs" style="left:0;top:0"></div>
|
|
75
|
+
<!-- the return of the problem, and the end -->
|
|
76
|
+
<div id="mask3" class="mask" style="left:560px;top:0;width:900px;height:140px"><div id="noback" class="d" style="position:absolute;left:0;top:20px;font-size:100px;line-height:1"></div></div>
|
|
77
|
+
<div id="mask4" class="mask" style="left:560px;top:0;width:800px;height:120px"><div id="bub2" class="bub them" style="left:0;top:40px;opacity:1;transform-origin:50% 100%">Does Tuesday work for everyone?</div></div>
|
|
78
|
+
<svg class="abs" style="left:0;top:0;overflow:visible" width="2000" height="1200"><circle id="ring" cx="960" cy="650" r="170" fill="none" stroke="#FFF" stroke-width="2" transform="rotate(-90 960 650)"/><rect id="pill" x="1184" y="790" width="210" height="54" rx="27" fill="none" stroke="#FFF" stroke-width="1.5"/></svg>
|
|
79
|
+
<div class="abs" style="left:790px;top:480px;width:340px;height:340px;border-radius:50%;overflow:hidden"><div id="ringfill" class="abs" style="left:0;top:0;width:340px;height:340px;clip-path:circle(0px at 170px 170px)"><div id="ring-sh" class="sh"></div></div></div>
|
|
80
|
+
<div id="orbit2" class="abs d" style="left:1180px;top:560px;font-size:150px;line-height:1"></div>
|
|
81
|
+
<div id="tag" class="abs" style="left:1186px;top:722px;font-size:44px;font-weight:500;letter-spacing:-0.03em;white-space:nowrap"></div>
|
|
82
|
+
<div id="pillt" class="abs m" style="left:1184px;top:790px;width:210px;height:54px;display:flex;align-items:center;justify-content:center;font-size:17px;opacity:0"></div>
|
|
83
|
+
<div class="abs" style="left:1185px;top:791px;width:208px;height:52px;border-radius:26px;overflow:hidden"><div id="pillfill" class="abs" style="left:0;top:0;width:208px;height:52px;clip-path:inset(0 100% 0 0)"><div id="pill-sh" class="sh"></div></div></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div id="hud"><div id="count" class="abs m" style="left:96px;top:64px;font-size:18px;opacity:0"></div></div>
|
|
87
|
+
</main>
|
|
88
|
+
<script type="module">
|
|
89
|
+
import { scene, progress, ease, clamp, lerp } from 'paper-motion';
|
|
90
|
+
import { typeOn, typingTimes, keySounds } from 'paper-motion/moves';
|
|
91
|
+
import { loadBrand } from 'paper-motion/brand';
|
|
92
|
+
|
|
93
|
+
const B = await loadBrand('./brand');
|
|
94
|
+
const WORDS = await (await fetch('./audio/words.json')).json();
|
|
95
|
+
const clean = (w) => w.toLowerCase().replace(/[^a-z0-9']/g, '');
|
|
96
|
+
const W = (word, n = 1) => { let k = 0; for (const x of WORDS) if (clean(x.w) === word && ++k === n) return x.t; throw new Error(`word "${word}" #${n} not spoken`); };
|
|
97
|
+
|
|
98
|
+
const FPS = 60, DUR = 39;
|
|
99
|
+
const s = scene({ width: 1920, height: 1080, fps: 30, duration: DUR, background: B.color('ground') });
|
|
100
|
+
const $ = (id) => document.getElementById(id);
|
|
101
|
+
const mk = (cls, parent, css = {}) => { const d = Object.assign(document.createElement('div'), { className: cls }); Object.assign(d.style, css); parent.append(d); return d; };
|
|
102
|
+
const use = (sel, id, extra = {}) => { const e = B.set.entries.find((x) => x.id === id); return s.shader(sel, e.shader, { ...e.props, ...extra }); };
|
|
103
|
+
use('#plane', 'dither-warp', { scale: 1.2 }); use('#bfill-sh', 'dither-warp', { scale: 0.16, size: 1.6 }); use('#band-sh', 'dither-warp', { scale: 0.5, size: 2 });
|
|
104
|
+
use('#card-sh', 'dither-warp', { scale: 0.45, size: 2 }); use('#ring-sh', 'warp', { scale: 0.6 }); use('#pill-sh', 'dither-warp', { scale: 0.14, size: 1.6 });
|
|
105
|
+
const punch = (t, at, amt = 0.1, decay = 7, f = 16) => (t < at ? 0 : amt * Math.exp(-(t - at) * decay) * Math.cos((t - at) * f));
|
|
106
|
+
const draw = (el, L, p) => { el.style.strokeDasharray = `${L}`; el.style.strokeDashoffset = `${L * (1 - p)}`; el.style.opacity = p > 0 ? 1 : 0; };
|
|
107
|
+
$('far').style.transform = 'translateZ(-420px)';
|
|
108
|
+
|
|
109
|
+
// ---------- timeline, keyed to the voice ----------
|
|
110
|
+
const V = { eleven: W('eleven'), emails: W('emails'), orbit: W('orbit', 1), you: W('you'), tell: W('tell'), who: W('who'), reads: W('reads'), gap: W('gap'), books: W('books'),
|
|
111
|
+
drops: W('drops'), out: W('out'), moves: W('moves'), tells: W('tells'), rest: W('rest'), no1: W('no', 1), forth: W('forth'), no2: W('no', 2), does: W('does'), work: W('work'), orbit2: W('orbit', 2), meetings2: W('meetings', 2), time2: W('time', 2) };
|
|
112
|
+
const T0 = 0.4, STEP = 0.3, BASE = 900, GAP = 84, LINE_Y = 840;
|
|
113
|
+
const MSG = [['them', 'Does Tuesday work for everyone?'], ['me', 'Not for me, sorry'], ['them', 'Thursday then?'], ['me', "I'm out Thursday"], ['them', 'Friday morning?'], ['me', 'Dentist at 9, after 10 is fine'], ['them', 'Friday 11?'], ['me', 'Can we make it 30 min?'], ['them', 'Sure. Friday 11, 30 min?'], ['me', "Wait, Friday's a holiday"], ['them', '…']];
|
|
114
|
+
const tArr = (i) => T0 + i * STEP;
|
|
115
|
+
const SQUASH = V.eleven - 0.3, tSq = (i) => SQUASH + i * 0.06;
|
|
116
|
+
const ELEVEN = V.emails - 0.1, ELEVEN_OUT = 5.95, RISE = [6.0, 6.8], ORBIT = V.orbit - 0.05, SHRINK = [7.7, 8.25], UNFOLD = [8.2, 9.05];
|
|
117
|
+
const TYPE_AT = 9.5, PROMPT = '30 min with the design team', times = typingTimes(PROMPT, { start: TYPE_AT, cps: 30, seed: 3 });
|
|
118
|
+
const ENTER = times[times.length - 1] + 0.35, CHIP_AT = ENTER + 0.25, NAMES = ['Mara', 'Jonas', 'Ayo', 'Lena', 'Teo', 'Sam'];
|
|
119
|
+
const CONN = CHIP_AT + 1.2, BTN = CONN + 0.45, FILL = BTN + 0.5, SPILL = FILL + 0.45;
|
|
120
|
+
const CAL0 = V.reads - 0.35, SCAN0 = V.gap - 0.6, SLOT = V.gap + 0.05, CARD = V.books, DROP = V.drops - 0.05, LIFT = V.moves - 0.1, LAND2 = V.moves + 0.75, TELL = V.tells;
|
|
121
|
+
const FOLD = 25.3, NOBACK = V.no1, BUB = V.no2 - 0.1, SQB = V.work + 0.25, RING = 32.3, ORBIT2 = V.orbit2 - 0.05, TAG = V.meetings2 - 0.05, PILL = V.time2 + 1.0;
|
|
122
|
+
|
|
123
|
+
// ---------- the thread ----------
|
|
124
|
+
const bubs = MSG.map(([who, text]) => { const b = mk(`bub ${who}`, $('thread')); b.textContent = text; return b; });
|
|
125
|
+
const dots = mk('dots', $('thread')); dots.innerHTML = '<i></i><i></i><i></i>';
|
|
126
|
+
await document.fonts.ready;
|
|
127
|
+
const widths = bubs.map((b) => { b.style.opacity = 1; const w = b.offsetWidth; b.style.opacity = 0; return w; });
|
|
128
|
+
const xOf = (i) => (MSG[i][0] === 'them' ? 600 : 1320 - widths[i]);
|
|
129
|
+
const yOf = (j, t) => { let y = BASE - 60; for (let k = j + 1; k < MSG.length; k++) y -= GAP * ease.outCubic(progress(t, tArr(k) - 0.05, tArr(k) + 0.3)); return y; };
|
|
130
|
+
|
|
131
|
+
// ---------- kinetic helpers ----------
|
|
132
|
+
const chars = (el, text) => { el.innerHTML = [...text].map((c) => `<span class="ch">${c === ' ' ? ' ' : c}</span>`).join(''); return [...el.children]; };
|
|
133
|
+
const elevenCs = chars($('eleven'), 'Eleven emails.'), orbitCs = chars($('orbit'), 'Orbit'), subCs = chars($('sub'), 'schedules meetings for you.');
|
|
134
|
+
const nobackCs = chars($('noback'), 'No back and forth.'), orbit2Cs = chars($('orbit2'), 'Orbit.'), tagCs = chars($('tag'), 'Meetings find their own time.');
|
|
135
|
+
function riseFrom(cs, t, at, { each = 0.03, dur = 0.6, h = 120 } = {}) { cs.forEach((c, i) => { const p = progress(t, at + i * each, at + i * each + dur, ease.outExpo); c.style.transform = `translateY(${(1 - p) * h}px)`; c.style.opacity = t >= at + i * each ? 1 : 0; }); }
|
|
136
|
+
function kin(cs, t, at, { each = 0.035, dur = 0.65, dy = 50, rot = 6 } = {}) { cs.forEach((c, i) => { const p = progress(t, at + i * each, at + i * each + dur, ease.outExpo); c.style.opacity = progress(t, at + i * each, at + i * each + dur * 0.4); c.style.transform = `translateY(${(1 - p) * dy}px) rotate(${(1 - p) * rot}deg) scale(${lerp(0.85, 1, p)})`; c.style.filter = p < 0.99 ? `blur(${(1 - p) * 12}px)` : 'none'; }); }
|
|
137
|
+
function leaveUp(el, t, at, dur = 0.45) { const p = progress(t, at, at + dur, ease.inExpo); el.style.opacity = 1 - p; el.style.filter = p > 0.01 ? `blur(${p * 14}px)` : 'none'; el.style.translate = `0 ${-p * 60}px`; }
|
|
138
|
+
|
|
139
|
+
// ---------- chips → column headers ----------
|
|
140
|
+
const chips = NAMES.map((n) => { const c = mk('chip', $('chips')); c.innerHTML = `<span class="av">${n[0]}</span>${n}<span class="ok">✓</span>`; return c; });
|
|
141
|
+
await new Promise((r) => requestAnimationFrame(r));
|
|
142
|
+
const chipW = chips.map((c) => { c.style.opacity = 1; const w = c.offsetWidth; c.style.opacity = 0; return w; });
|
|
143
|
+
const chipX = []; { let x = 420; chips.forEach((c, i) => { chipX.push(x); x += chipW[i] + 14; }); }
|
|
144
|
+
const ROW_Y = 500, SPAWN_X = 420 + 12 * 17.4, SPAWN_Y = 410;
|
|
145
|
+
const COLX = (i) => 405 + i * 186, COLW = 170, COLTOP = 520, ROW0 = 540, ROWH = 40, NROWS = 11, HEAD_Y = 470;
|
|
146
|
+
const COLZ = (i) => (i - 2.5) * 44;
|
|
147
|
+
|
|
148
|
+
// ---------- the calendars ----------
|
|
149
|
+
const BUSY = [[[0, 2], [5, 1], [8, 1]], [[1, 1], [4, 2], [9, 1]], [[0, 1], [2, 1], [7, 2]], [[1, 2], [5, 1], [8, 2]], [[0, 2], [4, 1], [7, 1], [9, 1]], [[2, 1], [4, 2], [8, 1]]];
|
|
150
|
+
const cols = NAMES.map((_, i) => {
|
|
151
|
+
const c = mk('col', $('cols'), { left: `${COLX(i)}px`, top: `${COLTOP}px`, width: `${COLW}px`, height: `${NROWS * ROWH + 20}px`, transform: `translateZ(${COLZ(i)}px)` });
|
|
152
|
+
c.innerHTML = `<svg class="abs" style="left:0;top:0;overflow:visible" width="${COLW}" height="${NROWS * ROWH + 20}"><path class="frame" d="M 0 ${NROWS * ROWH + 20} V 0 H ${COLW} V ${NROWS * ROWH + 20}" fill="none" stroke="#2A2A2A" stroke-width="1.5"/></svg>`;
|
|
153
|
+
const evs = BUSY[i].map(([r, n]) => mk('ev', c, { left: '8px', top: `${20 + r * ROWH + 3}px`, width: `${COLW - 16}px`, height: `${n * ROWH - 6}px` }));
|
|
154
|
+
const late = mk('ev', c, { left: '8px', top: `${20 + 3 * ROWH + 3}px`, width: `${COLW - 16}px`, height: `${ROWH - 6}px`, borderColor: '#8A8A8A' }); // Lena's new conflict
|
|
155
|
+
const slot = mk('abs', c, { left: '8px', top: `${20 + 3 * ROWH + 3}px`, width: `${COLW - 16}px`, height: `${ROWH - 6}px`, border: '1px solid #FFF', opacity: 0 });
|
|
156
|
+
return { c, frame: c.querySelector('.frame'), evs, late, slot };
|
|
157
|
+
});
|
|
158
|
+
const TIMES = ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00'].map((l, i) => { const d = mk('abs m', $('times'), { left: '366px', top: `${ROW0 + i * 2 * ROWH - 8}px`, fontSize: '12px', color: '#8A8A8A', opacity: 0 }); d.textContent = l; return d; });
|
|
159
|
+
const pings = chips.map(() => mk('ping', $('pings')));
|
|
160
|
+
const rowY = (r) => ROW0 + r * ROWH;
|
|
161
|
+
|
|
162
|
+
// ---------- the line / the field: the thread's remains, the input, and back again ----------
|
|
163
|
+
function lineRect(t) {
|
|
164
|
+
const merged = MSG.reduce((n, _, i) => n + ease.inOutCubic(progress(t, tSq(i) + 0.15, tSq(i) + 0.45)), 0);
|
|
165
|
+
const w0 = (merged / MSG.length) * 800, r = progress(t, RISE[0], RISE[1], ease.inOutCubic);
|
|
166
|
+
let R = { x: lerp(960 - w0 / 2, 400, r), y: lerp(LINE_Y, 396, r), w: lerp(w0, 1120, r), h: lerp(2, 64, ease.inOutCubic(progress(t, RISE[0] + 0.45, RISE[1] + 0.1))) };
|
|
167
|
+
const f = progress(t, FOLD + 0.7, FOLD + 1.05, ease.inOutCubic); // the field folds back into a line at the centre
|
|
168
|
+
R = { x: lerp(R.x, 560, f), y: lerp(R.y, 650, f), w: lerp(R.w, 800, f), h: lerp(R.h, 2, f) };
|
|
169
|
+
const g = progress(t, RING, RING + 0.5, ease.inCubic); // and the line shrinks to a point as the ring draws
|
|
170
|
+
return { x: lerp(R.x, 960, g), y: R.y, w: lerp(R.w, 0, g), h: R.h };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------- the camera follows what leads ----------
|
|
174
|
+
function target(t) {
|
|
175
|
+
const n = clamp(Math.floor((t - T0) / STEP) + 1, 1, MSG.length);
|
|
176
|
+
let x = 960, y = yOf(Math.max(0, n - 1), t) - 150, z = 1.12, yaw = 0;
|
|
177
|
+
const sq = progress(t, SQUASH, SQUASH + 0.9, ease.inOutCubic); y = lerp(y, LINE_Y - 120, sq); z = lerp(z, 1.0, sq);
|
|
178
|
+
const L = lineRect(t); const r = progress(t, RISE[0], RISE[1], ease.inOutCubic); y = lerp(y, L.y + 140, r);
|
|
179
|
+
const u = progress(t, UNFOLD[0], UNFOLD[1] + 0.3, ease.inOutCubic); y = lerp(y, 650, u); z = lerp(z, 0.92, u); yaw = lerp(yaw, -7, u);
|
|
180
|
+
const ty = progress(t, TYPE_AT - 0.3, TYPE_AT + 0.8, ease.inOutCubic); x = lerp(x, 860, ty); y = lerp(y, 520, ty); z = lerp(z, 1.14, ty); yaw = lerp(yaw, -2, ty);
|
|
181
|
+
const cx = progress(t, CHIP_AT - 0.1, CHIP_AT + 1.0, ease.inOutCubic); x = lerp(x, 960, cx); y = lerp(y, 580, cx); z = lerp(z, 1.02, cx);
|
|
182
|
+
const bt = progress(t, CONN, BTN + 0.5, ease.inOutCubic); x = lerp(x, 720, bt); y = lerp(y, 800, bt); z = lerp(z, 1.3, bt); yaw = lerp(yaw, 5, bt);
|
|
183
|
+
const sp = progress(t, SPILL, SPILL + 0.9, ease.inOutCubic); y = lerp(y, 920, sp); x = lerp(x, 900, sp); z = lerp(z, 1.1, sp); yaw = lerp(yaw, 0, sp);
|
|
184
|
+
const ca = progress(t, CAL0, CAL0 + 1.2, ease.inOutCubic); x = lerp(x, 960, ca); y = lerp(y, 720, ca); z = lerp(z, 0.9, ca); yaw = lerp(yaw, -12, ca); // step back: six calendars in depth
|
|
185
|
+
const sc = progress(t, SCAN0 - 0.2, SLOT + 0.6, ease.inOutCubic); y = lerp(y, rowY(3) + 60, sc); z = lerp(z, 1.12, sc); yaw = lerp(yaw, -4, sc); // lean in on the gap
|
|
186
|
+
const dp = progress(t, DROP - 0.2, DROP + 0.6, ease.inOutCubic); x = lerp(x, COLX(3) + 85, dp); y = lerp(y, rowY(3) - 20, dp); z = lerp(z, 1.2, dp); // Lena
|
|
187
|
+
const mv = progress(t, LIFT, LAND2 + 0.2, ease.inOutCubic); x = lerp(x, 960, mv); y = lerp(y, rowY(6) + 40, mv); z = lerp(z, 1.05, mv); // follow the card down
|
|
188
|
+
const tl = progress(t, TELL, TELL + 0.9, ease.inOutCubic); y = lerp(y, 640, tl); z = lerp(z, 0.92, tl); yaw = lerp(yaw, -8, tl); // see everyone get told
|
|
189
|
+
const fo = progress(t, FOLD, FOLD + 1.1, ease.inOutCubic); x = lerp(x, 960, fo); y = lerp(y, 650, fo); z = lerp(z, 1.0, fo); yaw = lerp(yaw, 0, fo);
|
|
190
|
+
const nb = progress(t, NOBACK, NOBACK + 0.8, ease.inOutCubic); y = lerp(y, 600, nb); z = lerp(z, 1.06, nb);
|
|
191
|
+
const bb = progress(t, BUB - 0.2, BUB + 0.6, ease.inOutCubic); y = lerp(y, 620, bb);
|
|
192
|
+
const rg = progress(t, RING, RING + 1.2, ease.inOutCubic); x = lerp(x, 1000, rg); y = lerp(y, 660, rg); z = lerp(z, 0.98, rg);
|
|
193
|
+
const en = progress(t, RING + 1.2, DUR, ease.linear); z = lerp(z, 1.08, en); x = lerp(x, 1040, en);
|
|
194
|
+
return [x, y, z, yaw];
|
|
195
|
+
}
|
|
196
|
+
const steps = Math.ceil(DUR * FPS) + 2, cache = [];
|
|
197
|
+
{ let [x, y, z, yw] = target(0), vx = 0, vy = 0, vz = 0, vw = 0; const k = 42, c = 12;
|
|
198
|
+
for (let i = 0; i < steps; i++) { const t = i / FPS, dt = 1 / FPS, [tx, ty, tz, tw] = target(t);
|
|
199
|
+
vx += ((tx - x) * k - vx * c) * dt; vy += ((ty - y) * k - vy * c) * dt; vz += ((tz - z) * k - vz * c) * dt; vw += ((tw - yw) * k - vw * c) * dt;
|
|
200
|
+
x += vx * dt; y += vy * dt; z += vz * dt; yw += vw * dt; cache.push([x, y, z, yw, vx, vy]); } }
|
|
201
|
+
const camAt = (t) => { const f = clamp(t * FPS, 0, steps - 2), i = Math.floor(f), u = f - i; return cache[i].map((v, j) => lerp(v, cache[i + 1][j], u)); };
|
|
202
|
+
|
|
203
|
+
s.update((t) => {
|
|
204
|
+
const [cx, cy, z, yawB, vx, vy] = camAt(t);
|
|
205
|
+
const bank = clamp(-vx / 320, -6, 6), tiltX = clamp(vy / 260, -5, 5), yaw = clamp(vx / 420, -8, 8) + yawB;
|
|
206
|
+
$('cam').style.transform = `translate(960px,540px) rotateZ(${bank + Math.sin(t * 0.7) * 0.15}deg) rotateX(${tiltX}deg) rotateY(${yaw}deg) scale(${z}) translate(${-cx}px,${-cy}px)`;
|
|
207
|
+
|
|
208
|
+
// ===== 1 · the thread piles, then squashes into the line =====
|
|
209
|
+
let n = 0;
|
|
210
|
+
bubs.forEach((b, i) => {
|
|
211
|
+
const at = tArr(i), p = progress(t, at, at + 0.5, ease.outBack), q = progress(t, at, at + 0.25); if (t >= at) n = i + 1;
|
|
212
|
+
const age = bubs.filter((_, k) => k > i && t >= tArr(k)).length, sq = progress(t, tSq(i), tSq(i) + 0.45, ease.inOutCubic);
|
|
213
|
+
Object.assign(b.style, { left: `${lerp(xOf(i), 960 - widths[i] / 2, sq)}px`, top: `${lerp(yOf(i, t), LINE_Y, sq)}px`, opacity: Math.min(q, 1 - age * 0.045) * (1 - progress(t, tSq(i) + 0.35, tSq(i) + 0.45)),
|
|
214
|
+
transform: `translate3d(0, ${(1 - p) * 60}px, ${-age * 16 * (1 - sq)}px) scale(${lerp(0.8, 1, p)}, ${lerp(lerp(0.8, 1, p), 0.04, sq)}) rotate(${(1 - p) * (i % 2 ? 3 : -3)}deg)`, filter: q < 0.99 ? `blur(${(1 - q) * 10}px)` : 'none' });
|
|
215
|
+
});
|
|
216
|
+
const next = bubs.findIndex((_, i) => t < tArr(i));
|
|
217
|
+
if (next > 0 && t > tArr(next) - 0.26) { Object.assign(dots.style, { opacity: 1, left: `${MSG[next][0] === 'them' ? 600 : 1320 - 84}px`, top: `${BASE - 60}px` }); dots.querySelectorAll('i').forEach((d, k) => { d.style.transform = `translateY(${Math.sin(t * 14 + k) * 3}px)`; }); } else dots.style.opacity = 0;
|
|
218
|
+
const hud1 = t > T0 - 0.1 && t < SQUASH + 0.4, hud2 = t > SQB + 0.5 && t < RING;
|
|
219
|
+
$('count').textContent = hud1 ? `${String(n).padStart(2, '0')} MESSAGES · 0 MEETINGS` : '00 EMAILS · 1 MEETING'; $('count').style.opacity = hud1 || hud2 ? 1 : 0; $('count').style.transform = `scale(${1 + punch(t, hud1 ? tArr(n - 1) : SQB + 0.5, 0.06, 10)})`;
|
|
220
|
+
|
|
221
|
+
// ===== 2 · the line =====
|
|
222
|
+
const L = lineRect(t);
|
|
223
|
+
Object.assign($('line').style, { left: `${L.x}px`, top: `${L.y}px`, width: `${L.w}px`, height: `${L.h}px`, opacity: L.w > 1 ? 1 : 0, transform: `scale(${1 + punch(t, tSq(MSG.length - 1) + 0.45, 0.03, 8)})` });
|
|
224
|
+
Object.assign($('mask1').style, { left: `${L.x}px`, top: `${L.y - 140}px` }); riseFrom(elevenCs, t, ELEVEN, { h: 130 }); leaveUp($('mask1'), t, ELEVEN_OUT);
|
|
225
|
+
Object.assign($('mask2').style, { left: `${L.x - 60}px`, top: `${L.y - 230}px` }); riseFrom(orbitCs, t, ORBIT, { each: 0.05, h: 200, dur: 0.7 }); riseFrom(subCs, t, ORBIT + 0.4, { each: 0.018, h: 60, dur: 0.5 });
|
|
226
|
+
const sh = progress(t, SHRINK[0], SHRINK[1], ease.inOutCubic);
|
|
227
|
+
$('orbit').style.transform = `translate(${(384 - L.x) * sh}px, ${(318 - (L.y - 230 + 20)) * sh}px) scale(${lerp(1, 0.125, sh)})`; $('orbit').style.opacity = 1 - progress(t, SHRINK[1] - 0.1, SHRINK[1]);
|
|
228
|
+
leaveUp($('sub'), t, SHRINK[0]);
|
|
229
|
+
|
|
230
|
+
// ===== 3 · the window unfolds from the field … and later folds back =====
|
|
231
|
+
const u1 = progress(t, UNFOLD[0], UNFOLD[0] + 0.4, ease.inOutCubic), u2 = progress(t, UNFOLD[0] + 0.25, UNFOLD[0] + 0.75, ease.inOutCubic), u3 = progress(t, UNFOLD[0] + 0.6, UNFOLD[1], ease.inOutCubic);
|
|
232
|
+
const f1 = progress(t, FOLD, FOLD + 0.45, ease.inOutCubic), f2 = progress(t, FOLD + 0.35, FOLD + 0.8, ease.inOutCubic), f3 = progress(t, FOLD + 0.6, FOLD + 0.95, ease.inOutCubic); // content, sides, header
|
|
233
|
+
const topY = lerp(lerp(396, 300, u1), 396, f3), headY = lerp(lerp(396, 356, u1), 396, f3), sideBottom = lerp(300, 1000, u2 * (1 - f2));
|
|
234
|
+
$('wtop').setAttribute('y1', topY); $('wtop').setAttribute('y2', topY); $('wtop').setAttribute('x1', lerp(400, 360, u1)); $('wtop').setAttribute('x2', lerp(1520, 1560, u1)); $('wtop').style.opacity = t >= UNFOLD[0] && f3 < 1 ? 1 : 0;
|
|
235
|
+
$('whead').setAttribute('y1', headY); $('whead').setAttribute('y2', headY); $('whead').style.opacity = t >= UNFOLD[0] + 0.1 && f3 < 1 ? 1 : 0;
|
|
236
|
+
['wl', 'wr'].forEach((id) => { const e = $(id); e.setAttribute('y1', topY); e.setAttribute('y2', Math.max(topY, sideBottom)); e.style.opacity = u1 > 0.9 && f3 < 1 ? 1 : 0; });
|
|
237
|
+
draw($('wbot'), 1200, u3); $('wbot').setAttribute('y1', sideBottom); $('wbot').setAttribute('y2', sideBottom); if (f2 > 0.98) $('wbot').style.opacity = 0;
|
|
238
|
+
$('wfill').style.opacity = u1 > 0.95 && f3 < 0.9 ? 1 : 0; $('wfill').style.clipPath = `inset(0 0 ${100 - 100 * u2 * (1 - f2)}% 0)`;
|
|
239
|
+
const pl = u2 * (1 - progress(t, FOLD + 0.2, FOLD + 0.8, ease.inOutCubic)); $('planeBox').style.clipPath = `inset(${50 - 50 * pl}% ${50 - 50 * pl}% ${50 - 50 * pl}% ${50 - 50 * pl}%)`;
|
|
240
|
+
const hdr = progress(t, SHRINK[1] - 0.08, SHRINK[1] + 0.05) * (1 - f3); $('wtitle').style.opacity = hdr; $('wcount').style.opacity = progress(t, SHRINK[1], SHRINK[1] + 0.25) * (1 - f3);
|
|
241
|
+
|
|
242
|
+
// ===== 4 · the prompt types; chips spawn from the words; a line draws the button; the fill spills =====
|
|
243
|
+
const inputGone = progress(t, CAL0, CAL0 + 0.4, ease.inCubic);
|
|
244
|
+
$('prompt').style.opacity = (t >= TYPE_AT - 0.05 ? 1 : 0) * (1 - inputGone); typeOn($('prompt'), PROMPT, t, { times });
|
|
245
|
+
{ const k = times.filter((x) => x <= t).length; $('cur').style.left = `${420 + k * 17.4 + 2}px`; $('cur').style.opacity = t >= TYPE_AT - 0.05 && t < CHIP_AT + 0.6 && (k < PROMPT.length || Math.floor(t * 2.4) % 2 === 0) ? 1 : 0; }
|
|
246
|
+
const ul = progress(t, ENTER, ENTER + 0.3, ease.outCubic) * (1 - progress(t, CHIP_AT + 0.8, CHIP_AT + 1.1)); Object.assign($('ul').style, { left: `${SPAWN_X}px`, width: `${ul * 11 * 17.4}px` });
|
|
247
|
+
$('line').style.opacity = (L.w > 1 ? 1 : 0) * (t > CAL0 && t < FOLD + 0.7 ? 1 - inputGone : 1);
|
|
248
|
+
const lastRight = chipX[5] + chipW[5]; $('conn').setAttribute('d', `M ${lastRight - 40} ${ROW_Y + 56} V 910 H 660`);
|
|
249
|
+
draw($('conn'), 1200, progress(t, CONN, CONN + 0.5, ease.inOutCubic) * (1 - inputGone)); draw($('btn'), 640, progress(t, BTN, BTN + 0.45, ease.inOutCubic) * (1 - inputGone));
|
|
250
|
+
$('btnt').textContent = 'FIND A TIME'.slice(0, Math.floor(11 * progress(t, BTN + 0.3, BTN + 0.7))); $('btnt').style.opacity = (t > BTN + 0.3 ? 1 : 0) * (1 - inputGone);
|
|
251
|
+
$('bfill').style.clipPath = `inset(0 ${100 - 100 * progress(t, FILL, FILL + 0.5, ease.inOutCubic)}% 0 0)`; $('bfill').style.opacity = 1 - inputGone;
|
|
252
|
+
// the band: spills out of the button, widens, lifts to the calendars' baseline, then becomes the scan line
|
|
253
|
+
const spl = progress(t, SPILL, SPILL + 0.6, ease.inOutCubic), wide = progress(t, SPILL + 0.5, SPILL + 1.1, ease.inOutCubic), lift = progress(t, CAL0, CAL0 + 0.5, ease.inOutCubic);
|
|
254
|
+
let bandTop = lerp(940, COLTOP + 20 + NROWS * ROWH - 40, lift), bandH = lerp(spl * 58, 40, lift), bandL = lerp(402, 400, wide), bandW = lerp(256, 1120, wide), bandOp = 1;
|
|
255
|
+
const scanRow = (t0, r0, r1, dur) => r0 + Math.min(r1 - r0, Math.floor(progress(t, t0, t0 + dur) * (r1 - r0 + 0.999)));
|
|
256
|
+
if (t >= SCAN0 - 0.5) { const toTop = progress(t, SCAN0 - 0.5, SCAN0, ease.inOutCubic); bandTop = lerp(bandTop, rowY(0), toTop); bandL = 400; bandW = 1120; bandH = ROWH; }
|
|
257
|
+
if (t >= SCAN0) { const r = t < LIFT ? scanRow(SCAN0, 0, 3, SLOT - SCAN0) : scanRow(LIFT + 0.15, 3, 6, LAND2 - LIFT - 0.4); bandTop = rowY(r); bandOp = t < SLOT + 0.5 ? 1 : t < LIFT + 0.1 ? 1 - progress(t, SLOT + 0.5, SLOT + 0.8) : progress(t, LIFT + 0.1, LIFT + 0.25) * (1 - progress(t, LAND2 - 0.2, LAND2)); }
|
|
258
|
+
Object.assign($('band').style, { left: `${bandL}px`, top: `${bandTop}px`, width: `${bandW}px`, height: `${bandH}px`, opacity: bandOp * (1 - f1) });
|
|
259
|
+
|
|
260
|
+
// ===== 5 · the calendars: chips rise to be the column headers, columns draw up from the band, busy blocks pop =====
|
|
261
|
+
let landed = 0;
|
|
262
|
+
chips.forEach((c, i) => {
|
|
263
|
+
const at = CHIP_AT + i * 0.1, p = progress(t, at, at + 0.6, ease.outBack), q = progress(t, at, at + 0.2); if (t >= at + 0.5) landed++;
|
|
264
|
+
const arc = Math.sin(p * Math.PI) * -70, h = progress(t, CAL0 + 0.1 + i * 0.06, CAL0 + 0.7 + i * 0.06, ease.inOutCubic);
|
|
265
|
+
const x = lerp(lerp(SPAWN_X + i * 30, chipX[i], p), COLX(i) + (COLW - chipW[i]) / 2, h), y = lerp(lerp(SPAWN_Y, ROW_Y, p) + arc, HEAD_Y, h);
|
|
266
|
+
const gone = i === 3 ? progress(t, DROP, DROP + 0.5, ease.inCubic) : 0; // Lena drops out
|
|
267
|
+
const told = i !== 3 ? progress(t, TELL + 0.55 + [0, 1, 2, 4, 5].indexOf(i) * 0.08, TELL + 0.75 + [0, 1, 2, 4, 5].indexOf(i) * 0.08) : 0;
|
|
268
|
+
Object.assign(c.style, { left: `${x}px`, top: `${y + gone * 160}px`, opacity: q * (1 - gone) * (1 - f1), borderColor: i === 3 && t > DROP ? '#8A8A8A' : '#FFF', color: i === 3 && t > DROP ? '#8A8A8A' : '#FFF',
|
|
269
|
+
transform: `translateZ(${COLZ(i) * h}px) scale(${lerp(0.25, 1, p) + punch(t, at + 0.5, 0.05, 9) + punch(t, TELL + 0.55 + Math.max(0, [0, 1, 2, 4, 5].indexOf(i)) * 0.08, i === 3 ? 0 : 0.1, 9)})`, filter: q < 0.99 ? `blur(${(1 - q) * 10}px)` : 'none' });
|
|
270
|
+
c.querySelector('.ok').style.opacity = told;
|
|
271
|
+
});
|
|
272
|
+
$('wcount').textContent = t < CARD + 0.3 ? `${landed} PEOPLE` : t < TELL + 0.6 ? '1 MEETING' : '5 NOTIFIED'; $('wcount').style.transform = `scale(${1 + punch(t, CHIP_AT + (landed - 1) * 0.1 + 0.5, 0.08, 10) + punch(t, CARD + 0.3, 0.08, 10) + punch(t, TELL + 0.6, 0.08, 10)})`;
|
|
273
|
+
cols.forEach((col, i) => {
|
|
274
|
+
const d = progress(t, CAL0 + 0.45 + i * 0.07, CAL0 + 1.15 + i * 0.07, ease.inOutCubic) * (1 - f1);
|
|
275
|
+
draw(col.frame, 2 * (NROWS * ROWH + 20) + COLW, d); col.c.style.opacity = t > CAL0 + 0.45 ? 1 - f1 : 0;
|
|
276
|
+
col.evs.forEach((e, k) => { const a = CAL0 + 0.9 + i * 0.07 + k * 0.08, p = progress(t, a, a + 0.35, ease.outBack); Object.assign(e.style, { opacity: progress(t, a, a + 0.2) * (1 - f1), transform: `scaleX(${p})`, transformOrigin: '0 50%' }); });
|
|
277
|
+
const lt = progress(t, DROP + 0.35, DROP + 0.6, ease.outBack); Object.assign(col.late.style, { opacity: i === 3 ? progress(t, DROP + 0.35, DROP + 0.5) * (1 - f1) : 0, transform: `scaleX(${lt})`, transformOrigin: '0 50%' });
|
|
278
|
+
col.slot.style.opacity = progress(t, SLOT + i * 0.04, SLOT + 0.2 + i * 0.04) * (1 - progress(t, CARD - 0.1, CARD + 0.1));
|
|
279
|
+
});
|
|
280
|
+
TIMES.forEach((d, i) => { d.style.opacity = progress(t, CAL0 + 0.8 + i * 0.05, CAL0 + 1.0 + i * 0.05) * (1 - f1); });
|
|
281
|
+
|
|
282
|
+
// ===== 6 · the booked card: the six lit slots merge into one; it lifts, glides and lands again; pings go out =====
|
|
283
|
+
const cp = progress(t, CARD - 0.05, CARD + 0.45, ease.outBack), lifted = progress(t, LIFT, LIFT + 0.35, ease.inOutCubic) * (1 - progress(t, LAND2 - 0.3, LAND2, ease.inOutCubic));
|
|
284
|
+
const cardRow = lerp(3, 6, progress(t, LIFT + 0.3, LAND2 - 0.05, ease.inOutCubic)), flash = t > DROP + 0.5 && t < LIFT ? (Math.sin((t - DROP) * 18) > 0 ? 1 : 0.35) : 1;
|
|
285
|
+
Object.assign($('card').style, { opacity: (t >= CARD - 0.05 ? 1 : 0) * (1 - f1), top: `${rowY(cardRow) + 2 - lifted * 10}px`, left: `${lerp(960, 400, cp)}px`, width: `${lerp(0, 1120, cp)}px`,
|
|
286
|
+
transform: `translateZ(${lifted * 90}px) scale(${1 + punch(t, CARD + 0.4, 0.04, 9) + punch(t, LAND2, 0.04, 9)})`, borderColor: flash > 0.5 ? '#FFF' : '#8A8A8A', boxShadow: `0 ${lifted * 30}px ${lifted * 60}px rgba(0,0,0,${lifted * 0.7})` });
|
|
287
|
+
const label = t < LIFT + 0.6 ? 'DESIGN SYNC · THU 10:30 → 11:00' : 'DESIGN SYNC · THU 12:00 → 12:30';
|
|
288
|
+
$('cardt').textContent = label.slice(0, Math.floor(label.length * (t < LIFT + 0.6 ? progress(t, CARD + 0.2, CARD + 0.9) : progress(t, LIFT + 0.6, LIFT + 1.1)))); $('booked').style.opacity = progress(t, CARD + 0.9, CARD + 1.1) * (t < LIFT ? 1 : progress(t, LAND2 + 0.2, LAND2 + 0.4));
|
|
289
|
+
pings.forEach((pg, i) => { if (i === 3) return; const k = [0, 1, 2, 4, 5].indexOf(i), a = TELL + k * 0.08, p = progress(t, a, a + 0.55, ease.inOutCubic);
|
|
290
|
+
const x0 = 960, y0 = rowY(6) + 24, x1 = COLX(i) + COLW / 2, y1 = HEAD_Y + 24;
|
|
291
|
+
Object.assign(pg.style, { left: `${lerp(x0, x1, p)}px`, top: `${lerp(y0, y1, p) - Math.sin(p * Math.PI) * 60}px`, opacity: t >= a && p < 1 ? 1 : 0, transform: `translateZ(${COLZ(i) * p}px)` }); });
|
|
292
|
+
|
|
293
|
+
// ===== 7 · the problem returns to the line, and is squashed =====
|
|
294
|
+
Object.assign($('mask3').style, { left: `${L.x}px`, top: `${L.y - 140}px`, width: `${Math.max(L.w, 10)}px` }); riseFrom(nobackCs, t, NOBACK, { h: 130 }); leaveUp($('mask3'), t, BUB - 0.05);
|
|
295
|
+
Object.assign($('mask4').style, { left: `${L.x}px`, top: `${L.y - 120}px`, width: `${Math.max(L.w, 10)}px` });
|
|
296
|
+
{ const up = progress(t, BUB, BUB + 0.6, ease.outExpo), sq = progress(t, SQB, SQB + 0.4, ease.inOutCubic); const bw = 440; Object.assign($('bub2').style, { left: `${Math.max(L.w, 10) / 2 - bw / 2}px`, width: `${bw}px`, textAlign: 'center', opacity: t >= BUB && sq < 1 ? 1 : 0, transform: `translateY(${(1 - up) * 90 + sq * 20}px) scaleY(${lerp(1, 0.04, sq)})` }); }
|
|
297
|
+
|
|
298
|
+
// ===== 8 · the end: the line curls into a ring filled with the warp; the name; the pill =====
|
|
299
|
+
const rd = progress(t, RING + 0.15, RING + 0.95, ease.inOutCubic); draw($('ring'), 2 * Math.PI * 170, rd);
|
|
300
|
+
$('ringfill').style.clipPath = `circle(${170 * progress(t, RING + 0.7, RING + 1.4, ease.inOutCubic)}px at 170px 170px)`;
|
|
301
|
+
kin(orbit2Cs, t, ORBIT2, { each: 0.05, dy: 80, rot: 8 }); kin(tagCs, t, TAG, { each: 0.02, dy: 36, rot: 3 });
|
|
302
|
+
draw($('pill'), 2 * (210 + 54), progress(t, PILL, PILL + 0.5, ease.inOutCubic));
|
|
303
|
+
$('pillt').textContent = 'TRY ORBIT'.slice(0, Math.floor(9 * progress(t, PILL + 0.4, PILL + 0.8))); $('pillt').style.opacity = t > PILL + 0.4 ? 1 : 0;
|
|
304
|
+
$('pillfill').style.clipPath = `inset(0 ${100 - 100 * progress(t, PILL + 1.0, PILL + 1.5, ease.inOutCubic)}% 0 0)`;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ---------- sound: the brand's cues under the voice ----------
|
|
308
|
+
MSG.forEach((_, i) => B.cue(s, 'text', tArr(i)));
|
|
309
|
+
B.cue(s, 'transition', SQUASH); B.cue(s, 'land', tSq(MSG.length - 1) + 0.45); B.cue(s, 'text', ELEVEN);
|
|
310
|
+
B.cue(s, 'transition', RISE[0]); B.cue(s, 'text', ORBIT); B.cue(s, 'transition', UNFOLD[0]); B.cue(s, 'land', SHRINK[1]);
|
|
311
|
+
keySounds(s, PROMPT, times, B.keys); B.cue(s, 'land', ENTER);
|
|
312
|
+
NAMES.forEach((_, i) => B.cue(s, 'land', CHIP_AT + i * 0.1 + 0.5));
|
|
313
|
+
B.cue(s, 'text', BTN); B.cue(s, 'transition', FILL); B.cue(s, 'whip', SPILL);
|
|
314
|
+
B.cue(s, 'transition', CAL0); NAMES.forEach((_, i) => B.cue(s, 'text', CAL0 + 0.45 + i * 0.07));
|
|
315
|
+
[0, 1, 2, 3].forEach((r) => B.cue(s, 'text', SCAN0 + (r / 4) * (SLOT - SCAN0))); B.cue(s, 'land', SLOT); B.cue(s, 'land', CARD + 0.4);
|
|
316
|
+
B.cue(s, 'text', DROP); B.cue(s, 'whip', LIFT); B.cue(s, 'land', LAND2); [0, 1, 2, 3, 4].forEach((k) => B.cue(s, 'text', TELL + 0.55 + k * 0.08));
|
|
317
|
+
B.cue(s, 'transition', FOLD); B.cue(s, 'land', FOLD + 1.05); B.cue(s, 'text', NOBACK); B.cue(s, 'text', BUB); B.cue(s, 'land', SQB + 0.4);
|
|
318
|
+
B.cue(s, 'transition', RING); B.cue(s, 'text', ORBIT2); B.cue(s, 'text', PILL); B.cue(s, 'transition', PILL + 1.0);
|
|
319
|
+
B.sound(s);
|
|
320
|
+
await s.start();
|
|
321
|
+
</script>
|
|
322
|
+
</body>
|
|
323
|
+
</html>
|