mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-11 19:09:20 +00:00
* feat(cli): one owner for init media placeholders, audio stays on the video Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(cli): init keeps a video's sound on the clip; silent files scaffold muted Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com> Co-Authored-By: kapelame <kapelame@users.noreply.github.com> * feat(registry): example templates keep the a-roll's sound on the video Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lint): video audio rules recommend keeping sound on the video Update video_missing_muted and video_audio_double_source message and fixHint text to recommend the new default: keep audio on the video with data-has-audio="true" (not muted). Tests verify the new wording is in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(parsers): a video is either audible (data-has-audio) or muted, never both Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): follow the compiler's audio rule in the audio-track toggle Confirm before background removal drops a video's sound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): hyperframes-core teaches sound on the video by default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): workflows and scaffold guidance keep a video's sound on the clip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: footage with sound keeps it on the video clip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): init from-file a-roll is timed, muted strip is attribute-exact The from-file a-roll video gets data-start="0" so a scaffold lints clean, and the orphaned audio-only slot comment is dropped. setVideoAudio now walks the tag attribute by attribute, so a class value containing the word muted survives. The ffprobe-missing warning says the video is assumed to have sound and how to switch to muted, and DEFAULT_META notes the failure surfaces at render until lint support lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): keep has-audio in step with the muted toggle, test cutout confirm Muting a video now clears has-audio and unmuting sets it to true, in both the Design and flat media panels, matching the Has audio track toggle. Audio elements are unchanged. Adds coverage for both panels plus the cutout window.confirm guard: declined leaves src alone, accepted swaps src and mutes, a silent video never prompts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false An audio element or an undefined hasAudio no longer picks up a muted attribute; only an explicit false on a video writes muted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: video_missing_muted rule text, silent-footage notes, consistent audible examples lint.mdx and cli.mdx describe the current video_missing_muted rule instead of the retired unmuted-video wording. The editing recipes say how to adapt for silent footage and require the outgoing clip's sound to end at a J-cut. The Remotion media example shows the audible mapping consistently, data-attributes states that has-audio is required unless muted, and the hairline frame note no longer claims the video is muted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): encode init media filenames and probe --audio duration Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded. --audio now passes the probed track length as the composition duration in both init paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lint): audio-state-aware media rules and time-aware double-source check video_missing_muted honors any explicit data-has-audio. video_audio_double_source only considers audible videos and only fires when the same-src clips overlap in time. duplicate_audio_track now covers audible videos. audio_file_without_element no longer claims silence when an audible video carries the sound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): audio-stream check for remotion ports, followable J-cut recipe Remotion Video maps to data-has-audio only when the file has an audio stream. The J/L-cut recipe now shows both shots with muted pictures and separate audio. embedded-captions removes the audio.mp3 scratch file it extracts. skills-manifest.json regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(lint): trim comments to satisfy the comment ratchet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): atomic muted edits, cutout keeps sound on a linked audio Muted and Has audio track commit one batched write (one persist, one undo entry) and remove data-has-audio instead of writing an empty marker. Unmuting a video probes its src: audible -> true, silent -> false, probe failed -> absent. Remove background on an audible video inserts a linked sibling <audio> with the original sound in the same write, replacing the drop-the-sound confirm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(producer): skip hidden videos when auto-adding video audio A video hidden in Studio is dropped by the mixer, so its src never reached existingAudioSrcs and the render re-added it at full volume. The video parser now records the shared hidden predicate and the auto-detect step skips hidden videos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): keep useDomEditSession under the 600-line cap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com> Co-authored-by: kapelame <kapelame@users.noreply.github.com>
607 lines
20 KiB
HTML
607 lines
20 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1920, height=1080" />
|
||
<title>Scene — HyperFrames Workflow Approve & Press</title>
|
||
|
||
<!--
|
||
Blueprint: workflow-approve-press
|
||
HyperFrames composition.tsx.
|
||
Same four-phase choreography, GSAP-driven, seek-safe.
|
||
|
||
Audio (assumed accompanying narration):
|
||
"This is AI that renders with you, not just for you.
|
||
Preview the result, render to MP4, then confirm."
|
||
|
||
Sub-phases (local time, 5.5s total):
|
||
0.17 – 0.72s Headline "AI edits WITH you" slides down
|
||
0.50 – 1.10s Center editor mockup scales in
|
||
0.67 – 2.17s Step indicators stagger-enter on the left (3D-tilted)
|
||
2.00 – 3.33s Step state machine: 1→complete, 2→active … 2→complete, 3→active
|
||
3.52 – 4.02s Confirm MP4 button (3D-tilted right) bouncy entry after render step is active
|
||
4.22 – 4.72s Button depression (linear, no overshoot)
|
||
4.72 – 5.22s Render confirmed: step 3 completes, color shifts to success green + checkmark pop
|
||
|
||
Asset placeholders:
|
||
./assets/editor-demo.mp4 — center product demo video. Replace with a
|
||
real export of the AI-editor flow. The
|
||
composition falls back to a CSS-mockup of
|
||
an editor timeline so it renders without
|
||
network or media files.
|
||
-->
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||
|
||
<style>
|
||
:root {
|
||
/* Palette — port of the the source COLORS / TYPOGRAPHY constants */
|
||
--text-primary: #ffffff;
|
||
--text-secondary: #cbd5e1;
|
||
--text-muted: #94a3b8;
|
||
--brand-purple: #a78bfa;
|
||
--brand-purple-glow: rgba(167, 139, 250, 0.6);
|
||
--brand-green: #22c55e;
|
||
--brand-green-glow: rgba(34, 197, 94, 0.6);
|
||
--panel-stroke: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
background: #000;
|
||
font-family:
|
||
"Inter",
|
||
system-ui,
|
||
-apple-system,
|
||
sans-serif;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* ============================================================
|
||
BACKGROUND — port of <FrostedGlassBackground />
|
||
============================================================ */
|
||
.bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
background:
|
||
radial-gradient(ellipse at 25% 30%, rgba(167, 139, 250, 0.2), transparent 55%),
|
||
radial-gradient(ellipse at 80% 70%, rgba(34, 197, 94, 0.12), transparent 55%),
|
||
linear-gradient(135deg, #1a1530 0%, #0a0815 70%);
|
||
}
|
||
|
||
/* Ambient warm glow on the right (button side) */
|
||
.ambient {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(
|
||
ellipse at 70% 50%,
|
||
var(--brand-purple-glow) 0%,
|
||
transparent 40%
|
||
);
|
||
opacity: 0.15;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Vignette */
|
||
.vignette {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.4) 100%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ============================================================
|
||
PHASE 1 — HEADLINE
|
||
============================================================ */
|
||
.headline-wrap {
|
||
position: absolute;
|
||
top: 80px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
opacity: 0; /* GSAP fades in */
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
}
|
||
.headline {
|
||
font-size: 96px;
|
||
font-weight: 800;
|
||
line-height: 1.2;
|
||
letter-spacing: -0.01em;
|
||
color: var(--text-primary);
|
||
}
|
||
.headline .accent {
|
||
color: var(--brand-purple);
|
||
text-shadow: 0 0 30px var(--brand-purple-glow);
|
||
}
|
||
|
||
/* ============================================================
|
||
PHASE 2 — CENTER DEMO
|
||
(Falls back to a CSS-mockup editor when ./assets/editor-demo.mp4
|
||
is missing. The mockup is purely decorative — no animation
|
||
inside it; the eye reads it as a paused product screenshot.)
|
||
============================================================ */
|
||
.demo-wrap {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 60%;
|
||
opacity: 0;
|
||
}
|
||
.demo-frame {
|
||
width: 1000px;
|
||
height: 600px;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
border: 2px solid var(--panel-stroke);
|
||
box-shadow:
|
||
0 0 40px var(--brand-purple-glow),
|
||
0 20px 60px rgba(0, 0, 0, 0.5);
|
||
position: relative;
|
||
}
|
||
.demo-frame video {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* CSS-mockup fallback for the editor demo */
|
||
.demo-mockup {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: grid;
|
||
grid-template-rows: 56px 1fr 100px;
|
||
background: linear-gradient(180deg, #1f1b3a 0%, #15112a 100%);
|
||
}
|
||
.mockup-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 0 20px;
|
||
background: #15112a;
|
||
border-bottom: 1px solid var(--panel-stroke);
|
||
}
|
||
.mockup-toolbar .dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.18);
|
||
}
|
||
.mockup-toolbar .dot.r {
|
||
background: #ef4444;
|
||
}
|
||
.mockup-toolbar .dot.y {
|
||
background: #eab308;
|
||
}
|
||
.mockup-toolbar .dot.g {
|
||
background: #22c55e;
|
||
}
|
||
.mockup-toolbar .title {
|
||
margin-left: 18px;
|
||
color: var(--text-secondary);
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
.mockup-canvas {
|
||
position: relative;
|
||
padding: 30px;
|
||
overflow: hidden;
|
||
background:
|
||
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 40px),
|
||
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 40px);
|
||
}
|
||
.mockup-canvas .preview-box {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 10px;
|
||
background: linear-gradient(135deg, rgba(167, 139, 250, 0.35), rgba(34, 197, 94, 0.2));
|
||
border: 1px solid var(--panel-stroke);
|
||
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.4);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-secondary);
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
}
|
||
.mockup-timeline {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding: 14px 20px;
|
||
background: #15112a;
|
||
border-top: 1px solid var(--panel-stroke);
|
||
}
|
||
.mockup-timeline .track {
|
||
height: 24px;
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
.mockup-timeline .clip {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
}
|
||
.mockup-timeline .t1 .a {
|
||
flex: 3;
|
||
background: var(--brand-purple);
|
||
opacity: 0.85;
|
||
}
|
||
.mockup-timeline .t1 .b {
|
||
flex: 2;
|
||
background: var(--brand-purple);
|
||
opacity: 0.55;
|
||
}
|
||
.mockup-timeline .t1 .c {
|
||
flex: 4;
|
||
background: var(--brand-purple);
|
||
opacity: 0.85;
|
||
}
|
||
.mockup-timeline .t2 .a {
|
||
flex: 5;
|
||
background: var(--brand-green);
|
||
opacity: 0.75;
|
||
}
|
||
.mockup-timeline .t2 .b {
|
||
flex: 2;
|
||
background: var(--brand-green);
|
||
opacity: 0.45;
|
||
}
|
||
.mockup-timeline .t2 .c {
|
||
flex: 2;
|
||
background: var(--brand-green);
|
||
opacity: 0.75;
|
||
}
|
||
|
||
/* ============================================================
|
||
PHASE 3 — LEFT FLANK: STEP INDICATORS (3D-TILTED)
|
||
============================================================ */
|
||
.steps-flank {
|
||
position: absolute;
|
||
left: 100px;
|
||
top: 55%;
|
||
transform: translateY(-50%) perspective(800px) rotateY(15deg);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 30px;
|
||
}
|
||
.step {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
opacity: 0; /* GSAP fades in */
|
||
}
|
||
.step-circle {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 2px solid var(--text-muted);
|
||
background-color: transparent;
|
||
box-shadow: none;
|
||
transition: none; /* Steps SNAP between states, no CSS transition */
|
||
flex-shrink: 0;
|
||
}
|
||
.step-num {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
}
|
||
.step-check {
|
||
display: none;
|
||
}
|
||
.step-label {
|
||
font-size: 28px;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* State machine — toggled via tl.set({ attr: data-state }) */
|
||
.step[data-state="active"] .step-circle {
|
||
border-color: var(--brand-purple);
|
||
box-shadow: 0 0 15px var(--brand-purple-glow);
|
||
}
|
||
.step[data-state="active"] .step-num {
|
||
color: var(--brand-purple);
|
||
}
|
||
.step[data-state="active"] .step-label {
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.step[data-state="complete"] .step-circle {
|
||
border-color: var(--brand-green);
|
||
background-color: var(--brand-green);
|
||
}
|
||
.step[data-state="complete"] .step-num {
|
||
display: none;
|
||
}
|
||
.step[data-state="complete"] .step-check {
|
||
display: inline-flex;
|
||
}
|
||
|
||
/* ============================================================
|
||
PHASE 4 — RIGHT FLANK: CONFIRM BUTTON (3D-TILTED)
|
||
============================================================ */
|
||
.button-flank {
|
||
position: absolute;
|
||
right: 70px;
|
||
top: 55%;
|
||
transform: translateY(-50%) perspective(800px) rotateY(-15deg);
|
||
}
|
||
.btn-press {
|
||
opacity: 0;
|
||
transform-origin: center center;
|
||
}
|
||
.btn {
|
||
--btn-glow-blur: 20px;
|
||
padding: 20px 50px;
|
||
border-radius: 12px;
|
||
background-color: var(--brand-purple);
|
||
box-shadow: 0 0 var(--btn-glow-blur) var(--brand-purple-glow);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
color: var(--text-primary);
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
.btn-check {
|
||
display: inline-flex;
|
||
transform-origin: center center;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div
|
||
id="root"
|
||
data-composition-id="interactive-workflow"
|
||
data-start="0"
|
||
data-duration="5.5"
|
||
data-width="1920"
|
||
data-height="1080"
|
||
style="position: relative; width: 1920px; height: 1080px; overflow: hidden"
|
||
>
|
||
<!-- Background layers -->
|
||
<div class="bg"></div>
|
||
|
||
<!-- PHASE 1: Headline -->
|
||
<div class="headline-wrap">
|
||
<div class="headline">
|
||
HyperFrames builds
|
||
<span class="accent">WITH</span>
|
||
you
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PHASE 2: Center demo (video with CSS-mockup fallback) -->
|
||
<div class="demo-wrap">
|
||
<div class="demo-frame">
|
||
<!--
|
||
Real asset path (replace for production).
|
||
If missing, the CSS mockup below shows through the transparent
|
||
poster region. Muted: decorative footage.
|
||
-->
|
||
<!-- Decorative CSS-mockup of an editor — paused screenshot feel. -->
|
||
<div class="demo-mockup">
|
||
<div class="mockup-toolbar">
|
||
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
||
<span class="title">HyperFrames — Project: Launch Promo</span>
|
||
</div>
|
||
<div class="mockup-canvas">
|
||
<div class="preview-box">Preview · 1920 × 1080</div>
|
||
</div>
|
||
<div class="mockup-timeline">
|
||
<div class="track t1">
|
||
<div class="clip a"></div>
|
||
<div class="clip b"></div>
|
||
<div class="clip c"></div>
|
||
</div>
|
||
<div class="track t2">
|
||
<div class="clip a"></div>
|
||
<div class="clip b"></div>
|
||
<div class="clip c"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PHASE 3: Left-flank step indicators (3D-tilted +15°) -->
|
||
<div class="steps-flank">
|
||
<div class="step step-1" data-step="1" data-state="pending">
|
||
<div class="step-circle">
|
||
<span class="step-num">1</span>
|
||
<svg class="step-check" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||
<path d="M20 6L9 17L4 12" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<span class="step-label">Compose HTML Scene</span>
|
||
</div>
|
||
|
||
<div class="step step-2" data-step="2" data-state="pending">
|
||
<div class="step-circle">
|
||
<span class="step-num">2</span>
|
||
<svg class="step-check" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||
<path d="M20 6L9 17L4 12" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<span class="step-label">Seek & Preview</span>
|
||
</div>
|
||
|
||
<div class="step step-3" data-step="3" data-state="pending">
|
||
<div class="step-circle">
|
||
<span class="step-num">3</span>
|
||
<svg class="step-check" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||
<path d="M20 6L9 17L4 12" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<span class="step-label">Render to MP4</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PHASE 4: Right-flank confirm button (3D-tilted -15°) -->
|
||
<div class="button-flank">
|
||
<div class="btn-press">
|
||
<div class="btn">
|
||
<span class="btn-check">
|
||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||
<path
|
||
d="M20 6L9 17L4 12"
|
||
stroke="#fff"
|
||
stroke-width="3"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
</span>
|
||
<span class="btn-label">Confirm MP4</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ambient + vignette overlays (pointer-events: none) -->
|
||
<div class="ambient"></div>
|
||
<div class="vignette"></div>
|
||
</div>
|
||
|
||
<script>
|
||
window.__timelines = window.__timelines || {};
|
||
const tl = gsap.timeline({ paused: true });
|
||
|
||
// ── Phase boundaries (seconds) — match the blueprint ────────────
|
||
const HEADLINE_START = 0.17;
|
||
const HEADLINE_END = 0.72;
|
||
const VIDEO_START = 0.5;
|
||
const STEPS_START = 0.67;
|
||
const STEP_STAGGER = 0.5;
|
||
const STEP_ACTIVE_T2 = 2.0; // step 1 → complete, step 2 → active
|
||
const STEP_ACTIVE_T3 = 3.33; // step 2 → complete, step 3 → active
|
||
const BUTTON_ENTER = 3.52; // after "Render to MP4" becomes active
|
||
const PRESS_FRAME = 4.22;
|
||
const PRESS_DURATION = 0.5;
|
||
const CHECK_POP = PRESS_FRAME + PRESS_DURATION;
|
||
const SCENE_END = 5.5; // matches data-duration on the root
|
||
|
||
// ── Phase 1: Headline slides down from top ─────────────────────
|
||
gsap.set(".demo-wrap", { xPercent: -50, yPercent: -50, scale: 0.8, opacity: 0 });
|
||
gsap.set(".step", { x: -30, opacity: 0 });
|
||
gsap.set(".btn-press", { scale: 0, opacity: 0 });
|
||
gsap.set(".btn-check", { scale: 0 });
|
||
|
||
tl.fromTo(
|
||
".headline-wrap",
|
||
{ y: -40, opacity: 0 },
|
||
{
|
||
y: 0,
|
||
opacity: 1,
|
||
duration: HEADLINE_END - HEADLINE_START,
|
||
ease: "back.out(1.2)", // ≈ the source SPRING_CONFIGS.entrance
|
||
},
|
||
HEADLINE_START,
|
||
);
|
||
|
||
// ── Phase 2: Center demo scales in ─────────────────────────────
|
||
// HyperFrames syncs the <video> element's currentTime to the seek
|
||
// position automatically — no .play() call needed (and one here
|
||
// would break deterministic rendering).
|
||
tl.to(".demo-wrap", { scale: 1, opacity: 1, duration: 0.6, ease: "power3.out" }, VIDEO_START);
|
||
|
||
// ── Phase 3a: Stagger entry of the step indicators ─────────────
|
||
tl.to(
|
||
".step",
|
||
{
|
||
x: 0,
|
||
opacity: 1,
|
||
duration: 0.4,
|
||
ease: "power3.out",
|
||
stagger: { each: STEP_STAGGER, from: "start" },
|
||
},
|
||
STEPS_START,
|
||
);
|
||
|
||
// ── Phase 3b: Step state machine (snap-toggled here) ──
|
||
// Step 1 enters in the "active" state because narration starts on it.
|
||
tl.set(".step-1", { attr: { "data-state": "active" } }, STEPS_START);
|
||
|
||
// First transition — step 1 finishes, step 2 takes over.
|
||
tl.set(".step-1", { attr: { "data-state": "complete" } }, STEP_ACTIVE_T2);
|
||
tl.set(".step-2", { attr: { "data-state": "active" } }, STEP_ACTIVE_T2);
|
||
|
||
// Second transition — step 2 finishes, step 3 takes over.
|
||
tl.set(".step-2", { attr: { "data-state": "complete" } }, STEP_ACTIVE_T3);
|
||
tl.set(".step-3", { attr: { "data-state": "active" } }, STEP_ACTIVE_T3);
|
||
|
||
// ── Phase 4a: Button bouncy entry ──────────────────────────────
|
||
tl.to(
|
||
".btn-press",
|
||
{ scale: 1, opacity: 1, duration: 0.5, ease: "back.out(1.4)", overwrite: "auto" },
|
||
BUTTON_ENTER,
|
||
);
|
||
|
||
// ── Phase 4b: Press (linear depression → linear return) ────────
|
||
// Two adjacent tweens on .btn-press — end value of (1) = start value of (2).
|
||
tl.to(".btn-press", { scale: 0.95, duration: 0.1, ease: "power1.out" }, PRESS_FRAME);
|
||
tl.to(
|
||
".btn-press",
|
||
{ scale: 1.0, duration: PRESS_DURATION - 0.1, ease: "power1.in" },
|
||
PRESS_FRAME + 0.1,
|
||
);
|
||
|
||
// ── Phase 4c: Color shift + label swap (at press end) ──────────
|
||
tl.to(
|
||
".btn",
|
||
{
|
||
backgroundColor: "#15803d",
|
||
boxShadow: "0 0 25px rgba(21, 128, 61, 0.68)",
|
||
duration: 0.3,
|
||
ease: "power2.out",
|
||
},
|
||
CHECK_POP,
|
||
);
|
||
tl.set(".btn-label", { textContent: "Approved!" }, CHECK_POP);
|
||
tl.set(".step-3", { attr: { "data-state": "complete" } }, CHECK_POP);
|
||
|
||
// ── Phase 4d: Checkmark pops into view ─────────────────────────
|
||
// back.out(1.6) ≈ the source spring({ stiffness: 200, damping: 15 }).
|
||
tl.to(".btn-check", { scale: 1, duration: 0.5, ease: "back.out(1.6)" }, CHECK_POP);
|
||
|
||
// ── Ambient pulsing glow on the button (Math.sin) ──
|
||
// Source: `glowIntensity = 20 + Math.sin(frame * 0.1) * 10` — i.e. blur
|
||
// oscillates 10 ↔ 30 around 20, with angular freq 0.1 rad/frame.
|
||
// At 60fps that's 6 rad/s, so full period ≈ 2π / 6 ≈ 1.05s.
|
||
// Finite yoyo replaces the continuous sine; `box-shadow` reads the
|
||
// tweened CSS variable so the blur is actually animatable.
|
||
const PULSE_PERIOD = 1.05;
|
||
const PULSE_HALVES = Math.max(2, Math.floor((SCENE_END - BUTTON_ENTER) / (PULSE_PERIOD / 2)));
|
||
tl.fromTo(
|
||
".btn",
|
||
{ "--btn-glow-blur": "10px" }, // source amplitude: 20 ± 10
|
||
{
|
||
"--btn-glow-blur": "30px",
|
||
duration: PULSE_PERIOD / 2,
|
||
ease: "sine.inOut",
|
||
yoyo: true,
|
||
repeat: PULSE_HALVES - 1,
|
||
},
|
||
BUTTON_ENTER,
|
||
);
|
||
|
||
window.__timelines["interactive-workflow"] = tl;
|
||
</script>
|
||
</body>
|
||
</html>
|