85453da49f
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
1375 lines
43 KiB
HTML
1375 lines
43 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1920, height=1080" />
|
||
<title>Scene — HyperFrames Problem Mockup Overwhelm</title>
|
||
|
||
<!--
|
||
Blueprint: problem-mockup-overwhelm (HyperFrames preview)
|
||
HyperFrames-native version of the scene-02-mockup-morph-overwhelm concept.
|
||
|
||
Choreography (4 phases, 6 seconds total):
|
||
0.00 – 0.55s Three HyperFrames workflow mockups spring-in (left → right → center)
|
||
Check panel left · render queue right · preview timeline center
|
||
0.33 – 0.95s Nine scattered HyperFrames tool icons stagger in around the cluster
|
||
3.20 – 3.80s MORPH:
|
||
center mockup compositor-scale down to the avatar footprint
|
||
borderRadius repaint (42px → 50% = reads as circle)
|
||
content fades out at 0-40% of morph
|
||
non-center mockups + icons exit concurrently
|
||
avatar pop (scale 0 → 1) starts at morph trigger
|
||
avatar layer opacity fades in at 50% of morph
|
||
at 85-100% of morph, mockup-center fades to 0 → avatar visible underneath
|
||
3.53 – 4.40s Eight task bubbles stagger-enter in a radial pattern around the avatar
|
||
4.40 – 6.00s Idle: bubble micro-float + avatar orbit dots + avatar breath
|
||
paint-only tween. The "rect → circle" effect is completed by the final 0.51s-onward
|
||
opacity hand-off to the avatar element rendered underneath at z-index 20.
|
||
- Both mockup layer and avatar layer stay in DOM (no `{showX && <Y />}` conditional render);
|
||
opacity gates visibility — required because HyperFrames seek can move time backwards.
|
||
- All `Math.sin(frame * ...)` continuous motion replaced by:
|
||
mockup float / bubble float / orbit dots → shared onUpdate scene-ticker reading tl.time()
|
||
avatar breath → finite sine.inOut yoyo (multiplicative onto pop scale)
|
||
- The animated dark-frosted-glass blob background was replaced with a static
|
||
gradient + frosted overlay for renderer determinism and file size.
|
||
- Avatar video source replaced with a CSS gradient circle (no asset required).
|
||
To use a real video, swap the inner .avatar-disc for a <video muted playsinline>.
|
||
- Bubbles are generated via JS `forEach` at script load — positions pre-baked in
|
||
inline `left`/`top`; GSAP tweens only `scale`/`opacity`/`x`/`y`.
|
||
|
||
Spring → ease mapping (from skill SKILL.md):
|
||
spring(stiffness:70, damping:14) → back.out(1.4) (mockup entry)
|
||
spring(stiffness:180, damping:14) → back.out(1.6) (icon entry)
|
||
spring(stiffness:80, damping:18) → power3.out (morph driver)
|
||
spring(stiffness:120, damping:14) → back.out(1.4) (avatar pop)
|
||
spring(stiffness:180, damping:12) → back.out(1.4) (bubble pop)
|
||
-->
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||
|
||
<style>
|
||
:root {
|
||
--bg-base: #0a0a0f;
|
||
--text-primary: #ffffff;
|
||
--surface-dark: #000000;
|
||
--surface-mid: #16213e;
|
||
--surface-light: rgba(255, 255, 255, 0.98);
|
||
--line-soft: rgba(139, 92, 246, 0.5);
|
||
--bubble-bg: rgba(255, 255, 255, 0.95);
|
||
--bubble-border: rgba(139, 92, 246, 0.5);
|
||
--bubble-text: #1f2937;
|
||
--brand-cyan: #06b6d4;
|
||
--brand-teal: #14b8a6;
|
||
--brand-green: #00f2ea;
|
||
--brand-blue: #0ea5e9;
|
||
--brand-purple: #8b5cf6;
|
||
--brand-pink: #ec4899;
|
||
--brand-orange: #fb923c;
|
||
--brand-ink: #0f0f0f;
|
||
--accent-lime: #facc15;
|
||
--accent-mint: #ffffff;
|
||
--warn-amber: #ffc107;
|
||
--tt-cyan: #00f2ea;
|
||
}
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
background: var(--bg-base);
|
||
font-family:
|
||
"Inter",
|
||
system-ui,
|
||
-apple-system,
|
||
sans-serif;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* ============================================================
|
||
BACKGROUND (dark frosted glass, matching TSX reference)
|
||
============================================================ */
|
||
.bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
background: radial-gradient(
|
||
ellipse 120% 100% at 50% 20%,
|
||
rgba(30, 30, 45, 1) 0%,
|
||
rgba(18, 18, 28, 1) 40%,
|
||
rgba(10, 10, 15, 1) 100%
|
||
);
|
||
}
|
||
.bg::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(15, 15, 25, 0.4);
|
||
backdrop-filter: blur(80px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(80px) saturate(180%);
|
||
}
|
||
.bg::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background:
|
||
linear-gradient(
|
||
180deg,
|
||
rgba(255, 255, 255, 0.08) 0%,
|
||
rgba(255, 255, 255, 0.02) 5%,
|
||
transparent 15%,
|
||
transparent 100%
|
||
),
|
||
linear-gradient(0deg, rgba(139, 92, 246, 0.05) 0%, transparent 20%, transparent 100%);
|
||
}
|
||
.bg-frost {
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: 0.5;
|
||
filter: blur(120px);
|
||
background:
|
||
radial-gradient(
|
||
circle 550px at 30% 35%,
|
||
rgba(139, 92, 246, 0.7) 0%,
|
||
rgba(139, 92, 246, 0.3) 40%,
|
||
transparent 70%
|
||
),
|
||
radial-gradient(
|
||
circle 500px at 70% 65%,
|
||
rgba(6, 182, 212, 0.6) 0%,
|
||
rgba(6, 182, 212, 0.25) 40%,
|
||
transparent 70%
|
||
),
|
||
radial-gradient(
|
||
circle 400px at 50% 80%,
|
||
rgba(251, 146, 60, 0.5) 0%,
|
||
rgba(251, 146, 60, 0.2) 40%,
|
||
transparent 70%
|
||
),
|
||
radial-gradient(
|
||
circle 350px at 20% 60%,
|
||
rgba(236, 72, 153, 0.4) 0%,
|
||
rgba(236, 72, 153, 0.15) 40%,
|
||
transparent 70%
|
||
);
|
||
}
|
||
.vignette {
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image:
|
||
radial-gradient(
|
||
ellipse 80% 80% at 50% 50%,
|
||
transparent 0%,
|
||
transparent 50%,
|
||
rgba(0, 0, 0, 0.3) 100%
|
||
),
|
||
url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"),
|
||
repeating-linear-gradient(
|
||
0deg,
|
||
transparent,
|
||
transparent 2px,
|
||
rgba(255, 255, 255, 0.00075) 2px,
|
||
rgba(255, 255, 255, 0.00075) 4px
|
||
);
|
||
pointer-events: none;
|
||
z-index: 400;
|
||
}
|
||
|
||
/* ============================================================
|
||
MOCKUP CLUSTER (Phase 1-2)
|
||
============================================================ */
|
||
.mockup-cluster {
|
||
position: absolute;
|
||
inset: 0;
|
||
perspective: 1400px;
|
||
transform-style: preserve-3d;
|
||
}
|
||
|
||
/* Fitness-style phone shells */
|
||
.mockup {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 320px;
|
||
height: 650px;
|
||
border-radius: 42px;
|
||
background: transparent;
|
||
transform-origin: center center;
|
||
transform-style: preserve-3d;
|
||
will-change: transform, opacity;
|
||
}
|
||
.mockup-left {
|
||
z-index: 10;
|
||
}
|
||
.mockup-right {
|
||
z-index: 12;
|
||
}
|
||
.mockup-center {
|
||
z-index: 25;
|
||
background: var(--surface-dark);
|
||
box-shadow:
|
||
0 24px 70px rgba(0, 0, 0, 0.4),
|
||
0 0 40px rgba(0, 242, 234, 0.15);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.phone-shell {
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: 42px;
|
||
background: #000000;
|
||
padding: 8px;
|
||
box-shadow:
|
||
0 18px 60px rgba(0, 0, 0, 0.3),
|
||
0 4px 16px rgba(0, 0, 0, 0.18),
|
||
inset 0 0 0 3px #333333;
|
||
overflow: hidden;
|
||
will-change: opacity;
|
||
}
|
||
|
||
.phone-notch {
|
||
position: absolute;
|
||
top: 8px;
|
||
left: 50%;
|
||
width: 100px;
|
||
height: 26px;
|
||
border-radius: 0 0 18px 18px;
|
||
background: #000000;
|
||
transform: translateX(-50%);
|
||
z-index: 5;
|
||
}
|
||
|
||
.phone-screen {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 35px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.mockup-left .phone-screen,
|
||
.mockup-right .phone-screen {
|
||
background: rgba(255, 255, 255, 0.98);
|
||
padding-top: 30px;
|
||
}
|
||
|
||
.mockup-center .phone-screen {
|
||
background: #000000;
|
||
}
|
||
|
||
.mockup-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
will-change: opacity;
|
||
}
|
||
|
||
/* --- HyperFrames Check (left) --- */
|
||
.yt-header {
|
||
padding: 14px 18px;
|
||
background: #f9f9f9;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font: 600 16px/1 Inter;
|
||
color: #0f0f0f;
|
||
}
|
||
.yt-header svg .hf-logo-left {
|
||
fill: url(#hf-mini-grad-left);
|
||
}
|
||
.yt-body {
|
||
padding: 18px;
|
||
color: #0f0f0f;
|
||
}
|
||
.yt-body h3 {
|
||
font: 700 17px/1.2 Inter;
|
||
margin-bottom: 12px;
|
||
}
|
||
.yt-thumb {
|
||
height: 130px;
|
||
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
||
border-radius: 10px;
|
||
margin-bottom: 14px;
|
||
padding: 18px;
|
||
display: grid;
|
||
gap: 8px;
|
||
align-content: center;
|
||
}
|
||
.code-line {
|
||
height: 10px;
|
||
border-radius: 5px;
|
||
background: rgba(255, 255, 255, 0.24);
|
||
}
|
||
.code-line:nth-child(1) {
|
||
width: 74%;
|
||
background: #ff0000;
|
||
}
|
||
.code-line:nth-child(2) {
|
||
width: 58%;
|
||
}
|
||
.code-line:nth-child(3) {
|
||
width: 86%;
|
||
}
|
||
.code-line:nth-child(4) {
|
||
width: 46%;
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
.code-line:nth-child(5) {
|
||
width: 68%;
|
||
}
|
||
.code-line:nth-child(6) {
|
||
width: 52%;
|
||
background: rgba(255, 255, 255, 0.28);
|
||
}
|
||
.yt-progress-fill {
|
||
width: 72%;
|
||
height: 100%;
|
||
background: #ff0000;
|
||
border-radius: 3px;
|
||
}
|
||
.yt-progress-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font: 500 12px/1 Inter;
|
||
color: #606060;
|
||
margin-bottom: 6px;
|
||
}
|
||
.yt-progress-track {
|
||
height: 5px;
|
||
background: #e5e5e5;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
.yt-task-label {
|
||
font: 500 12px/1 Inter;
|
||
color: #606060;
|
||
margin-top: 16px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.yt-task {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
font: 400 12px/1.2 Inter;
|
||
color: #606060;
|
||
}
|
||
.yt-task::before {
|
||
content: "";
|
||
width: 13px;
|
||
height: 13px;
|
||
border: 2px solid #909090;
|
||
border-radius: 50%;
|
||
background: transparent;
|
||
flex-shrink: 0;
|
||
}
|
||
.yt-footer {
|
||
margin-top: auto;
|
||
padding: 12px 18px;
|
||
background: #fff4e5;
|
||
border-top: 1px solid #ffe0b2;
|
||
font: 500 12px/1 Inter;
|
||
color: #e65100;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* --- HyperFrames Preview (center) — content fades during morph --- */
|
||
.tt-statusbar {
|
||
padding: 10px 14px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font: 500 12px/1 Inter;
|
||
color: var(--accent-mint);
|
||
}
|
||
.tt-preview {
|
||
flex: 1;
|
||
position: relative;
|
||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||
}
|
||
.tt-overlay {
|
||
position: absolute;
|
||
top: 14px;
|
||
left: 14px;
|
||
right: 14px;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
border-radius: 10px;
|
||
padding: 10px 12px;
|
||
border: none;
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
}
|
||
.tt-overlay-title {
|
||
font: 500 12px/1 Inter;
|
||
color: white;
|
||
margin-bottom: 4px;
|
||
}
|
||
.tt-overlay-sub {
|
||
font: 400 10px/1.2 Inter;
|
||
color: rgba(216, 254, 255, 0.72);
|
||
}
|
||
.tt-play {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 60px;
|
||
height: 60px;
|
||
margin: -30px 0 0 -30px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.3);
|
||
box-shadow: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22px;
|
||
color: white;
|
||
}
|
||
.tt-side {
|
||
position: absolute;
|
||
right: 12px;
|
||
bottom: 70px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
font: 500 10px/1 Inter;
|
||
color: white;
|
||
text-align: center;
|
||
}
|
||
.tt-side div {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
}
|
||
.tt-side span {
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
color: #ffffff;
|
||
}
|
||
.tt-timeline {
|
||
padding: 12px;
|
||
background: #111111;
|
||
}
|
||
.tt-strip {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.tt-clip {
|
||
flex: 1;
|
||
height: 32px;
|
||
border-radius: 4px;
|
||
background: #333333;
|
||
opacity: 0.6;
|
||
}
|
||
.tt-clip.active {
|
||
background: var(--tt-cyan);
|
||
opacity: 1;
|
||
}
|
||
.tt-note {
|
||
font: 500 10px/1 Inter;
|
||
color: #888888;
|
||
text-align: center;
|
||
}
|
||
.tt-tabs {
|
||
padding: 10px 18px;
|
||
background: #000000;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
font: 500 11px/1 Inter;
|
||
color: white;
|
||
}
|
||
|
||
/* --- HyperFrames Render (right) --- */
|
||
.ig-header {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #efefef;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font: 600 15px/1 Inter;
|
||
color: #262626;
|
||
}
|
||
.ig-body {
|
||
padding: 14px;
|
||
flex: 1;
|
||
color: #262626;
|
||
}
|
||
.ig-body h3 {
|
||
font: 500 13px/1 Inter;
|
||
margin-bottom: 12px;
|
||
}
|
||
.ig-warn {
|
||
background: #fff3cd;
|
||
border: 1px solid #ffc107;
|
||
border-radius: 10px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.ig-warn-title {
|
||
font: 500 12px/1.1 Inter;
|
||
color: #856404;
|
||
}
|
||
.ig-warn-sub {
|
||
font: 400 11px/1.2 Inter;
|
||
color: #856404;
|
||
margin-top: 4px;
|
||
}
|
||
.ig-task-label {
|
||
font: 500 13px/1 Inter;
|
||
color: #262626;
|
||
margin-bottom: 8px;
|
||
}
|
||
.ig-task {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
font: 400 12px/1.2 Inter;
|
||
color: #666666;
|
||
}
|
||
.ig-task::before {
|
||
content: "";
|
||
width: 12px;
|
||
height: 12px;
|
||
border: 2px solid #c7c7c7;
|
||
border-radius: 50%;
|
||
background: transparent;
|
||
flex-shrink: 0;
|
||
}
|
||
.ig-time {
|
||
margin-top: 14px;
|
||
background: #ffebee;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
font: 500 12px/1 Inter;
|
||
color: #c62828;
|
||
}
|
||
.ig-footer {
|
||
margin-top: auto;
|
||
padding: 12px 14px;
|
||
border-top: 1px solid #efefef;
|
||
background: #fafafa;
|
||
}
|
||
.ig-share {
|
||
background: linear-gradient(90deg, #833ab4, #fd1d1d, #fcaf45);
|
||
border-radius: 10px;
|
||
padding: 11px 0;
|
||
text-align: center;
|
||
font: 600 13px/1 Inter;
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* ============================================================
|
||
PLATFORM ICONS — scattered, positions pre-baked in CSS
|
||
============================================================ */
|
||
.platform-icon {
|
||
position: absolute;
|
||
border-radius: 22%;
|
||
background: rgba(255, 255, 255, 0.98);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
||
border: none;
|
||
z-index: 30;
|
||
will-change: transform, opacity;
|
||
}
|
||
.platform-icon svg {
|
||
width: 62%;
|
||
height: 62%;
|
||
}
|
||
.platform-icon .icon-type {
|
||
font: 800 25px/1 Inter;
|
||
letter-spacing: 0;
|
||
color: #063238;
|
||
}
|
||
.platform-icon .icon-small {
|
||
font-size: 20px;
|
||
}
|
||
|
||
/* Nine icon positions — taken from the the source SCATTERED_ICONS array */
|
||
.pi-youtube {
|
||
left: calc(22% + 100px);
|
||
top: calc(20% - 179px);
|
||
width: 102px;
|
||
height: 102px;
|
||
}
|
||
.pi-google {
|
||
left: calc(60% + 16px);
|
||
top: calc(15% - 113px);
|
||
width: 96px;
|
||
height: 96px;
|
||
}
|
||
.pi-instagram {
|
||
left: calc(88% - 73px);
|
||
top: calc(25% - 103px);
|
||
width: 102px;
|
||
height: 102px;
|
||
}
|
||
.pi-tiktok {
|
||
left: 10%;
|
||
top: calc(48% - 101px);
|
||
width: 90px;
|
||
height: 90px;
|
||
}
|
||
.pi-tripadvisor {
|
||
left: calc(94% - 89px);
|
||
top: calc(55% - 45px);
|
||
width: 84px;
|
||
height: 84px;
|
||
}
|
||
.pi-facebook {
|
||
left: 15%;
|
||
top: 75%;
|
||
width: 90px;
|
||
height: 90px;
|
||
}
|
||
.pi-twitter {
|
||
left: 42%;
|
||
top: 85%;
|
||
width: 84px;
|
||
height: 84px;
|
||
}
|
||
.pi-linkedin {
|
||
left: 70%;
|
||
top: 85%;
|
||
width: 78px;
|
||
height: 78px;
|
||
}
|
||
.pi-yelp {
|
||
left: calc(92% - 165px);
|
||
top: calc(72% + 4px);
|
||
width: 90px;
|
||
height: 90px;
|
||
}
|
||
|
||
/* ============================================================
|
||
AVATAR + BUBBLES (Phase 3-4)
|
||
Initially opacity 0; opacity tween reveals at morph hand-off.
|
||
============================================================ */
|
||
.avatar-with-bubbles {
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: 0;
|
||
will-change: opacity;
|
||
}
|
||
.avatar-stage {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
z-index: 20;
|
||
will-change: transform;
|
||
}
|
||
.avatar-ring {
|
||
width: 220px;
|
||
height: 220px;
|
||
border-radius: 50%;
|
||
padding: 5px;
|
||
background: linear-gradient(135deg, #14b8a6, #06b6d4, #0ea5e9);
|
||
box-shadow:
|
||
0 0 60px rgba(20, 184, 166, 0.5),
|
||
0 0 120px rgba(6, 182, 212, 0.3);
|
||
}
|
||
.avatar-disc {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||
background:
|
||
radial-gradient(circle at 40% 35%, rgba(255, 255, 255, 0.2), transparent 50%),
|
||
conic-gradient(from 40deg, #14b8a6 0%, #06b6d4 35%, #0ea5e9 65%, #8b5cf6 100%);
|
||
overflow: hidden;
|
||
}
|
||
.orbit-dot {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 10px;
|
||
height: 10px;
|
||
margin: -5px 0 0 -5px;
|
||
border-radius: 50%;
|
||
will-change: transform, opacity;
|
||
}
|
||
.orbit-dot:nth-child(1) {
|
||
background: var(--accent-lime);
|
||
}
|
||
.orbit-dot:nth-child(2) {
|
||
background: var(--brand-purple);
|
||
}
|
||
.orbit-dot:nth-child(3) {
|
||
background: var(--accent-lime);
|
||
}
|
||
.orbit-dot:nth-child(4) {
|
||
background: var(--brand-purple);
|
||
}
|
||
|
||
.task-bubble {
|
||
position: absolute;
|
||
z-index: 30;
|
||
background: var(--bubble-bg);
|
||
border: 3px solid var(--bubble-border);
|
||
border-radius: 24px;
|
||
padding: 22px 34px;
|
||
box-shadow: 0 12px 35px rgba(139, 92, 246, 0.3);
|
||
font: 500 26px/1.4 Inter;
|
||
color: var(--bubble-text);
|
||
white-space: nowrap;
|
||
will-change: transform, opacity;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div
|
||
id="root"
|
||
data-composition-id="main"
|
||
data-start="0"
|
||
data-duration="6"
|
||
data-width="1920"
|
||
data-height="1080"
|
||
>
|
||
<div class="bg"></div>
|
||
<div class="bg-frost"></div>
|
||
|
||
<div
|
||
id="problem-mockup-overwhelm-scene"
|
||
class="scene clip"
|
||
data-start="0"
|
||
data-duration="6"
|
||
data-track-index="1"
|
||
>
|
||
<!-- ===== Phase 1-2: Mockup cluster + scattered icons ===== -->
|
||
<div class="mockup-cluster">
|
||
<!-- HyperFrames Check mockup (left) -->
|
||
<div class="mockup mockup-left">
|
||
<div class="phone-shell">
|
||
<div class="phone-notch"></div>
|
||
<div class="phone-screen">
|
||
<div class="mockup-content yt-content">
|
||
<div class="yt-header">
|
||
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
|
||
<defs>
|
||
<linearGradient id="hf-mini-grad-left" x1="2" y1="4" x2="22" y2="20">
|
||
<stop offset="0%" stop-color="#ff0000" />
|
||
<stop offset="100%" stop-color="#cc0000" />
|
||
</linearGradient>
|
||
</defs>
|
||
<path
|
||
class="hf-logo-left"
|
||
d="M4 9.5 11.5 5c2.7-1.6 5.7.8 5.1 3.8L15 17.5c-.5 2.8-3.8 4-6 2.2L3.2 15c-2-1.6-1.5-4.2.8-5.5Z"
|
||
/>
|
||
<path
|
||
class="hf-logo-left"
|
||
d="M13.2 6.5 20.8 11c2.4 1.4 2.4 4.6 0 6l-7.6 4.5c-2.6 1.5-5.8-.7-5.2-3.6l1.8-9c.5-2.8 3-3.8 3.4-2.4Z"
|
||
opacity="0.82"
|
||
/>
|
||
</svg>
|
||
HyperFrames Check
|
||
</div>
|
||
<div class="yt-body">
|
||
<h3>Validate composition</h3>
|
||
<div class="yt-thumb">
|
||
<div class="code-line"></div>
|
||
<div class="code-line"></div>
|
||
<div class="code-line"></div>
|
||
<div class="code-line"></div>
|
||
<div class="code-line"></div>
|
||
<div class="code-line"></div>
|
||
</div>
|
||
<div class="yt-progress-row"><span>Lint + inspect</span><span>72%</span></div>
|
||
<div class="yt-progress-track"><div class="yt-progress-fill"></div></div>
|
||
<div class="yt-task-label">Render-safe rules:</div>
|
||
<div class="yt-task">data-start / duration set</div>
|
||
<div class="yt-task">Timeline registered</div>
|
||
<div class="yt-task">Assets resolve locally</div>
|
||
<div class="yt-task">Text fits every frame</div>
|
||
</div>
|
||
<div class="yt-footer">npm run check before render</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HyperFrames Preview mockup (center, morph target) -->
|
||
<div class="mockup mockup-center">
|
||
<div class="phone-shell">
|
||
<div class="phone-notch"></div>
|
||
<div class="phone-screen">
|
||
<div class="mockup-content tt-content">
|
||
<div class="tt-statusbar"><span>9:41</span><span>HF</span></div>
|
||
<div class="tt-preview">
|
||
<div class="tt-overlay">
|
||
<div class="tt-overlay-title">Scene 05 · seekable</div>
|
||
<div class="tt-overlay-sub">GSAP timeline + data clips</div>
|
||
</div>
|
||
<div class="tt-play">HF</div>
|
||
<div class="tt-side">
|
||
<div><span>12</span>Clips</div>
|
||
<div><span>4</span>Tracks</div>
|
||
<div><span>V</span>Vars</div>
|
||
<div><span>MP4</span>Render</div>
|
||
</div>
|
||
</div>
|
||
<div class="tt-timeline">
|
||
<div class="tt-strip">
|
||
<div class="tt-clip"></div>
|
||
<div class="tt-clip"></div>
|
||
<div class="tt-clip active"></div>
|
||
<div class="tt-clip"></div>
|
||
<div class="tt-clip"></div>
|
||
<div class="tt-clip"></div>
|
||
<div class="tt-clip"></div>
|
||
<div class="tt-clip"></div>
|
||
</div>
|
||
<div class="tt-note">0 errors · frame-safe render</div>
|
||
</div>
|
||
<div class="tt-tabs">
|
||
<span>Clips</span><span>Tracks</span><span>Vars</span><span>Render</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HyperFrames Render mockup (right) -->
|
||
<div class="mockup mockup-right">
|
||
<div class="phone-shell">
|
||
<div class="phone-notch"></div>
|
||
<div class="phone-screen">
|
||
<div class="mockup-content ig-content">
|
||
<div class="ig-header">
|
||
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
|
||
<rect
|
||
x="3"
|
||
y="4"
|
||
width="18"
|
||
height="14"
|
||
rx="3"
|
||
fill="none"
|
||
stroke="#833ab4"
|
||
stroke-width="2"
|
||
/>
|
||
<path d="M9 9.2 15 12 9 14.8Z" fill="#fd1d1d" />
|
||
<path d="M6 21h12" stroke="#fcaf45" stroke-width="2" stroke-linecap="round" />
|
||
</svg>
|
||
Render Queue
|
||
</div>
|
||
<div class="ig-body">
|
||
<h3>Export HyperFrames scene</h3>
|
||
<div class="ig-warn">
|
||
<div class="ig-warn-title">Ready for MP4 render</div>
|
||
<div class="ig-warn-sub">lint · validate · inspect passed</div>
|
||
</div>
|
||
<div class="ig-task-label">Pipeline:</div>
|
||
<div class="ig-task">Preview locally</div>
|
||
<div class="ig-task">Validate runtime</div>
|
||
<div class="ig-task">Inspect layout</div>
|
||
<div class="ig-task">Render MP4</div>
|
||
<div class="ig-task">Publish share link</div>
|
||
<div class="ig-time">Draft render: ~45 sec</div>
|
||
</div>
|
||
<div class="ig-footer">
|
||
<div class="ig-share">Publish link</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Nine scattered HyperFrames workflow icons -->
|
||
<div class="platform-icon pi-youtube">
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<defs>
|
||
<linearGradient id="hf-orbit-grad" x1="2" y1="4" x2="22" y2="20">
|
||
<stop offset="0%" stop-color="#14b8a6" />
|
||
<stop offset="100%" stop-color="#06b6d4" />
|
||
</linearGradient>
|
||
</defs>
|
||
<path
|
||
d="M4 9.5 11.5 5c2.7-1.6 5.7.8 5.1 3.8L15 17.5c-.5 2.8-3.8 4-6 2.2L3.2 15c-2-1.6-1.5-4.2.8-5.5Z"
|
||
fill="url(#hf-orbit-grad)"
|
||
/>
|
||
<path
|
||
d="M13.2 6.5 20.8 11c2.4 1.4 2.4 4.6 0 6l-7.6 4.5c-2.6 1.5-5.8-.7-5.2-3.6l1.8-9c.5-2.8 3-3.8 3.4-2.4Z"
|
||
fill="url(#hf-orbit-grad)"
|
||
opacity="0.82"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div class="platform-icon pi-google">
|
||
<span class="icon-type"></></span>
|
||
</div>
|
||
<div class="platform-icon pi-instagram">
|
||
<span class="icon-type icon-small">GS</span>
|
||
</div>
|
||
<div class="platform-icon pi-tiktok">
|
||
<span class="icon-type icon-small">CSS</span>
|
||
</div>
|
||
<div class="platform-icon pi-tripadvisor">
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<path
|
||
d="M12 3 20 7.5v9L12 21 4 16.5v-9Z"
|
||
fill="none"
|
||
stroke="#063238"
|
||
stroke-width="1.6"
|
||
/>
|
||
<path
|
||
d="M12 3v9l8-4.5M12 12 4 7.5M12 12v9"
|
||
stroke="#06b6d4"
|
||
stroke-width="1.6"
|
||
stroke-linecap="round"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div class="platform-icon pi-facebook">
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<path
|
||
d="M4 14v-4M8 17V7M12 19V5M16 16V8M20 13v-2"
|
||
stroke="#063238"
|
||
stroke-width="2.4"
|
||
stroke-linecap="round"
|
||
/>
|
||
<path
|
||
d="M8 17V7M16 16V8"
|
||
stroke="#06b6d4"
|
||
stroke-width="2.4"
|
||
stroke-linecap="round"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div class="platform-icon pi-twitter">
|
||
<span class="icon-type icon-small">CAP</span>
|
||
</div>
|
||
<div class="platform-icon pi-linkedin">
|
||
<span class="icon-type icon-small">MP4</span>
|
||
</div>
|
||
<div class="platform-icon pi-yelp">
|
||
<span class="icon-type icon-small">PUB</span>
|
||
</div>
|
||
</div>
|
||
<!-- /.mockup-cluster -->
|
||
|
||
<!-- ===== Phase 3-4: Avatar + task bubbles ===== -->
|
||
<div class="avatar-with-bubbles" id="avatar-bubbles">
|
||
<div class="avatar-stage" id="avatar-stage">
|
||
<div class="avatar-ring">
|
||
<div class="avatar-disc"></div>
|
||
</div>
|
||
<div class="orbit-dot" id="orbit-0"></div>
|
||
<div class="orbit-dot" id="orbit-1"></div>
|
||
<div class="orbit-dot" id="orbit-2"></div>
|
||
<div class="orbit-dot" id="orbit-3"></div>
|
||
</div>
|
||
<!-- task bubbles appended by JS -->
|
||
</div>
|
||
</div>
|
||
<!-- /.scene -->
|
||
|
||
<div class="vignette"></div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ================================================================
|
||
CONSTANTS
|
||
================================================================ */
|
||
const W = 1920,
|
||
H = 1080;
|
||
const MOCKUP_CENTER_W = 320; // .mockup-center intrinsic width (matches CSS)
|
||
const MOCKUP_CENTER_H = 650;
|
||
const AVATAR_DIAMETER = 220;
|
||
const MORPH_END_SCALE_X = AVATAR_DIAMETER / MOCKUP_CENTER_W; // ≈ 0.6875
|
||
const MORPH_END_SCALE_Y = AVATAR_DIAMETER / MOCKUP_CENTER_H; // ≈ 0.3385
|
||
|
||
const TIMING = {
|
||
// Phase 1 — center stack → side-phone fan-out
|
||
mockupCenterAt: 0.08,
|
||
mockupLeftAt: 0.2,
|
||
mockupRightAt: 0.28,
|
||
mockupEntryDur: 0.7,
|
||
mockupFanDur: 1.08,
|
||
|
||
// Phase 2 — icon entries
|
||
iconsAt: 0.33,
|
||
iconEntryDur: 0.45,
|
||
iconStagger: 0.07,
|
||
|
||
// Phase 3 — morph
|
||
morphAt: 3.2,
|
||
morphDur: 0.6,
|
||
|
||
// Avatar (pop concurrent with morph; layer opacity in at 50% of morph)
|
||
avatarPopAt: 3.2,
|
||
avatarPopDur: 0.55,
|
||
avatarLayerInAt: 3.5,
|
||
avatarLayerInDur: 0.3,
|
||
|
||
// Phase 4 — bubbles
|
||
bubblesAt: 3.53,
|
||
bubbleDur: 0.45,
|
||
bubbleStagger: 0.07,
|
||
};
|
||
|
||
const BUBBLE_TASKS = [
|
||
{ label: "Missing data-duration", angle: 270, dx: 14, dy: -8 }, // top
|
||
{ label: "Timeline not registered", angle: 315, dx: 122, dy: 63 }, // top-right
|
||
{ label: "Same-track overlaps", angle: 0, dx: 86, dy: 63 }, // right
|
||
{ label: "Text overflow at 5s", angle: 45, dx: 79, dy: -9 }, // bottom-right
|
||
{ label: "Asset path mismatch", angle: 90 }, // bottom
|
||
{ label: "Non-deterministic motion", angle: 135, dx: -124, dy: -32 }, // bottom-left
|
||
{ label: "Sub-comp wiring drift", angle: 180, dx: -79, dy: 1 }, // left
|
||
{ label: "Render blocked by console", angle: 225, dx: -128, dy: 45 }, // top-left
|
||
];
|
||
const BUBBLE_RADIUS = 420; // px from screen center
|
||
const BUBBLE_CENTER_X = W / 2;
|
||
const BUBBLE_CENTER_Y = H / 2;
|
||
|
||
/* ================================================================
|
||
INITIAL STATE (GSAP owns the transform from here on)
|
||
================================================================ */
|
||
// All phones start stacked at center, then the side phones fan out like the Fitness showcase.
|
||
gsap.set(".mockup-left", {
|
||
xPercent: -50,
|
||
yPercent: -50,
|
||
x: 0,
|
||
y: 0,
|
||
rotation: 0,
|
||
rotationY: 0,
|
||
scale: 0.86,
|
||
opacity: 0,
|
||
});
|
||
gsap.set(".mockup-right", {
|
||
xPercent: -50,
|
||
yPercent: -50,
|
||
x: 0,
|
||
y: 0,
|
||
rotation: 0,
|
||
rotationY: 0,
|
||
scale: 0.86,
|
||
opacity: 0,
|
||
});
|
||
gsap.set(".mockup-center", {
|
||
xPercent: -50,
|
||
yPercent: -50,
|
||
x: 0,
|
||
y: 0,
|
||
rotation: 0,
|
||
rotationY: 0,
|
||
scale: 0.84,
|
||
opacity: 0,
|
||
});
|
||
|
||
gsap.set(".platform-icon", { scale: 0, opacity: 0 });
|
||
|
||
// Avatar — sits centered. xPercent/yPercent centering, pop starts at scale 0.
|
||
gsap.set(".avatar-stage", { xPercent: -50, yPercent: -50, scale: 0 });
|
||
|
||
/* Build task bubbles deterministically at script load.
|
||
Positions baked into inline style; GSAP only tweens scale/opacity. */
|
||
const stage = document.getElementById("avatar-bubbles");
|
||
BUBBLE_TASKS.forEach((task, i) => {
|
||
const rad = (task.angle * Math.PI) / 180;
|
||
const x = BUBBLE_CENTER_X + Math.cos(rad) * BUBBLE_RADIUS + (task.dx || 0);
|
||
const y = BUBBLE_CENTER_Y + Math.sin(rad) * BUBBLE_RADIUS + (task.dy || 0);
|
||
const el = document.createElement("div");
|
||
el.className = "task-bubble";
|
||
el.id = "bubble-" + i;
|
||
el.style.left = x + "px";
|
||
el.style.top = y + "px";
|
||
el.textContent = task.label;
|
||
stage.appendChild(el);
|
||
// GSAP-owned centering + initial hidden state.
|
||
gsap.set(el, { xPercent: -50, yPercent: -50, scale: 0, opacity: 0 });
|
||
});
|
||
|
||
/* ================================================================
|
||
TIMELINE
|
||
================================================================ */
|
||
window.__timelines = window.__timelines || {};
|
||
const tl = gsap.timeline({ paused: true });
|
||
window.__timelines["main"] = tl;
|
||
|
||
/* ---------------------------------------------------------------
|
||
PHASE 1: Mockup entries (center stack → fan-out)
|
||
spring(70, 14) → back.out(1.4)
|
||
--------------------------------------------------------------- */
|
||
tl.to(
|
||
".mockup-center",
|
||
{ opacity: 1, scale: 1.0, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
|
||
TIMING.mockupCenterAt,
|
||
);
|
||
tl.to(
|
||
".mockup-left",
|
||
{ opacity: 1, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
|
||
TIMING.mockupLeftAt,
|
||
);
|
||
tl.to(
|
||
".mockup-left",
|
||
{
|
||
x: -370,
|
||
rotation: -10,
|
||
rotationY: 6,
|
||
scale: 0.92,
|
||
duration: TIMING.mockupFanDur,
|
||
ease: "expo.out",
|
||
},
|
||
TIMING.mockupLeftAt,
|
||
);
|
||
tl.to(
|
||
".mockup-right",
|
||
{ opacity: 1, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
|
||
TIMING.mockupRightAt,
|
||
);
|
||
tl.to(
|
||
".mockup-right",
|
||
{
|
||
x: 370,
|
||
rotation: 10,
|
||
rotationY: -6,
|
||
scale: 0.92,
|
||
duration: TIMING.mockupFanDur,
|
||
ease: "expo.out",
|
||
},
|
||
TIMING.mockupRightAt,
|
||
);
|
||
|
||
/* ---------------------------------------------------------------
|
||
PHASE 2: Platform icons (staggered)
|
||
spring(180, 14) → back.out(1.6)
|
||
--------------------------------------------------------------- */
|
||
tl.to(
|
||
".platform-icon",
|
||
{
|
||
scale: 1,
|
||
opacity: 1,
|
||
duration: TIMING.iconEntryDur,
|
||
ease: "back.out(1.6)",
|
||
stagger: { each: TIMING.iconStagger, from: "start" },
|
||
},
|
||
TIMING.iconsAt,
|
||
);
|
||
|
||
/* ---------------------------------------------------------------
|
||
PHASE 3: MORPH (the core glue)
|
||
spring(80, 18) → power3.out
|
||
--------------------------------------------------------------- */
|
||
// 1. Center mockup compositor-scales toward the avatar footprint.
|
||
tl.to(
|
||
".mockup-center",
|
||
{
|
||
scaleX: MORPH_END_SCALE_X,
|
||
scaleY: MORPH_END_SCALE_Y,
|
||
duration: TIMING.morphDur,
|
||
ease: "power3.out",
|
||
},
|
||
TIMING.morphAt,
|
||
);
|
||
|
||
// 2. borderRadius repaint — reads as a circle by morph end.
|
||
tl.to(
|
||
".mockup-center",
|
||
{
|
||
borderRadius: "50%",
|
||
duration: TIMING.morphDur,
|
||
ease: "power3.out",
|
||
},
|
||
TIMING.morphAt,
|
||
);
|
||
|
||
// 3. Surface shifts: black card → cyan-gradient glow.
|
||
tl.to(
|
||
".mockup-center",
|
||
{
|
||
background: "linear-gradient(135deg, #14B8A6, #06B6D4, #0EA5E9)",
|
||
boxShadow: "0 0 50px rgba(20, 184, 166, 0.50), 0 0 100px rgba(6, 182, 212, 0.25)",
|
||
duration: TIMING.morphDur,
|
||
ease: "power3.out",
|
||
},
|
||
TIMING.morphAt,
|
||
);
|
||
|
||
// 4. Content fades during first 40% of morph (hides aspect-ratio mismatch).
|
||
tl.to(
|
||
".mockup-center .mockup-content",
|
||
{
|
||
opacity: 0,
|
||
duration: TIMING.morphDur * 0.4,
|
||
ease: "power2.out",
|
||
},
|
||
TIMING.morphAt,
|
||
);
|
||
|
||
// 4b. Fade the physical phone shell early so the morph reads as a clean surface hand-off.
|
||
tl.to(
|
||
".mockup-center .phone-shell",
|
||
{
|
||
opacity: 0,
|
||
duration: TIMING.morphDur * 0.45,
|
||
ease: "power2.out",
|
||
},
|
||
TIMING.morphAt,
|
||
);
|
||
|
||
// 5. Hand-off: at 85-100% of morph, container fades to 0 → avatar reveals underneath.
|
||
tl.to(
|
||
".mockup-center",
|
||
{
|
||
opacity: 0,
|
||
duration: TIMING.morphDur * 0.15,
|
||
ease: "none",
|
||
},
|
||
TIMING.morphAt + TIMING.morphDur * 0.85,
|
||
);
|
||
|
||
/* Non-center mockups exit concurrently with morph */
|
||
tl.to(
|
||
[".mockup-left", ".mockup-right"],
|
||
{
|
||
opacity: 0,
|
||
scale: 0.85,
|
||
duration: TIMING.morphDur * 0.55,
|
||
ease: "power2.out",
|
||
},
|
||
TIMING.morphAt,
|
||
);
|
||
|
||
/* Platform icons exit concurrently with morph (edge-out stagger) */
|
||
tl.to(
|
||
".platform-icon",
|
||
{
|
||
opacity: 0,
|
||
scale: 0.85,
|
||
duration: TIMING.morphDur * 0.5,
|
||
ease: "power2.out",
|
||
stagger: { each: 0.025, from: "edges" },
|
||
},
|
||
TIMING.morphAt,
|
||
);
|
||
|
||
/* Avatar pop — spring(120, 14) → back.out(1.4), concurrent with morph trigger */
|
||
tl.fromTo(
|
||
".avatar-stage",
|
||
{ scale: 0 },
|
||
{ scale: 1, duration: TIMING.avatarPopDur, ease: "back.out(1.4)" },
|
||
TIMING.avatarPopAt,
|
||
);
|
||
|
||
/* Avatar layer opacity in at 50% of morph */
|
||
tl.to(
|
||
"#avatar-bubbles",
|
||
{
|
||
opacity: 1,
|
||
duration: TIMING.avatarLayerInDur,
|
||
ease: "power2.out",
|
||
},
|
||
TIMING.avatarLayerInAt,
|
||
);
|
||
|
||
/* ---------------------------------------------------------------
|
||
PHASE 4: Task bubbles (staggered radial entry)
|
||
spring(180, 12) → back.out(1.4)
|
||
--------------------------------------------------------------- */
|
||
tl.to(
|
||
".task-bubble",
|
||
{
|
||
scale: 1,
|
||
opacity: 0.95,
|
||
duration: TIMING.bubbleDur,
|
||
ease: "back.out(1.4)",
|
||
stagger: { each: TIMING.bubbleStagger, from: "start" },
|
||
},
|
||
TIMING.bubblesAt,
|
||
);
|
||
|
||
/* ---------------------------------------------------------------
|
||
CONTINUOUS MOTION (shared scene-ticker onUpdate)
|
||
Reads tl.time() each frame; cheaper than N independent onUpdates.
|
||
Drives:
|
||
- mockup floating (Phase 1-2 only — gated to t < morphAt)
|
||
- platform-icon floating (same)
|
||
- orbit dots around avatar
|
||
- avatar breath (multiplicative on pop scale)
|
||
- bubble micro-float (after bubblesAt)
|
||
--------------------------------------------------------------- */
|
||
const orbitDots = [
|
||
document.getElementById("orbit-0"),
|
||
document.getElementById("orbit-1"),
|
||
document.getElementById("orbit-2"),
|
||
document.getElementById("orbit-3"),
|
||
];
|
||
const ORBIT_RADIUS = 130;
|
||
const ORBIT_SPEED = 2; // degrees per timeline-time unit (i.e. per second × 30)
|
||
|
||
const avatarStage = document.getElementById("avatar-stage");
|
||
const bubbleEls = Array.from(document.querySelectorAll(".task-bubble"));
|
||
const mockupLeft = document.querySelector(".mockup-left");
|
||
const mockupRight = document.querySelector(".mockup-right");
|
||
const platformIcons = Array.from(document.querySelectorAll(".platform-icon"));
|
||
|
||
tl.to(
|
||
{ tick: 0 },
|
||
{
|
||
tick: 1,
|
||
duration: 6.0,
|
||
ease: "none",
|
||
onUpdate: function () {
|
||
const t = tl.time();
|
||
|
||
/* Mockup + icon floating (only while they're visible — pre-morph) */
|
||
if (t < TIMING.morphAt) {
|
||
const mlY = Math.sin(t * 0.36) * 3;
|
||
const mrY = Math.sin(t * 0.45 + 1) * 3;
|
||
gsap.set(mockupLeft, { y: mlY });
|
||
gsap.set(mockupRight, { y: mrY });
|
||
|
||
platformIcons.forEach((el, i) => {
|
||
const fx = Math.sin((t * 30 + i * 30) * 0.025) * 5;
|
||
const fy = Math.cos((t * 30 + i * 20) * 0.03) * 4;
|
||
gsap.set(el, { x: fx, y: fy });
|
||
});
|
||
}
|
||
|
||
/* Avatar breath (multiplicative on pop scale, only after pop completes) */
|
||
const popEnd = TIMING.avatarPopAt + TIMING.avatarPopDur;
|
||
if (t >= popEnd) {
|
||
const idleT = t - popEnd;
|
||
const breath = 1 + Math.sin(idleT * 0.72) * 0.03;
|
||
// popScale at this point is 1.0 (back.out settled). Multiplicative → still 1.0 × breath.
|
||
gsap.set(avatarStage, { scale: 1 * breath });
|
||
}
|
||
|
||
/* Orbit dots — always cycling once avatar layer is visible */
|
||
if (t >= TIMING.avatarLayerInAt) {
|
||
orbitDots.forEach((dot, i) => {
|
||
const angle = (t * 30 * ORBIT_SPEED + i * 90) * (Math.PI / 180);
|
||
const x = Math.cos(angle) * ORBIT_RADIUS;
|
||
const y = Math.sin(angle) * ORBIT_RADIUS;
|
||
const op = 0.7 + Math.sin(t * 30 * 0.1 + i) * 0.3;
|
||
gsap.set(dot, { x: x, y: y, opacity: op });
|
||
});
|
||
}
|
||
|
||
/* Bubble micro-float (after they enter) */
|
||
if (t >= TIMING.bubblesAt + TIMING.bubbleDur) {
|
||
bubbleEls.forEach((el, i) => {
|
||
const fx = Math.sin((t * 30 + i * 24) * 0.015) * 5;
|
||
const fy = Math.cos((t * 30 + i * 30) * 0.018) * 4;
|
||
gsap.set(el, { x: fx, y: fy });
|
||
});
|
||
}
|
||
},
|
||
},
|
||
0,
|
||
);
|
||
</script>
|
||
</body>
|
||
</html>
|