mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-05 05:37:09 +00:00
feat(cli): move skills to repo root, support multi-CLI install
- Move skills from .claude/skills/ to skills/ (tool-agnostic location) - Install to Claude Code, Gemini CLI, Codex CLI by default - Add --claude, --gemini, --codex, --cursor flags for targeting specific tools - Update build script to copy from skills/ instead of .claude/skills/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Vance Ingalls
co-authored by
Claude Opus 4.6
parent
4f07e7f804
commit
ccee93bb99
@@ -0,0 +1,804 @@
|
||||
# Emotion-to-Motion Iteration 3 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add multi-scene choreography phases, mood transitions, and intra-scene choreography patterns to the emotion-to-motion skill, then evaluate with 30-60s compositions.
|
||||
|
||||
**Architecture:** The existing SKILL.md gets three new sections appended (phases, transition matrix, intra-scene choreography). A new evals.json replaces the old one with 2 multi-scene test cases. A/B compositions (with_skill vs without_skill) are generated, deployed to HyperFrames Studio, and graded against 8 criteria.
|
||||
|
||||
**Tech Stack:** GSAP 3.14.2 (CustomEase, SplitText, CustomBounce, CustomWiggle), HyperFrames composition format, HyperFrames Studio for preview
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-25-emotion-to-motion-iter3-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Action | Path | Responsibility |
|
||||
|--------|------|---------------|
|
||||
| Modify | `~/.claude/skills/emotion-to-motion/SKILL.md` | Add 3 new sections after existing mood recipes |
|
||||
| Modify | `~/.claude/skills/emotion-to-motion/evals/evals.json` | Replace with 2 multi-scene test cases |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/eval_metadata.json` | Test case 1 metadata |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/outputs/index.html` | Test case 1 with-skill composition |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/outputs/grading.json` | Test case 1 with-skill grading |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/timing.json` | Test case 1 with-skill token/time metrics |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/outputs/index.html` | Test case 1 without-skill composition |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/outputs/grading.json` | Test case 1 without-skill grading |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/timing.json` | Test case 1 without-skill token/time metrics |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/eval_metadata.json` | Test case 2 metadata |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/with_skill/outputs/index.html` | Test case 2 with-skill composition |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/with_skill/outputs/grading.json` | Test case 2 with-skill grading |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/with_skill/timing.json` | Test case 2 with-skill token/time metrics |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/without_skill/outputs/index.html` | Test case 2 without-skill composition |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/without_skill/outputs/grading.json` | Test case 2 without-skill grading |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/without_skill/timing.json` | Test case 2 without-skill token/time metrics |
|
||||
| Create | `~/.claude/skills/emotion-to-motion-workspace/iteration-3/benchmark.json` | Aggregate scores across both test cases |
|
||||
| Create | `packages/studio/data/projects/high-contrast-arc-with-skill/index.html` | Studio preview for case 1 with-skill |
|
||||
| Create | `packages/studio/data/projects/high-contrast-arc-without-skill/index.html` | Studio preview for case 1 without-skill |
|
||||
| Create | `packages/studio/data/projects/gradual-escalation-with-skill/index.html` | Studio preview for case 2 with-skill |
|
||||
| Create | `packages/studio/data/projects/gradual-escalation-without-skill/index.html` | Studio preview for case 2 without-skill |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update SKILL.md — Add Choreography Phases Section
|
||||
|
||||
**Files:**
|
||||
- Modify: `~/.claude/skills/emotion-to-motion/SKILL.md` (append after line 269, after the "Punchy / Bold" recipe)
|
||||
|
||||
- [ ] **Step 1: Read the existing SKILL.md to confirm current end**
|
||||
|
||||
Read `~/.claude/skills/emotion-to-motion/SKILL.md` — verify it ends at line 269 with the Punchy/Bold recipe and there's no existing multi-scene content.
|
||||
|
||||
- [ ] **Step 2: Append the Choreography Phases section**
|
||||
|
||||
Add the following after the last line of SKILL.md:
|
||||
|
||||
```markdown
|
||||
|
||||
## Multi-Scene Compositions: Choreography Phases
|
||||
|
||||
For compositions longer than 15 seconds or with multiple moods, decompose the prompt into **phases** — ordered segments, each with its own mood and choreography.
|
||||
|
||||
### Phase Structure
|
||||
|
||||
Each phase has:
|
||||
- **Mood** — one canonical mood (dramatic, playful, elegant, energetic, calm, tense, punchy)
|
||||
- **Duration** — seconds in the master timeline
|
||||
- **Content** — what's visually happening
|
||||
- **Entrance → Emphasis → Exit** — choreography pattern (see Intra-Scene Choreography below)
|
||||
|
||||
### Mood Synonym Resolution
|
||||
|
||||
Resolve evocative language to canonical moods before decomposing:
|
||||
|
||||
| Synonym | Canonical Mood |
|
||||
|---------|---------------|
|
||||
| explosive, intense, fierce | punchy |
|
||||
| serene, peaceful, tranquil | calm |
|
||||
| triumphant, victorious, celebratory | playful |
|
||||
| suspenseful, ominous, creeping | tense |
|
||||
| luxurious, refined, sophisticated | elegant |
|
||||
| exciting, dynamic, high-energy | energetic |
|
||||
| cinematic, epic, grand | dramatic |
|
||||
|
||||
### Phase Decomposition
|
||||
|
||||
Handle two input styles:
|
||||
|
||||
**Explicit per-scene moods** — extract mood (resolving synonyms), duration, and content directly:
|
||||
> "Scene 1 (tense, 15s): text fragments drift. Scene 2 (punchy, 10s): title shatters. Scene 3 (calm, 20s): debris floats."
|
||||
|
||||
**Arc-based descriptions** — decompose into phases:
|
||||
> "Build from quiet unease to cathartic release to calm aftermath"
|
||||
|
||||
1. Map mood anchors: quiet unease → tense, cathartic release → dramatic, calm aftermath → calm
|
||||
2. Allocate durations by weight:
|
||||
|
||||
| Mood | Weight |
|
||||
|------|--------|
|
||||
| calm, elegant | 1.2 |
|
||||
| dramatic, tense | 1.0 |
|
||||
| energetic, playful | 0.9 |
|
||||
| punchy | 0.7 |
|
||||
|
||||
Formula: `phase_duration = (weight / sum_of_all_weights) * target_total`, rounded to nearest second.
|
||||
|
||||
**Minimum phase duration:** 6s — clamp and redistribute from the longest phase.
|
||||
|
||||
Expect 3-5 phases for 30-60s compositions. Allow up to 7 if the prompt explicitly calls for more.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- **Single mood for 30-60s:** One phase, no transitions. Apply entrance/emphasis/exit across the full duration.
|
||||
- **Adjacent same-mood phases:** Merge unless content descriptions feature different primary subjects (e.g., "product close-up" vs "lifestyle scene"). If kept separate, use a soft transition.
|
||||
- **More than 7 phases:** Consolidate phases with similar moods.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the append**
|
||||
|
||||
Read `~/.claude/skills/emotion-to-motion/SKILL.md` from line 269 onward — confirm the Choreography Phases section is present and correctly formatted.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/emotion-to-motion && git add SKILL.md && git commit -m "feat(skill): add choreography phases section for multi-scene compositions"
|
||||
```
|
||||
|
||||
(If not a git repo, skip commit — this is a local skill file.)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update SKILL.md — Add Transition Matrix Section
|
||||
|
||||
**Files:**
|
||||
- Modify: `~/.claude/skills/emotion-to-motion/SKILL.md` (append after Choreography Phases section)
|
||||
|
||||
- [ ] **Step 1: Append the Transition Matrix section**
|
||||
|
||||
Add immediately after the Edge Cases subsection:
|
||||
|
||||
```markdown
|
||||
|
||||
## Transition Matrix
|
||||
|
||||
Transitions between phases are inferred from the mood delta. Three categories:
|
||||
|
||||
### Hard Transitions (high contrast)
|
||||
- **Overlap:** 0.3-0.8s
|
||||
- **Technique:** Scene N's elements snap out (opacity: 0, scale: 0.95) while Scene N+1's elements slam in simultaneously
|
||||
- **GSAP pattern:**
|
||||
```js
|
||||
// Phase N exit — hard snap out
|
||||
phaseTl.to(".phase-n-elements", { opacity: 0, scale: 0.95, duration: 0.3, ease: "power3.in" }, exitStart);
|
||||
// Phase N+1 entrance — slam in (overlaps exit by 0.3-0.8s)
|
||||
nextPhaseTl.from(".phase-n1-elements", { opacity: 0, scale: 1.1, duration: 0.4, ease: "power4.out" }, 0);
|
||||
```
|
||||
|
||||
### Soft Transitions (low contrast)
|
||||
- **Overlap:** 1.5-3s
|
||||
- **Technique:** Scene N's elements slowly fade/drift out while Scene N+1's elements gradually emerge underneath
|
||||
- **GSAP pattern:**
|
||||
```js
|
||||
// Phase N exit — slow fade out
|
||||
phaseTl.to(".phase-n-elements", { opacity: 0, y: -20, duration: 2, ease: "sine.inOut" }, exitStart);
|
||||
// Phase N+1 entrance — fade in underneath (overlaps by 1.5-3s)
|
||||
nextPhaseTl.from(".phase-n1-elements", { opacity: 0, y: 20, duration: 2, ease: "sine.inOut" }, 0);
|
||||
```
|
||||
|
||||
### Escalation Transitions (building intensity)
|
||||
- **Overlap:** 0.8-1.5s
|
||||
- **Technique:** Scene N's elements accelerate their exit, Scene N+1's entrance inherits that momentum
|
||||
- **GSAP pattern:**
|
||||
```js
|
||||
// Phase N exit — accelerating out
|
||||
phaseTl.to(".phase-n-elements", { opacity: 0, x: -100, duration: 0.8, ease: "power3.in" }, exitStart);
|
||||
// Phase N+1 entrance — momentum carry (overlaps by 0.8-1.5s)
|
||||
nextPhaseTl.from(".phase-n1-elements", { opacity: 0, x: 100, duration: 0.6, ease: "power3.out" }, 0);
|
||||
```
|
||||
|
||||
### Lookup Table
|
||||
|
||||
| From ↓ To → | dramatic | playful | elegant | energetic | calm | tense | punchy |
|
||||
|-------------|----------|---------|---------|-----------|------|-------|--------|
|
||||
| dramatic | soft | hard | soft | escalation | soft | soft | hard |
|
||||
| playful | hard | soft | hard | soft | hard | hard | hard |
|
||||
| elegant | soft | hard | soft | escalation | soft | escalation | hard |
|
||||
| energetic | hard | soft | hard | soft | hard | hard | hard |
|
||||
| calm | escalation | hard | soft | escalation | soft | escalation | hard |
|
||||
| tense | escalation | hard | soft | escalation | soft | soft | hard |
|
||||
| punchy | hard | soft | hard | hard | hard | hard | soft |
|
||||
|
||||
**Rationale for non-obvious entries:**
|
||||
- playful→punchy = hard: flowing → staccato, a cut emphasizes the shift
|
||||
- energetic→punchy = hard: sustained → staccato, textural change needs a cut
|
||||
- tense→elegant = soft: both share restraint, low contrast
|
||||
- punchy→energetic = hard: staccato → sustained, fundamental rhythm change
|
||||
|
||||
**Overlap budget constraint:** Transition overlap is drawn from the exit of phase N and entrance of phase N+1 (not additional time). If combined overlap on both sides of a phase exceeds 50% of its entrance + exit allocation, clamp to 50%.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the append**
|
||||
|
||||
Read the SKILL.md from the "## Transition Matrix" heading — confirm all subsections are present and the lookup table renders correctly.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/emotion-to-motion && git add SKILL.md && git commit -m "feat(skill): add transition matrix for multi-scene mood transitions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Update SKILL.md — Add Intra-Scene Choreography Section
|
||||
|
||||
**Files:**
|
||||
- Modify: `~/.claude/skills/emotion-to-motion/SKILL.md` (append after Transition Matrix section)
|
||||
|
||||
- [ ] **Step 1: Append the Intra-Scene Choreography section**
|
||||
|
||||
Add immediately after the Transition Matrix section:
|
||||
|
||||
```markdown
|
||||
|
||||
## Intra-Scene Choreography
|
||||
|
||||
Each phase has three acts: **entrance**, **emphasis**, and **exit**. The mood determines the default pattern for each.
|
||||
|
||||
### Act Duration Proportions
|
||||
|
||||
| Mood | Entrance | Emphasis | Exit |
|
||||
|------|----------|----------|------|
|
||||
| dramatic | 25% | 55% | 20% |
|
||||
| playful | 20% | 60% | 20% |
|
||||
| elegant | 20% | 65% | 15% |
|
||||
| energetic | 15% | 65% | 20% |
|
||||
| calm | 20% | 65% | 15% |
|
||||
| tense | 25% | 50% | 25% |
|
||||
| punchy | 15% | 60% | 25% |
|
||||
|
||||
### Entrance Patterns
|
||||
|
||||
**Cascade** (energetic, playful): Rapid staggered entrances, 0.05-0.1s gaps between elements.
|
||||
```js
|
||||
tl.from(".phase-elements", {
|
||||
opacity: 0, y: 30, scale: 0.9,
|
||||
duration: 0.4, ease: "back.out(1.7)",
|
||||
stagger: { each: 0.07, from: "center" } // playful: from center; energetic: from start
|
||||
}, entranceStart);
|
||||
```
|
||||
|
||||
**Slow reveal** (dramatic, tense): Elements emerge one at a time with long pauses between.
|
||||
```js
|
||||
elements.forEach((el, i) => {
|
||||
tl.from(el, {
|
||||
opacity: 0, y: 15, duration: 0.8, ease: "power2.out"
|
||||
}, entranceStart + i * 1.2); // 1.2s between each — builds anticipation
|
||||
});
|
||||
```
|
||||
|
||||
**Ensemble** (elegant, calm): Elements fade in together or in 2-3 groups.
|
||||
```js
|
||||
tl.from(".phase-elements-group1", {
|
||||
opacity: 0, y: 10, duration: 1.2, ease: "sine.inOut"
|
||||
}, entranceStart);
|
||||
tl.from(".phase-elements-group2", {
|
||||
opacity: 0, y: 10, duration: 1.2, ease: "sine.inOut"
|
||||
}, entranceStart + 0.4); // slight offset, but feels unified
|
||||
```
|
||||
|
||||
**Impact** (punchy): Hero element hits first and hard, supporting elements follow in quick succession.
|
||||
```js
|
||||
tl.from("#hero", {
|
||||
opacity: 0, scale: 1.3, duration: 0.15, ease: "power4.out"
|
||||
}, entranceStart);
|
||||
tl.from(".supporting", {
|
||||
opacity: 0, y: 20, duration: 0.2, ease: "power3.out",
|
||||
stagger: 0.04
|
||||
}, entranceStart + 0.1);
|
||||
```
|
||||
|
||||
### Emphasis Patterns
|
||||
|
||||
**Pulse** (energetic, punchy): Subtle scale or brightness oscillation on key elements.
|
||||
```js
|
||||
tl.to("#hero", {
|
||||
scale: 1.02, duration: 0.4, ease: "sine.inOut",
|
||||
repeat: -1, yoyo: true
|
||||
}, emphasisStart);
|
||||
// Note: use repeat count based on emphasis duration, not -1, in compositions
|
||||
```
|
||||
|
||||
**Drift** (calm, elegant): Slow continuous motion — gentle float, rotation, or parallax shift.
|
||||
```js
|
||||
tl.to(".bg-element", { y: -15, duration: emphasisDuration, ease: "sine.inOut" }, emphasisStart);
|
||||
tl.to(".mid-element", { y: -8, rotation: 0.5, duration: emphasisDuration, ease: "sine.inOut" }, emphasisStart);
|
||||
```
|
||||
|
||||
**Tension hold** (tense, dramatic): Near-stillness with micro-tremor on one element.
|
||||
```js
|
||||
gsap.registerPlugin(CustomWiggle);
|
||||
CustomWiggle.create("microTremor", { wiggles: 12, type: "uniform" });
|
||||
tl.to("#tension-element", {
|
||||
x: 2, ease: "microTremor", duration: emphasisDuration
|
||||
}, emphasisStart);
|
||||
```
|
||||
|
||||
**Play** (playful): Secondary action — idle wobbles, bounces, rotation on supporting elements.
|
||||
```js
|
||||
gsap.registerPlugin(CustomWiggle);
|
||||
CustomWiggle.create("wobble", { wiggles: 4, type: "easeOut" });
|
||||
tl.to(".supporting", {
|
||||
rotation: 3, ease: "wobble", duration: 1.5,
|
||||
stagger: { each: 0.3, repeat: -1 }
|
||||
}, emphasisStart);
|
||||
// Floating idle on decorative elements
|
||||
tl.to(".decorative", {
|
||||
y: -8, duration: 1.2, ease: "sine.inOut",
|
||||
yoyo: true, repeat: -1
|
||||
}, emphasisStart);
|
||||
```
|
||||
|
||||
### Exit Patterns
|
||||
|
||||
**Scatter** (playful, energetic): Elements exit in different directions with stagger.
|
||||
```js
|
||||
tl.to(".phase-elements", {
|
||||
opacity: 0, x: "random(-100,100)", y: "random(-80,80)",
|
||||
rotation: "random(-15,15)", duration: 0.5, ease: "power2.in",
|
||||
stagger: 0.05
|
||||
}, exitStart);
|
||||
```
|
||||
|
||||
**Fade recede** (calm, elegant): Opacity + slight scale down, elements sink back.
|
||||
```js
|
||||
tl.to(".phase-elements", {
|
||||
opacity: 0, scale: 0.95, y: 10,
|
||||
duration: 1.2, ease: "sine.inOut", stagger: 0.1
|
||||
}, exitStart);
|
||||
```
|
||||
|
||||
**Snap out** (dramatic, punchy): Hard opacity to 0, no ease out — just gone.
|
||||
```js
|
||||
tl.to(".phase-elements", {
|
||||
opacity: 0, duration: 0.08, ease: "none"
|
||||
}, exitStart);
|
||||
```
|
||||
|
||||
**Compress** (tense): Elements slowly contract inward before the next scene hits.
|
||||
```js
|
||||
tl.to(".phase-elements", {
|
||||
scale: 0.9, x: 0, y: 0, opacity: 0.3,
|
||||
duration: 1.5, ease: "power4.in"
|
||||
}, exitStart);
|
||||
```
|
||||
|
||||
### Mood-to-Default-Pattern Mapping
|
||||
|
||||
| Mood | Entrance | Emphasis | Exit |
|
||||
|------|----------|----------|------|
|
||||
| dramatic | slow reveal | tension hold | snap out |
|
||||
| playful | cascade | play | scatter |
|
||||
| elegant | ensemble | drift | fade recede |
|
||||
| energetic | cascade | pulse | scatter |
|
||||
| calm | ensemble | drift | fade recede |
|
||||
| tense | slow reveal | tension hold | compress |
|
||||
| punchy | impact | pulse | snap out |
|
||||
|
||||
Prompt-level overrides take precedence: if the prompt says "the title slams in," use impact entrance regardless of mood default.
|
||||
|
||||
### Timeline Assembly
|
||||
|
||||
Compose phases into a single master timeline:
|
||||
|
||||
```js
|
||||
const master = gsap.timeline({ paused: true });
|
||||
|
||||
// Phase 1 (0s - 15s)
|
||||
const phase1 = gsap.timeline();
|
||||
// ... entrance/emphasis/exit choreography for phase 1 ...
|
||||
master.add(phase1, 0);
|
||||
|
||||
// Transition 1 overlap: phase1 exit overlaps phase2 entrance
|
||||
// Look up transition type from matrix: e.g., tense→punchy = hard (0.3-0.8s)
|
||||
const t1Overlap = 0.5; // within hard range
|
||||
|
||||
// Phase 2 (14.5s - 24.5s) — starts t1Overlap before phase1 ends
|
||||
const phase2 = gsap.timeline();
|
||||
// ... entrance/emphasis/exit choreography for phase 2 ...
|
||||
master.add(phase2, 15 - t1Overlap);
|
||||
|
||||
// Continue for remaining phases...
|
||||
```
|
||||
|
||||
Each phase is a nested `gsap.timeline()` added to master at the correct position. This keeps phase choreography self-contained while the master timeline handles inter-phase timing.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the full SKILL.md**
|
||||
|
||||
Read the SKILL.md and verify:
|
||||
1. Original 12 principles + mood recipes are intact (lines 1-269)
|
||||
2. Choreography Phases section follows
|
||||
3. Transition Matrix section follows
|
||||
4. Intra-Scene Choreography section follows with all patterns and GSAP code examples
|
||||
5. No duplicate content or broken markdown
|
||||
|
||||
- [ ] **Step 3: Add the script placement reminder**
|
||||
|
||||
Append at the very end of SKILL.md:
|
||||
|
||||
```markdown
|
||||
|
||||
## Important: Script Placement
|
||||
|
||||
All `<script>` tags go at the end of `<body>`, never in `<head>`. Always call `gsap.registerPlugin()` for every plugin loaded via CDN before using it.
|
||||
|
||||
```js
|
||||
<!-- At end of <body> -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/SplitText.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomBounce.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomWiggle.min.js"></script>
|
||||
<script>
|
||||
gsap.registerPlugin(CustomEase, SplitText, CustomBounce, CustomWiggle);
|
||||
// ... timeline code ...
|
||||
</script>
|
||||
```
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/emotion-to-motion && git add SKILL.md && git commit -m "feat(skill): add intra-scene choreography patterns and script placement guide"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update evals.json with Multi-Scene Test Cases
|
||||
|
||||
**Files:**
|
||||
- Modify: `~/.claude/skills/emotion-to-motion/evals/evals.json`
|
||||
|
||||
- [ ] **Step 1: Read the existing evals.json**
|
||||
|
||||
Read `~/.claude/skills/emotion-to-motion/evals/evals.json` — note the existing format so we match it.
|
||||
|
||||
- [ ] **Step 2: Replace evals.json with iteration-3 test cases**
|
||||
|
||||
Write the following (replaces entire file — iteration 1/2 evals are preserved in the workspace):
|
||||
|
||||
```json
|
||||
{
|
||||
"skill_name": "emotion-to-motion",
|
||||
"iteration": 3,
|
||||
"evals": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "high-contrast-arc",
|
||||
"prompt": "A thriller title sequence: open with creeping unease as fragments of text drift in darkness, then SNAP — explosive reveal of the title with shattering glass, then settle into calm aftermath with slow floating debris. Total duration ~45 seconds. The title is 'SHATTERED'. Dark background, white and red accents.",
|
||||
"expected_phases": [
|
||||
{ "mood": "tense", "duration_s": 15, "entrance": "slow reveal", "emphasis": "tension hold", "exit": "compress" },
|
||||
{ "mood": "punchy", "duration_s": 10, "entrance": "impact", "emphasis": "pulse", "exit": "snap out" },
|
||||
{ "mood": "calm", "duration_s": 20, "entrance": "ensemble", "emphasis": "drift", "exit": "fade recede" }
|
||||
],
|
||||
"expected_transitions": [
|
||||
{ "from": "tense", "to": "punchy", "type": "hard", "overlap_range": [0.3, 0.8] },
|
||||
{ "from": "punchy", "to": "calm", "type": "hard", "overlap_range": [0.3, 0.8] }
|
||||
],
|
||||
"grading_criteria": [
|
||||
"phase-presence",
|
||||
"entrance-choreography",
|
||||
"emphasis-choreography",
|
||||
"exit-choreography",
|
||||
"transition-technique",
|
||||
"duration-fidelity",
|
||||
"mood-appropriate-easing",
|
||||
"valid-hyperframes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "gradual-escalation",
|
||||
"prompt": "A luxury fitness brand launch: begin with elegant product beauty shots of a sleek fitness tracker called 'APEX', slowly build energy as athletes appear in motion, crescendo into triumphant celebration with the brand tagline 'Rise Above'. Total duration ~40 seconds. Black and gold palette, modern sans-serif typography.",
|
||||
"expected_phases": [
|
||||
{ "mood": "elegant", "duration_s": 15, "entrance": "ensemble", "emphasis": "drift", "exit": "fade recede" },
|
||||
{ "mood": "energetic", "duration_s": 12, "entrance": "cascade", "emphasis": "pulse", "exit": "scatter" },
|
||||
{ "mood": "playful", "duration_s": 13, "entrance": "cascade", "emphasis": "play", "exit": "scatter" }
|
||||
],
|
||||
"expected_transitions": [
|
||||
{ "from": "elegant", "to": "energetic", "type": "escalation", "overlap_range": [0.8, 1.5] },
|
||||
{ "from": "energetic", "to": "playful", "type": "soft", "overlap_range": [1.5, 3.0] }
|
||||
],
|
||||
"grading_criteria": [
|
||||
"phase-presence",
|
||||
"entrance-choreography",
|
||||
"emphasis-choreography",
|
||||
"exit-choreography",
|
||||
"transition-technique",
|
||||
"duration-fidelity",
|
||||
"mood-appropriate-easing",
|
||||
"valid-hyperframes"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the JSON is valid**
|
||||
|
||||
```bash
|
||||
python3 -c "import json; json.load(open('$HOME/.claude/skills/emotion-to-motion/evals/evals.json'))" && echo "Valid JSON"
|
||||
```
|
||||
|
||||
Expected: `Valid JSON`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/emotion-to-motion && git add evals/evals.json && git commit -m "feat(evals): update test cases for iteration 3 multi-scene compositions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Create Iteration-3 Workspace and Eval Metadata
|
||||
|
||||
**Files:**
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/eval_metadata.json`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/eval_metadata.json`
|
||||
|
||||
- [ ] **Step 1: Create directory structure**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/{with_skill/outputs,without_skill/outputs}
|
||||
mkdir -p ~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/{with_skill/outputs,without_skill/outputs}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create eval_metadata.json for high-contrast-arc**
|
||||
|
||||
Write to `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/eval_metadata.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"eval_id": 0,
|
||||
"eval_name": "high-contrast-arc",
|
||||
"prompt": "A thriller title sequence: open with creeping unease as fragments of text drift in darkness, then SNAP — explosive reveal of the title with shattering glass, then settle into calm aftermath with slow floating debris. Total duration ~45 seconds. The title is 'SHATTERED'. Dark background, white and red accents.",
|
||||
"expected_phases": ["tense (15s)", "punchy (10s)", "calm (20s)"],
|
||||
"expected_transitions": ["tense→punchy: hard", "punchy→calm: hard"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create eval_metadata.json for gradual-escalation**
|
||||
|
||||
Write to `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/eval_metadata.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"eval_id": 1,
|
||||
"eval_name": "gradual-escalation",
|
||||
"prompt": "A luxury fitness brand launch: begin with elegant product beauty shots of a sleek fitness tracker called 'APEX', slowly build energy as athletes appear in motion, crescendo into triumphant celebration with the brand tagline 'Rise Above'. Total duration ~40 seconds. Black and gold palette, modern sans-serif typography.",
|
||||
"expected_phases": ["elegant (15s)", "energetic (12s)", "playful (13s)"],
|
||||
"expected_transitions": ["elegant→energetic: escalation", "energetic→playful: soft"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify directory structure**
|
||||
|
||||
```bash
|
||||
find ~/.claude/skills/emotion-to-motion-workspace/iteration-3 -type f | sort
|
||||
```
|
||||
|
||||
Expected output shows both `eval_metadata.json` files and the empty output directories.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Generate "High Contrast Arc" WITHOUT Skill
|
||||
|
||||
**Files:**
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/outputs/index.html`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/timing.json`
|
||||
|
||||
**Important:** This composition must be generated by a **fresh subagent that does NOT have access to the emotion-to-motion SKILL.md**. The subagent should only receive the prompt and the HyperFrames composition format requirements.
|
||||
|
||||
- [ ] **Step 1: Dispatch a fresh subagent with ONLY the prompt**
|
||||
|
||||
The subagent receives:
|
||||
- The prompt from eval_metadata.json
|
||||
- HyperFrames composition format rules: `data-composition-id` attribute, `gsap.timeline({ paused: true })`, `window.__timelines["id"] = tl`, 1920x1080 viewport, GSAP 3.14.2 from jsdelivr CDN, all `<script>` tags at end of `<body>`, `gsap.registerPlugin()` for all plugins
|
||||
- **NO** emotion-to-motion skill content
|
||||
|
||||
Record the token count and duration in timing.json.
|
||||
|
||||
- [ ] **Step 2: Save the output**
|
||||
|
||||
Save the generated HTML to `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/outputs/index.html`
|
||||
|
||||
- [ ] **Step 3: Save timing metrics**
|
||||
|
||||
Write timing.json:
|
||||
```json
|
||||
{
|
||||
"total_tokens": <actual>,
|
||||
"duration_ms": <actual>,
|
||||
"total_duration_seconds": <actual>
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Validate the output**
|
||||
|
||||
Verify the HTML:
|
||||
1. Contains `data-composition-id` attribute
|
||||
2. Contains `window.__timelines` assignment
|
||||
3. Uses `gsap.timeline({ paused: true })`
|
||||
4. GSAP scripts are at end of `<body>`
|
||||
5. Total timeline duration is approximately 45s
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Generate "High Contrast Arc" WITH Skill
|
||||
|
||||
**Files:**
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/outputs/index.html`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/timing.json`
|
||||
|
||||
**Important:** This composition must be generated by a **fresh subagent that HAS the full emotion-to-motion SKILL.md** loaded. The subagent receives the prompt AND the skill.
|
||||
|
||||
- [ ] **Step 1: Dispatch a fresh subagent with the prompt AND the full SKILL.md content**
|
||||
|
||||
The subagent receives:
|
||||
- The prompt from eval_metadata.json
|
||||
- The **full content** of `~/.claude/skills/emotion-to-motion/SKILL.md` (all 12 principles, mood recipes, choreography phases, transition matrix, intra-scene choreography)
|
||||
- HyperFrames composition format rules (same as Task 6)
|
||||
|
||||
Record the token count and duration.
|
||||
|
||||
- [ ] **Step 2: Save the output**
|
||||
|
||||
Save the generated HTML to `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/outputs/index.html`
|
||||
|
||||
- [ ] **Step 3: Save timing metrics**
|
||||
|
||||
Write timing.json with actual values.
|
||||
|
||||
- [ ] **Step 4: Validate the output**
|
||||
|
||||
Same validation as Task 6, PLUS:
|
||||
1. Contains 3 distinct phases (tense, punchy, calm)
|
||||
2. Uses CustomEase for tension curves
|
||||
3. Has transition choreography between phases
|
||||
4. Entrance/emphasis/exit patterns are present per phase
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Generate "Gradual Escalation" WITHOUT Skill
|
||||
|
||||
**Files:**
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/without_skill/outputs/index.html`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/without_skill/timing.json`
|
||||
|
||||
Same approach as Task 6 but with the gradual-escalation prompt. **No skill loaded.**
|
||||
|
||||
- [ ] **Step 1: Dispatch fresh subagent with only the prompt**
|
||||
- [ ] **Step 2: Save output to index.html**
|
||||
- [ ] **Step 3: Save timing.json**
|
||||
- [ ] **Step 4: Validate output** (data-composition-id, __timelines, paused timeline, scripts at end of body, ~40s duration)
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Generate "Gradual Escalation" WITH Skill
|
||||
|
||||
**Files:**
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/with_skill/outputs/index.html`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/with_skill/timing.json`
|
||||
|
||||
Same approach as Task 7 but with the gradual-escalation prompt. **Full SKILL.md loaded.**
|
||||
|
||||
- [ ] **Step 1: Dispatch fresh subagent with prompt AND full SKILL.md**
|
||||
- [ ] **Step 2: Save output to index.html**
|
||||
- [ ] **Step 3: Save timing.json**
|
||||
- [ ] **Step 4: Validate output** (same as Task 8 PLUS: 3 phases elegant→energetic→playful, escalation + soft transitions, choreography patterns per phase)
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Grade All 4 Compositions
|
||||
|
||||
**Files:**
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/outputs/grading.json`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/outputs/grading.json`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/with_skill/outputs/grading.json`
|
||||
- Create: `~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/without_skill/outputs/grading.json`
|
||||
|
||||
- [ ] **Step 1: Read all 4 index.html files**
|
||||
|
||||
Read each composition output to analyze against the 8 grading criteria.
|
||||
|
||||
- [ ] **Step 2: Grade high-contrast-arc/with_skill**
|
||||
|
||||
Analyze the composition against the 8 criteria using the 0/1/2 scoring rubric from the spec (see `docs/superpowers/specs/2026-03-25-emotion-to-motion-iter3-design.md`, Grading Criteria section). Write grading.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"eval_name": "high-contrast-arc",
|
||||
"variant": "with_skill",
|
||||
"criteria": [
|
||||
{ "name": "phase-presence", "score": <0|1|2>, "evidence": "<specific evidence from the HTML>" },
|
||||
{ "name": "entrance-choreography", "score": <0|1|2>, "evidence": "<...>" },
|
||||
{ "name": "emphasis-choreography", "score": <0|1|2>, "evidence": "<...>" },
|
||||
{ "name": "exit-choreography", "score": <0|1|2>, "evidence": "<...>" },
|
||||
{ "name": "transition-technique", "score": <0|1|2>, "evidence": "<...>" },
|
||||
{ "name": "duration-fidelity", "score": <0|1|2>, "evidence": "<...>" },
|
||||
{ "name": "mood-appropriate-easing", "score": <0|1|2>, "evidence": "<...>" },
|
||||
{ "name": "valid-hyperframes", "score": <0|1|2>, "evidence": "<...>" }
|
||||
],
|
||||
"total_score": <sum>,
|
||||
"max_score": 16
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Grade high-contrast-arc/without_skill**
|
||||
|
||||
Same format, same rubric.
|
||||
|
||||
- [ ] **Step 4: Grade gradual-escalation/with_skill**
|
||||
|
||||
Same format, same rubric.
|
||||
|
||||
- [ ] **Step 5: Grade gradual-escalation/without_skill**
|
||||
|
||||
Same format, same rubric.
|
||||
|
||||
- [ ] **Step 6: Create benchmark.json**
|
||||
|
||||
Write to `~/.claude/skills/emotion-to-motion-workspace/iteration-3/benchmark.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"iteration": 3,
|
||||
"date": "2026-03-25",
|
||||
"cases": [
|
||||
{
|
||||
"name": "high-contrast-arc",
|
||||
"with_skill_score": <total>,
|
||||
"without_skill_score": <total>,
|
||||
"delta": <with - without>
|
||||
},
|
||||
{
|
||||
"name": "gradual-escalation",
|
||||
"with_skill_score": <total>,
|
||||
"without_skill_score": <total>,
|
||||
"delta": <with - without>
|
||||
}
|
||||
],
|
||||
"average_with_skill": <avg>,
|
||||
"average_without_skill": <avg>,
|
||||
"average_delta": <avg delta>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: Deploy to HyperFrames Studio for Visual Review
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/studio/data/projects/high-contrast-arc-with-skill/index.html`
|
||||
- Create: `packages/studio/data/projects/high-contrast-arc-without-skill/index.html`
|
||||
- Create: `packages/studio/data/projects/gradual-escalation-with-skill/index.html`
|
||||
- Create: `packages/studio/data/projects/gradual-escalation-without-skill/index.html`
|
||||
|
||||
- [ ] **Step 1: Copy compositions to studio projects**
|
||||
|
||||
```bash
|
||||
cp ~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/with_skill/outputs/index.html packages/studio/data/projects/high-contrast-arc-with-skill/index.html
|
||||
cp ~/.claude/skills/emotion-to-motion-workspace/iteration-3/high-contrast-arc/without_skill/outputs/index.html packages/studio/data/projects/high-contrast-arc-without-skill/index.html
|
||||
cp ~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/with_skill/outputs/index.html packages/studio/data/projects/gradual-escalation-with-skill/index.html
|
||||
cp ~/.claude/skills/emotion-to-motion-workspace/iteration-3/gradual-escalation/without_skill/outputs/index.html packages/studio/data/projects/gradual-escalation-without-skill/index.html
|
||||
```
|
||||
|
||||
(Create the project directories first with `mkdir -p` if they don't exist.)
|
||||
|
||||
- [ ] **Step 2: Verify studio picks up the projects**
|
||||
|
||||
Start or refresh the studio dev server. All 4 projects should appear in the project list.
|
||||
|
||||
- [ ] **Step 3: Present results to user**
|
||||
|
||||
Report:
|
||||
1. Benchmark scores (with vs without, per case and average)
|
||||
2. Key qualitative differences observed during grading
|
||||
3. Studio URLs for visual review
|
||||
4. Recommendations for next iteration based on results
|
||||
|
||||
---
|
||||
|
||||
### Task 12: Update Memory
|
||||
|
||||
**Files:**
|
||||
- Modify: `~/.claude/projects/-Users-vanceingalls-src-hyperframes/memory/project_emotion_to_motion_skill.md`
|
||||
|
||||
- [ ] **Step 1: Update the project memory with iteration 3 status**
|
||||
|
||||
Update the memory file to reflect:
|
||||
- Iteration 3 complete
|
||||
- Multi-scene choreography phases, transition matrix, and intra-scene choreography added to SKILL.md
|
||||
- 2 test cases (high-contrast-arc, gradual-escalation) at 40-45s duration
|
||||
- Benchmark scores (fill in actual scores)
|
||||
- Any learnings or issues discovered during generation
|
||||
@@ -0,0 +1,318 @@
|
||||
# Emotion-to-Motion Skill: Iteration 3 Design
|
||||
|
||||
## Status
|
||||
- **Date:** 2026-03-25
|
||||
- **Iteration:** 3 (previous: iterations 1 & 2 with 8-12s single-scene compositions)
|
||||
- **Goal:** Support 30-60s multi-scene compositions with mood transitions and intra-scene choreography
|
||||
|
||||
## Background
|
||||
|
||||
Iterations 1 and 2 validated that the emotion-to-motion skill improves mood alignment in GSAP compositions — custom eases, appropriate plugin choices, and motion that matches emotional intent. However, compositions were short (8-12s) and single-scene. Key findings:
|
||||
|
||||
- Playful/fun showed the clearest improvement with choreography principles
|
||||
- Dramatic and elegant showed subtler differences (restraint is inherent to the mood)
|
||||
- Short compositions don't give enough room to demonstrate the skill's value
|
||||
- No support for mood transitions or multi-scene arcs
|
||||
|
||||
This iteration addresses all of these by introducing choreography phases, a transition matrix, and intra-scene choreography patterns.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Choreography Phases
|
||||
|
||||
A composition is decomposed into an ordered list of **phases**. Each phase has:
|
||||
|
||||
- **Mood** — one of the canonical moods: dramatic, playful, elegant, energetic, calm, tense, punchy
|
||||
- **Duration** — seconds this phase occupies in the master timeline
|
||||
- **Content description** — what's visually happening (elements, text, imagery)
|
||||
- **Entrance pattern** — how elements arrive (see Section 3)
|
||||
- **Emphasis pattern** — how the scene holds attention mid-phase (see Section 3)
|
||||
- **Exit pattern** — how elements leave, feeding into the transition (see Section 3)
|
||||
|
||||
#### Mood Synonym Resolution
|
||||
|
||||
Prompts often use evocative language that doesn't match canonical mood names. Resolve synonyms before phase decomposition:
|
||||
|
||||
| Synonym | Canonical Mood |
|
||||
|---------|---------------|
|
||||
| explosive, intense, fierce | punchy |
|
||||
| serene, peaceful, tranquil | calm |
|
||||
| triumphant, victorious, celebratory | playful |
|
||||
| suspenseful, ominous, creeping | tense |
|
||||
| luxurious, refined, sophisticated | elegant |
|
||||
| exciting, dynamic, high-energy | energetic |
|
||||
| cinematic, epic, grand | dramatic |
|
||||
|
||||
#### Mood Recipe Reference
|
||||
|
||||
Each canonical mood has a recipe (defined in the existing SKILL.md) that provides:
|
||||
- **Easing curves** — GSAP ease strings (e.g., `"power4.in"`, `CustomEase.create(...)`)
|
||||
- **Duration range** — min-max seconds per tween (e.g., dramatic: 0.8-2.5s, punchy: 0.1-0.4s)
|
||||
- **Preferred plugins** — GSAP plugins to load (e.g., dramatic: CustomEase, SplitText; playful: CustomBounce, CustomWiggle)
|
||||
- **Intensity range** — movement magnitude bounds (e.g., elegant: y 5-20px, rotation 0-2deg; energetic: y 30-80px, rotation 5-15deg)
|
||||
|
||||
#### Phase Decomposition Rules
|
||||
|
||||
The skill handles two input styles:
|
||||
|
||||
**Explicit per-scene moods:** The prompt specifies each scene's mood directly.
|
||||
> "Scene 1 (tense, 15s): fragments of text drift in darkness. Scene 2 (punchy, 10s): title shatters into view. Scene 3 (calm, 20s): debris floats gently."
|
||||
|
||||
The skill extracts mood (resolving synonyms), duration, and content directly from the prompt.
|
||||
|
||||
**Arc-based descriptions:** The prompt describes an overall emotional journey.
|
||||
> "Build from quiet unease to cathartic release to calm aftermath"
|
||||
|
||||
The skill decomposes this into phases by:
|
||||
1. Identifying the mood anchors in the description (quiet unease = tense, cathartic release = dramatic, calm aftermath = calm)
|
||||
2. Allocating durations using weighted distribution (see table below)
|
||||
3. Generating content suggestions that serve each mood
|
||||
|
||||
**Duration allocation weights** — multiply each phase's weight by the target total, divide by the sum of all weights, round to nearest second:
|
||||
|
||||
| Mood | Weight | Rationale |
|
||||
|------|--------|-----------|
|
||||
| calm, elegant | 1.2 | Need room to breathe, long durations |
|
||||
| dramatic, tense | 1.0 | Standard pacing |
|
||||
| energetic, playful | 0.9 | Sustained energy gets tiring |
|
||||
| punchy | 0.7 | Impact depends on brevity |
|
||||
|
||||
**Minimum phase duration:** 6s. If allocation produces a phase shorter than 6s, clamp to 6s and redistribute the deficit from the longest phase.
|
||||
|
||||
For a 30-60s composition, expect 3-5 phases at 8-15s each. Allow up to 7 phases if the prompt explicitly calls for them, but warn that individual phase durations may compress.
|
||||
|
||||
#### Edge Cases
|
||||
|
||||
- **Single-mood prompts:** If the prompt describes one mood for 30-60s, treat as one phase with no transitions. Apply entrance/emphasis/exit choreography across the full duration.
|
||||
- **Adjacent same-mood phases:** If two consecutive phases share the same canonical mood, merge them into one phase unless their content descriptions feature different primary subjects or visual elements (e.g., "product close-up" vs. "lifestyle scene"). If kept separate, use a soft transition.
|
||||
- **More than 7 phases:** Suggest consolidating phases with similar moods. The agent should not produce more than 7 phases in a single composition.
|
||||
|
||||
### 2. Transition Matrix
|
||||
|
||||
Transitions between phases are inferred from the **mood delta** — how different adjacent phases feel. Three categories:
|
||||
|
||||
#### Hard Transitions (high contrast mood shifts)
|
||||
- **Overlap:** 0.3-0.8s
|
||||
- **Technique:** Scene N's elements snap out (opacity 0, scale 0.95) while Scene N+1's elements slam in simultaneously
|
||||
- **Feels like:** A film cut — the energy shift IS the transition
|
||||
- **When:** tense→punchy, calm→punchy, elegant→energetic, any shift where the moods are fundamentally opposed
|
||||
|
||||
#### Soft Transitions (low contrast mood shifts)
|
||||
- **Overlap:** 1.5-3s
|
||||
- **Technique:** Scene N's elements slowly fade/drift out while Scene N+1's elements gradually emerge underneath
|
||||
- **Feels like:** A long crossfade — elements from both scenes coexist briefly
|
||||
- **When:** elegant→calm, dramatic→tense, calm→elegant, any shift between adjacent emotional temperatures
|
||||
|
||||
#### Escalation Transitions (building intensity)
|
||||
- **Overlap:** 0.8-1.5s
|
||||
- **Technique:** Scene N's elements accelerate their exit (speeding up ease curves), Scene N+1's entrance inherits that momentum
|
||||
- **Feels like:** A tempo ramp — motion gets faster before the new scene lands
|
||||
- **When:** calm→energetic, tense→dramatic, elegant→punchy, any shift that increases intensity
|
||||
|
||||
#### Full Transition Lookup Table
|
||||
|
||||
| From \ To | dramatic | playful | elegant | energetic | calm | tense | punchy |
|
||||
|-------------|------------|------------|------------|------------|------------|------------|------------|
|
||||
| dramatic | soft | hard | soft | escalation | soft | soft | hard |
|
||||
| playful | hard | soft | hard | soft | hard | hard | hard |
|
||||
| elegant | soft | hard | soft | escalation | soft | escalation | hard |
|
||||
| energetic | hard | soft | hard | soft | hard | hard | hard |
|
||||
| calm | escalation | hard | soft | escalation | soft | escalation | hard |
|
||||
| tense | escalation | hard | soft | escalation | soft | soft | hard |
|
||||
| punchy | hard | soft | hard | hard | hard | hard | soft |
|
||||
|
||||
**Matrix rationale for non-obvious entries:**
|
||||
- playful→punchy = hard: playful is flowing, punchy is staccato — a cut emphasizes the character shift
|
||||
- energetic→punchy = hard: both are high-energy but sustained vs. staccato — a cut marks the textural shift
|
||||
- tense→elegant = soft: both share restraint and controlled motion, low contrast warrants a crossfade
|
||||
- punchy→energetic = hard: staccato to sustained flow is a fundamental rhythm change
|
||||
|
||||
### 3. Intra-Scene Choreography Patterns
|
||||
|
||||
Each phase gets choreography patterns for its three acts (entrance, emphasis, exit), derived from its mood.
|
||||
|
||||
#### Entrance Choreography — how elements arrive
|
||||
|
||||
| Pattern | Moods | Description |
|
||||
|--------------|--------------------|-----------------------------------------------------------------------------|
|
||||
| Cascade | energetic, playful | Rapid staggered entrances, each element slightly offset (0.05-0.1s gaps) |
|
||||
| Slow reveal | dramatic, tense | Elements emerge one at a time with long pauses between, building anticipation |
|
||||
| Ensemble | elegant, calm | Elements fade in together or in 2-3 groups, no single element demands attention |
|
||||
| Impact | punchy | Hero element hits first and hard, supporting elements follow in quick succession |
|
||||
|
||||
#### Emphasis Choreography — how the scene holds attention mid-phase
|
||||
|
||||
| Pattern | Moods | Description |
|
||||
|---------------|--------------------|--------------------------------------------------------------------|
|
||||
| Pulse | energetic, punchy | Subtle scale or brightness oscillation on key elements, keeps rhythm |
|
||||
| Drift | calm, elegant | Slow continuous motion — gentle float, rotation, or parallax shift |
|
||||
| Tension hold | tense, dramatic | Near-stillness with micro-tremor on one element, building unease |
|
||||
| Play | playful | Secondary action — idle wobbles, bounces, rotation on supporting elements |
|
||||
|
||||
#### Exit Choreography — how elements leave (feeds into transition)
|
||||
|
||||
| Pattern | Moods | Description |
|
||||
|---------------|--------------------|--------------------------------------------------------------------|
|
||||
| Scatter | playful, energetic | Elements exit in different directions with stagger |
|
||||
| Fade recede | calm, elegant | Opacity + slight scale down, elements sink back |
|
||||
| Snap out | dramatic, punchy | Hard opacity to 0, no ease out — just gone |
|
||||
| Compress | tense | Elements slowly contract inward before the next scene hits |
|
||||
|
||||
#### Mood-to-Default-Pattern Mapping
|
||||
|
||||
| Mood | Entrance | Emphasis | Exit |
|
||||
|------------|-------------|---------------|--------------|
|
||||
| dramatic | slow reveal | tension hold | snap out |
|
||||
| playful | cascade | play | scatter |
|
||||
| elegant | ensemble | drift | fade recede |
|
||||
| energetic | cascade | pulse | scatter |
|
||||
| calm | ensemble | drift | fade recede |
|
||||
| tense | slow reveal | tension hold | compress |
|
||||
| punchy | impact | pulse | snap out |
|
||||
|
||||
Prompt-level overrides take precedence: if the prompt says "the title slams in," use impact entrance regardless of mood default.
|
||||
|
||||
### 4. Timeline Assembly
|
||||
|
||||
The master GSAP timeline is composed as follows:
|
||||
|
||||
```
|
||||
masterTimeline
|
||||
├── phase1Timeline (0s - 15s)
|
||||
│ ├── entrance (0s - 3s)
|
||||
│ ├── emphasis (3s - 12s)
|
||||
│ └── exit (12s - 15s)
|
||||
├── transition1 (overlaps phase1 exit + phase2 entrance)
|
||||
├── phase2Timeline (14.2s - 24.2s)
|
||||
│ ├── entrance (14.2s - 17s)
|
||||
│ ├── emphasis (17s - 21.5s)
|
||||
│ └── exit (21.5s - 24.2s)
|
||||
├── transition2 (overlaps phase2 exit + phase3 entrance)
|
||||
└── phase3Timeline (23s - 43s)
|
||||
├── entrance (23s - 26s)
|
||||
├── emphasis (26s - 40s)
|
||||
└── exit (40s - 43s)
|
||||
```
|
||||
|
||||
- Transitions overlap the exit of phase N and entrance of phase N+1
|
||||
- Overlap duration comes from the transition matrix category
|
||||
- Transition overlap is drawn from the exit duration of phase N and entrance duration of phase N+1 (not additional time). If the combined overlap on both sides of a phase exceeds 50% of its entrance + exit allocation, clamp to 50% and shorten the overlap
|
||||
- Each phase timeline is a nested `gsap.timeline()` added to the master at the correct position
|
||||
- Phase entrance/emphasis/exit durations scale proportionally to the phase's total duration using these defaults:
|
||||
|
||||
| Mood | Entrance | Emphasis | Exit | Rationale |
|
||||
|------------|----------|----------|-------|-----------|
|
||||
| dramatic | 25% | 55% | 20% | Slow reveal needs more entrance time |
|
||||
| playful | 20% | 60% | 20% | Balanced — emphasis carries the fun |
|
||||
| elegant | 20% | 65% | 15% | Long drift emphasis, gentle fade exit |
|
||||
| energetic | 15% | 65% | 20% | Fast cascade entrance, sustained pulse |
|
||||
| calm | 20% | 65% | 15% | Gentle ensemble in, long drift, soft exit |
|
||||
| tense | 25% | 50% | 25% | Slow reveal + long compress exit for buildup |
|
||||
| punchy | 15% | 60% | 25% | Impact entrance is fast, snap out exit needs setup |
|
||||
|
||||
### 5. Skill File Changes
|
||||
|
||||
The existing `~/.claude/skills/emotion-to-motion/SKILL.md` gets three new sections appended:
|
||||
|
||||
1. **Choreography Phases** — phase decomposition rules for explicit and arc-based prompts
|
||||
2. **Transition Matrix** — the full lookup table with overlap durations and technique descriptions
|
||||
3. **Intra-Scene Choreography** — entrance/emphasis/exit pattern definitions with mood-to-default mapping
|
||||
|
||||
Existing mood recipes remain unchanged — they provide the easing curves, duration ranges, and plugin choices that the new choreography patterns reference.
|
||||
|
||||
### 6. Known Issue: Subagent Script Placement
|
||||
|
||||
From iteration 1/2 learnings: subagents sometimes place `<script>` tags in `<head>` instead of body and forget `gsap.registerPlugin()`. The skill update should include an explicit reminder:
|
||||
|
||||
> All `<script>` tags go at the end of `<body>`. Always call `gsap.registerPlugin()` for every plugin loaded via CDN before using it.
|
||||
|
||||
## Evaluation
|
||||
|
||||
### Test Cases
|
||||
|
||||
#### Case 1: High Contrast Arc (~45s)
|
||||
|
||||
**Prompt:**
|
||||
> "A thriller title sequence: open with creeping unease as fragments of text drift in darkness, then SNAP — explosive reveal of the title with shattering glass, then settle into calm aftermath with slow floating debris"
|
||||
|
||||
**Expected phases:**
|
||||
- Phase 1 — tense (15s): text fragments drift, darkness, slow reveal entrance, tension hold emphasis, compress exit
|
||||
- Phase 2 — punchy (10s): title shatter, impact entrance, pulse emphasis, snap out exit (synonym: "explosive" → punchy)
|
||||
- Phase 3 — calm (20s): floating debris, ensemble entrance, drift emphasis, fade recede exit
|
||||
|
||||
**Expected transitions:**
|
||||
- tense → punchy: hard (0.3-0.8s overlap, compressed elements snap away as title slams in)
|
||||
- punchy → calm: hard (0.3-0.8s overlap, title snaps out as debris fades in — high contrast shift)
|
||||
|
||||
#### Case 2: Gradual Escalation (~40s)
|
||||
|
||||
**Prompt:**
|
||||
> "A luxury fitness brand launch: begin with elegant product beauty shots, slowly build energy as athletes appear in motion, crescendo into triumphant celebration with the brand tagline"
|
||||
|
||||
**Expected phases:**
|
||||
- Phase 1 — elegant (15s): product shots, ensemble entrance, drift emphasis, fade recede exit
|
||||
- Phase 2 — energetic (12s): athletes in motion, cascade entrance, pulse emphasis, scatter exit
|
||||
- Phase 3 — playful (13s): celebration + tagline, cascade entrance, play emphasis, scatter exit (synonym: "triumphant" → playful)
|
||||
|
||||
**Expected transitions:**
|
||||
- elegant → energetic: escalation (0.8-1.5s overlap, product shots accelerate out as athletes carry momentum in)
|
||||
- energetic → playful: soft (1.5-3s overlap, athletes blend into celebration)
|
||||
|
||||
### Grading Criteria (8 per case)
|
||||
|
||||
Each criterion is scored: **0** (absent), **1** (partially present), **2** (fully present). Total possible: 16.
|
||||
|
||||
| # | Criterion | 0 (absent) | 1 (partial) | 2 (full) |
|
||||
|---|-----------|------------|-------------|----------|
|
||||
| 1 | **Phase presence** | One flat mood throughout | Mood shifts exist but phases aren't distinct (no clear boundaries) | Distinct phases with identifiable mood shifts and clear boundaries |
|
||||
| 2 | **Entrance choreography** | Elements appear with no entrance animation | Entrances exist but don't match mood pattern (e.g., cascade on a tense phase) | Mood-appropriate entrance pattern per the mapping table |
|
||||
| 3 | **Emphasis choreography** | Dead air mid-phase — elements are static | Some motion exists but doesn't match mood (e.g., pulse on a calm phase) | Mood-appropriate emphasis pattern sustains attention through the phase |
|
||||
| 4 | **Exit choreography** | Elements hard-disappear or stay until next scene overwrites them | Exit animations exist but don't coordinate with the transition | Elements leave with mood-appropriate exit pattern that feeds into the transition |
|
||||
| 5 | **Transition technique** | No transitions — phases are hard cuts with no choreography | Transitions exist but wrong type for the mood delta (e.g., soft crossfade between tense→punchy) | Transition matches expected category from the matrix (hard/soft/escalation) |
|
||||
| 6 | **Duration fidelity** | Total under 25s or over 70s | Total is 30-60s but phase proportions don't match the prompt | Total is 30-60s and phases are proportioned appropriately |
|
||||
| 7 | **Mood-appropriate easing** | Generic eases only (power1, linear) across all phases | Some mood-specific eases but inconsistent | Each phase uses eases from its mood recipe (CustomEase for dramatic, elastic for playful, etc.) |
|
||||
| 8 | **Valid HyperFrames** | Missing GSAP, broken HTML, or scripts in head | Valid HTML + GSAP but missing registerPlugin or wrong version | Well-formed HTML, GSAP 3.14.2, all plugins registered, scripts at end of body |
|
||||
|
||||
### Evaluation Structure
|
||||
|
||||
```
|
||||
emotion-to-motion-workspace/
|
||||
iteration-3/
|
||||
high-contrast-arc/
|
||||
with_skill/
|
||||
outputs/
|
||||
index.html
|
||||
grading.json
|
||||
timing.json
|
||||
without_skill/
|
||||
outputs/
|
||||
index.html
|
||||
grading.json
|
||||
timing.json
|
||||
eval_metadata.json
|
||||
gradual-escalation/
|
||||
with_skill/
|
||||
outputs/
|
||||
index.html
|
||||
grading.json
|
||||
timing.json
|
||||
without_skill/
|
||||
outputs/
|
||||
index.html
|
||||
grading.json
|
||||
timing.json
|
||||
eval_metadata.json
|
||||
benchmark.json
|
||||
```
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Changes to HyperFrames core packages — this is purely a skill-level update
|
||||
- New GSAP plugins beyond what's already supported
|
||||
- Audio/music synchronization
|
||||
- Interactive or scroll-driven compositions
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GSAP 3.14.2 on jsdelivr CDN (all plugins available)
|
||||
- GSAP plugin support branch merged or available (feat/gsap-plugin-support — code complete, 95 tests)
|
||||
@@ -0,0 +1,388 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>GSAP Plugin Showcase</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomBounce.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomWiggle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MorphSVGPlugin.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/DrawSVGPlugin.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/SplitText.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Syne:wght@400;500;600;700;800&display=swap");
|
||||
body, html { margin:0; padding:0; background:#050505; overflow:hidden; width:1920px; height:1080px; }
|
||||
#root { position:relative; width:1920px; height:1080px; background:#050505; overflow:hidden; }
|
||||
.section { position:absolute; top:0; left:0; width:1920px; height:1080px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" data-composition-id="plugin-showcase" data-width="1920" data-height="1080" data-duration="42">
|
||||
|
||||
<!-- ═══ S1: SplitText — Masked Line Reveal + 3D Char Cascade (0-7s) ═══ -->
|
||||
<div id="s1" class="section">
|
||||
<!-- Masked lines: overflow clip containers -->
|
||||
<div style="position:absolute;top:260px;left:160px;overflow:hidden;height:160px;">
|
||||
<div id="t1" style="font-family:'Syne',sans-serif;font-weight:800;font-size:150px;color:#fff;line-height:1;white-space:nowrap;transform:translateY(160px);">SPLIT</div>
|
||||
</div>
|
||||
<div style="position:absolute;top:420px;left:160px;overflow:hidden;height:160px;">
|
||||
<div id="t2" style="font-family:'Syne',sans-serif;font-weight:800;font-size:150px;color:transparent;-webkit-text-stroke:1.5px #fff;line-height:1;white-space:nowrap;transform:translateY(160px);">EVERY</div>
|
||||
</div>
|
||||
<div style="position:absolute;top:580px;left:160px;overflow:hidden;height:160px;">
|
||||
<div id="t3" style="font-family:'Syne',sans-serif;font-weight:800;font-size:150px;color:#fff;line-height:1;white-space:nowrap;transform:translateY(160px);">LETTER</div>
|
||||
</div>
|
||||
<!-- Color accent chars overlaid -->
|
||||
<div style="position:absolute;top:260px;left:160px;overflow:hidden;height:160px;">
|
||||
<div id="t1c" style="font-family:'Syne',sans-serif;font-weight:800;font-size:150px;color:#e84393;line-height:1;white-space:nowrap;opacity:0;">SPLIT</div>
|
||||
</div>
|
||||
<!-- Horizontal rule -->
|
||||
<div id="s1-rule" style="position:absolute;top:780px;left:160px;width:0;height:1px;background:#fff;"></div>
|
||||
<div id="s1-sub" style="position:absolute;top:800px;left:160px;font-family:'Space Grotesk',sans-serif;font-weight:300;font-size:18px;color:rgba(255,255,255,0.3);letter-spacing:0.15em;text-transform:uppercase;opacity:0;">SplitText — per-character reveal with mask, stagger, and 3D rotation</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ S2: MorphSVG — Continuous Shape Dance (7-14s) ═══ -->
|
||||
<div id="s2" class="section" style="opacity:0;">
|
||||
<svg viewBox="0 0 1920 1080" style="position:absolute;top:0;left:0;width:1920px;height:1080px;">
|
||||
<defs>
|
||||
<filter id="glow"><feGaussianBlur stdDeviation="8" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
||||
<linearGradient id="mg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#e84393"/><stop offset="50%" stop-color="#6c5ce7"/><stop offset="100%" stop-color="#00cec9"/></linearGradient>
|
||||
</defs>
|
||||
<!-- Main shape — starts as circle -->
|
||||
<path id="ms" d="M 960,340 C 1071,340 1160,429 1160,540 C 1160,651 1071,740 960,740 C 849,740 760,651 760,540 C 760,429 849,340 960,340 Z" fill="none" stroke="url(#mg)" stroke-width="2" filter="url(#glow)" opacity="0"/>
|
||||
<!-- Shadow/echo shape (follows with delay) -->
|
||||
<path id="ms-echo" d="M 960,340 C 1071,340 1160,429 1160,540 C 1160,651 1071,740 960,740 C 849,740 760,651 760,540 C 760,429 849,340 960,340 Z" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1" opacity="0"/>
|
||||
<!-- Morph targets -->
|
||||
<path id="mt-star" d="M 960,320 L 1000,470 L 1160,470 L 1030,570 L 1080,720 L 960,630 L 840,720 L 890,570 L 760,470 L 920,470 Z" opacity="0"/>
|
||||
<path id="mt-diamond" d="M 960,320 L 1180,540 L 960,760 L 740,540 Z" opacity="0"/>
|
||||
<path id="mt-flower" d="M 960,340 C 1020,340 1060,440 1060,440 C 1060,440 1160,400 1160,460 C 1160,520 1060,520 1060,520 C 1060,520 1160,580 1160,640 C 1160,700 1060,660 1060,660 C 1060,660 1020,760 960,760 C 900,760 860,660 860,660 C 860,660 760,700 760,640 C 760,580 860,520 860,520 C 860,520 760,520 760,460 C 760,400 860,440 860,440 C 860,440 900,340 960,340 Z" opacity="0"/>
|
||||
<path id="mt-infinity" d="M 960,540 C 960,440 1100,380 1160,460 C 1220,540 1100,700 960,540 C 820,380 700,540 760,620 C 820,700 960,640 960,540 Z" opacity="0"/>
|
||||
<path id="mt-heart" d="M 960,420 C 960,380 1000,320 1060,320 C 1140,320 1180,400 1180,460 C 1180,580 960,740 960,740 C 960,740 740,580 740,460 C 740,400 780,320 860,320 C 920,320 960,380 960,420 Z" opacity="0"/>
|
||||
</svg>
|
||||
<div id="s2-label" style="position:absolute;bottom:80px;left:0;width:1920px;text-align:center;font-family:'Space Grotesk',sans-serif;font-weight:300;font-size:16px;color:rgba(255,255,255,0.2);letter-spacing:0.2em;text-transform:uppercase;opacity:0;">MorphSVGPlugin — rotational interpolation with echo trail</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ S3: DrawSVG — Architectural Blueprint (14-21s) ═══ -->
|
||||
<div id="s3" class="section" style="opacity:0;">
|
||||
<svg viewBox="0 0 1920 1080" style="position:absolute;top:0;left:0;width:1920px;height:1080px;">
|
||||
<!-- Fine grid -->
|
||||
<g id="d-grid" opacity="0.04">
|
||||
<line x1="0" y1="180" x2="1920" y2="180" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="0" y1="360" x2="1920" y2="360" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="0" y1="540" x2="1920" y2="540" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="0" y1="720" x2="1920" y2="720" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="0" y1="900" x2="1920" y2="900" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="384" y1="0" x2="384" y2="1080" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="768" y1="0" x2="768" y2="1080" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="1152" y1="0" x2="1152" y2="1080" stroke="#fff" stroke-width="0.5"/>
|
||||
<line x1="1536" y1="0" x2="1536" y2="1080" stroke="#fff" stroke-width="0.5"/>
|
||||
</g>
|
||||
<!-- Building: foundation -->
|
||||
<rect id="db-base" x="560" y="780" width="800" height="8" fill="none" stroke="#fff" stroke-width="1"/>
|
||||
<!-- Columns -->
|
||||
<line id="db-col1" x1="620" y1="780" x2="620" y2="380" stroke="#fff" stroke-width="1"/>
|
||||
<line id="db-col2" x1="820" y1="780" x2="820" y2="380" stroke="#fff" stroke-width="1"/>
|
||||
<line id="db-col3" x1="1100" y1="780" x2="1100" y2="380" stroke="#fff" stroke-width="1"/>
|
||||
<line id="db-col4" x1="1300" y1="780" x2="1300" y2="380" stroke="#fff" stroke-width="1"/>
|
||||
<!-- Roof / triangle -->
|
||||
<path id="db-roof" d="M 540,380 L 960,200 L 1380,380 Z" fill="none" stroke="#fff" stroke-width="1"/>
|
||||
<!-- Windows -->
|
||||
<rect id="db-w1" x="660" y="480" width="120" height="160" rx="4" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="0.8"/>
|
||||
<rect id="db-w2" x="860" y="480" width="200" height="260" rx="4" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="0.8"/>
|
||||
<rect id="db-w3" x="1140" y="480" width="120" height="160" rx="4" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="0.8"/>
|
||||
<!-- Door arch -->
|
||||
<path id="db-door" d="M 910,780 L 910,620 C 910,590 960,560 960,560 C 960,560 1010,590 1010,620 L 1010,780" fill="none" stroke="#fff" stroke-width="1"/>
|
||||
<!-- Detail: pediment decoration -->
|
||||
<path id="db-pedi" d="M 680,380 L 960,260 L 1240,380" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="0.8"/>
|
||||
<!-- Steps -->
|
||||
<line id="db-step1" x1="580" y1="800" x2="1340" y2="800" stroke="rgba(255,255,255,0.4)" stroke-width="0.8"/>
|
||||
<line id="db-step2" x1="600" y1="816" x2="1320" y2="816" stroke="rgba(255,255,255,0.3)" stroke-width="0.8"/>
|
||||
<line id="db-step3" x1="620" y1="832" x2="1300" y2="832" stroke="rgba(255,255,255,0.2)" stroke-width="0.8"/>
|
||||
<!-- Dimension lines -->
|
||||
<line id="db-dim-h" x1="540" y1="860" x2="1380" y2="860" stroke="#e84393" stroke-width="0.5" stroke-dasharray="4,4"/>
|
||||
<line id="db-dim-v" x1="1420" y1="200" x2="1420" y2="780" stroke="#6c5ce7" stroke-width="0.5" stroke-dasharray="4,4"/>
|
||||
<!-- Dimension labels -->
|
||||
<text id="db-dim-ht" x="960" y="880" text-anchor="middle" font-family="Space Grotesk, sans-serif" font-size="12" fill="#e84393" opacity="0">24.0m</text>
|
||||
<text id="db-dim-vt" x="1445" y="500" font-family="Space Grotesk, sans-serif" font-size="12" fill="#6c5ce7" opacity="0" transform="rotate(90,1445,500)">16.5m</text>
|
||||
</svg>
|
||||
<div id="s3-label" style="position:absolute;top:48px;left:64px;font-family:'Space Grotesk',sans-serif;font-weight:300;font-size:16px;color:rgba(255,255,255,0.2);letter-spacing:0.2em;text-transform:uppercase;opacity:0;">DrawSVGPlugin — progressive architectural reveal</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ S4: MotionPath — Double Helix (21-28s) ═══ -->
|
||||
<div id="s4" class="section" style="opacity:0;">
|
||||
<svg viewBox="0 0 1920 1080" style="position:absolute;top:0;left:0;width:1920px;height:1080px;">
|
||||
<!-- Helix strand paths -->
|
||||
<path id="helix-a" d="M 260,540 C 370,240 590,840 700,540 C 810,240 1030,840 1140,540 C 1250,240 1470,840 1580,540" fill="none" stroke="rgba(232,67,147,0.15)" stroke-width="1"/>
|
||||
<path id="helix-b" d="M 260,540 C 370,840 590,240 700,540 C 810,840 1030,240 1140,540 C 1250,840 1470,240 1580,540" fill="none" stroke="rgba(108,92,231,0.15)" stroke-width="1"/>
|
||||
<!-- Rungs connecting the helixes -->
|
||||
<line class="rung" x1="480" y1="440" x2="480" y2="640" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
|
||||
<line class="rung" x1="700" y1="540" x2="700" y2="540" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
|
||||
<line class="rung" x1="920" y1="640" x2="920" y2="440" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
|
||||
<line class="rung" x1="1140" y1="540" x2="1140" y2="540" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
|
||||
<line class="rung" x1="1360" y1="440" x2="1360" y2="640" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
|
||||
</svg>
|
||||
<!-- Helix particles (strand A — pink) -->
|
||||
<div id="ha1" style="position:absolute;width:14px;height:14px;border-radius:50%;background:#e84393;box-shadow:0 0 20px rgba(232,67,147,0.5);opacity:0;"></div>
|
||||
<div id="ha2" style="position:absolute;width:10px;height:10px;border-radius:50%;background:rgba(232,67,147,0.6);opacity:0;"></div>
|
||||
<div id="ha3" style="position:absolute;width:6px;height:6px;border-radius:50%;background:rgba(232,67,147,0.3);opacity:0;"></div>
|
||||
<!-- Helix particles (strand B — purple) -->
|
||||
<div id="hb1" style="position:absolute;width:14px;height:14px;border-radius:50%;background:#6c5ce7;box-shadow:0 0 20px rgba(108,92,231,0.5);opacity:0;"></div>
|
||||
<div id="hb2" style="position:absolute;width:10px;height:10px;border-radius:50%;background:rgba(108,92,231,0.6);opacity:0;"></div>
|
||||
<div id="hb3" style="position:absolute;width:6px;height:6px;border-radius:50%;background:rgba(108,92,231,0.3);opacity:0;"></div>
|
||||
<div id="s4-label" style="position:absolute;bottom:80px;left:0;width:1920px;text-align:center;font-family:'Space Grotesk',sans-serif;font-weight:300;font-size:16px;color:rgba(255,255,255,0.2);letter-spacing:0.2em;text-transform:uppercase;opacity:0;">MotionPathPlugin — double helix with trailing particles</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ S5: CustomEase + Bounce + Wiggle — Physics Playground (28-35s) ═══ -->
|
||||
<div id="s5" class="section" style="opacity:0;">
|
||||
<!-- Pendulum -->
|
||||
<div id="pendulum-arm" style="position:absolute;top:100px;left:356px;width:2px;height:400px;background:rgba(255,255,255,0.15);transform-origin:top center;">
|
||||
<div id="pendulum-bob" style="position:absolute;bottom:-30px;left:-29px;width:60px;height:60px;border-radius:50%;background:linear-gradient(135deg,#e84393,#6c5ce7);"></div>
|
||||
</div>
|
||||
<div style="position:absolute;top:96px;left:351px;width:12px;height:12px;border-radius:50%;background:#333;border:2px solid rgba(255,255,255,0.3);"></div>
|
||||
<!-- Newton's cradle: 5 balls -->
|
||||
<div style="position:absolute;top:100px;left:900px;">
|
||||
<div style="position:relative;width:300px;height:10px;">
|
||||
<div id="nc-arm1" style="position:absolute;left:0;top:0;width:1px;height:300px;background:rgba(255,255,255,0.1);transform-origin:top center;">
|
||||
<div id="nc1" style="position:absolute;bottom:-20px;left:-20px;width:40px;height:40px;border-radius:50%;background:#fff;"></div>
|
||||
</div>
|
||||
<div id="nc-arm2" style="position:absolute;left:60px;top:0;width:1px;height:300px;background:rgba(255,255,255,0.1);transform-origin:top center;">
|
||||
<div style="position:absolute;bottom:-20px;left:-20px;width:40px;height:40px;border-radius:50%;background:#ddd;"></div>
|
||||
</div>
|
||||
<div id="nc-arm3" style="position:absolute;left:120px;top:0;width:1px;height:300px;background:rgba(255,255,255,0.1);transform-origin:top center;">
|
||||
<div style="position:absolute;bottom:-20px;left:-20px;width:40px;height:40px;border-radius:50%;background:#bbb;"></div>
|
||||
</div>
|
||||
<div id="nc-arm4" style="position:absolute;left:180px;top:0;width:1px;height:300px;background:rgba(255,255,255,0.1);transform-origin:top center;">
|
||||
<div style="position:absolute;bottom:-20px;left:-20px;width:40px;height:40px;border-radius:50%;background:#ddd;"></div>
|
||||
</div>
|
||||
<div id="nc-arm5" style="position:absolute;left:240px;top:0;width:1px;height:300px;background:rgba(255,255,255,0.1);transform-origin:top center;">
|
||||
<div style="position:absolute;bottom:-20px;left:-20px;width:40px;height:40px;border-radius:50%;background:#fff;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bouncing ball with squash/stretch -->
|
||||
<div id="squash-ball" style="position:absolute;top:200px;left:1500px;width:80px;height:80px;border-radius:50%;background:linear-gradient(135deg,#55efc4,#00b894);transform-origin:center bottom;opacity:0;"></div>
|
||||
<div style="position:absolute;top:800px;left:1400px;width:280px;height:1px;background:rgba(255,255,255,0.08);"></div>
|
||||
<!-- Labels -->
|
||||
<div style="position:absolute;top:560px;left:280px;font-family:'Space Grotesk',sans-serif;font-size:14px;color:rgba(255,255,255,0.2);letter-spacing:0.15em;text-align:center;width:160px;">PENDULUM<br><span style="color:#e84393;">CustomWiggle</span></div>
|
||||
<div style="position:absolute;top:560px;left:940px;font-family:'Space Grotesk',sans-serif;font-size:14px;color:rgba(255,255,255,0.2);letter-spacing:0.15em;text-align:center;width:200px;">NEWTON'S CRADLE<br><span style="color:#6c5ce7;">CustomEase</span></div>
|
||||
<div style="position:absolute;top:900px;left:1460px;font-family:'Space Grotesk',sans-serif;font-size:14px;color:rgba(255,255,255,0.2);letter-spacing:0.15em;text-align:center;width:180px;">SQUASH & STRETCH<br><span style="color:#00b894;">CustomBounce</span></div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ S6: Finale (35-42s) ═══ -->
|
||||
<div id="s6" class="section" style="opacity:0;">
|
||||
<svg viewBox="0 0 1920 1080" style="position:absolute;top:0;left:0;width:1920px;height:1080px;">
|
||||
<defs><linearGradient id="fg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#e84393"/><stop offset="100%" stop-color="#6c5ce7"/></linearGradient></defs>
|
||||
<path id="fr1" d="M 960,240 C 1125.7,240 1260,374.3 1260,540 C 1260,705.7 1125.7,840 960,840 C 794.3,840 660,705.7 660,540 C 660,374.3 794.3,240 960,240 Z" fill="none" stroke="url(#fg)" stroke-width="1.5"/>
|
||||
<path id="fr2" d="M 960,180 C 1158.8,180 1320,341.2 1320,540 C 1320,738.8 1158.8,900 960,900 C 761.2,900 600,738.8 600,540 C 600,341.2 761.2,180 960,180 Z" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
|
||||
<path id="f-morph" d="M 960,490 C 987.6,490 1010,512.4 1010,540 C 1010,567.6 987.6,590 960,590 C 932.4,590 910,567.6 910,540 C 910,512.4 932.4,490 960,490 Z" fill="url(#fg)" opacity="0"/>
|
||||
<path id="f-morph-target" d="M 960,490 L 978,522 L 1010,530 L 985,555 L 990,590 L 960,572 L 930,590 L 935,555 L 910,530 L 942,522 Z" opacity="0"/>
|
||||
</svg>
|
||||
<div id="f-dot" style="position:absolute;width:8px;height:8px;border-radius:50%;background:#e84393;box-shadow:0 0 12px rgba(232,67,147,0.5);opacity:0;"></div>
|
||||
<div id="f-text" style="position:absolute;top:660px;left:0;width:1920px;text-align:center;font-family:'Syne',sans-serif;font-weight:800;font-size:64px;color:#fff;opacity:0;">Seven Plugins. One Timeline.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
gsap.registerPlugin(MotionPathPlugin, CustomEase, CustomBounce, CustomWiggle, MorphSVGPlugin, DrawSVGPlugin, SplitText);
|
||||
|
||||
CustomEase.create("snap", "M0,0 C0.14,0 0.27,0.58 0.32,0.72 0.42,1 0.62,1 1,1");
|
||||
CustomEase.create("cradleSwing", "M0,0 C0.3,1 0.7,1 1,0");
|
||||
CustomBounce.create("squashBounce", { strength: 0.6, squash: 4 });
|
||||
CustomWiggle.create("pendulum", { wiggles: 6, type: "easeOut" });
|
||||
|
||||
var s1a = SplitText.create("#t1", { type: "chars" });
|
||||
var s1b = SplitText.create("#t2", { type: "chars" });
|
||||
var s1c = SplitText.create("#t3", { type: "chars" });
|
||||
var s1color = SplitText.create("#t1c", { type: "chars" });
|
||||
var sFin = SplitText.create("#f-text", { type: "chars" });
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* ═══ S1: SplitText (0-7s) ═══ */
|
||||
|
||||
/* Line 1: masked slide up per-char with 3D rotation */
|
||||
tl.to(s1a.chars, {
|
||||
y: -160, duration: 0.6, ease: "power3.out",
|
||||
stagger: 0.04
|
||||
}, 0.3);
|
||||
/* Overlay pink chars flash then fade — color echo effect */
|
||||
tl.set("#t1c", { opacity: 1 }, 0.3);
|
||||
tl.to(s1color.chars, {
|
||||
y: -160, duration: 0.6, ease: "power3.out",
|
||||
stagger: 0.04
|
||||
}, 0.35);
|
||||
tl.to("#t1c", { opacity: 0, duration: 0.4 }, 1.0);
|
||||
|
||||
/* Line 2: chars slide up with rotationX — 3D flip feel */
|
||||
tl.to(s1b.chars, {
|
||||
y: -160, rotationX: 0, duration: 0.5, ease: "power3.out",
|
||||
stagger: 0.03
|
||||
}, 0.8);
|
||||
tl.from(s1b.chars, {
|
||||
rotationX: -90, duration: 0.5, ease: "power3.out",
|
||||
stagger: 0.03
|
||||
}, 0.8);
|
||||
|
||||
/* Line 3: chars slide up from center outward */
|
||||
tl.to(s1c.chars, {
|
||||
y: -160, duration: 0.7, ease: "elastic.out(1,0.5)",
|
||||
stagger: { each: 0.05, from: "center" }
|
||||
}, 1.5);
|
||||
|
||||
/* Rule + subtitle */
|
||||
tl.to("#s1-rule", { width: 500, duration: 0.8, ease: "power3.out" }, 2.8);
|
||||
tl.to("#s1-sub", { opacity: 1, duration: 0.4 }, 3.0);
|
||||
|
||||
/* Exit: each line slides up and out through its mask */
|
||||
tl.to(s1a.chars, { y: -320, stagger: 0.02, duration: 0.3, ease: "power2.in" }, 5.5);
|
||||
tl.to(s1b.chars, { y: -320, stagger: 0.02, duration: 0.3, ease: "power2.in" }, 5.6);
|
||||
tl.to(s1c.chars, { y: -320, stagger: { each: 0.02, from: "end" }, duration: 0.3, ease: "power2.in" }, 5.7);
|
||||
tl.to("#s1-rule", { width: 0, duration: 0.2, ease: "power2.in" }, 6.0);
|
||||
tl.to("#s1-sub", { opacity: 0, duration: 0.2 }, 6.0);
|
||||
tl.to("#s1", { opacity: 0, duration: 0.3 }, 6.5);
|
||||
|
||||
/* ═══ S2: MorphSVG (7-14s) ═══ */
|
||||
|
||||
tl.to("#s2", { opacity: 1, duration: 0.3 }, 7.0);
|
||||
tl.to("#ms", { opacity: 1, duration: 0.4 }, 7.2);
|
||||
tl.to("#ms-echo", { opacity: 1, duration: 0.4 }, 7.4);
|
||||
tl.to("#s2-label", { opacity: 1, duration: 0.4 }, 7.3);
|
||||
|
||||
/* Continuous morph chain with echo delay */
|
||||
var morphChain = [
|
||||
{ target: "#mt-star", time: 7.8 },
|
||||
{ target: "#mt-diamond", time: 8.8 },
|
||||
{ target: "#mt-flower", time: 9.8 },
|
||||
{ target: "#mt-infinity", time: 10.8 },
|
||||
{ target: "#mt-heart", time: 11.8 }
|
||||
];
|
||||
for (var i = 0; i < morphChain.length; i++) {
|
||||
var m = morphChain[i];
|
||||
tl.to("#ms", { morphSVG: { shape: m.target, type: "rotational" }, duration: 0.8, ease: "power2.inOut" }, m.time);
|
||||
tl.to("#ms-echo", { morphSVG: { shape: m.target, type: "rotational" }, duration: 0.8, ease: "power2.inOut" }, m.time + 0.15);
|
||||
}
|
||||
|
||||
tl.to("#s2", { opacity: 0, duration: 0.4 }, 13.5);
|
||||
|
||||
/* ═══ S3: DrawSVG — Blueprint (14-21s) ═══ */
|
||||
|
||||
tl.to("#s3", { opacity: 1, duration: 0.3 }, 14.0);
|
||||
tl.to("#s3-label", { opacity: 1, duration: 0.3 }, 14.1);
|
||||
|
||||
/* Set everything to 0 */
|
||||
tl.set(["#db-base","#db-col1","#db-col2","#db-col3","#db-col4","#db-roof","#db-w1","#db-w2","#db-w3","#db-door","#db-pedi","#db-step1","#db-step2","#db-step3","#db-dim-h","#db-dim-v"], { drawSVG: "0%" }, 14.0);
|
||||
|
||||
/* Foundation draws first */
|
||||
tl.to("#db-base", { drawSVG: "100%", duration: 0.6, ease: "none" }, 14.5);
|
||||
/* Steps draw down */
|
||||
tl.to("#db-step1", { drawSVG: "100%", duration: 0.4, ease: "none" }, 14.8);
|
||||
tl.to("#db-step2", { drawSVG: "100%", duration: 0.4, ease: "none" }, 14.9);
|
||||
tl.to("#db-step3", { drawSVG: "100%", duration: 0.4, ease: "none" }, 15.0);
|
||||
/* Columns rise */
|
||||
tl.to("#db-col1", { drawSVG: "100%", duration: 0.8, ease: "power2.out" }, 15.2);
|
||||
tl.to("#db-col2", { drawSVG: "100%", duration: 0.8, ease: "power2.out" }, 15.3);
|
||||
tl.to("#db-col3", { drawSVG: "100%", duration: 0.8, ease: "power2.out" }, 15.4);
|
||||
tl.to("#db-col4", { drawSVG: "100%", duration: 0.8, ease: "power2.out" }, 15.5);
|
||||
/* Roof draws */
|
||||
tl.to("#db-roof", { drawSVG: "100%", duration: 1.0, ease: "power2.inOut" }, 16.0);
|
||||
tl.to("#db-pedi", { drawSVG: "100%", duration: 0.6, ease: "power2.inOut" }, 16.5);
|
||||
/* Windows appear */
|
||||
tl.to("#db-w1", { drawSVG: "100%", duration: 0.5, ease: "snap" }, 17.0);
|
||||
tl.to("#db-w2", { drawSVG: "100%", duration: 0.6, ease: "snap" }, 17.1);
|
||||
tl.to("#db-w3", { drawSVG: "100%", duration: 0.5, ease: "snap" }, 17.2);
|
||||
/* Door */
|
||||
tl.to("#db-door", { drawSVG: "100%", duration: 0.8, ease: "power2.inOut" }, 17.5);
|
||||
/* Dimension lines + labels */
|
||||
tl.to("#db-dim-h", { drawSVG: "100%", duration: 0.6, ease: "none" }, 18.2);
|
||||
tl.to("#db-dim-v", { drawSVG: "100%", duration: 0.6, ease: "none" }, 18.4);
|
||||
tl.to("#db-dim-ht", { opacity: 1, duration: 0.3 }, 18.6);
|
||||
tl.to("#db-dim-vt", { opacity: 1, duration: 0.3 }, 18.8);
|
||||
|
||||
tl.to("#s3", { opacity: 0, duration: 0.4 }, 20.5);
|
||||
|
||||
/* ═══ S4: MotionPath — Double Helix (21-28s) ═══ */
|
||||
|
||||
tl.to("#s4", { opacity: 1, duration: 0.3 }, 21.0);
|
||||
tl.to("#s4-label", { opacity: 1, duration: 0.3 }, 21.1);
|
||||
|
||||
/* Strand A particles */
|
||||
tl.set("#ha1", { opacity: 1 }, 21.3);
|
||||
tl.to("#ha1", { motionPath: { path: "#helix-a", align: "#helix-a", alignOrigin: [0.5,0.5] }, duration: 5.5, ease: "none" }, 21.3);
|
||||
tl.set("#ha2", { opacity: 1 }, 21.6);
|
||||
tl.to("#ha2", { motionPath: { path: "#helix-a", align: "#helix-a", alignOrigin: [0.5,0.5] }, duration: 5.2, ease: "none" }, 21.6);
|
||||
tl.set("#ha3", { opacity: 1 }, 21.9);
|
||||
tl.to("#ha3", { motionPath: { path: "#helix-a", align: "#helix-a", alignOrigin: [0.5,0.5] }, duration: 4.9, ease: "none" }, 21.9);
|
||||
|
||||
/* Strand B particles (opposite wave) */
|
||||
tl.set("#hb1", { opacity: 1 }, 21.3);
|
||||
tl.to("#hb1", { motionPath: { path: "#helix-b", align: "#helix-b", alignOrigin: [0.5,0.5] }, duration: 5.5, ease: "none" }, 21.3);
|
||||
tl.set("#hb2", { opacity: 1 }, 21.6);
|
||||
tl.to("#hb2", { motionPath: { path: "#helix-b", align: "#helix-b", alignOrigin: [0.5,0.5] }, duration: 5.2, ease: "none" }, 21.6);
|
||||
tl.set("#hb3", { opacity: 1 }, 21.9);
|
||||
tl.to("#hb3", { motionPath: { path: "#helix-b", align: "#helix-b", alignOrigin: [0.5,0.5] }, duration: 4.9, ease: "none" }, 21.9);
|
||||
|
||||
/* Rungs pulse as particles pass */
|
||||
tl.to(".rung", { opacity: 0.2, duration: 0.3, stagger: 0.8, ease: "power2.out" }, 22.0);
|
||||
tl.to(".rung", { opacity: 0.04, duration: 0.5, stagger: 0.8, ease: "power2.in" }, 22.5);
|
||||
|
||||
tl.to("#s4", { opacity: 0, duration: 0.4 }, 27.5);
|
||||
|
||||
/* ═══ S5: Easing Physics Playground (28-35s) ═══ */
|
||||
|
||||
tl.to("#s5", { opacity: 1, duration: 0.3 }, 28.0);
|
||||
|
||||
/* Pendulum swings with CustomWiggle */
|
||||
tl.to("#pendulum-arm", { rotation: 40, duration: 4, ease: "pendulum" }, 28.3);
|
||||
|
||||
/* Newton's cradle: ball 1 swings out then back, ball 5 responds */
|
||||
tl.to("#nc-arm1", { rotation: -30, duration: 0.4, ease: "power2.out" }, 28.5);
|
||||
tl.to("#nc-arm1", { rotation: 0, duration: 0.3, ease: "power2.in" }, 28.9);
|
||||
tl.to("#nc-arm5", { rotation: 30, duration: 0.3, ease: "power2.out" }, 29.2);
|
||||
tl.to("#nc-arm5", { rotation: 0, duration: 0.3, ease: "power2.in" }, 29.5);
|
||||
/* Second swing (smaller) */
|
||||
tl.to("#nc-arm1", { rotation: -20, duration: 0.35, ease: "power2.out" }, 29.8);
|
||||
tl.to("#nc-arm1", { rotation: 0, duration: 0.25, ease: "power2.in" }, 30.15);
|
||||
tl.to("#nc-arm5", { rotation: 20, duration: 0.25, ease: "power2.out" }, 30.4);
|
||||
tl.to("#nc-arm5", { rotation: 0, duration: 0.25, ease: "power2.in" }, 30.65);
|
||||
/* Third swing (even smaller) */
|
||||
tl.to("#nc-arm1", { rotation: -12, duration: 0.3, ease: "power2.out" }, 30.9);
|
||||
tl.to("#nc-arm1", { rotation: 0, duration: 0.2, ease: "power2.in" }, 31.2);
|
||||
tl.to("#nc-arm5", { rotation: 12, duration: 0.2, ease: "power2.out" }, 31.4);
|
||||
tl.to("#nc-arm5", { rotation: 0, duration: 0.2, ease: "power2.in" }, 31.6);
|
||||
|
||||
/* Bouncing ball with squash/stretch */
|
||||
tl.set("#squash-ball", { opacity: 1 }, 28.5);
|
||||
tl.to("#squash-ball", { y: 520, duration: 2.0, ease: "squashBounce" }, 28.5);
|
||||
/* Squash on each bounce impact — scaleX widens, scaleY compresses */
|
||||
tl.to("#squash-ball", { scaleX: 1.3, scaleY: 0.7, duration: 0.08, ease: "power2.out" }, 29.3);
|
||||
tl.to("#squash-ball", { scaleX: 1, scaleY: 1, duration: 0.3, ease: "elastic.out(1,0.3)" }, 29.38);
|
||||
tl.to("#squash-ball", { scaleX: 1.2, scaleY: 0.8, duration: 0.06, ease: "power2.out" }, 30.0);
|
||||
tl.to("#squash-ball", { scaleX: 1, scaleY: 1, duration: 0.2, ease: "elastic.out(1,0.4)" }, 30.06);
|
||||
|
||||
tl.to("#s5", { opacity: 0, duration: 0.4 }, 34.5);
|
||||
|
||||
/* ═══ S6: Finale (35-42s) ═══ */
|
||||
|
||||
tl.to("#s6", { opacity: 1, duration: 0.3 }, 35.0);
|
||||
|
||||
tl.set(["#fr1","#fr2"], { drawSVG: "0%" }, 35.0);
|
||||
tl.to("#fr1", { drawSVG: "100%", duration: 1.5, ease: "power2.inOut" }, 35.2);
|
||||
tl.to("#fr2", { drawSVG: "100%", duration: 2.0, ease: "power2.inOut" }, 35.4);
|
||||
|
||||
tl.to("#f-morph", { opacity: 1, duration: 0.3 }, 35.5);
|
||||
tl.to("#f-morph", { morphSVG: { shape: "#f-morph-target", type: "rotational" }, duration: 1.5, ease: "power2.inOut" }, 36.0);
|
||||
|
||||
tl.set("#f-dot", { opacity: 1 }, 35.5);
|
||||
tl.to("#f-dot", { motionPath: { path: "#fr1", align: "#fr1", alignOrigin: [0.5,0.5] }, duration: 5.0, ease: "none" }, 35.5);
|
||||
|
||||
tl.set("#f-text", { opacity: 1 }, 36.5);
|
||||
tl.from(sFin.chars, { opacity: 0, y: 30, scale: 0.9, duration: 0.4, ease: "snap", stagger: 0.025 }, 36.5);
|
||||
|
||||
tl.to("#s6", { opacity: 0, duration: 1.0 }, 40.5);
|
||||
|
||||
window.__timelines["plugin-showcase"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template id="captions-template">
|
||||
<div data-composition-id="captions" data-width="1920" data-height="1080" data-duration="18">
|
||||
<div class="captions-container">
|
||||
<div id="caption-box" class="caption-box">
|
||||
<span id="caption-text" class="caption-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[data-composition-id="captions"] .captions-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 230px; /* Position at y: 850 (1080 - 230 = 850) */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-composition-id="captions"] .caption-box {
|
||||
background-color: #7a6248;
|
||||
padding: 12px 32px;
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 100px;
|
||||
max-width: 80%;
|
||||
opacity: 0;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
[data-composition-id="captions"] .caption-text {
|
||||
color: #f5f0e0;
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const script = [
|
||||
{ text: "We", start: 0.119, end: 0.259 },
|
||||
{ text: "asked", start: 0.319, end: 0.479 },
|
||||
{ text: "what", start: 0.519, end: 0.659 },
|
||||
{ text: "you", start: 0.699, end: 0.819 },
|
||||
{ text: "needed.", start: 0.859, end: 1.819 },
|
||||
{ text: "Forty-seven", start: 1.86, end: 2.299 },
|
||||
{ text: "percent", start: 2.399, end: 2.679 },
|
||||
{ text: "of", start: 2.7, end: 2.799 },
|
||||
{ text: "you", start: 2.839, end: 2.939 },
|
||||
{ text: "said", start: 3.039, end: 3.179 },
|
||||
{ text: "motion", start: 3.24, end: 3.559 },
|
||||
{ text: "graphics,", start: 3.579, end: 4.599 },
|
||||
{ text: "sixty-two", start: 4.679, end: 5.179 },
|
||||
{ text: "percent", start: 5.299, end: 5.759 },
|
||||
{ text: "said", start: 5.859, end: 5.98 },
|
||||
{ text: "static", start: 6.079, end: 6.399 },
|
||||
{ text: "content", start: 6.46, end: 6.879 },
|
||||
{ text: "was", start: 6.92, end: 7.079 },
|
||||
{ text: "costing", start: 7.099, end: 7.48 },
|
||||
{ text: "you", start: 7.5, end: 7.579 },
|
||||
{ text: "attention,", start: 7.679, end: 8.659 },
|
||||
{ text: "and", start: 8.699, end: 8.86 },
|
||||
{ text: "three", start: 8.88, end: 9.06 },
|
||||
{ text: "out", start: 9.079, end: 9.18 },
|
||||
{ text: "of", start: 9.199, end: 9.34 },
|
||||
{ text: "four", start: 9.38, end: 9.799 },
|
||||
{ text: "said", start: 9.84, end: 10.0 },
|
||||
{ text: "you", start: 10.019, end: 10.159 },
|
||||
{ text: "know", start: 10.179, end: 10.36 },
|
||||
{ text: "the", start: 10.38, end: 10.42 },
|
||||
{ text: "look", start: 10.519, end: 10.699 },
|
||||
{ text: "you", start: 10.739, end: 10.859 },
|
||||
{ text: "want", start: 10.98, end: 11.34 },
|
||||
{ text: "but", start: 11.359, end: 11.52 },
|
||||
{ text: "don't", start: 11.56, end: 11.779 },
|
||||
{ text: "have", start: 11.819, end: 11.94 },
|
||||
{ text: "the", start: 11.96, end: 12.06 },
|
||||
{ text: "editing", start: 12.079, end: 12.4 },
|
||||
{ text: "skills", start: 12.52, end: 12.86 },
|
||||
{ text: "to", start: 12.88, end: 13.0 },
|
||||
{ text: "get", start: 13.019, end: 13.18 },
|
||||
{ text: "there.", start: 13.22, end: 14.22 },
|
||||
{ text: "So", start: 14.239, end: 14.399 },
|
||||
{ text: "we", start: 14.42, end: 14.52 },
|
||||
{ text: "built", start: 14.619, end: 14.88 },
|
||||
{ text: "Hyperframes.", start: 15.079, end: 16.02 },
|
||||
];
|
||||
|
||||
// Group words into lines (max 5 words per line)
|
||||
const lines = [];
|
||||
for (let i = 0; i < script.length; i += 5) {
|
||||
const lineWords = script.slice(i, i + 5);
|
||||
lines.push({
|
||||
text: lineWords.map((w) => w.text).join(" "),
|
||||
start: lineWords[0].start,
|
||||
end: lineWords[lineWords.length - 1].end,
|
||||
});
|
||||
}
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const box = document.querySelector('[data-composition-id="captions"] #caption-box');
|
||||
const textEl = document.querySelector('[data-composition-id="captions"] #caption-text');
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Fade in and set text
|
||||
tl.to(
|
||||
box,
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.1,
|
||||
ease: "power2.out",
|
||||
onStart: () => {
|
||||
textEl.textContent = line.text;
|
||||
},
|
||||
},
|
||||
line.start,
|
||||
);
|
||||
|
||||
// Fade out at the end of the line
|
||||
tl.to(
|
||||
box,
|
||||
{
|
||||
opacity: 0,
|
||||
duration: 0.1,
|
||||
ease: "power2.in",
|
||||
},
|
||||
line.end,
|
||||
);
|
||||
});
|
||||
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template id="graphics-template">
|
||||
<div data-composition-id="graphics" data-width="1920" data-height="1080" data-duration="14">
|
||||
<!-- Moment 1: 47% Stat (1.8s - 4.5s) -->
|
||||
<div id="moment-1" class="moment circle-stat">
|
||||
<div class="stat-content">
|
||||
<div class="stat-text">47%</div>
|
||||
<div class="stat-desc">Need Motion Graphics</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Moment 2: 62% Stat (4.6s - 8.6s) -->
|
||||
<div id="moment-2" class="moment pill-stat">
|
||||
<div class="stat-text">62%</div>
|
||||
</div>
|
||||
|
||||
<!-- Moment 3: Editing Skills (8.8s - 14s) -->
|
||||
<div id="moment-3" class="moment rect-stat">
|
||||
<div class="icon-shape">
|
||||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M20,20 Q50,0 80,20 Q100,50 80,80 Q50,100 20,80 Q0,50 20,20"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-text">Editing Skills</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[data-composition-id="graphics"] {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "Outfit", sans-serif;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-composition-id="graphics"] .moment {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
/* We'll use GSAP to handle the centering and drift */
|
||||
}
|
||||
|
||||
/* Moment 1: Forest Green Circle */
|
||||
[data-composition-id="graphics"] #moment-1 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background-color: #3b5e3a;
|
||||
border-radius: 50%;
|
||||
left: 1400px;
|
||||
top: 540px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
[data-composition-id="graphics"] .stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-composition-id="graphics"] .stat-desc {
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
margin-top: 10px;
|
||||
max-width: 300px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Moment 2: Ochre Pill */
|
||||
[data-composition-id="graphics"] #moment-2 {
|
||||
width: 500px;
|
||||
height: 220px;
|
||||
background-color: #cc8832;
|
||||
border-radius: 110px;
|
||||
left: 960px;
|
||||
top: 200px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Moment 3: Terracotta Rounded Rect */
|
||||
[data-composition-id="graphics"] #moment-3 {
|
||||
width: 550px;
|
||||
height: 180px;
|
||||
background-color: #c45d3e;
|
||||
border-radius: 40px;
|
||||
left: 400px;
|
||||
top: 540px;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 0 40px;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
[data-composition-id="graphics"] .stat-text {
|
||||
font-size: 80px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
[data-composition-id="graphics"] #moment-3 .stat-text {
|
||||
font-size: 54px;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
[data-composition-id="graphics"] .icon-shape {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
[data-composition-id="graphics"] .icon-shape svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Moment 1: 1.8s - 4.5s
|
||||
// Gentle drift into place and settle
|
||||
tl.fromTo(
|
||||
"#moment-1",
|
||||
{ opacity: 0, x: "+=40", y: "+=15", scale: 0.8 },
|
||||
{ opacity: 1, x: 0, y: 0, scale: 1, duration: 1.2, ease: "back.out(1.7)" },
|
||||
1.8,
|
||||
);
|
||||
// Hold until 4.5s, then fade out
|
||||
tl.to("#moment-1", { opacity: 0, scale: 0.8, duration: 0.3, ease: "power2.in" }, 4.2);
|
||||
|
||||
// Moment 2: 4.6s - 8.6s
|
||||
// Gentle drift into place and settle
|
||||
tl.fromTo(
|
||||
"#moment-2",
|
||||
{ opacity: 0, y: "-=40", x: "+=10", scale: 0.8 },
|
||||
{ opacity: 1, y: 0, x: 0, scale: 1, duration: 1.2, ease: "back.out(1.7)" },
|
||||
4.6,
|
||||
);
|
||||
// Hold until 8.6s, then fade out
|
||||
tl.to("#moment-2", { opacity: 0, scale: 0.8, duration: 0.3, ease: "power2.in" }, 8.3);
|
||||
|
||||
// Moment 3: 8.8s - 14s
|
||||
// Gentle drift into place and settle
|
||||
tl.fromTo(
|
||||
"#moment-3",
|
||||
{ opacity: 0, x: "-=40", y: "-=10", scale: 0.8 },
|
||||
{ opacity: 1, x: 0, y: 0, scale: 1, duration: 1.2, ease: "back.out(1.7)" },
|
||||
8.8,
|
||||
);
|
||||
// Hold until end (14s)
|
||||
tl.to("#moment-3", { opacity: 0, duration: 0.5, ease: "power2.in" }, 13.5);
|
||||
|
||||
window.__timelines["graphics"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template id="intro-template">
|
||||
<div data-composition-id="intro" data-width="1920" data-height="1080" data-duration="3">
|
||||
<div class="container">
|
||||
<div class="title-card">
|
||||
<h1 class="title">Hyperframes</h1>
|
||||
<p class="subtitle">Design simplified.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Import a rounded humanist sans-serif font */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@400;600&display=swap");
|
||||
|
||||
[data-composition-id="intro"] .container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start; /* Align to left for speaker card */
|
||||
align-items: center;
|
||||
padding-left: 5%;
|
||||
font-family: "Outfit", sans-serif;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-composition-id="intro"] .title-card {
|
||||
background-color: #3b5e3a; /* Forest Green for contrast */
|
||||
padding: 40px 60px;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
transform: translateX(-100%); /* Start off-screen left */
|
||||
}
|
||||
|
||||
[data-composition-id="intro"] .title {
|
||||
font-size: 100px;
|
||||
font-weight: 600;
|
||||
color: #f5f0e0; /* Cream text on green card */
|
||||
margin: 0;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
[data-composition-id="intro"] .subtitle {
|
||||
font-size: 50px;
|
||||
font-weight: 400;
|
||||
color: #cc8832; /* Ochre accent */
|
||||
margin: 10px 0 0 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Animation: Speaker card slides in from the left
|
||||
tl.to(
|
||||
'[data-composition-id="intro"] .title-card',
|
||||
{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
},
|
||||
0.2,
|
||||
);
|
||||
|
||||
// Exit: Slide off left at 1.8s (synced with 47% graphic)
|
||||
tl.to(
|
||||
'[data-composition-id="intro"] .title-card',
|
||||
{
|
||||
x: "-120%",
|
||||
opacity: 0,
|
||||
duration: 0.6,
|
||||
ease: "power2.in",
|
||||
},
|
||||
1.8,
|
||||
);
|
||||
|
||||
window.__timelines["intro"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,275 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Warm Grain - Hyperframes</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f0e0; /* Cream background */
|
||||
font-family: "Outfit", "Lexend", sans-serif;
|
||||
}
|
||||
|
||||
#main-composition {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Paper texture and Grain overlay */
|
||||
#grain-overlay-comp {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Grain animation */
|
||||
@keyframes grain-noise {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
10% {
|
||||
transform: translate(-5%, -5%);
|
||||
}
|
||||
20% {
|
||||
transform: translate(-10%, 5%);
|
||||
}
|
||||
30% {
|
||||
transform: translate(5%, -10%);
|
||||
}
|
||||
40% {
|
||||
transform: translate(-5%, 15%);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-10%, 5%);
|
||||
}
|
||||
60% {
|
||||
transform: translate(15%, 0);
|
||||
}
|
||||
70% {
|
||||
transform: translate(0, 10%);
|
||||
}
|
||||
80% {
|
||||
transform: translate(-15%, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate(10%, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.grain-texture {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: url("https://www.transparenttextures.com/patterns/natural-paper.png");
|
||||
opacity: 0.15;
|
||||
animation: grain-noise 0.5s steps(1) infinite;
|
||||
}
|
||||
|
||||
#a-roll {
|
||||
position: absolute;
|
||||
border-radius: 16px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.comp-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="main-composition"
|
||||
data-composition-id="main-video"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="17"
|
||||
>
|
||||
<!-- Background Layer -->
|
||||
<div
|
||||
id="grain-overlay-comp"
|
||||
data-composition-id="grain-overlay"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="17"
|
||||
data-track-index="100"
|
||||
>
|
||||
<div class="grain-texture"></div>
|
||||
<script>
|
||||
const grainTl = gsap.timeline({ paused: true });
|
||||
window.__timelines["grain-overlay"] = grainTl;
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- A-Roll Video -->
|
||||
|
||||
|
||||
<!-- Compositions -->
|
||||
<div
|
||||
id="intro-layer"
|
||||
class="comp-layer"
|
||||
data-composition-id="intro"
|
||||
data-composition-src="compositions/intro.html"
|
||||
data-start="0"
|
||||
data-duration="2.5"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="graphics-layer"
|
||||
class="comp-layer"
|
||||
data-composition-id="graphics"
|
||||
data-composition-src="compositions/graphics.html"
|
||||
data-start="0"
|
||||
data-duration="17"
|
||||
data-track-index="2"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="captions-layer"
|
||||
class="comp-layer"
|
||||
data-composition-id="captions"
|
||||
data-composition-src="compositions/captions.html"
|
||||
data-start="0"
|
||||
data-duration="17"
|
||||
data-track-index="3"
|
||||
></div>
|
||||
|
||||
<!-- SFX -->
|
||||
<audio
|
||||
id="sfx-swipe-1"
|
||||
src=""
|
||||
data-start="1.8"
|
||||
data-duration="1"
|
||||
data-track-index="4"
|
||||
></audio>
|
||||
<audio
|
||||
id="sfx-swipe-2"
|
||||
src=""
|
||||
data-start="4.6"
|
||||
data-duration="1"
|
||||
data-track-index="4"
|
||||
></audio>
|
||||
<audio
|
||||
id="sfx-swipe-3"
|
||||
src=""
|
||||
data-start="8.8"
|
||||
data-duration="1"
|
||||
data-track-index="4"
|
||||
></audio>
|
||||
|
||||
<script>
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// A-Roll Framing Strategy
|
||||
// 1. 0s-1.8s: Intro scale up from small (60%) to medium (80%) - Shifted right slightly to clear speaker card
|
||||
// 2. 1.8s-4.6s: Grid view / Shift Left (x: 600, scale: 70%) for 47% graphic
|
||||
// 3. 4.6s-8.8s: Shift Center-Bottom (y: 800, scale: 70%) for 62% graphic (top)
|
||||
// 4. 8.8s-14s: Shift Right (x: 1320, scale: 75%) for Editing Skills graphic (left)
|
||||
// 5. 14s-17s: Final Reveal (Center, scale: 100%)
|
||||
|
||||
// Initial state
|
||||
gsap.set("#a-roll", {
|
||||
x: 1100,
|
||||
y: 540,
|
||||
xPercent: -50,
|
||||
yPercent: -50,
|
||||
width: 1920 * 0.6,
|
||||
height: 1080 * 0.6,
|
||||
});
|
||||
|
||||
// 1. Intro Scale Up (0s - 1.8s)
|
||||
tl.to(
|
||||
"#a-roll",
|
||||
{
|
||||
x: 1200,
|
||||
width: 1920 * 0.8,
|
||||
height: 1080 * 0.8,
|
||||
duration: 1.8,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
// 2. Moment 1: 47% Graphic Trigger (1.8s) - Shift Left
|
||||
tl.to(
|
||||
"#a-roll",
|
||||
{
|
||||
x: 600,
|
||||
y: 540,
|
||||
width: 1920 * 0.7,
|
||||
height: 1080 * 0.7,
|
||||
duration: 0.5,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
1.8,
|
||||
);
|
||||
|
||||
// 3. Moment 2: 62% Graphic Trigger (4.6s) - Shift Center-Bottom
|
||||
tl.to(
|
||||
"#a-roll",
|
||||
{
|
||||
x: 960,
|
||||
y: 750,
|
||||
width: 1920 * 0.7,
|
||||
height: 1080 * 0.7,
|
||||
duration: 0.5,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
4.6,
|
||||
);
|
||||
|
||||
// 4. Moment 3: Editing Skills Trigger (8.8s) - Shift Right
|
||||
tl.to(
|
||||
"#a-roll",
|
||||
{
|
||||
x: 1320,
|
||||
y: 540,
|
||||
width: 1920 * 0.75,
|
||||
height: 1080 * 0.75,
|
||||
duration: 0.5,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
8.8,
|
||||
);
|
||||
|
||||
// 5. Final Reveal (14s - 17s)
|
||||
tl.to(
|
||||
"#a-roll",
|
||||
{
|
||||
x: 960,
|
||||
y: 540,
|
||||
width: 1920 * 1.0,
|
||||
height: 1080 * 1.0,
|
||||
duration: 1.0,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
14,
|
||||
);
|
||||
|
||||
window.__timelines["main-video"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "my-video",
|
||||
"name": "my-video",
|
||||
"createdAt": "2026-03-25T22:43:12.140Z"
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
"build": "bun run build:studio && tsup && bun run build:runtime && bun run build:copy",
|
||||
"build:studio": "cd ../studio && bun run build",
|
||||
"build:runtime": "tsx scripts/build-runtime.ts",
|
||||
"build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true) && (cp -r ../../.claude/skills/compose-video ../../.claude/skills/captions ../../.claude/skills/transitions ../../.claude/skills/media ../../.claude/skills/social-media dist/skills/ 2>/dev/null || true)",
|
||||
"build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true) && (cp -r ../../skills/compose-video ../../skills/captions ../../skills/transitions ../../skills/media ../../skills/social-media dist/skills/ 2>/dev/null || true)",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -54,7 +54,7 @@ const GSAP_CACHE = join(homedir(), ".cache", "hyperframes", "gsap-skills");
|
||||
|
||||
function getBundledSkillsDir(): string {
|
||||
const dir = dirname(fileURLToPath(import.meta.url));
|
||||
const devPath = resolve(dir, "..", "..", "..", "..", ".claude", "skills");
|
||||
const devPath = resolve(dir, "..", "..", "..", "..", "skills");
|
||||
const builtPath = resolve(dir, "skills");
|
||||
return existsSync(devPath) ? devPath : builtPath;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,988 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1920, height=1080">
|
||||
<title>APEX Fitness Tracker - Rise Above</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #000; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
||||
|
||||
.composition {
|
||||
background: #000;
|
||||
font-family: 'Helvetica Neue', 'Arial', sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Phase 1: Product Beauty Shots ── */
|
||||
.product-stage {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.product-glow {
|
||||
position: absolute; width: 500px; height: 500px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(212,175,55,0.25) 0%, rgba(212,175,55,0) 70%);
|
||||
top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.product-body {
|
||||
position: relative; width: 260px; height: 340px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.watch-case {
|
||||
position: absolute; top: 40px; left: 30px;
|
||||
width: 200px; height: 240px;
|
||||
border-radius: 50px;
|
||||
background: linear-gradient(145deg, #1a1a1a 0%, #0d0d0d 50%, #1a1a1a 100%);
|
||||
border: 2px solid rgba(212,175,55,0.6);
|
||||
box-shadow: 0 0 40px rgba(212,175,55,0.15), inset 0 0 30px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.watch-screen {
|
||||
position: absolute; top: 65px; left: 55px;
|
||||
width: 150px; height: 190px;
|
||||
border-radius: 35px;
|
||||
background: linear-gradient(180deg, #0a0a0a 0%, #111 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
position: absolute; top: 20px; left: 15px; width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screen-time {
|
||||
font-size: 42px; font-weight: 200; letter-spacing: 2px;
|
||||
color: #D4AF37; opacity: 0;
|
||||
}
|
||||
|
||||
.screen-hr {
|
||||
font-size: 14px; color: rgba(212,175,55,0.7); margin-top: 8px;
|
||||
letter-spacing: 1px; opacity: 0;
|
||||
}
|
||||
|
||||
.screen-hr-value {
|
||||
font-size: 28px; font-weight: 300; color: #D4AF37; opacity: 0;
|
||||
}
|
||||
|
||||
.watch-crown {
|
||||
position: absolute; top: 130px; right: 20px;
|
||||
width: 12px; height: 35px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(180deg, #D4AF37, #8B7225, #D4AF37);
|
||||
box-shadow: 2px 0 8px rgba(212,175,55,0.3);
|
||||
}
|
||||
|
||||
.watch-band-top, .watch-band-bottom {
|
||||
position: absolute; left: 70px; width: 120px; height: 60px;
|
||||
background: linear-gradient(180deg, #1a1a1a, #111);
|
||||
border: 1px solid rgba(212,175,55,0.15);
|
||||
}
|
||||
|
||||
.watch-band-top {
|
||||
top: -15px; border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.watch-band-bottom {
|
||||
bottom: -15px; border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.product-name-reveal {
|
||||
position: absolute; top: 480px; left: 50%; transform: translateX(-50%);
|
||||
font-size: 72px; font-weight: 100; letter-spacing: 30px;
|
||||
color: #D4AF37; opacity: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.product-tagline-small {
|
||||
position: absolute; top: 560px; left: 50%; transform: translateX(-50%);
|
||||
font-size: 16px; font-weight: 300; letter-spacing: 8px;
|
||||
color: rgba(255,255,255,0.5); opacity: 0;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Gold accent lines */
|
||||
.gold-line {
|
||||
position: absolute; height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #D4AF37, transparent);
|
||||
opacity: 0;
|
||||
}
|
||||
.gold-line-1 { top: 200px; left: 0; width: 100%; }
|
||||
.gold-line-2 { bottom: 200px; left: 0; width: 100%; }
|
||||
.gold-line-left { top: 0; left: 300px; width: 1px; height: 100%;
|
||||
background: linear-gradient(180deg, transparent, #D4AF37, transparent); }
|
||||
.gold-line-right { top: 0; right: 300px; width: 1px; height: 100%;
|
||||
background: linear-gradient(180deg, transparent, #D4AF37, transparent); }
|
||||
|
||||
/* ── Phase 2: Spec Highlights ── */
|
||||
.spec-panel {
|
||||
position: absolute; opacity: 0;
|
||||
padding: 20px 30px;
|
||||
border-left: 2px solid #D4AF37;
|
||||
}
|
||||
|
||||
.spec-label {
|
||||
font-size: 11px; font-weight: 400; letter-spacing: 4px;
|
||||
color: rgba(212,175,55,0.6); text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
font-size: 36px; font-weight: 200; color: #fff;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.spec-1 { top: 300px; left: 160px; }
|
||||
.spec-2 { top: 300px; right: 160px; text-align: right; border-left: none; border-right: 2px solid #D4AF37; }
|
||||
.spec-3 { top: 500px; left: 160px; }
|
||||
.spec-4 { top: 500px; right: 160px; text-align: right; border-left: none; border-right: 2px solid #D4AF37; }
|
||||
|
||||
/* ── Phase 3: Athletes in Motion ── */
|
||||
.athlete-stage {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
opacity: 0; overflow: hidden;
|
||||
}
|
||||
|
||||
.athlete-panel {
|
||||
position: absolute; top: 0; height: 100%;
|
||||
overflow: hidden; opacity: 0;
|
||||
}
|
||||
|
||||
.athlete-panel-1 { left: 0; width: 33.33%; }
|
||||
.athlete-panel-2 { left: 33.33%; width: 33.34%; }
|
||||
.athlete-panel-3 { left: 66.67%; width: 33.33%; }
|
||||
|
||||
.athlete-silhouette {
|
||||
position: absolute; bottom: 0; width: 100%; height: 85%;
|
||||
display: flex; align-items: flex-end; justify-content: center;
|
||||
}
|
||||
|
||||
/* Runner silhouette */
|
||||
.runner-figure {
|
||||
position: relative; width: 200px; height: 400px;
|
||||
}
|
||||
|
||||
.runner-torso {
|
||||
position: absolute; top: 40px; left: 80px;
|
||||
width: 40px; height: 120px;
|
||||
background: linear-gradient(180deg, #D4AF37, #8B7225);
|
||||
border-radius: 20px;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
.runner-head {
|
||||
position: absolute; top: 0; left: 82px;
|
||||
width: 36px; height: 36px;
|
||||
background: #D4AF37; border-radius: 50%;
|
||||
}
|
||||
|
||||
.runner-leg-front {
|
||||
position: absolute; top: 140px; left: 60px;
|
||||
width: 20px; height: 160px;
|
||||
background: linear-gradient(180deg, #B8962E, #8B7225);
|
||||
border-radius: 10px;
|
||||
transform: rotate(25deg); transform-origin: top center;
|
||||
}
|
||||
|
||||
.runner-leg-back {
|
||||
position: absolute; top: 140px; left: 110px;
|
||||
width: 20px; height: 160px;
|
||||
background: linear-gradient(180deg, #8B7225, #5C4C19);
|
||||
border-radius: 10px;
|
||||
transform: rotate(-30deg); transform-origin: top center;
|
||||
}
|
||||
|
||||
.runner-arm-front {
|
||||
position: absolute; top: 50px; left: 50px;
|
||||
width: 16px; height: 110px;
|
||||
background: linear-gradient(180deg, #D4AF37, #8B7225);
|
||||
border-radius: 8px;
|
||||
transform: rotate(-35deg); transform-origin: top center;
|
||||
}
|
||||
|
||||
.runner-arm-back {
|
||||
position: absolute; top: 50px; left: 120px;
|
||||
width: 16px; height: 110px;
|
||||
background: linear-gradient(180deg, #8B7225, #5C4C19);
|
||||
border-radius: 8px;
|
||||
transform: rotate(25deg); transform-origin: top center;
|
||||
}
|
||||
|
||||
/* Cyclist silhouette */
|
||||
.cyclist-figure {
|
||||
position: relative; width: 280px; height: 300px;
|
||||
}
|
||||
|
||||
.bike-wheel-front, .bike-wheel-back {
|
||||
position: absolute; bottom: 0;
|
||||
width: 120px; height: 120px;
|
||||
border: 3px solid #D4AF37; border-radius: 50%;
|
||||
}
|
||||
.bike-wheel-back { left: 0; }
|
||||
.bike-wheel-front { right: 0; }
|
||||
|
||||
.bike-frame {
|
||||
position: absolute; bottom: 40px; left: 55px;
|
||||
width: 170px; height: 100px;
|
||||
}
|
||||
|
||||
.bike-frame-line {
|
||||
position: absolute; background: #B8962E; border-radius: 2px;
|
||||
}
|
||||
.frame-top { top: 0; left: 0; width: 170px; height: 4px; transform: rotate(-10deg); }
|
||||
.frame-down { top: 0; left: 0; width: 4px; height: 100px; transform: rotate(15deg); }
|
||||
.frame-seat { top: 0; right: 20px; width: 4px; height: 100px; transform: rotate(-5deg); }
|
||||
|
||||
.cyclist-body {
|
||||
position: absolute; bottom: 140px; left: 80px;
|
||||
width: 30px; height: 90px;
|
||||
background: linear-gradient(180deg, #D4AF37, #8B7225);
|
||||
border-radius: 15px;
|
||||
transform: rotate(-25deg);
|
||||
}
|
||||
|
||||
.cyclist-head {
|
||||
position: absolute; bottom: 225px; left: 65px;
|
||||
width: 30px; height: 30px;
|
||||
background: #D4AF37; border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Lifter silhouette */
|
||||
.lifter-figure {
|
||||
position: relative; width: 260px; height: 380px;
|
||||
}
|
||||
|
||||
.lifter-torso {
|
||||
position: absolute; top: 50px; left: 90px;
|
||||
width: 80px; height: 140px;
|
||||
background: linear-gradient(180deg, #D4AF37, #8B7225);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.lifter-head {
|
||||
position: absolute; top: 5px; left: 110px;
|
||||
width: 40px; height: 40px;
|
||||
background: #D4AF37; border-radius: 50%;
|
||||
}
|
||||
|
||||
.barbell {
|
||||
position: absolute; top: 30px; left: 10px;
|
||||
width: 240px; height: 6px;
|
||||
background: linear-gradient(90deg, #888, #D4AF37, #888);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.barbell-plate-left, .barbell-plate-right {
|
||||
position: absolute; top: 15px;
|
||||
width: 14px; height: 50px;
|
||||
background: #D4AF37; border-radius: 4px;
|
||||
}
|
||||
.barbell-plate-left { left: 5px; }
|
||||
.barbell-plate-right { right: 5px; }
|
||||
|
||||
.lifter-leg-left, .lifter-leg-right {
|
||||
position: absolute; top: 180px; width: 30px; height: 150px;
|
||||
background: linear-gradient(180deg, #B8962E, #8B7225);
|
||||
border-radius: 15px;
|
||||
}
|
||||
.lifter-leg-left { left: 90px; }
|
||||
.lifter-leg-right { left: 140px; }
|
||||
|
||||
/* Panel dividers */
|
||||
.panel-divider {
|
||||
position: absolute; top: 0; width: 2px; height: 100%;
|
||||
background: linear-gradient(180deg, transparent, #D4AF37, transparent);
|
||||
opacity: 0; z-index: 10;
|
||||
}
|
||||
.panel-divider-1 { left: 33.33%; }
|
||||
.panel-divider-2 { left: 66.67%; }
|
||||
|
||||
.athlete-label {
|
||||
position: absolute; bottom: 60px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 14px; font-weight: 300; letter-spacing: 6px;
|
||||
color: rgba(255,255,255,0.6); text-transform: uppercase;
|
||||
white-space: nowrap; opacity: 0;
|
||||
}
|
||||
|
||||
/* Motion lines behind athletes */
|
||||
.motion-line {
|
||||
position: absolute; height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(212,175,55,0.4), transparent);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ── Phase 4: Data Visualization / Pulse ── */
|
||||
.data-stage {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid rgba(212,175,55,0.3);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pulse-ring-1 { width: 200px; height: 200px; }
|
||||
.pulse-ring-2 { width: 400px; height: 400px; }
|
||||
.pulse-ring-3 { width: 600px; height: 600px; }
|
||||
.pulse-ring-4 { width: 800px; height: 800px; }
|
||||
.pulse-ring-5 { width: 1000px; height: 1000px; }
|
||||
|
||||
.heartbeat-line {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 800px; height: 200px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.heartbeat-line svg { width: 100%; height: 100%; }
|
||||
.heartbeat-path { fill: none; stroke: #D4AF37; stroke-width: 2; }
|
||||
|
||||
.stat-counter {
|
||||
position: absolute; text-align: center; opacity: 0;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 64px; font-weight: 100; color: #D4AF37;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 14px; font-weight: 300; letter-spacing: 4px;
|
||||
color: rgba(255,255,255,0.5); text-transform: uppercase;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-bpm { top: 350px; left: 50%; transform: translateX(-50%); }
|
||||
.stat-steps { top: 350px; left: 200px; }
|
||||
.stat-cals { top: 350px; right: 200px; }
|
||||
|
||||
/* ── Phase 5: Crescendo / Celebration ── */
|
||||
.crescendo-stage {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.victory-burst {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 0; height: 0;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(212,175,55,0.4) 0%, transparent 70%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gold-particle {
|
||||
position: absolute; width: 6px; height: 6px;
|
||||
background: #D4AF37; border-radius: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.confetti {
|
||||
position: absolute; width: 8px; height: 20px;
|
||||
border-radius: 2px; opacity: 0;
|
||||
}
|
||||
|
||||
/* Tagline */
|
||||
.tagline-container {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center; opacity: 0;
|
||||
}
|
||||
|
||||
.tagline-rise, .tagline-above {
|
||||
font-size: 120px; font-weight: 100; letter-spacing: 20px;
|
||||
text-transform: uppercase; line-height: 1.1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tagline-rise { color: #fff; }
|
||||
.tagline-above { color: #D4AF37; }
|
||||
|
||||
.tagline-bar {
|
||||
width: 0; height: 2px;
|
||||
background: #D4AF37;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* ── Phase 6: Logo Lockup ── */
|
||||
.logo-stage {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.logo-apex {
|
||||
font-size: 140px; font-weight: 100; letter-spacing: 40px;
|
||||
color: #D4AF37; text-transform: uppercase;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.logo-divider {
|
||||
width: 0; height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #D4AF37, transparent);
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.logo-sub {
|
||||
font-size: 18px; font-weight: 300; letter-spacing: 12px;
|
||||
color: rgba(255,255,255,0.6); text-transform: uppercase;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.logo-tagline-final {
|
||||
font-size: 24px; font-weight: 200; letter-spacing: 10px;
|
||||
color: rgba(212,175,55,0.8); text-transform: uppercase;
|
||||
margin-top: 40px; opacity: 0;
|
||||
}
|
||||
|
||||
.corner-accent {
|
||||
position: absolute; width: 60px; height: 60px;
|
||||
border-color: rgba(212,175,55,0.3); border-style: solid;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.corner-tl { top: 40px; left: 40px; border-width: 1px 0 0 1px; }
|
||||
.corner-tr { top: 40px; right: 40px; border-width: 1px 1px 0 0; }
|
||||
.corner-bl { bottom: 40px; left: 40px; border-width: 0 0 1px 1px; }
|
||||
.corner-br { bottom: 40px; right: 40px; border-width: 0 1px 1px 0; }
|
||||
|
||||
/* ── Global overlays ── */
|
||||
.vignette {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.6) 100%);
|
||||
pointer-events: none; z-index: 100;
|
||||
}
|
||||
|
||||
.scan-line {
|
||||
position: absolute; left: 0; width: 100%; height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(212,175,55,0.15), transparent);
|
||||
opacity: 0; z-index: 50;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.flash-overlay {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: #D4AF37; opacity: 0; z-index: 90;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="composition" data-composition-id="gradual-escalation-apex" style="width: 1920px; height: 1080px; position: relative; overflow: hidden;">
|
||||
|
||||
<!-- Vignette overlay -->
|
||||
<div class="vignette"></div>
|
||||
<div class="scan-line"></div>
|
||||
<div class="flash-overlay"></div>
|
||||
|
||||
<!-- Phase 1: Product Beauty Shot -->
|
||||
<div class="product-stage">
|
||||
<div class="product-glow"></div>
|
||||
<div class="product-body">
|
||||
<div class="watch-band-top"></div>
|
||||
<div class="watch-case">
|
||||
<div class="watch-crown"></div>
|
||||
</div>
|
||||
<div class="watch-screen">
|
||||
<div class="screen-content">
|
||||
<div class="screen-time">10:24</div>
|
||||
<div class="screen-hr">BPM</div>
|
||||
<div class="screen-hr-value">72</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="watch-band-bottom"></div>
|
||||
</div>
|
||||
<div class="product-name-reveal">APEX</div>
|
||||
<div class="product-tagline-small">Precision Engineered Performance</div>
|
||||
|
||||
<div class="gold-line gold-line-1"></div>
|
||||
<div class="gold-line gold-line-2"></div>
|
||||
<div class="gold-line gold-line-left"></div>
|
||||
<div class="gold-line gold-line-right"></div>
|
||||
|
||||
<!-- Spec panels -->
|
||||
<div class="spec-panel spec-1">
|
||||
<div class="spec-label">Battery Life</div>
|
||||
<div class="spec-value">14 Days</div>
|
||||
</div>
|
||||
<div class="spec-panel spec-2">
|
||||
<div class="spec-label">Water Resist</div>
|
||||
<div class="spec-value">100m</div>
|
||||
</div>
|
||||
<div class="spec-panel spec-3">
|
||||
<div class="spec-label">Weight</div>
|
||||
<div class="spec-value">38g</div>
|
||||
</div>
|
||||
<div class="spec-panel spec-4">
|
||||
<div class="spec-label">Display</div>
|
||||
<div class="spec-value">AMOLED</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 3: Athletes in Motion -->
|
||||
<div class="athlete-stage">
|
||||
<div class="panel-divider panel-divider-1"></div>
|
||||
<div class="panel-divider panel-divider-2"></div>
|
||||
|
||||
<div class="athlete-panel athlete-panel-1">
|
||||
<div class="athlete-silhouette">
|
||||
<div class="runner-figure">
|
||||
<div class="runner-head"></div>
|
||||
<div class="runner-arm-back"></div>
|
||||
<div class="runner-torso"></div>
|
||||
<div class="runner-arm-front"></div>
|
||||
<div class="runner-leg-back"></div>
|
||||
<div class="runner-leg-front"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="athlete-label">Run</div>
|
||||
<!-- Motion lines -->
|
||||
<div class="motion-line" style="top: 300px; left: -200px; width: 500px;"></div>
|
||||
<div class="motion-line" style="top: 450px; left: -100px; width: 400px;"></div>
|
||||
<div class="motion-line" style="top: 600px; left: -150px; width: 450px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="athlete-panel athlete-panel-2">
|
||||
<div class="athlete-silhouette">
|
||||
<div class="cyclist-figure">
|
||||
<div class="cyclist-head"></div>
|
||||
<div class="cyclist-body"></div>
|
||||
<div class="bike-frame">
|
||||
<div class="bike-frame-line frame-top"></div>
|
||||
<div class="bike-frame-line frame-down"></div>
|
||||
<div class="bike-frame-line frame-seat"></div>
|
||||
</div>
|
||||
<div class="bike-wheel-back"></div>
|
||||
<div class="bike-wheel-front"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="athlete-label">Ride</div>
|
||||
<div class="motion-line" style="top: 350px; left: -200px; width: 500px;"></div>
|
||||
<div class="motion-line" style="top: 500px; left: -100px; width: 400px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="athlete-panel athlete-panel-3">
|
||||
<div class="athlete-silhouette">
|
||||
<div class="lifter-figure">
|
||||
<div class="lifter-head"></div>
|
||||
<div class="barbell">
|
||||
<div class="barbell-plate-left"></div>
|
||||
<div class="barbell-plate-right"></div>
|
||||
</div>
|
||||
<div class="lifter-torso"></div>
|
||||
<div class="lifter-leg-left"></div>
|
||||
<div class="lifter-leg-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="athlete-label">Lift</div>
|
||||
<div class="motion-line" style="top: 400px; left: -200px; width: 500px;"></div>
|
||||
<div class="motion-line" style="top: 550px; left: -100px; width: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 4: Data / Pulse -->
|
||||
<div class="data-stage">
|
||||
<div class="pulse-ring pulse-ring-1"></div>
|
||||
<div class="pulse-ring pulse-ring-2"></div>
|
||||
<div class="pulse-ring pulse-ring-3"></div>
|
||||
<div class="pulse-ring pulse-ring-4"></div>
|
||||
<div class="pulse-ring pulse-ring-5"></div>
|
||||
|
||||
<div class="heartbeat-line">
|
||||
<svg viewBox="0 0 800 200" preserveAspectRatio="none">
|
||||
<path class="heartbeat-path" d="M0,100 L150,100 L180,100 L200,30 L220,170 L240,60 L260,130 L280,100 L400,100 L430,100 L450,25 L470,175 L490,55 L510,135 L530,100 L650,100 L680,100 L700,30 L720,170 L740,60 L760,130 L780,100 L800,100" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="stat-counter stat-bpm">
|
||||
<div class="stat-number" id="bpm-number">0</div>
|
||||
<div class="stat-unit">Beats Per Minute</div>
|
||||
</div>
|
||||
<div class="stat-counter stat-steps">
|
||||
<div class="stat-number" id="steps-number">0</div>
|
||||
<div class="stat-unit">Daily Steps</div>
|
||||
</div>
|
||||
<div class="stat-counter stat-cals">
|
||||
<div class="stat-number" id="cals-number">0</div>
|
||||
<div class="stat-unit">Calories Burned</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 5: Crescendo -->
|
||||
<div class="crescendo-stage">
|
||||
<div class="victory-burst"></div>
|
||||
|
||||
<!-- Gold particles -->
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
<div class="gold-particle" style="top: 50%; left: 50%;"></div>
|
||||
|
||||
<!-- Confetti -->
|
||||
<div class="confetti" style="top: -20px; left: 10%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 20%; background: #fff;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 30%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 40%; background: #B8962E;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 50%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 60%; background: #fff;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 70%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 80%; background: #B8962E;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 90%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 15%; background: #fff;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 25%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 35%; background: #B8962E;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 45%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 55%; background: #fff;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 65%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 75%; background: #B8962E;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 85%; background: #D4AF37;"></div>
|
||||
<div class="confetti" style="top: -20px; left: 95%; background: #fff;"></div>
|
||||
|
||||
<!-- Tagline -->
|
||||
<div class="tagline-container">
|
||||
<div class="tagline-rise">Rise</div>
|
||||
<div class="tagline-bar"></div>
|
||||
<div class="tagline-above">Above</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 6: Logo Lockup -->
|
||||
<div class="logo-stage">
|
||||
<div class="logo-apex">APEX</div>
|
||||
<div class="logo-divider"></div>
|
||||
<div class="logo-sub">Fitness Technology</div>
|
||||
<div class="logo-tagline-final">Rise Above</div>
|
||||
<div class="corner-accent corner-tl"></div>
|
||||
<div class="corner-accent corner-tr"></div>
|
||||
<div class="corner-accent corner-bl"></div>
|
||||
<div class="corner-accent corner-br"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Scripts at end of body -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/DrawSVGPlugin.min.js"></script>
|
||||
<script>
|
||||
gsap.registerPlugin(CustomEase, DrawSVGPlugin);
|
||||
|
||||
// Custom eases
|
||||
CustomEase.create("luxuryReveal", "M0,0 C0.14,0 0.242,0.438 0.272,0.561 0.313,0.728 0.354,0.963 0.362,1 0.37,0.985 0.414,0.873 0.455,0.811 0.51,0.726 0.573,0.753 0.626,0.8 0.681,0.849 0.735,0.953 0.775,0.979 0.85,1.03 0.919,1.004 1,1");
|
||||
CustomEase.create("smoothPower", "M0,0 C0.25,0.1 0.25,1 1,1");
|
||||
CustomEase.create("dramaticIn", "M0,0 C0.55,0.055 0.675,0.19 1,1");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Helper: counter animation
|
||||
function animateCounter(el, target, duration, tl, position) {
|
||||
const obj = { val: 0 };
|
||||
tl.to(obj, {
|
||||
val: target,
|
||||
duration: duration,
|
||||
ease: "power2.out",
|
||||
onUpdate: function() {
|
||||
if (el) {
|
||||
el.textContent = Math.round(obj.val).toLocaleString();
|
||||
}
|
||||
}
|
||||
}, position);
|
||||
}
|
||||
|
||||
const particles = document.querySelectorAll('.gold-particle');
|
||||
const confetti = document.querySelectorAll('.confetti');
|
||||
const motionLines = document.querySelectorAll('.motion-line');
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// PHASE 1: PRODUCT BEAUTY SHOT (0s - 12s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
// Fade in from black
|
||||
tl.to('.product-stage', { opacity: 1, duration: 1.5, ease: "power2.inOut" }, 0);
|
||||
|
||||
// Ambient glow breathes in
|
||||
tl.to('.product-glow', { opacity: 1, scale: 1.2, duration: 3, ease: "smoothPower" }, 0.5);
|
||||
|
||||
// Watch fades up with subtle float
|
||||
tl.to('.product-body', { opacity: 1, y: -20, duration: 2.5, ease: "luxuryReveal" }, 1);
|
||||
|
||||
// Screen elements illuminate
|
||||
tl.to('.screen-time', { opacity: 1, duration: 1.2, ease: "power2.out" }, 2.5);
|
||||
tl.to('.screen-hr', { opacity: 1, duration: 1, ease: "power2.out" }, 3.2);
|
||||
tl.to('.screen-hr-value', { opacity: 1, duration: 1, ease: "power2.out" }, 3.5);
|
||||
|
||||
// Product name reveals with letter spacing animation
|
||||
tl.fromTo('.product-name-reveal',
|
||||
{ opacity: 0, letterSpacing: '60px' },
|
||||
{ opacity: 1, letterSpacing: '30px', duration: 2, ease: "power3.out" }, 4);
|
||||
|
||||
// Tagline slides in
|
||||
tl.fromTo('.product-tagline-small',
|
||||
{ opacity: 0, y: 15 },
|
||||
{ opacity: 1, y: 0, duration: 1.5, ease: "power2.out" }, 5.5);
|
||||
|
||||
// Gold accent lines draw in
|
||||
tl.fromTo('.gold-line-1', { opacity: 0, scaleX: 0 }, { opacity: 0.3, scaleX: 1, duration: 2, ease: "power2.inOut" }, 3);
|
||||
tl.fromTo('.gold-line-2', { opacity: 0, scaleX: 0 }, { opacity: 0.3, scaleX: 1, duration: 2, ease: "power2.inOut" }, 3.3);
|
||||
tl.fromTo('.gold-line-left', { opacity: 0, scaleY: 0 }, { opacity: 0.2, scaleY: 1, duration: 2, ease: "power2.inOut" }, 3.6);
|
||||
tl.fromTo('.gold-line-right', { opacity: 0, scaleY: 0 }, { opacity: 0.2, scaleY: 1, duration: 2, ease: "power2.inOut" }, 3.9);
|
||||
|
||||
// Subtle glow pulse
|
||||
tl.to('.product-glow', { scale: 1.4, opacity: 0.8, duration: 2, ease: "sine.inOut", yoyo: true, repeat: 1 }, 5);
|
||||
|
||||
// Spec panels stagger in
|
||||
tl.fromTo('.spec-1', { opacity: 0, x: -30 }, { opacity: 1, x: 0, duration: 1, ease: "power2.out" }, 7);
|
||||
tl.fromTo('.spec-2', { opacity: 0, x: 30 }, { opacity: 1, x: 0, duration: 1, ease: "power2.out" }, 7.3);
|
||||
tl.fromTo('.spec-3', { opacity: 0, x: -30 }, { opacity: 1, x: 0, duration: 1, ease: "power2.out" }, 7.6);
|
||||
tl.fromTo('.spec-4', { opacity: 0, x: 30 }, { opacity: 1, x: 0, duration: 1, ease: "power2.out" }, 7.9);
|
||||
|
||||
// Scan line sweeps
|
||||
tl.fromTo('.scan-line', { opacity: 0, top: 0 }, { opacity: 0.5, top: '100%', duration: 2, ease: "none" }, 6);
|
||||
|
||||
// Hold and breathe
|
||||
tl.to('.product-body', { y: -30, duration: 3, ease: "sine.inOut" }, 9);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// TRANSITION 1→2: Product fades, energy shifts (12s - 14s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
tl.to('.product-stage', { opacity: 0, duration: 1.5, ease: "power2.inOut" }, 12);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// PHASE 2: ATHLETES IN MOTION (13s - 23s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
tl.to('.athlete-stage', { opacity: 1, duration: 1, ease: "power2.inOut" }, 13);
|
||||
|
||||
// Panel dividers slash in
|
||||
tl.fromTo('.panel-divider-1', { opacity: 0, scaleY: 0 }, { opacity: 0.8, scaleY: 1, duration: 0.8, ease: "power3.out" }, 13.5);
|
||||
tl.fromTo('.panel-divider-2', { opacity: 0, scaleY: 0 }, { opacity: 0.8, scaleY: 1, duration: 0.8, ease: "power3.out" }, 13.7);
|
||||
|
||||
// Athletes burst in from below — staggered
|
||||
tl.fromTo('.athlete-panel-1',
|
||||
{ opacity: 0, y: 100 },
|
||||
{ opacity: 1, y: 0, duration: 1.2, ease: "dramaticIn" }, 14);
|
||||
tl.fromTo('.athlete-panel-2',
|
||||
{ opacity: 0, y: 100 },
|
||||
{ opacity: 1, y: 0, duration: 1.2, ease: "dramaticIn" }, 14.3);
|
||||
tl.fromTo('.athlete-panel-3',
|
||||
{ opacity: 0, y: 100 },
|
||||
{ opacity: 1, y: 0, duration: 1.2, ease: "dramaticIn" }, 14.6);
|
||||
|
||||
// Athlete labels
|
||||
tl.to('.athlete-panel-1 .athlete-label', { opacity: 1, duration: 0.8, ease: "power2.out" }, 15.5);
|
||||
tl.to('.athlete-panel-2 .athlete-label', { opacity: 1, duration: 0.8, ease: "power2.out" }, 15.8);
|
||||
tl.to('.athlete-panel-3 .athlete-label', { opacity: 1, duration: 0.8, ease: "power2.out" }, 16.1);
|
||||
|
||||
// Motion lines streak across
|
||||
motionLines.forEach(function(line, i) {
|
||||
tl.fromTo(line,
|
||||
{ opacity: 0, x: -300 },
|
||||
{ opacity: 0.6, x: 200, duration: 1.5, ease: "power2.out" }, 15 + i * 0.15);
|
||||
tl.to(line, { opacity: 0, x: 500, duration: 1, ease: "power2.in" }, 16.5 + i * 0.1);
|
||||
});
|
||||
|
||||
// Runner leg animation
|
||||
tl.to('.runner-leg-front', { rotation: -15, duration: 0.3, ease: "sine.inOut", yoyo: true, repeat: 5 }, 15);
|
||||
tl.to('.runner-leg-back', { rotation: 15, duration: 0.3, ease: "sine.inOut", yoyo: true, repeat: 5 }, 15);
|
||||
tl.to('.runner-arm-front', { rotation: -20, duration: 0.3, ease: "sine.inOut", yoyo: true, repeat: 5 }, 15);
|
||||
tl.to('.runner-arm-back', { rotation: 15, duration: 0.3, ease: "sine.inOut", yoyo: true, repeat: 5 }, 15);
|
||||
|
||||
// Cyclist wheel spin
|
||||
tl.to('.bike-wheel-front', { rotation: 720, duration: 4, ease: "power1.inOut" }, 15);
|
||||
tl.to('.bike-wheel-back', { rotation: 720, duration: 4, ease: "power1.inOut" }, 15);
|
||||
|
||||
// Lifter barbell press up
|
||||
tl.to('.barbell', { y: -40, duration: 0.8, ease: "power3.out", yoyo: true, repeat: 2, repeatDelay: 0.4 }, 15.5);
|
||||
tl.to(['.barbell-plate-left', '.barbell-plate-right'], { y: -40, duration: 0.8, ease: "power3.out", yoyo: true, repeat: 2, repeatDelay: 0.4 }, 15.5);
|
||||
|
||||
// Energy building — subtle camera-style zoom on panels
|
||||
tl.to('.athlete-stage', { scale: 1.05, duration: 4, ease: "power1.in", transformOrigin: "center center" }, 18);
|
||||
|
||||
// Scan line sweep
|
||||
tl.fromTo('.scan-line', { opacity: 0, top: 0 }, { opacity: 0.3, top: '100%', duration: 1.5, ease: "none" }, 17);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// TRANSITION 2→3: Quick wipe (22s - 24s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
tl.to('.athlete-stage', { opacity: 0, duration: 1, ease: "power3.inOut" }, 22);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// PHASE 3: DATA PULSE / HEARTBEAT (23s - 30s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
tl.to('.data-stage', { opacity: 1, duration: 0.8, ease: "power2.inOut" }, 23);
|
||||
|
||||
// Pulse rings expand outward in sequence
|
||||
tl.fromTo('.pulse-ring-1', { opacity: 0, scale: 0.5 }, { opacity: 0.8, scale: 1, duration: 1, ease: "power2.out" }, 23.3);
|
||||
tl.fromTo('.pulse-ring-2', { opacity: 0, scale: 0.5 }, { opacity: 0.6, scale: 1, duration: 1, ease: "power2.out" }, 23.6);
|
||||
tl.fromTo('.pulse-ring-3', { opacity: 0, scale: 0.5 }, { opacity: 0.4, scale: 1, duration: 1, ease: "power2.out" }, 23.9);
|
||||
tl.fromTo('.pulse-ring-4', { opacity: 0, scale: 0.5 }, { opacity: 0.25, scale: 1, duration: 1, ease: "power2.out" }, 24.2);
|
||||
tl.fromTo('.pulse-ring-5', { opacity: 0, scale: 0.5 }, { opacity: 0.15, scale: 1, duration: 1, ease: "power2.out" }, 24.5);
|
||||
|
||||
// Pulse rings throb
|
||||
tl.to('.pulse-ring-1', { scale: 1.15, opacity: 0.5, duration: 0.6, ease: "sine.inOut", yoyo: true, repeat: 5 }, 24.5);
|
||||
tl.to('.pulse-ring-2', { scale: 1.1, opacity: 0.4, duration: 0.6, ease: "sine.inOut", yoyo: true, repeat: 5 }, 24.6);
|
||||
|
||||
// Heartbeat line draws
|
||||
tl.fromTo('.heartbeat-line', { opacity: 0 }, { opacity: 1, duration: 0.5 }, 24);
|
||||
tl.fromTo('.heartbeat-path', { drawSVG: "0%" }, { drawSVG: "100%", duration: 3, ease: "none" }, 24.2);
|
||||
|
||||
// Stat counters
|
||||
tl.to('.stat-bpm', { opacity: 1, duration: 0.5 }, 25);
|
||||
animateCounter(document.getElementById('bpm-number'), 156, 2, tl, 25);
|
||||
|
||||
tl.to('.stat-steps', { opacity: 1, duration: 0.5 }, 25.5);
|
||||
animateCounter(document.getElementById('steps-number'), 12847, 2.5, tl, 25.5);
|
||||
|
||||
tl.to('.stat-cals', { opacity: 1, duration: 0.5 }, 26);
|
||||
animateCounter(document.getElementById('cals-number'), 2340, 2.5, tl, 26);
|
||||
|
||||
// Intensify: rings pulse faster
|
||||
tl.to('.pulse-ring-3', { scale: 1.2, opacity: 0.5, duration: 0.4, ease: "sine.inOut", yoyo: true, repeat: 3 }, 27.5);
|
||||
|
||||
// Scan line
|
||||
tl.fromTo('.scan-line', { opacity: 0, top: 0 }, { opacity: 0.4, top: '100%', duration: 1, ease: "none" }, 27);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// TRANSITION 3→4: Flash into crescendo (29s - 31s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
tl.to('.data-stage', { opacity: 0, duration: 0.5, ease: "power3.in" }, 29.5);
|
||||
tl.to('.flash-overlay', { opacity: 0.6, duration: 0.15, ease: "power4.in" }, 30);
|
||||
tl.to('.flash-overlay', { opacity: 0, duration: 0.5, ease: "power2.out" }, 30.15);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// PHASE 4: CRESCENDO / CELEBRATION (30s - 36s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
tl.to('.crescendo-stage', { opacity: 1, duration: 0.3, ease: "power2.out" }, 30.2);
|
||||
|
||||
// Victory burst expands
|
||||
tl.fromTo('.victory-burst',
|
||||
{ width: 0, height: 0, opacity: 0 },
|
||||
{ width: 1400, height: 1400, opacity: 0.6, duration: 1.5, ease: "power3.out" }, 30.3);
|
||||
tl.to('.victory-burst', { opacity: 0, duration: 1.5, ease: "power2.in" }, 31.5);
|
||||
|
||||
// Gold particles explode outward
|
||||
particles.forEach(function(p, i) {
|
||||
var angle = (i / particles.length) * Math.PI * 2;
|
||||
var dist = 300 + Math.random() * 400;
|
||||
var xDest = Math.cos(angle) * dist;
|
||||
var yDest = Math.sin(angle) * dist;
|
||||
tl.fromTo(p,
|
||||
{ opacity: 0, x: 0, y: 0, scale: 0 },
|
||||
{ opacity: 1, x: xDest, y: yDest, scale: 1 + Math.random() * 2, duration: 1.2, ease: "power3.out" }, 30.3 + Math.random() * 0.3);
|
||||
tl.to(p, { opacity: 0, duration: 0.8, ease: "power2.in" }, 31.5 + Math.random() * 0.3);
|
||||
});
|
||||
|
||||
// Confetti falls
|
||||
confetti.forEach(function(c, i) {
|
||||
tl.fromTo(c,
|
||||
{ opacity: 0, y: -40, rotation: 0 },
|
||||
{ opacity: 0.8, y: 1100, rotation: 360 + Math.random() * 360,
|
||||
duration: 3 + Math.random() * 2, ease: "power1.in" }, 30.5 + Math.random() * 0.5);
|
||||
});
|
||||
|
||||
// TAGLINE: "Rise Above"
|
||||
tl.fromTo('.tagline-container', { opacity: 0 }, { opacity: 1, duration: 0.3 }, 31);
|
||||
|
||||
tl.fromTo('.tagline-rise',
|
||||
{ opacity: 0, y: 60, scale: 0.8 },
|
||||
{ opacity: 1, y: 0, scale: 1, duration: 1.2, ease: "power3.out" }, 31.2);
|
||||
|
||||
tl.fromTo('.tagline-bar',
|
||||
{ width: 0 },
|
||||
{ width: 200, duration: 0.8, ease: "power2.inOut" }, 32);
|
||||
|
||||
tl.fromTo('.tagline-above',
|
||||
{ opacity: 0, y: -60, scale: 0.8 },
|
||||
{ opacity: 1, y: 0, scale: 1, duration: 1.2, ease: "power3.out" }, 32.2);
|
||||
|
||||
// Tagline pulses with gold shimmer
|
||||
tl.to('.tagline-above', { textShadow: "0 0 40px rgba(212,175,55,0.8)", duration: 0.8, ease: "sine.inOut", yoyo: true, repeat: 1 }, 33.5);
|
||||
|
||||
// Flash transition
|
||||
tl.to('.flash-overlay', { opacity: 0.4, duration: 0.1 }, 35);
|
||||
tl.to('.flash-overlay', { opacity: 0, duration: 0.4 }, 35.1);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// PHASE 5: LOGO LOCKUP (35s - 40s)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
tl.to('.crescendo-stage', { opacity: 0, duration: 0.8, ease: "power2.inOut" }, 35);
|
||||
|
||||
tl.to('.logo-stage', { opacity: 1, duration: 0.8, ease: "power2.inOut" }, 35.5);
|
||||
|
||||
// APEX logo scales in
|
||||
tl.fromTo('.logo-apex',
|
||||
{ opacity: 0, scale: 1.5, letterSpacing: '80px' },
|
||||
{ opacity: 1, scale: 1, letterSpacing: '40px', duration: 1.8, ease: "power3.out" }, 35.8);
|
||||
|
||||
// Divider line
|
||||
tl.fromTo('.logo-divider',
|
||||
{ width: 0 },
|
||||
{ width: 300, duration: 1, ease: "power2.inOut" }, 36.8);
|
||||
|
||||
// Sub text
|
||||
tl.fromTo('.logo-sub',
|
||||
{ opacity: 0, y: 10 },
|
||||
{ opacity: 1, y: 0, duration: 1, ease: "power2.out" }, 37.3);
|
||||
|
||||
// Final tagline
|
||||
tl.fromTo('.logo-tagline-final',
|
||||
{ opacity: 0, y: 10 },
|
||||
{ opacity: 1, y: 0, duration: 1, ease: "power2.out" }, 37.8);
|
||||
|
||||
// Corner accents
|
||||
tl.to('.corner-tl', { opacity: 1, duration: 0.6, ease: "power2.out" }, 37);
|
||||
tl.to('.corner-tr', { opacity: 1, duration: 0.6, ease: "power2.out" }, 37.15);
|
||||
tl.to('.corner-br', { opacity: 1, duration: 0.6, ease: "power2.out" }, 37.3);
|
||||
tl.to('.corner-bl', { opacity: 1, duration: 0.6, ease: "power2.out" }, 37.45);
|
||||
|
||||
// Gentle breathe on final logo
|
||||
tl.to('.logo-apex', {
|
||||
textShadow: "0 0 60px rgba(212,175,55,0.4)",
|
||||
duration: 1.5, ease: "sine.inOut"
|
||||
}, 38);
|
||||
|
||||
// Hold on final frame through 40s
|
||||
tl.to({}, { duration: 1 }, 39);
|
||||
|
||||
// Register the timeline
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["gradual-escalation-apex"] = tl;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,686 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1920, height=1080">
|
||||
<title>SHATTERED — Title Sequence</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #000; display: flex; align-items: center; justify-content: center; min-height: 100vh; overflow: hidden; }
|
||||
|
||||
.composition {
|
||||
background: #0a0a0a;
|
||||
font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* === PHASE 1: Creeping Unease — drifting text fragments === */
|
||||
.vignette {
|
||||
position: absolute; inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.85) 100%);
|
||||
opacity: 0;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fragment {
|
||||
position: absolute;
|
||||
color: rgba(255, 255, 255, 0.06);
|
||||
font-size: 22px;
|
||||
letter-spacing: 8px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.fragment-0 { top: 180px; left: 200px; font-size: 18px; }
|
||||
.fragment-1 { top: 340px; left: 1400px; font-size: 26px; }
|
||||
.fragment-2 { top: 520px; left: 600px; font-size: 16px; }
|
||||
.fragment-3 { top: 700px; left: 1050px; font-size: 20px; }
|
||||
.fragment-4 { top: 260px; left: 850px; font-size: 14px; }
|
||||
.fragment-5 { top: 800px; left: 300px; font-size: 24px; }
|
||||
.fragment-6 { top: 450px; left: 1600px; font-size: 15px; }
|
||||
.fragment-7 { top: 150px; left: 1100px; font-size: 19px; }
|
||||
|
||||
.red-line {
|
||||
position: absolute;
|
||||
background: rgba(180, 20, 20, 0.3);
|
||||
height: 1px;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.red-line-0 { top: 400px; left: 100px; }
|
||||
.red-line-1 { top: 650px; left: 800px; }
|
||||
.red-line-2 { top: 280px; left: 500px; }
|
||||
|
||||
/* === PHASE 2: Title and Impact === */
|
||||
.anticipation-darken {
|
||||
position: absolute; inset: 0;
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
z-index: 55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
}
|
||||
.title-text {
|
||||
font-size: 180px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
letter-spacing: 28px;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.title-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 180px;
|
||||
font-weight: 900;
|
||||
color: transparent;
|
||||
letter-spacing: 28px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 0 60px rgba(200, 30, 30, 0.6), 0 0 120px rgba(200, 30, 30, 0.3);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
.title-shadow {
|
||||
position: absolute;
|
||||
top: 52%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 180px;
|
||||
font-weight: 900;
|
||||
color: transparent;
|
||||
letter-spacing: 28px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 8px 30px rgba(0,0,0,0.8);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* Shockwave ring */
|
||||
.shockwave {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 2px solid rgba(200, 30, 30, 0.5);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
opacity: 0;
|
||||
z-index: 90;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* === Glass Shards === */
|
||||
.shard {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0; height: 0;
|
||||
opacity: 0;
|
||||
z-index: 80;
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Each shard is a CSS triangle */
|
||||
.shard-light {
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 20px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.shard-red {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 16px solid rgba(200, 40, 40, 0.5);
|
||||
}
|
||||
.shard-dim {
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 12px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Shard reflections (secondary — drag behind shards) */
|
||||
.shard-reflection {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
z-index: 79;
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* === PHASE 3: Floating Debris === */
|
||||
.debris {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
z-index: 60;
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Subtitle */
|
||||
.subtitle {
|
||||
position: absolute;
|
||||
top: 62%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
font-size: 24px;
|
||||
letter-spacing: 14px;
|
||||
color: rgba(200, 40, 40, 0.7);
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
z-index: 100;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
/* Ambient red pulse on background */
|
||||
.bg-pulse {
|
||||
position: absolute; inset: 0;
|
||||
background: radial-gradient(ellipse at center, rgba(120, 10, 10, 0.08) 0%, transparent 70%);
|
||||
opacity: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="composition" data-composition-id="high-contrast-arc-shattered-v2" style="width: 1920px; height: 1080px; position: relative; overflow: hidden;">
|
||||
|
||||
<!-- Background pulse -->
|
||||
<div class="bg-pulse" id="bg-pulse"></div>
|
||||
|
||||
<!-- Vignette -->
|
||||
<div class="vignette" id="vignette"></div>
|
||||
|
||||
<!-- Anticipation darkening layer -->
|
||||
<div class="anticipation-darken" id="anticipation-darken"></div>
|
||||
|
||||
<!-- Phase 1: Text fragments -->
|
||||
<div class="fragment fragment-0" id="frag-0">NOTHING</div>
|
||||
<div class="fragment fragment-1" id="frag-1">IS SAFE</div>
|
||||
<div class="fragment fragment-2" id="frag-2">THE TRUTH</div>
|
||||
<div class="fragment fragment-3" id="frag-3">BREAKS</div>
|
||||
<div class="fragment fragment-4" id="frag-4">EVERY MIRROR</div>
|
||||
<div class="fragment fragment-5" id="frag-5">LIES</div>
|
||||
<div class="fragment fragment-6" id="frag-6">BENEATH</div>
|
||||
<div class="fragment fragment-7" id="frag-7">THE SURFACE</div>
|
||||
|
||||
<!-- Red accent lines -->
|
||||
<div class="red-line red-line-0" id="red-line-0"></div>
|
||||
<div class="red-line red-line-1" id="red-line-1"></div>
|
||||
<div class="red-line red-line-2" id="red-line-2"></div>
|
||||
|
||||
<!-- Glass shards (generated positions, scattered from center) -->
|
||||
<div class="shard shard-light" id="shard-0"></div>
|
||||
<div class="shard shard-red" id="shard-1"></div>
|
||||
<div class="shard shard-dim" id="shard-2"></div>
|
||||
<div class="shard shard-light" id="shard-3"></div>
|
||||
<div class="shard shard-red" id="shard-4"></div>
|
||||
<div class="shard shard-dim" id="shard-5"></div>
|
||||
<div class="shard shard-light" id="shard-6"></div>
|
||||
<div class="shard shard-red" id="shard-7"></div>
|
||||
<div class="shard shard-dim" id="shard-8"></div>
|
||||
<div class="shard shard-light" id="shard-9"></div>
|
||||
<div class="shard shard-red" id="shard-10"></div>
|
||||
<div class="shard shard-dim" id="shard-11"></div>
|
||||
<div class="shard shard-light" id="shard-12"></div>
|
||||
<div class="shard shard-red" id="shard-13"></div>
|
||||
<div class="shard shard-light" id="shard-14"></div>
|
||||
<div class="shard shard-dim" id="shard-15"></div>
|
||||
<div class="shard shard-red" id="shard-16"></div>
|
||||
<div class="shard shard-light" id="shard-17"></div>
|
||||
|
||||
<!-- Shard reflections (drag behind shards) -->
|
||||
<div class="shard-reflection" id="ref-0"></div>
|
||||
<div class="shard-reflection" id="ref-1"></div>
|
||||
<div class="shard-reflection" id="ref-2"></div>
|
||||
<div class="shard-reflection" id="ref-3"></div>
|
||||
<div class="shard-reflection" id="ref-4"></div>
|
||||
<div class="shard-reflection" id="ref-5"></div>
|
||||
<div class="shard-reflection" id="ref-6"></div>
|
||||
<div class="shard-reflection" id="ref-7"></div>
|
||||
<div class="shard-reflection" id="ref-8"></div>
|
||||
|
||||
<!-- Shockwave -->
|
||||
<div class="shockwave" id="shockwave"></div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="title-container" id="title-container">
|
||||
<div class="title-shadow" id="title-shadow">SHATTERED</div>
|
||||
<div class="title-glow" id="title-glow">SHATTERED</div>
|
||||
<div class="title-text" id="title-text">SHATTERED</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<div class="subtitle" id="subtitle">A THRILLER</div>
|
||||
|
||||
<!-- Phase 3: Floating debris particles -->
|
||||
<div class="debris" id="debris-0" style="width:4px;height:4px;top:200px;left:300px;"></div>
|
||||
<div class="debris" id="debris-1" style="width:3px;height:6px;top:400px;left:1500px;"></div>
|
||||
<div class="debris" id="debris-2" style="width:5px;height:3px;top:600px;left:900px;"></div>
|
||||
<div class="debris" id="debris-3" style="width:2px;height:5px;top:300px;left:1200px;"></div>
|
||||
<div class="debris" id="debris-4" style="width:6px;height:2px;top:750px;left:500px;"></div>
|
||||
<div class="debris" id="debris-5" style="width:3px;height:3px;top:150px;left:1600px;"></div>
|
||||
<div class="debris" id="debris-6" style="width:4px;height:5px;top:850px;left:800px;"></div>
|
||||
<div class="debris" id="debris-7" style="width:2px;height:4px;top:500px;left:200px;"></div>
|
||||
<div class="debris" id="debris-8" style="width:5px;height:2px;top:680px;left:1350px;"></div>
|
||||
<div class="debris" id="debris-9" style="width:3px;height:4px;top:100px;left:700px;"></div>
|
||||
<div class="debris" id="debris-10" style="width:4px;height:3px;top:920px;left:1100px;"></div>
|
||||
<div class="debris" id="debris-11" style="width:2px;height:6px;top:350px;left:400px;"></div>
|
||||
<div class="debris" id="debris-12" style="width:3px;height:2px;top:550px;left:1700px;background:rgba(200,40,40,0.1);"></div>
|
||||
<div class="debris" id="debris-13" style="width:5px;height:4px;top:780px;left:150px;"></div>
|
||||
<div class="debris" id="debris-14" style="width:2px;height:3px;top:430px;left:1000px;background:rgba(200,40,40,0.1);"></div>
|
||||
<div class="debris" id="debris-15" style="width:4px;height:2px;top:620px;left:1800px;"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- GSAP Core + Plugins -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js"></script>
|
||||
<script>
|
||||
gsap.registerPlugin(CustomEase, MotionPathPlugin);
|
||||
|
||||
// ============================================================
|
||||
// TIMING SCALE: 0.08s | 0.4s | 1.2s | 3s
|
||||
// Multiples/fractions used: 0.08, 0.4, 1.2, 2.4, 3, 6, 9, 12
|
||||
// ============================================================
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["high-contrast-arc-shattered-v2"] = tl;
|
||||
|
||||
// Custom eases
|
||||
// Creep: barely moves for most of the duration, then rushes at the end
|
||||
CustomEase.create("creep", "M0,0 C0.6,0.02 0.65,0.04 0.75,0.08 0.85,0.3 0.92,0.65 1,1");
|
||||
// Tension snap: holds even longer, snaps harder
|
||||
CustomEase.create("tensionSnap", "M0,0 C0.8,0 0.7,0.05 0.8,0.1 0.9,0.4 0.95,0.7 1,1");
|
||||
|
||||
// ============================================================
|
||||
// PHASE 1: CREEPING UNEASE (0s - 18s)
|
||||
// Mood: Tense. Slow reveals, micro-drift, uncomfortable holds.
|
||||
// ============================================================
|
||||
|
||||
// Vignette tightens slowly — staging through light
|
||||
tl.to("#vignette", { opacity: 0.7, duration: 6, ease: "power1.in" }, 0);
|
||||
tl.to("#vignette", { opacity: 0.9, duration: 12, ease: "sine.inOut" }, 6);
|
||||
|
||||
// Background pulse — barely visible red warmth creeping in
|
||||
tl.to("#bg-pulse", { opacity: 0.4, duration: 6, ease: "creep" }, 3);
|
||||
|
||||
// Text fragments — creep in one at a time, long uncomfortable gaps
|
||||
// Each fragment drifts slightly after appearing (moving hold)
|
||||
const fragments = [
|
||||
{ id: "#frag-0", enterAt: 1.2, driftX: 8, driftY: -3 },
|
||||
{ id: "#frag-1", enterAt: 3.6, driftX: -5, driftY: 4 },
|
||||
{ id: "#frag-2", enterAt: 5.4, driftX: 6, driftY: -5 },
|
||||
{ id: "#frag-3", enterAt: 7.2, driftX: -7, driftY: 3 },
|
||||
{ id: "#frag-4", enterAt: 8.4, driftX: 4, driftY: -4 },
|
||||
{ id: "#frag-5", enterAt: 10.2, driftX: -6, driftY: 5 },
|
||||
{ id: "#frag-6", enterAt: 11.4, driftX: 5, driftY: -3 },
|
||||
{ id: "#frag-7", enterAt: 12.6, driftX: -4, driftY: 4 }
|
||||
];
|
||||
|
||||
fragments.forEach((f) => {
|
||||
// Entrance: creeping in from slightly off (barely moves for 80%, then arrives)
|
||||
tl.fromTo(f.id,
|
||||
{ opacity: 0, y: 15 },
|
||||
{ opacity: 1, y: 0, duration: 3, ease: "creep" },
|
||||
f.enterAt
|
||||
);
|
||||
|
||||
// Moving hold: perpetual micro-drift after entrance — nothing is ever still
|
||||
tl.to(f.id, {
|
||||
x: f.driftX, y: f.driftY,
|
||||
duration: 6, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, f.enterAt + 3);
|
||||
});
|
||||
|
||||
// Red accent lines — thin, creeping, ominous
|
||||
tl.to("#red-line-0", { width: 180, opacity: 0.6, duration: 3, ease: "creep" }, 4.8);
|
||||
tl.to("#red-line-1", { width: 120, opacity: 0.4, duration: 3, ease: "creep" }, 7.2);
|
||||
tl.to("#red-line-2", { width: 200, opacity: 0.5, duration: 3, ease: "creep" }, 9.6);
|
||||
|
||||
// Red lines also get a moving hold — slow width oscillation
|
||||
["#red-line-0", "#red-line-1", "#red-line-2"].forEach((id, i) => {
|
||||
tl.to(id, {
|
||||
width: "+=15", opacity: "-=0.05",
|
||||
duration: 3, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 2
|
||||
}, 8 + i * 1.2);
|
||||
});
|
||||
|
||||
// Phase 1 Exit: fragments compress inward (tense exit style)
|
||||
fragments.forEach((f, i) => {
|
||||
tl.to(f.id, {
|
||||
x: 0, y: 0, opacity: 0,
|
||||
duration: 1.2, ease: "power2.in"
|
||||
}, 15.6 + i * 0.08);
|
||||
});
|
||||
|
||||
// Red lines fade
|
||||
tl.to(".red-line", { opacity: 0, width: 0, duration: 1.2, ease: "power2.in" }, 16);
|
||||
|
||||
// ============================================================
|
||||
// PHASE 2: ANTICIPATION + EXPLOSIVE REVEAL (18s - 28s)
|
||||
// Transition: Hard cut from tense to punchy
|
||||
// 2.5s anticipation → 0.08s slam → secondaries AFTER
|
||||
// ============================================================
|
||||
|
||||
// --- ANTICIPATION: 18s - 20.4s (2.4s of near-stillness) ---
|
||||
// The silence IS the anticipation.
|
||||
|
||||
// Scene darkens — a held breath
|
||||
tl.to("#anticipation-darken", {
|
||||
opacity: 0.4, duration: 2.4, ease: "tensionSnap"
|
||||
}, 18);
|
||||
|
||||
// Vignette tightens further
|
||||
tl.to("#vignette", { opacity: 1, duration: 2.4, ease: "power1.in" }, 18);
|
||||
|
||||
// Background pulse dims — the world contracts
|
||||
tl.to("#bg-pulse", { opacity: 0.1, duration: 2.4, ease: "power2.in" }, 18);
|
||||
|
||||
// Barely perceptible camera zoom (via inner wrapper to not break studio)
|
||||
// Omitted — scaling .composition breaks HyperFrames Studio rendering
|
||||
|
||||
// --- THE SLAM: 20.4s — 0.08s duration. NOTHING else moves. ---
|
||||
const slamTime = 20.4;
|
||||
|
||||
// Title container appears
|
||||
tl.set("#title-container", { opacity: 1 }, slamTime);
|
||||
|
||||
// Title text: scale overshoot with squash (volume preserved)
|
||||
// Arrives at scaleY: 0.7, scaleX: 1.43 (volume = 0.7 * 1.43 = 1.001)
|
||||
tl.fromTo("#title-text",
|
||||
{ scaleY: 2.2, scaleX: 0.45, opacity: 0 },
|
||||
{ scaleY: 0.7, scaleX: 1.43, opacity: 1, duration: 0.08, ease: "power4.out" },
|
||||
slamTime
|
||||
);
|
||||
|
||||
// Squash recovery — elastic settle
|
||||
tl.to("#title-text", {
|
||||
scaleY: 1.08, scaleX: 0.93, duration: 0.4, ease: "power3.out"
|
||||
}, slamTime + 0.08);
|
||||
tl.to("#title-text", {
|
||||
scaleY: 0.97, scaleX: 1.03, duration: 0.4, ease: "sine.inOut"
|
||||
}, slamTime + 0.48);
|
||||
tl.to("#title-text", {
|
||||
scaleY: 1, scaleX: 1, duration: 0.4, ease: "sine.out"
|
||||
}, slamTime + 0.88);
|
||||
|
||||
// Camera zoom reset omitted — see above
|
||||
|
||||
// --- SECONDARY ACTIONS: all AFTER the slam (slamTime + 0.08 and later) ---
|
||||
|
||||
// Anticipation darken layer clears
|
||||
tl.to("#anticipation-darken", {
|
||||
opacity: 0, duration: 0.4, ease: "power2.out"
|
||||
}, slamTime + 0.08);
|
||||
|
||||
// Title shadow — DRAGS 0.05s behind the title
|
||||
tl.fromTo("#title-shadow",
|
||||
{ opacity: 0, scaleY: 0.7, scaleX: 1.43 },
|
||||
{ opacity: 0.6, scaleY: 1, scaleX: 1, duration: 0.4, ease: "power2.out" },
|
||||
slamTime + 0.05
|
||||
);
|
||||
|
||||
// Title glow — DRAGS 0.08s behind the title
|
||||
tl.to("#title-glow", {
|
||||
opacity: 0.7, duration: 1.2, ease: "sine.out"
|
||||
}, slamTime + 0.08);
|
||||
|
||||
// Shockwave — expands AFTER slam
|
||||
tl.fromTo("#shockwave",
|
||||
{ scale: 0.5, opacity: 0.7 },
|
||||
{ scale: 15, opacity: 0, duration: 1.2, ease: "power2.out" },
|
||||
slamTime + 0.12
|
||||
);
|
||||
|
||||
// Vignette reacts
|
||||
tl.to("#vignette", {
|
||||
opacity: 0.5, duration: 0.4, ease: "power2.out"
|
||||
}, slamTime + 0.16);
|
||||
|
||||
// Background pulse flares red after impact
|
||||
tl.to("#bg-pulse", {
|
||||
opacity: 0.8, duration: 0.08, ease: "power4.out"
|
||||
}, slamTime + 0.12);
|
||||
tl.to("#bg-pulse", {
|
||||
opacity: 0.2, duration: 1.2, ease: "sine.out"
|
||||
}, slamTime + 0.2);
|
||||
|
||||
// === GLASS SHARDS — scatter on ARCS after slam ===
|
||||
// Faster shards = flatter arcs, lighter = faster
|
||||
// Each shard has a reflection that drags 0.04-0.06s behind
|
||||
|
||||
const shardData = [
|
||||
// { id, refId, angle (degrees), speed (px), arcDepth, dur, rotation }
|
||||
{ id: "#shard-0", ref: "#ref-0", x: -420, y: -280, arc: -90, dur: 1.2, rot: -220 },
|
||||
{ id: "#shard-1", ref: "#ref-1", x: 380, y: -310, arc: -80, dur: 1.2, rot: 195 },
|
||||
{ id: "#shard-2", ref: null, x: -280, y: 200, arc: 60, dur: 0.4, rot: -150 },
|
||||
{ id: "#shard-3", ref: "#ref-2", x: 500, y: -180, arc: -50, dur: 0.4, rot: 280 },
|
||||
{ id: "#shard-4", ref: null, x: -600, y: -120, arc: -30, dur: 0.4, rot: -310 },
|
||||
{ id: "#shard-5", ref: "#ref-3", x: 200, y: 320, arc: 100, dur: 1.2, rot: 160 },
|
||||
{ id: "#shard-6", ref: null, x: -350, y: -400, arc: -110, dur: 1.2, rot: -240 },
|
||||
{ id: "#shard-7", ref: "#ref-4", x: 650, y: 100, arc: 25, dur: 0.4, rot: 190 },
|
||||
{ id: "#shard-8", ref: null, x: -150, y: 350, arc: 80, dur: 1.2, rot: -130 },
|
||||
{ id: "#shard-9", ref: "#ref-5", x: 300, y: -450, arc: -120, dur: 1.2, rot: 260 },
|
||||
{ id: "#shard-10", ref: null, x: -500, y: 250, arc: 70, dur: 0.4, rot: -200 },
|
||||
{ id: "#shard-11", ref: "#ref-6", x: 450, y: 280, arc: 55, dur: 1.2, rot: 175 },
|
||||
{ id: "#shard-12", ref: null, x: -250, y: -350, arc: -95, dur: 0.4, rot: -265 },
|
||||
{ id: "#shard-13", ref: "#ref-7", x: 550, y: -250, arc: -45, dur: 0.4, rot: 230 },
|
||||
{ id: "#shard-14", ref: null, x: -450, y: 150, arc: 40, dur: 1.2, rot: -180 },
|
||||
{ id: "#shard-15", ref: "#ref-8", x: 180, y: -200, arc: -70, dur: 0.4, rot: 145 },
|
||||
{ id: "#shard-16", ref: null, x: -320, y: -150, arc: -35, dur: 0.4, rot: -160 },
|
||||
{ id: "#shard-17", ref: null, x: 400, y: 350, arc: 90, dur: 1.2, rot: 210 }
|
||||
];
|
||||
|
||||
const shardLaunchTime = slamTime + 0.12;
|
||||
|
||||
shardData.forEach((s, i) => {
|
||||
const stagger = i * 0.02; // slight cascade, not simultaneous
|
||||
const launchT = shardLaunchTime + stagger;
|
||||
|
||||
// Shard appears and moves on an arc
|
||||
// Arc: x goes to target, y arcs via a midpoint
|
||||
const midY = s.arc; // arc depth — deeper for slower shards
|
||||
const halfDur = s.dur / 2;
|
||||
|
||||
tl.set(s.id, { opacity: 1 }, launchT);
|
||||
|
||||
// Horizontal + rotation over full duration
|
||||
tl.to(s.id, {
|
||||
x: s.x, rotation: s.rot,
|
||||
duration: s.dur, ease: "power2.out"
|
||||
}, launchT);
|
||||
|
||||
// Vertical arc: up first half, down second half (parabolic arc)
|
||||
tl.to(s.id, {
|
||||
y: midY, duration: halfDur, ease: "power2.out"
|
||||
}, launchT);
|
||||
tl.to(s.id, {
|
||||
y: s.y, duration: halfDur, ease: "power2.in"
|
||||
}, launchT + halfDur);
|
||||
|
||||
// Fade out at end
|
||||
tl.to(s.id, {
|
||||
opacity: 0, duration: 0.4, ease: "power1.in"
|
||||
}, launchT + s.dur - 0.4);
|
||||
|
||||
// Reflection DRAGS behind shard by 0.04-0.06s
|
||||
if (s.ref) {
|
||||
const dragDelay = 0.04 + (i % 3) * 0.01;
|
||||
tl.set(s.ref, { opacity: 0.4 }, launchT + dragDelay);
|
||||
tl.to(s.ref, {
|
||||
x: s.x * 0.9, y: s.y * 0.9, rotation: s.rot * 0.7,
|
||||
duration: s.dur, ease: "power2.out"
|
||||
}, launchT + dragDelay);
|
||||
tl.to(s.ref, {
|
||||
opacity: 0, duration: 0.4, ease: "power1.in"
|
||||
}, launchT + dragDelay + s.dur - 0.4);
|
||||
}
|
||||
});
|
||||
|
||||
// Title moving hold: subtle breathing after it settles (from ~21.8s onward)
|
||||
const titleSettleTime = slamTime + 1.2 + 0.08;
|
||||
tl.to("#title-text", {
|
||||
scaleY: 1.005, scaleX: 0.995,
|
||||
duration: 3, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, titleSettleTime);
|
||||
|
||||
tl.to("#title-shadow", {
|
||||
y: 2, opacity: 0.55,
|
||||
duration: 3, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, titleSettleTime + 0.06);
|
||||
|
||||
tl.to("#title-glow", {
|
||||
opacity: 0.5,
|
||||
duration: 3, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, titleSettleTime + 0.08);
|
||||
|
||||
// Subtitle enters — proportional exaggeration (slight scale overshoot matching title's world)
|
||||
tl.fromTo("#subtitle",
|
||||
{ opacity: 0, y: 15, scaleX: 1.06 },
|
||||
{ opacity: 0.7, y: 0, scaleX: 1, duration: 1.2, ease: "power3.out" },
|
||||
slamTime + 1.6
|
||||
);
|
||||
|
||||
// Subtitle moving hold
|
||||
tl.to("#subtitle", {
|
||||
y: -2, opacity: 0.65,
|
||||
duration: 3, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, slamTime + 2.8);
|
||||
|
||||
// ============================================================
|
||||
// PHASE 3: CALM AFTERMATH (28s - 45s)
|
||||
// Mood: Calm. Floating debris, deep arcs, perpetual moving holds.
|
||||
// Transition: Soft dissolve from impact energy into serenity
|
||||
// ============================================================
|
||||
|
||||
const phase3Start = 28;
|
||||
|
||||
// Vignette softens
|
||||
tl.to("#vignette", {
|
||||
opacity: 0.4, duration: 3, ease: "sine.inOut"
|
||||
}, phase3Start);
|
||||
|
||||
// Background pulse settles to gentle warmth
|
||||
tl.to("#bg-pulse", {
|
||||
opacity: 0.15, duration: 3, ease: "sine.inOut"
|
||||
}, phase3Start);
|
||||
|
||||
// Title glow dims to soft
|
||||
tl.to("#title-glow", {
|
||||
opacity: 0.3, duration: 3, ease: "sine.inOut"
|
||||
}, phase3Start);
|
||||
|
||||
// Floating debris — each on deep, curved arcs (slow = deep arcs)
|
||||
// These drift perpetually with moving holds
|
||||
const debrisData = [
|
||||
{ id: "#debris-0", startAt: 28, driftX: 80, driftY: -40, arcY: -25, dur: 12 },
|
||||
{ id: "#debris-1", startAt: 28.4, driftX: -60, driftY: -50, arcY: -30, dur: 9 },
|
||||
{ id: "#debris-2", startAt: 29.2, driftX: 50, driftY: -35, arcY: -20, dur: 12 },
|
||||
{ id: "#debris-3", startAt: 28.8, driftX: -70, driftY: -45, arcY: -28, dur: 9 },
|
||||
{ id: "#debris-4", startAt: 30, driftX: 90, driftY: -30, arcY: -18, dur: 12 },
|
||||
{ id: "#debris-5", startAt: 29.6, driftX: -40, driftY: -55, arcY: -35, dur: 9 },
|
||||
{ id: "#debris-6", startAt: 30.4, driftX: 65, driftY: -25, arcY: -15, dur: 12 },
|
||||
{ id: "#debris-7", startAt: 28.4, driftX: -55, driftY: -40, arcY: -22, dur: 9 },
|
||||
{ id: "#debris-8", startAt: 31.2, driftX: 45, driftY: -50, arcY: -30, dur: 12 },
|
||||
{ id: "#debris-9", startAt: 29.2, driftX: -75, driftY: -35, arcY: -20, dur: 9 },
|
||||
{ id: "#debris-10", startAt: 30, driftX: 55, driftY: -45, arcY: -25, dur: 12 },
|
||||
{ id: "#debris-11", startAt: 30.8, driftX: -50, driftY: -30, arcY: -18, dur: 9 },
|
||||
{ id: "#debris-12", startAt: 31.2, driftX: 35, driftY: -55, arcY: -32, dur: 12 },
|
||||
{ id: "#debris-13", startAt: 29.6, driftX: -80, driftY: -40, arcY: -24, dur: 9 },
|
||||
{ id: "#debris-14", startAt: 30.4, driftX: 60, driftY: -35, arcY: -20, dur: 12 },
|
||||
{ id: "#debris-15", startAt: 31.6, driftX: -45, driftY: -50, arcY: -28, dur: 9 }
|
||||
];
|
||||
|
||||
debrisData.forEach((d) => {
|
||||
// Fade in gently
|
||||
tl.to(d.id, {
|
||||
opacity: 0.3, duration: 3, ease: "sine.inOut"
|
||||
}, d.startAt);
|
||||
|
||||
// Deep arc drift — x and y as separate tweens for arc shape
|
||||
// Horizontal: slow continuous
|
||||
tl.to(d.id, {
|
||||
x: d.driftX, duration: d.dur, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, d.startAt + 1.2);
|
||||
|
||||
// Vertical: slower, deeper arc (different duration creates curve)
|
||||
tl.to(d.id, {
|
||||
y: d.driftY, duration: d.dur * 0.8, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, d.startAt + 1.2);
|
||||
|
||||
// Subtle rotation drift — nothing moves in a straight line
|
||||
tl.to(d.id, {
|
||||
rotation: d.driftX > 0 ? 15 : -15,
|
||||
duration: d.dur * 1.2, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 3
|
||||
}, d.startAt + 1.2);
|
||||
});
|
||||
|
||||
// Final fade: everything gently recedes at the very end
|
||||
// Title fades last (it has the most mass, the most presence)
|
||||
tl.to(".debris", {
|
||||
opacity: 0, duration: 3, ease: "sine.in"
|
||||
}, 42);
|
||||
|
||||
tl.to("#subtitle", {
|
||||
opacity: 0, y: 10, duration: 3, ease: "sine.in"
|
||||
}, 41.5);
|
||||
|
||||
tl.to("#title-glow", {
|
||||
opacity: 0, duration: 3, ease: "sine.inOut"
|
||||
}, 42);
|
||||
|
||||
tl.to("#title-shadow", {
|
||||
opacity: 0, duration: 3, ease: "sine.inOut"
|
||||
}, 42);
|
||||
|
||||
tl.to("#title-text", {
|
||||
opacity: 0, duration: 3, ease: "sine.in"
|
||||
}, 42.5);
|
||||
|
||||
tl.to("#vignette", {
|
||||
opacity: 0, duration: 3, ease: "sine.in"
|
||||
}, 42);
|
||||
|
||||
tl.to("#bg-pulse", {
|
||||
opacity: 0, duration: 3, ease: "sine.in"
|
||||
}, 42);
|
||||
|
||||
// Ensure timeline reaches 45s
|
||||
tl.set({}, {}, 45);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,702 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1920, height=1080">
|
||||
<title>SHATTERED - Thriller Title Sequence</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #000; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
||||
|
||||
.composition {
|
||||
background: #0a0a0a;
|
||||
font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Phase 1: Tense - Drifting text fragments in darkness */
|
||||
.fragment {
|
||||
position: absolute;
|
||||
font-size: 18px;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.fragment.red { color: rgba(180, 30, 30, 0.5); }
|
||||
.fragment-1 { top: 200px; left: 300px; font-size: 14px; }
|
||||
.fragment-2 { top: 480px; left: 1200px; font-size: 16px; }
|
||||
.fragment-3 { top: 650px; left: 500px; font-size: 12px; }
|
||||
.fragment-4 { top: 350px; left: 900px; font-size: 20px; }
|
||||
.fragment-5 { top: 750px; left: 150px; font-size: 15px; }
|
||||
.fragment-6 { top: 150px; left: 1400px; font-size: 13px; }
|
||||
.fragment-7 { top: 550px; left: 700px; font-size: 17px; }
|
||||
.fragment-8 { top: 880px; left: 1000px; font-size: 11px; }
|
||||
|
||||
/* Tension line */
|
||||
.tension-line {
|
||||
position: absolute;
|
||||
top: 540px;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1px;
|
||||
background: rgba(180, 30, 30, 0.3);
|
||||
transform: scaleX(0);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
/* Phase 2: Punchy - Title reveal */
|
||||
.title-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
.title-text {
|
||||
font-size: 180px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 30px;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 60px rgba(180, 30, 30, 0.8), 0 0 120px rgba(180, 30, 30, 0.4);
|
||||
}
|
||||
|
||||
/* Glass shards */
|
||||
.shard {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
.shard svg { width: 100%; height: 100%; }
|
||||
.shard-1 { top: 300px; left: 400px; width: 80px; height: 120px; }
|
||||
.shard-2 { top: 200px; left: 800px; width: 60px; height: 90px; }
|
||||
.shard-3 { top: 500px; left: 1200px; width: 100px; height: 70px; }
|
||||
.shard-4 { top: 600px; left: 600px; width: 50px; height: 110px; }
|
||||
.shard-5 { top: 150px; left: 1000px; width: 70px; height: 80px; }
|
||||
.shard-6 { top: 700px; left: 350px; width: 90px; height: 60px; }
|
||||
.shard-7 { top: 400px; left: 1400px; width: 55px; height: 100px; }
|
||||
.shard-8 { top: 800px; left: 900px; width: 75px; height: 85px; }
|
||||
.shard-9 { top: 250px; left: 200px; width: 65px; height: 95px; }
|
||||
.shard-10 { top: 650px; left: 1100px; width: 85px; height: 55px; }
|
||||
.shard-11 { top: 350px; left: 700px; width: 45px; height: 75px; }
|
||||
.shard-12 { top: 500px; left: 300px; width: 70px; height: 65px; }
|
||||
|
||||
/* Impact flash */
|
||||
.impact-flash {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: radial-gradient(ellipse at center, rgba(255,255,255,0.9) 0%, rgba(180,30,30,0.3) 40%, transparent 70%);
|
||||
opacity: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* Red pulse ring */
|
||||
.pulse-ring {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
width: 200px; height: 200px;
|
||||
border: 2px solid rgba(180, 30, 30, 0.6);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0;
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
/* Phase 3: Calm - Floating debris */
|
||||
.debris {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
.debris-dot {
|
||||
width: 4px; height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.debris-line {
|
||||
width: 30px; height: 1px;
|
||||
background: rgba(180, 30, 30, 0.2);
|
||||
transform-origin: left center;
|
||||
}
|
||||
.debris-shard-small {
|
||||
width: 8px; height: 12px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
clip-path: polygon(50% 0%, 100% 100%, 0% 80%);
|
||||
}
|
||||
|
||||
.debris-1 { top: 200px; left: 300px; }
|
||||
.debris-2 { top: 400px; left: 700px; }
|
||||
.debris-3 { top: 600px; left: 1100px; }
|
||||
.debris-4 { top: 300px; left: 1500px; }
|
||||
.debris-5 { top: 750px; left: 400px; }
|
||||
.debris-6 { top: 150px; left: 900px; }
|
||||
.debris-7 { top: 850px; left: 1300px; }
|
||||
.debris-8 { top: 500px; left: 200px; }
|
||||
.debris-9 { top: 350px; left: 1050px; }
|
||||
.debris-10 { top: 680px; left: 600px; }
|
||||
.debris-11 { top: 100px; left: 1600px; }
|
||||
.debris-12 { top: 900px; left: 800px; }
|
||||
.debris-13 { top: 450px; left: 1400px; }
|
||||
.debris-14 { top: 250px; left: 500px; }
|
||||
.debris-15 { top: 780px; left: 1000px; }
|
||||
|
||||
/* Settled title for Phase 3 */
|
||||
.title-settled {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 160px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 25px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
opacity: 0;
|
||||
z-index: 6;
|
||||
text-shadow: 0 0 40px rgba(180, 30, 30, 0.3);
|
||||
}
|
||||
|
||||
/* Subtitle */
|
||||
.subtitle {
|
||||
position: absolute;
|
||||
top: calc(50% + 110px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 18px;
|
||||
letter-spacing: 12px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(180, 30, 30, 0.5);
|
||||
opacity: 0;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
/* Ambient vignette */
|
||||
.vignette {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.7) 100%);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Dark overlay for tension */
|
||||
.dark-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="composition" data-composition-id="high-contrast-arc-shattered" style="width: 1920px; height: 1080px; position: relative; overflow: hidden;">
|
||||
|
||||
<!-- Ambient vignette (always visible) -->
|
||||
<div class="vignette"></div>
|
||||
<div class="dark-overlay"></div>
|
||||
|
||||
<!-- Phase 1: Tense - Text fragments drifting in darkness -->
|
||||
<div class="fragment fragment-1">no escape</div>
|
||||
<div class="fragment fragment-2 red">the truth</div>
|
||||
<div class="fragment fragment-3">beneath the surface</div>
|
||||
<div class="fragment fragment-4 red">broken</div>
|
||||
<div class="fragment fragment-5">who are you</div>
|
||||
<div class="fragment fragment-6">silence</div>
|
||||
<div class="fragment fragment-7 red">shattered</div>
|
||||
<div class="fragment fragment-8">in the dark</div>
|
||||
|
||||
<div class="tension-line"></div>
|
||||
|
||||
<!-- Phase 2: Punchy - Title reveal with glass shatter -->
|
||||
<div class="impact-flash"></div>
|
||||
|
||||
<div class="title-container">
|
||||
<div class="title-text">SHATTERED</div>
|
||||
</div>
|
||||
|
||||
<div class="pulse-ring"></div>
|
||||
|
||||
<!-- Glass shards (SVG for angular shattering glass shapes) -->
|
||||
<div class="shard shard-1"><svg viewBox="0 0 80 120"><polygon points="40,0 80,70 50,120 0,90" fill="rgba(255,255,255,0.15)" stroke="rgba(255,255,255,0.3)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-2"><svg viewBox="0 0 60 90"><polygon points="30,0 60,50 40,90 0,60" fill="rgba(180,30,30,0.1)" stroke="rgba(180,30,30,0.3)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-3"><svg viewBox="0 0 100 70"><polygon points="50,0 100,40 70,70 0,50" fill="rgba(255,255,255,0.12)" stroke="rgba(255,255,255,0.25)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-4"><svg viewBox="0 0 50 110"><polygon points="25,0 50,60 30,110 0,80" fill="rgba(180,30,30,0.08)" stroke="rgba(180,30,30,0.2)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-5"><svg viewBox="0 0 70 80"><polygon points="35,0 70,45 50,80 0,60" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-6"><svg viewBox="0 0 90 60"><polygon points="45,0 90,35 60,60 0,40" fill="rgba(255,255,255,0.13)" stroke="rgba(255,255,255,0.25)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-7"><svg viewBox="0 0 55 100"><polygon points="28,0 55,55 35,100 0,75" fill="rgba(180,30,30,0.1)" stroke="rgba(180,30,30,0.25)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-8"><svg viewBox="0 0 75 85"><polygon points="38,0 75,50 55,85 0,60" fill="rgba(255,255,255,0.11)" stroke="rgba(255,255,255,0.22)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-9"><svg viewBox="0 0 65 95"><polygon points="33,0 65,55 45,95 0,70" fill="rgba(255,255,255,0.09)" stroke="rgba(255,255,255,0.2)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-10"><svg viewBox="0 0 85 55"><polygon points="43,0 85,30 60,55 0,40" fill="rgba(180,30,30,0.12)" stroke="rgba(180,30,30,0.25)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-11"><svg viewBox="0 0 45 75"><polygon points="23,0 45,40 30,75 0,55" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.18)" stroke-width="0.5"/></svg></div>
|
||||
<div class="shard shard-12"><svg viewBox="0 0 70 65"><polygon points="35,0 70,35 50,65 0,48" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="0.5"/></svg></div>
|
||||
|
||||
<!-- Phase 3: Calm - Floating debris -->
|
||||
<div class="debris debris-1"><div class="debris-dot"></div></div>
|
||||
<div class="debris debris-2"><div class="debris-shard-small"></div></div>
|
||||
<div class="debris debris-3"><div class="debris-line"></div></div>
|
||||
<div class="debris debris-4"><div class="debris-dot"></div></div>
|
||||
<div class="debris debris-5"><div class="debris-shard-small"></div></div>
|
||||
<div class="debris debris-6"><div class="debris-line"></div></div>
|
||||
<div class="debris debris-7"><div class="debris-dot"></div></div>
|
||||
<div class="debris debris-8"><div class="debris-shard-small"></div></div>
|
||||
<div class="debris debris-9"><div class="debris-line"></div></div>
|
||||
<div class="debris debris-10"><div class="debris-dot"></div></div>
|
||||
<div class="debris debris-11"><div class="debris-shard-small"></div></div>
|
||||
<div class="debris debris-12"><div class="debris-line"></div></div>
|
||||
<div class="debris debris-13"><div class="debris-dot"></div></div>
|
||||
<div class="debris debris-14"><div class="debris-shard-small"></div></div>
|
||||
<div class="debris debris-15"><div class="debris-line"></div></div>
|
||||
|
||||
<!-- Settled title for calm phase -->
|
||||
<div class="title-settled">SHATTERED</div>
|
||||
<div class="subtitle">a thriller</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- GSAP 3.14.2 CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomWiggle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomBounce.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/SplitText.min.js"></script>
|
||||
<script>
|
||||
// Register ALL plugins before use
|
||||
gsap.registerPlugin(CustomEase, CustomWiggle, CustomBounce, SplitText);
|
||||
|
||||
// =========================================================================
|
||||
// PHASE DECOMPOSITION (from emotion-to-motion skill)
|
||||
// =========================================================================
|
||||
// Phase 1: TENSE (0s - 15s) — creeping unease
|
||||
// Entrance: slow reveal (25% = 3.75s)
|
||||
// Emphasis: tension hold w/ CustomWiggle micro-tremor (50% = 7.5s)
|
||||
// Exit: compress (25% = 3.75s)
|
||||
//
|
||||
// Transition: tense -> punchy = HARD (0.5s overlap)
|
||||
//
|
||||
// Phase 2: PUNCHY (14.5s - 24.5s) — explosive reveal
|
||||
// Entrance: impact (15% = 1.5s), SplitText chars
|
||||
// Emphasis: pulse (60% = 6s)
|
||||
// Exit: snap out (25% = 2.5s)
|
||||
//
|
||||
// Transition: punchy -> calm = HARD (0.5s overlap)
|
||||
//
|
||||
// Phase 3: CALM (24s - 44s) — aftermath with floating debris
|
||||
// Entrance: ensemble (20% = 4s)
|
||||
// Emphasis: drift (65% = 13s)
|
||||
// Exit: fade recede (15% = 3s)
|
||||
// =========================================================================
|
||||
|
||||
// Custom eases (Principle 6: Slow In, Slow Out — easing is non-negotiable)
|
||||
// Tense: creeping ease — barely moves for 80%, then rushes (tense recipe)
|
||||
CustomEase.create("creepingDread", "M0,0 C0.05,0.01 0.1,0.02 0.2,0.03 0.4,0.05 0.6,0.08 0.75,0.15 0.85,0.3 0.92,0.6 1,1");
|
||||
|
||||
// Micro-tremor for tension hold (tense emphasis: 14 wiggles, uniform type)
|
||||
CustomWiggle.create("microTremor", { wiggles: 14, type: "uniform" });
|
||||
|
||||
// =========================================================================
|
||||
// MASTER TIMELINE
|
||||
// =========================================================================
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Register for HyperFrames
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["high-contrast-arc-shattered"] = tl;
|
||||
|
||||
// =========================================================================
|
||||
// PHASE 1: TENSE (0s - 15s)
|
||||
// Mood recipe: creeping motion, subtle tremor, no release, stagger from end
|
||||
// =========================================================================
|
||||
const phase1 = gsap.timeline();
|
||||
|
||||
// --- Phase 1 Entrance: Slow Reveal (0s - 3.75s) ---
|
||||
// Principle 3 (Staging): elements emerge one at a time with long pauses
|
||||
// Principle 9 (Timing): 1-2s durations = slow, cinematic
|
||||
// Tense recipe: stagger from end — things approach from the unknown
|
||||
var fragments = gsap.utils.toArray(".fragment");
|
||||
|
||||
// Dark overlay creeps in (Principle 8: Secondary Action reinforces mood)
|
||||
phase1.to(".dark-overlay", {
|
||||
opacity: 0.5, duration: 3, ease: "creepingDread"
|
||||
}, 0);
|
||||
|
||||
// Fragments emerge one at a time from end (tense: stagger from end)
|
||||
fragments.forEach(function(el, i) {
|
||||
var reverseIndex = fragments.length - 1 - i;
|
||||
var startTime = reverseIndex * 0.45;
|
||||
// Principle 7 (Arcs): slight curved drift, not straight-line
|
||||
phase1.fromTo(el,
|
||||
{ opacity: 0, y: 15, x: -5 },
|
||||
{ opacity: 0.4, y: 0, x: 0, duration: 1.2, ease: "power2.out" },
|
||||
startTime
|
||||
);
|
||||
});
|
||||
|
||||
// --- Phase 1 Emphasis: Tension Hold (3.75s - 11.25s) ---
|
||||
// Skill pattern: near-stillness with micro-tremor on one element
|
||||
// CustomWiggle 14 wiggles, uniform — no bounce, no elastic (tension doesn't resolve)
|
||||
var emphasisStart1 = 3.75;
|
||||
var emphasisDuration1 = 7.5;
|
||||
|
||||
// Micro-tremor on the key "shattered" fragment (tension hold emphasis pattern)
|
||||
phase1.to(".fragment-7", {
|
||||
x: 2, ease: "microTremor", duration: emphasisDuration1
|
||||
}, emphasisStart1);
|
||||
|
||||
// Very slow drift on other fragments (Principle 11: Solid Drawing — parallax depth)
|
||||
fragments.forEach(function(el, i) {
|
||||
var driftAmount = (i % 2 === 0) ? -3 : 3;
|
||||
var driftY = (i % 3 === 0) ? -2 : 1;
|
||||
phase1.to(el, {
|
||||
x: "+=" + driftAmount,
|
||||
y: "+=" + driftY,
|
||||
duration: emphasisDuration1,
|
||||
ease: "sine.inOut"
|
||||
}, emphasisStart1);
|
||||
});
|
||||
|
||||
// Tension line crawls across (creeping ease — barely moves, then rushes)
|
||||
phase1.to(".tension-line", {
|
||||
scaleX: 1, duration: emphasisDuration1, ease: "creepingDread"
|
||||
}, emphasisStart1);
|
||||
|
||||
// Subtle red pulse on tension line (Principle 8: Secondary Action)
|
||||
phase1.to(".tension-line", {
|
||||
opacity: 0.6, duration: 1.25, ease: "sine.inOut",
|
||||
yoyo: true, repeat: Math.floor(emphasisDuration1 / 2.5)
|
||||
}, emphasisStart1);
|
||||
|
||||
// --- Phase 1 Exit: Compress (11.25s - 15s) ---
|
||||
// Tense exit pattern: elements slowly contract inward before next scene hits
|
||||
var exitStart1 = 11.25;
|
||||
var exitDuration1 = 3.75;
|
||||
|
||||
phase1.to(".fragment", {
|
||||
scale: 0.9, x: 0, y: 0, opacity: 0.15,
|
||||
duration: exitDuration1, ease: "power4.in",
|
||||
stagger: 0.05
|
||||
}, exitStart1);
|
||||
|
||||
phase1.to(".tension-line", {
|
||||
opacity: 0.8, scaleY: 2, duration: exitDuration1, ease: "power4.in"
|
||||
}, exitStart1);
|
||||
|
||||
phase1.to(".dark-overlay", {
|
||||
opacity: 0.8, duration: exitDuration1, ease: "power3.in"
|
||||
}, exitStart1);
|
||||
|
||||
// Add Phase 1 to master
|
||||
tl.add(phase1, 0);
|
||||
|
||||
// =========================================================================
|
||||
// TRANSITION: tense -> punchy = HARD (0.3-0.8s overlap)
|
||||
// Matrix lookup: tense->punchy = hard
|
||||
// Technique: Phase 1 snaps out, Phase 2 slams in simultaneously
|
||||
// =========================================================================
|
||||
var t1Overlap = 0.5;
|
||||
|
||||
// =========================================================================
|
||||
// PHASE 2: PUNCHY (14.5s - 24.5s)
|
||||
// Mood recipe: hard stops, squash on impact, rhythmic timing (0.04-0.08s stagger),
|
||||
// minimal follow-through — things STOP
|
||||
// =========================================================================
|
||||
var phase2Start = 15 - t1Overlap;
|
||||
var phase2 = gsap.timeline();
|
||||
|
||||
// --- Phase 2 Entrance: Impact (0s - 1.5s) ---
|
||||
// Punchy entrance: hero element hits first and HARD, supporting elements follow
|
||||
// Principle 1 (Squash & Stretch): squash on impact, settle to 1
|
||||
// Principle 2 (Anticipation): brief screen compress before the slam
|
||||
// Principle 10 (Exaggeration): scale 1.4 overshoot for impact
|
||||
|
||||
// Hard snap-out of remaining Phase 1 elements (hard transition)
|
||||
phase2.to(".fragment", {
|
||||
opacity: 0, scale: 0.95, duration: 0.15, ease: "power3.in"
|
||||
}, 0);
|
||||
phase2.to(".tension-line", {
|
||||
opacity: 0, duration: 0.1, ease: "none"
|
||||
}, 0);
|
||||
|
||||
// Brief anticipation: screen goes fully dark (Principle 2)
|
||||
phase2.to(".dark-overlay", {
|
||||
opacity: 1, duration: 0.2, ease: "power2.in"
|
||||
}, 0);
|
||||
|
||||
// Impact flash — instantaneous white burst (Principle 10: Exaggeration)
|
||||
phase2.to(".impact-flash", {
|
||||
opacity: 1, duration: 0.06, ease: "power4.out"
|
||||
}, 0.3);
|
||||
phase2.to(".impact-flash", {
|
||||
opacity: 0, duration: 0.5, ease: "power3.out"
|
||||
}, 0.36);
|
||||
|
||||
// SplitText on title for per-character reveal (requested plugin)
|
||||
var titleSplit = new SplitText(".title-text", { type: "chars" });
|
||||
var titleChars = titleSplit.chars;
|
||||
|
||||
// Title container visible
|
||||
phase2.set(".title-text", { opacity: 1 }, 0.3);
|
||||
|
||||
// Characters SLAM in with squash-stretch (Principle 1)
|
||||
// Punchy recipe: rhythmic 0.04s stagger, from center for radial impact
|
||||
phase2.fromTo(titleChars,
|
||||
{ opacity: 0, scaleY: 0.5, scaleX: 1.3, y: 15 },
|
||||
{
|
||||
opacity: 1, scaleY: 1, scaleX: 1, y: 0,
|
||||
duration: 0.15, ease: "power4.out",
|
||||
stagger: { each: 0.04, from: "center" }
|
||||
},
|
||||
0.3
|
||||
);
|
||||
|
||||
// Dark overlay clears after impact
|
||||
phase2.to(".dark-overlay", {
|
||||
opacity: 0, duration: 0.4, ease: "power2.out"
|
||||
}, 0.4);
|
||||
|
||||
// Glass shards explode outward (Principle 5: Follow-through, Principle 8: Secondary Action)
|
||||
// Rhythmic stagger = punchy recipe (consistent 0.04s beat)
|
||||
var shards = gsap.utils.toArray(".shard");
|
||||
|
||||
shards.forEach(function(shard, i) {
|
||||
var angle = (i / shards.length) * Math.PI * 2;
|
||||
var distance = 80 + ((i * 17) % 120);
|
||||
var xBurst = Math.cos(angle) * distance;
|
||||
var yBurst = Math.sin(angle) * distance;
|
||||
var rot = ((i % 2 === 0) ? 1 : -1) * (15 + (i * 5) % 45);
|
||||
|
||||
// Initial burst outward
|
||||
phase2.fromTo(shard,
|
||||
{ opacity: 0, scale: 0.3 },
|
||||
{
|
||||
opacity: 0.7, scale: 1,
|
||||
x: xBurst * 0.3, y: yBurst * 0.3,
|
||||
rotation: rot * 0.3,
|
||||
duration: 0.15, ease: "power4.out"
|
||||
},
|
||||
0.34 + i * 0.04
|
||||
);
|
||||
});
|
||||
|
||||
// Pulse ring expands from center (Principle 8: Secondary Action reinforces impact)
|
||||
phase2.to(".pulse-ring", {
|
||||
opacity: 0.6, scale: 8, duration: 0.6, ease: "power3.out"
|
||||
}, 0.32);
|
||||
phase2.to(".pulse-ring", {
|
||||
opacity: 0, duration: 0.4, ease: "power2.out"
|
||||
}, 0.92);
|
||||
|
||||
// --- Phase 2 Emphasis: Pulse (1.5s - 7.5s) ---
|
||||
// Punchy emphasis: subtle scale oscillation on key element
|
||||
// Principle 9 (Timing): consistent rhythmic beat
|
||||
var emphasisStart2 = 1.5;
|
||||
var emphasisDuration2 = 6;
|
||||
|
||||
// Title pulse — subtle scale throb (punchy pulse pattern)
|
||||
var pulseRepeat2 = Math.floor(emphasisDuration2 / 0.8) - 1;
|
||||
phase2.to(".title-text", {
|
||||
scale: 1.015, duration: 0.4, ease: "sine.inOut",
|
||||
yoyo: true, repeat: pulseRepeat2
|
||||
}, emphasisStart2);
|
||||
|
||||
// Red glow throb on title (Principle 8: Secondary Action)
|
||||
phase2.to(".title-text", {
|
||||
textShadow: "0 0 80px rgba(180,30,30,1), 0 0 160px rgba(180,30,30,0.6)",
|
||||
duration: 0.6, ease: "sine.inOut",
|
||||
yoyo: true, repeat: Math.floor(emphasisDuration2 / 1.2) - 1
|
||||
}, emphasisStart2);
|
||||
|
||||
// Shards continue slow drift outward during emphasis (Principle 5: Follow-through)
|
||||
shards.forEach(function(shard, i) {
|
||||
var angle = (i / shards.length) * Math.PI * 2;
|
||||
var distance = 80 + ((i * 17) % 120);
|
||||
var xDrift = Math.cos(angle) * distance;
|
||||
var yDrift = Math.sin(angle) * distance;
|
||||
var rot = ((i % 2 === 0) ? 1 : -1) * (15 + (i * 5) % 45);
|
||||
|
||||
phase2.to(shard, {
|
||||
x: xDrift, y: yDrift, rotation: rot,
|
||||
duration: emphasisDuration2, ease: "power1.out"
|
||||
}, emphasisStart2);
|
||||
});
|
||||
|
||||
// --- Phase 2 Exit: Snap Out (7.5s - 10s) ---
|
||||
// Punchy exit: hard opacity to 0 — just gone. No ease out.
|
||||
var exitStart2 = 7.5;
|
||||
|
||||
// Title snaps out (snap out pattern: duration 0.08, ease "none")
|
||||
phase2.to(".title-text", {
|
||||
opacity: 0, duration: 0.08, ease: "none"
|
||||
}, exitStart2);
|
||||
|
||||
// Shards snap out in quick rhythmic succession
|
||||
phase2.to(".shard", {
|
||||
opacity: 0, duration: 0.08, ease: "none",
|
||||
stagger: 0.03
|
||||
}, exitStart2 + 0.1);
|
||||
|
||||
// Darkness slams back in (inverted flash for hard transition out)
|
||||
phase2.to(".dark-overlay", {
|
||||
opacity: 0.6, duration: 0.1, ease: "power4.out"
|
||||
}, exitStart2);
|
||||
phase2.to(".dark-overlay", {
|
||||
opacity: 0.2, duration: 1.5, ease: "sine.out"
|
||||
}, exitStart2 + 0.1);
|
||||
|
||||
// Add Phase 2 to master at overlap position
|
||||
tl.add(phase2, phase2Start);
|
||||
|
||||
// =========================================================================
|
||||
// TRANSITION: punchy -> calm = HARD (0.3-0.8s overlap)
|
||||
// Matrix lookup: punchy->calm = hard
|
||||
// =========================================================================
|
||||
var t2Overlap = 0.5;
|
||||
|
||||
// =========================================================================
|
||||
// PHASE 3: CALM (24s - 44s)
|
||||
// Mood recipe: slow and continuous (1.5-3s durations), sine.inOut everything,
|
||||
// no sudden movements, generous spacing (30%+ gap),
|
||||
// breathing motion for holds
|
||||
// =========================================================================
|
||||
var phase3Start = phase2Start + 10 - t2Overlap;
|
||||
var phase3 = gsap.timeline();
|
||||
|
||||
// --- Phase 3 Entrance: Ensemble (0s - 4s) ---
|
||||
// Calm entrance: elements fade in together in 2-3 groups
|
||||
// Principle 3 (Staging): 20-30% overlap between groups — elements breathe
|
||||
// Principle 4 (Pose to Pose): two states + right ease = enough
|
||||
|
||||
// Group 1: settled title emerges softly
|
||||
phase3.fromTo(".title-settled",
|
||||
{ opacity: 0, y: 10, scale: 0.98 },
|
||||
{ opacity: 0.7, y: 0, scale: 1, duration: 2.5, ease: "sine.inOut" },
|
||||
0
|
||||
);
|
||||
|
||||
// Group 2: subtitle follows with slight offset (ensemble: feels unified)
|
||||
phase3.fromTo(".subtitle",
|
||||
{ opacity: 0, y: 8 },
|
||||
{ opacity: 0.5, y: 0, duration: 2, ease: "sine.inOut" },
|
||||
0.8
|
||||
);
|
||||
|
||||
// Group 3: debris particles appear (30%+ gap between groups = calm recipe)
|
||||
var debris = gsap.utils.toArray(".debris");
|
||||
|
||||
// First debris group (even indices)
|
||||
debris.forEach(function(el, i) {
|
||||
if (i % 2 === 0) {
|
||||
phase3.fromTo(el,
|
||||
{ opacity: 0, y: 5 },
|
||||
{ opacity: 0.6, y: 0, duration: 1.5, ease: "sine.inOut" },
|
||||
1.5 + i * 0.12
|
||||
);
|
||||
}
|
||||
});
|
||||
// Second debris group (odd indices)
|
||||
debris.forEach(function(el, i) {
|
||||
if (i % 2 !== 0) {
|
||||
phase3.fromTo(el,
|
||||
{ opacity: 0, y: 5 },
|
||||
{ opacity: 0.5, y: 0, duration: 1.5, ease: "sine.inOut" },
|
||||
2.2 + i * 0.1
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Dark overlay settles to subtle
|
||||
phase3.to(".dark-overlay", {
|
||||
opacity: 0.1, duration: 3, ease: "sine.inOut"
|
||||
}, 0);
|
||||
|
||||
// --- Phase 3 Emphasis: Drift (4s - 17s) ---
|
||||
// Calm emphasis: slow continuous motion, gentle float, parallax shift
|
||||
// Principle 7 (Arcs): curved paths on debris for organic feel
|
||||
// Principle 12 (Appeal): nothing jarring, motion matches serene aftermath
|
||||
var emphasisStart3 = 4;
|
||||
var emphasisDuration3 = 13;
|
||||
|
||||
// Debris gentle drift (Principle 11: Solid Drawing — parallax layers create depth)
|
||||
debris.forEach(function(el, i) {
|
||||
var driftX = (i % 3 === 0) ? -15 : (i % 3 === 1) ? 12 : -8;
|
||||
var driftY = (i % 2 === 0) ? -10 : 8;
|
||||
var rotDrift = (i % 2 === 0) ? 15 : -10;
|
||||
var speed = emphasisDuration3 * (0.8 + (i % 4) * 0.1);
|
||||
|
||||
// Slow continuous float (calm: 1.5-3s+ durations, sine.inOut)
|
||||
phase3.to(el, {
|
||||
x: "+=" + driftX, y: "+=" + driftY,
|
||||
rotation: rotDrift,
|
||||
duration: speed, ease: "sine.inOut"
|
||||
}, emphasisStart3);
|
||||
});
|
||||
|
||||
// Title breathing motion (calm recipe: gentle scale oscillation)
|
||||
var breathCount = Math.floor(emphasisDuration3 / 4) - 1;
|
||||
phase3.to(".title-settled", {
|
||||
scale: 1.008, duration: 2, ease: "sine.inOut",
|
||||
yoyo: true, repeat: breathCount
|
||||
}, emphasisStart3);
|
||||
|
||||
// Subtle title glow breathing (Principle 8: Secondary Action reinforces calm)
|
||||
phase3.to(".title-settled", {
|
||||
textShadow: "0 0 50px rgba(180,30,30,0.4), 0 0 100px rgba(180,30,30,0.15)",
|
||||
duration: 3, ease: "sine.inOut",
|
||||
yoyo: true, repeat: Math.floor(emphasisDuration3 / 6) - 1
|
||||
}, emphasisStart3);
|
||||
|
||||
// Subtitle gentle drift (Principle 5: Follow-through on secondary elements)
|
||||
phase3.to(".subtitle", {
|
||||
y: -3, duration: emphasisDuration3, ease: "sine.inOut"
|
||||
}, emphasisStart3);
|
||||
|
||||
// Very slow vignette breathing (atmospheric ambiance)
|
||||
phase3.to(".dark-overlay", {
|
||||
opacity: 0.15, duration: 4, ease: "sine.inOut",
|
||||
yoyo: true, repeat: 1
|
||||
}, emphasisStart3 + 2);
|
||||
|
||||
// --- Phase 3 Exit: Fade Recede (17s - 20s) ---
|
||||
// Calm exit: opacity + slight scale down, elements sink back
|
||||
// Principle 5 (Follow-through): staggered finish times
|
||||
var exitStart3 = 17;
|
||||
var exitDuration3 = 3;
|
||||
|
||||
// Debris slowly recedes
|
||||
phase3.to(".debris", {
|
||||
opacity: 0, scale: 0.95, y: "+=10",
|
||||
duration: exitDuration3, ease: "sine.inOut",
|
||||
stagger: 0.08
|
||||
}, exitStart3);
|
||||
|
||||
// Title slowly fades (Principle 12: Appeal — matches aftermath tone)
|
||||
phase3.to(".title-settled", {
|
||||
opacity: 0, scale: 0.97, y: 5,
|
||||
duration: exitDuration3, ease: "sine.inOut"
|
||||
}, exitStart3);
|
||||
|
||||
// Subtitle fades last (Principle 3: Staging — last thing the eye sees)
|
||||
phase3.to(".subtitle", {
|
||||
opacity: 0, y: 5,
|
||||
duration: exitDuration3 * 0.8, ease: "sine.inOut"
|
||||
}, exitStart3 + 0.5);
|
||||
|
||||
// Final darkness
|
||||
phase3.to(".dark-overlay", {
|
||||
opacity: 0.9, duration: exitDuration3, ease: "sine.inOut"
|
||||
}, exitStart3);
|
||||
|
||||
// Add Phase 3 to master at overlap position
|
||||
tl.add(phase3, phase3Start);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,747 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1920, height=1080">
|
||||
<title>SHATTERED - Thriller Title Sequence</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #000; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
||||
|
||||
.composition {
|
||||
background: #0a0a0a;
|
||||
font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* --- Phase 1: Creeping fragments --- */
|
||||
.fragment {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 6px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
font-weight: 300;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.fragment.red {
|
||||
color: rgba(200, 30, 30, 0.2);
|
||||
}
|
||||
|
||||
/* --- Noise / texture overlay --- */
|
||||
.noise-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
opacity: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(255,255,255,0.015) 2px,
|
||||
rgba(255,255,255,0.015) 4px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* --- Vignette --- */
|
||||
.vignette {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.8) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 40;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* --- Scan lines --- */
|
||||
.scanlines {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
opacity: 0;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 3px,
|
||||
rgba(0,0,0,0.15) 3px,
|
||||
rgba(0,0,0,0.15) 6px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
/* --- Red pulse flash --- */
|
||||
.red-flash {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: radial-gradient(ellipse at center, rgba(180,20,20,0.6) 0%, rgba(100,0,0,0.3) 50%, transparent 80%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
/* --- White flash for impact --- */
|
||||
.white-flash {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: #fff;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
/* --- Main title --- */
|
||||
.title-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 20;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-main {
|
||||
font-size: 180px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 30px;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
text-shadow: 0 0 60px rgba(200, 30, 30, 0.8), 0 0 120px rgba(200, 30, 30, 0.4);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* --- Subtitle --- */
|
||||
.subtitle {
|
||||
position: absolute;
|
||||
top: 62%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 22px;
|
||||
letter-spacing: 18px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(200, 30, 30, 0.9);
|
||||
opacity: 0;
|
||||
font-weight: 300;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* --- Glass shards --- */
|
||||
.shard {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: 25;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shard svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* --- Crack lines --- */
|
||||
.crack-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0.9), rgba(200,30,30,0.6), transparent);
|
||||
opacity: 0;
|
||||
transform-origin: left center;
|
||||
z-index: 22;
|
||||
}
|
||||
|
||||
/* --- Floating debris --- */
|
||||
.debris {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.debris-dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.debris-dot.red {
|
||||
background: rgba(200, 30, 30, 0.4);
|
||||
}
|
||||
|
||||
.debris-line {
|
||||
width: 20px;
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* --- Horizontal rule accent --- */
|
||||
.hr-accent {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(200,30,30,0.8), transparent);
|
||||
opacity: 0;
|
||||
z-index: 18;
|
||||
}
|
||||
|
||||
/* --- Ambient glow --- */
|
||||
.ambient-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.glow-red {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: rgba(180, 20, 20, 0.15);
|
||||
top: 40%;
|
||||
left: 45%;
|
||||
}
|
||||
|
||||
.glow-white {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
top: 35%;
|
||||
left: 55%;
|
||||
}
|
||||
|
||||
/* --- Edge light streaks --- */
|
||||
.light-streak {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.light-streak.top { top: 0; left: 0; width: 100%; background: linear-gradient(90deg, transparent, rgba(200,30,30,0.3), transparent); }
|
||||
.light-streak.bottom { bottom: 0; left: 0; width: 100%; background: linear-gradient(90deg, transparent, rgba(200,30,30,0.3), transparent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="composition" data-composition-id="high-contrast-arc-shattered" style="width: 1920px; height: 1080px; position: relative; overflow: hidden;">
|
||||
|
||||
<!-- Atmosphere layers -->
|
||||
<div class="noise-overlay"></div>
|
||||
<div class="vignette"></div>
|
||||
<div class="scanlines"></div>
|
||||
<div class="red-flash"></div>
|
||||
<div class="white-flash"></div>
|
||||
|
||||
<!-- Ambient glows -->
|
||||
<div class="ambient-glow glow-red"></div>
|
||||
<div class="ambient-glow glow-white"></div>
|
||||
|
||||
<!-- Light streaks -->
|
||||
<div class="light-streak top"></div>
|
||||
<div class="light-streak bottom"></div>
|
||||
|
||||
<!-- Horizontal rule accent -->
|
||||
<div class="hr-accent"></div>
|
||||
|
||||
<!-- Phase 1: Drifting text fragments -->
|
||||
<div class="fragment" style="top: 15%; left: 10%;">no escape</div>
|
||||
<div class="fragment" style="top: 30%; left: 75%;">broken</div>
|
||||
<div class="fragment red" style="top: 55%; left: 5%;">fear</div>
|
||||
<div class="fragment" style="top: 70%; left: 65%;">silence</div>
|
||||
<div class="fragment red" style="top: 20%; left: 50%;">shatter</div>
|
||||
<div class="fragment" style="top: 80%; left: 30%;">the truth</div>
|
||||
<div class="fragment" style="top: 45%; left: 85%;">lies</div>
|
||||
<div class="fragment red" style="top: 10%; left: 40%;">blood</div>
|
||||
<div class="fragment" style="top: 65%; left: 15%;">darkness</div>
|
||||
<div class="fragment" style="top: 40%; left: 55%;">hidden</div>
|
||||
<div class="fragment red" style="top: 85%; left: 70%;">revenge</div>
|
||||
<div class="fragment" style="top: 25%; left: 25%;">nothing left</div>
|
||||
|
||||
<!-- Main title -->
|
||||
<div class="title-container">
|
||||
<div class="title-main">SHATTERED</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<div class="subtitle">EVERY TRUTH HAS A BREAKING POINT</div>
|
||||
|
||||
<!-- Glass shards (SVG triangles) -->
|
||||
<div class="shard" style="width:120px;height:100px;top:30%;left:20%;">
|
||||
<svg viewBox="0 0 120 100"><polygon points="60,5 115,95 5,95" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:80px;height:70px;top:25%;left:70%;">
|
||||
<svg viewBox="0 0 80 70"><polygon points="10,5 75,30 40,65" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:100px;height:90px;top:60%;left:15%;">
|
||||
<svg viewBox="0 0 100 90"><polygon points="50,5 95,85 5,60" fill="none" stroke="rgba(200,30,30,0.5)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:90px;height:80px;top:55%;left:75%;">
|
||||
<svg viewBox="0 0 90 80"><polygon points="5,10 85,5 45,75" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:60px;height:55px;top:20%;left:45%;">
|
||||
<svg viewBox="0 0 60 55"><polygon points="30,3 57,50 3,50" fill="none" stroke="rgba(200,30,30,0.6)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:70px;height:65px;top:70%;left:50%;">
|
||||
<svg viewBox="0 0 70 65"><polygon points="5,5 65,20 35,60" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:110px;height:95px;top:40%;left:35%;">
|
||||
<svg viewBox="0 0 110 95"><polygon points="55,5 105,90 5,70" fill="none" stroke="rgba(200,30,30,0.4)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:50px;height:45px;top:15%;left:60%;">
|
||||
<svg viewBox="0 0 50 45"><polygon points="25,3 47,42 3,42" fill="none" stroke="rgba(255,255,255,0.7)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:85px;height:75px;top:75%;left:25%;">
|
||||
<svg viewBox="0 0 85 75"><polygon points="42,5 80,70 5,50" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
<div class="shard" style="width:65px;height:58px;top:65%;left:80%;">
|
||||
<svg viewBox="0 0 65 58"><polygon points="5,8 60,5 32,55" fill="none" stroke="rgba(200,30,30,0.5)" stroke-width="1"/></svg>
|
||||
</div>
|
||||
|
||||
<!-- Crack lines radiating from center -->
|
||||
<div class="crack-line" style="width: 500px; transform: rotate(-15deg);"></div>
|
||||
<div class="crack-line" style="width: 450px; transform: rotate(25deg);"></div>
|
||||
<div class="crack-line" style="width: 600px; transform: rotate(-45deg);"></div>
|
||||
<div class="crack-line" style="width: 380px; transform: rotate(55deg);"></div>
|
||||
<div class="crack-line" style="width: 520px; transform: rotate(-70deg);"></div>
|
||||
<div class="crack-line" style="width: 470px; transform: rotate(10deg);"></div>
|
||||
<div class="crack-line" style="width: 550px; transform: rotate(80deg);"></div>
|
||||
<div class="crack-line" style="width: 400px; transform: rotate(-35deg);"></div>
|
||||
<div class="crack-line" style="width: 350px; transform: rotate(42deg);"></div>
|
||||
<div class="crack-line" style="width: 480px; transform: rotate(-60deg);"></div>
|
||||
<div class="crack-line" style="width: 420px; transform: rotate(70deg);"></div>
|
||||
<div class="crack-line" style="width: 560px; transform: rotate(-5deg);"></div>
|
||||
|
||||
<!-- Floating debris for aftermath -->
|
||||
<div class="debris" style="top:20%;left:10%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:30%;left:85%;"><div class="debris-dot red"></div></div>
|
||||
<div class="debris" style="top:60%;left:25%;"><div class="debris-line"></div></div>
|
||||
<div class="debris" style="top:15%;left:50%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:75%;left:70%;"><div class="debris-dot red"></div></div>
|
||||
<div class="debris" style="top:45%;left:5%;"><div class="debris-line"></div></div>
|
||||
<div class="debris" style="top:80%;left:40%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:10%;left:30%;"><div class="debris-dot red"></div></div>
|
||||
<div class="debris" style="top:50%;left:90%;"><div class="debris-line"></div></div>
|
||||
<div class="debris" style="top:35%;left:60%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:70%;left:15%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:25%;left:75%;"><div class="debris-line"></div></div>
|
||||
<div class="debris" style="top:55%;left:45%;"><div class="debris-dot red"></div></div>
|
||||
<div class="debris" style="top:85%;left:55%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:40%;left:35%;"><div class="debris-line"></div></div>
|
||||
<div class="debris" style="top:65%;left:80%;"><div class="debris-dot red"></div></div>
|
||||
<div class="debris" style="top:90%;left:20%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:5%;left:65%;"><div class="debris-line"></div></div>
|
||||
<div class="debris" style="top:48%;left:12%;"><div class="debris-dot"></div></div>
|
||||
<div class="debris" style="top:72%;left:88%;"><div class="debris-dot red"></div></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Scripts at end of body -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/SplitText.min.js"></script>
|
||||
<script>
|
||||
gsap.registerPlugin(CustomEase, SplitText);
|
||||
|
||||
// Custom eases
|
||||
CustomEase.create("creep", "M0,0 C0.1,0 0.15,0.02 0.2,0.04 0.35,0.1 0.4,0.12 0.5,0.5 0.6,0.88 0.65,0.95 1,1");
|
||||
CustomEase.create("shatterIn", "M0,0 C0,0 0.05,1.2 0.1,1 0.15,0.85 0.18,1.05 0.22,1 0.3,0.95 0.5,1 1,1");
|
||||
CustomEase.create("glassOut", "M0,0 C0.2,0.8 0.3,1 0.5,1 0.7,1 0.8,0.95 1,0.85");
|
||||
CustomEase.create("driftSlow", "M0,0 C0.25,0.1 0.25,1 1,1");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Register timeline
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["high-contrast-arc-shattered"] = tl;
|
||||
|
||||
// --- Element references ---
|
||||
const fragments = document.querySelectorAll(".fragment");
|
||||
const shards = document.querySelectorAll(".shard");
|
||||
const crackLines = document.querySelectorAll(".crack-line");
|
||||
const debrisEls = document.querySelectorAll(".debris");
|
||||
const titleMain = document.querySelector(".title-main");
|
||||
const subtitle = document.querySelector(".subtitle");
|
||||
const noiseOverlay = document.querySelector(".noise-overlay");
|
||||
const vignette = document.querySelector(".vignette");
|
||||
const scanlines = document.querySelector(".scanlines");
|
||||
const redFlash = document.querySelector(".red-flash");
|
||||
const whiteFlash = document.querySelector(".white-flash");
|
||||
const hrAccent = document.querySelector(".hr-accent");
|
||||
const glowRed = document.querySelector(".glow-red");
|
||||
const glowWhite = document.querySelector(".glow-white");
|
||||
const lightStreakTop = document.querySelector(".light-streak.top");
|
||||
const lightStreakBottom = document.querySelector(".light-streak.bottom");
|
||||
|
||||
// Split title text for per-character animation
|
||||
const splitTitle = new SplitText(titleMain, { type: "chars" });
|
||||
const titleChars = splitTitle.chars;
|
||||
|
||||
// Set initial states for title chars
|
||||
gsap.set(titleChars, { opacity: 0, scale: 3, rotationX: -90, rotationY: gsap.utils.wrap([-30, 30]), transformOrigin: "50% 50% -50px" });
|
||||
|
||||
// =====================================================
|
||||
// PHASE 1: CREEPING UNEASE (0s - 18s)
|
||||
// Fragments drift in darkness, slow tension building
|
||||
// =====================================================
|
||||
|
||||
// Fade in atmospheric layers
|
||||
tl.to(vignette, { opacity: 1, duration: 3, ease: "power1.inOut" }, 0);
|
||||
tl.to(noiseOverlay, { opacity: 0.5, duration: 4, ease: "power1.inOut" }, 0.5);
|
||||
tl.to(scanlines, { opacity: 0.4, duration: 5, ease: "power1.inOut" }, 1);
|
||||
|
||||
// Fragments fade in one by one, drifting slowly
|
||||
fragments.forEach(function(frag, i) {
|
||||
var delay = 1 + i * 1.2;
|
||||
var driftX = (Math.random() - 0.5) * 80;
|
||||
var driftY = (Math.random() - 0.5) * 40;
|
||||
|
||||
tl.to(frag, {
|
||||
opacity: frag.classList.contains("red") ? 0.25 : 0.18,
|
||||
duration: 2.5,
|
||||
ease: "creep"
|
||||
}, delay);
|
||||
|
||||
// Slow continuous drift
|
||||
tl.to(frag, {
|
||||
x: driftX,
|
||||
y: driftY,
|
||||
duration: 15,
|
||||
ease: "none"
|
||||
}, delay);
|
||||
});
|
||||
|
||||
// Subtle red ambient glow pulsing
|
||||
tl.to(glowRed, { opacity: 0.12, duration: 6, ease: "power1.inOut" }, 2);
|
||||
tl.to(glowRed, {
|
||||
scale: 1.3,
|
||||
duration: 8,
|
||||
ease: "sine.inOut",
|
||||
yoyo: true,
|
||||
repeat: 1
|
||||
}, 2);
|
||||
|
||||
// Occasional flicker on random fragments (unease)
|
||||
tl.to(fragments[2], { opacity: 0, duration: 0.05 }, 8);
|
||||
tl.to(fragments[2], { opacity: 0.25, duration: 0.05 }, 8.1);
|
||||
tl.to(fragments[2], { opacity: 0, duration: 0.03 }, 8.15);
|
||||
tl.to(fragments[2], { opacity: 0.25, duration: 0.15 }, 8.2);
|
||||
|
||||
tl.to(fragments[7], { opacity: 0, duration: 0.04 }, 11);
|
||||
tl.to(fragments[7], { opacity: 0.18, duration: 0.06 }, 11.08);
|
||||
tl.to(fragments[7], { opacity: 0, duration: 0.03 }, 11.2);
|
||||
tl.to(fragments[7], { opacity: 0.18, duration: 0.1 }, 11.25);
|
||||
|
||||
tl.to(fragments[4], { opacity: 0, duration: 0.05 }, 13.5);
|
||||
tl.to(fragments[4], { opacity: 0.3, duration: 0.03 }, 13.55);
|
||||
tl.to(fragments[4], { opacity: 0, duration: 0.04 }, 13.6);
|
||||
tl.to(fragments[4], { opacity: 0.25, duration: 0.2 }, 13.65);
|
||||
|
||||
// Growing red pulse at ~15s (tension peak)
|
||||
tl.to(redFlash, { opacity: 0.08, duration: 1.5, ease: "power2.in" }, 14);
|
||||
tl.to(redFlash, { opacity: 0, duration: 1, ease: "power2.out" }, 15.5);
|
||||
|
||||
// Slight camera-shake-like movement on fragments pre-impact
|
||||
tl.to(fragments, {
|
||||
x: "+=3",
|
||||
duration: 0.08,
|
||||
ease: "none",
|
||||
stagger: { each: 0.02, from: "random" }
|
||||
}, 16.5);
|
||||
tl.to(fragments, {
|
||||
x: "-=6",
|
||||
duration: 0.08,
|
||||
ease: "none",
|
||||
stagger: { each: 0.02, from: "random" }
|
||||
}, 16.6);
|
||||
tl.to(fragments, {
|
||||
x: "+=3",
|
||||
duration: 0.1,
|
||||
ease: "power2.out",
|
||||
stagger: { each: 0.02, from: "random" }
|
||||
}, 16.7);
|
||||
|
||||
// Second stronger tremor
|
||||
tl.to(fragments, {
|
||||
x: "+=5",
|
||||
y: "+=2",
|
||||
duration: 0.06,
|
||||
ease: "none",
|
||||
stagger: { each: 0.015, from: "random" }
|
||||
}, 17.2);
|
||||
tl.to(fragments, {
|
||||
x: "-=10",
|
||||
y: "-=4",
|
||||
duration: 0.06,
|
||||
ease: "none",
|
||||
stagger: { each: 0.015, from: "random" }
|
||||
}, 17.3);
|
||||
tl.to(fragments, {
|
||||
x: "+=5",
|
||||
y: "+=2",
|
||||
duration: 0.12,
|
||||
ease: "power2.out",
|
||||
stagger: { each: 0.015, from: "random" }
|
||||
}, 17.4);
|
||||
|
||||
// Rapid fragment flicker right before the snap
|
||||
tl.to(fragments, {
|
||||
opacity: 0,
|
||||
duration: 0.03,
|
||||
stagger: { each: 0.01, from: "random" }
|
||||
}, 17.8);
|
||||
tl.to(fragments, {
|
||||
opacity: 0.3,
|
||||
duration: 0.03,
|
||||
stagger: { each: 0.01, from: "random" }
|
||||
}, 17.85);
|
||||
tl.to(fragments, {
|
||||
opacity: 0,
|
||||
duration: 0.03,
|
||||
stagger: { each: 0.01, from: "random" }
|
||||
}, 17.9);
|
||||
|
||||
// =====================================================
|
||||
// PHASE 2: THE SNAP - EXPLOSIVE REVEAL (18s - 26s)
|
||||
// White flash, title slams in, glass shatters
|
||||
// =====================================================
|
||||
|
||||
var snapTime = 18;
|
||||
|
||||
// Kill all fragments
|
||||
tl.to(fragments, { opacity: 0, duration: 0.1 }, snapTime);
|
||||
|
||||
// WHITE FLASH - the snap moment
|
||||
tl.to(whiteFlash, { opacity: 1, duration: 0.08, ease: "power4.in" }, snapTime);
|
||||
tl.to(whiteFlash, { opacity: 0, duration: 0.6, ease: "power2.out" }, snapTime + 0.1);
|
||||
|
||||
// Second flash echo
|
||||
tl.to(whiteFlash, { opacity: 0.4, duration: 0.04 }, snapTime + 0.5);
|
||||
tl.to(whiteFlash, { opacity: 0, duration: 0.3, ease: "power2.out" }, snapTime + 0.55);
|
||||
|
||||
// RED flash hit
|
||||
tl.to(redFlash, { opacity: 0.7, duration: 0.1, ease: "power4.in" }, snapTime + 0.15);
|
||||
tl.to(redFlash, { opacity: 0, duration: 1.5, ease: "power2.out" }, snapTime + 0.3);
|
||||
|
||||
// TITLE CHARS slam in with stagger
|
||||
tl.to(titleChars, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
rotationX: 0,
|
||||
rotationY: 0,
|
||||
duration: 0.6,
|
||||
ease: "shatterIn",
|
||||
stagger: {
|
||||
each: 0.04,
|
||||
from: "center"
|
||||
}
|
||||
}, snapTime + 0.1);
|
||||
|
||||
// Title container slight overshoot scale
|
||||
tl.fromTo(titleMain,
|
||||
{ scale: 1.15 },
|
||||
{ scale: 1, duration: 0.8, ease: "power3.out", opacity: 1 },
|
||||
snapTime + 0.1
|
||||
);
|
||||
|
||||
// Crack lines burst from center
|
||||
crackLines.forEach(function(line, i) {
|
||||
var lineDelay = snapTime + 0.12 + (i * 0.02);
|
||||
tl.to(line, {
|
||||
opacity: 0.9,
|
||||
duration: 0.1,
|
||||
ease: "power4.in"
|
||||
}, lineDelay);
|
||||
tl.fromTo(line,
|
||||
{ width: 0 },
|
||||
{ width: line.style.width, duration: 0.25, ease: "power4.out" },
|
||||
lineDelay
|
||||
);
|
||||
// Fade crack lines out slowly
|
||||
tl.to(line, {
|
||||
opacity: 0,
|
||||
duration: 2,
|
||||
ease: "power1.inOut"
|
||||
}, lineDelay + 0.8);
|
||||
});
|
||||
|
||||
// Glass shards explode outward from center
|
||||
shards.forEach(function(shard, i) {
|
||||
var angle = (i / shards.length) * Math.PI * 2;
|
||||
var distance = 200 + Math.random() * 400;
|
||||
var targetX = Math.cos(angle) * distance;
|
||||
var targetY = Math.sin(angle) * distance;
|
||||
|
||||
// Start at center
|
||||
gsap.set(shard, {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
xPercent: -50,
|
||||
yPercent: -50,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 0,
|
||||
rotation: 0
|
||||
});
|
||||
|
||||
tl.to(shard, {
|
||||
opacity: 0.8,
|
||||
scale: 1,
|
||||
x: targetX * 0.3,
|
||||
y: targetY * 0.3,
|
||||
rotation: (Math.random() - 0.5) * 90,
|
||||
duration: 0.15,
|
||||
ease: "power4.out"
|
||||
}, snapTime + 0.08 + (i * 0.02));
|
||||
|
||||
// Continue outward and fade
|
||||
tl.to(shard, {
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
rotation: (Math.random() - 0.5) * 180,
|
||||
opacity: 0,
|
||||
duration: 2.5,
|
||||
ease: "glassOut"
|
||||
}, snapTime + 0.25 + (i * 0.02));
|
||||
});
|
||||
|
||||
// HR accent line expands under title
|
||||
tl.to(hrAccent, { opacity: 1, width: 600, duration: 0.8, ease: "power3.out" }, snapTime + 0.3);
|
||||
|
||||
// Light streaks flash
|
||||
tl.to(lightStreakTop, { opacity: 0.8, duration: 0.1 }, snapTime + 0.05);
|
||||
tl.to(lightStreakTop, { opacity: 0, duration: 1.5, ease: "power2.out" }, snapTime + 0.2);
|
||||
tl.to(lightStreakBottom, { opacity: 0.8, duration: 0.1 }, snapTime + 0.1);
|
||||
tl.to(lightStreakBottom, { opacity: 0, duration: 1.5, ease: "power2.out" }, snapTime + 0.25);
|
||||
|
||||
// Scanlines intensify then settle
|
||||
tl.to(scanlines, { opacity: 0.7, duration: 0.1 }, snapTime);
|
||||
tl.to(scanlines, { opacity: 0.3, duration: 2, ease: "power1.out" }, snapTime + 0.3);
|
||||
|
||||
// Title text glow pulse
|
||||
tl.to(titleMain, {
|
||||
textShadow: "0 0 80px rgba(200,30,30,1), 0 0 160px rgba(200,30,30,0.7), 0 0 240px rgba(180,20,20,0.4)",
|
||||
duration: 0.3,
|
||||
ease: "power2.in"
|
||||
}, snapTime + 0.15);
|
||||
tl.to(titleMain, {
|
||||
textShadow: "0 0 40px rgba(200,30,30,0.5), 0 0 80px rgba(200,30,30,0.25), 0 0 120px rgba(180,20,20,0.1)",
|
||||
duration: 3,
|
||||
ease: "power2.out"
|
||||
}, snapTime + 0.5);
|
||||
|
||||
// Second aftershock tremor on title
|
||||
tl.to(".title-container", { x: 8, duration: 0.04, ease: "none" }, snapTime + 0.7);
|
||||
tl.to(".title-container", { x: -12, duration: 0.04, ease: "none" }, snapTime + 0.74);
|
||||
tl.to(".title-container", { x: 6, duration: 0.04, ease: "none" }, snapTime + 0.78);
|
||||
tl.to(".title-container", { x: -3, duration: 0.04, ease: "none" }, snapTime + 0.82);
|
||||
tl.to(".title-container", { x: 0, duration: 0.15, ease: "power2.out" }, snapTime + 0.86);
|
||||
|
||||
// Subtitle fades in
|
||||
tl.to(subtitle, {
|
||||
opacity: 1,
|
||||
duration: 2,
|
||||
ease: "power1.inOut"
|
||||
}, snapTime + 2.5);
|
||||
|
||||
// Ambient glows ignite
|
||||
tl.to(glowRed, { opacity: 0.3, scale: 1.5, duration: 1, ease: "power2.out" }, snapTime + 0.2);
|
||||
tl.to(glowWhite, { opacity: 0.12, scale: 1.3, duration: 1.2, ease: "power2.out" }, snapTime + 0.3);
|
||||
|
||||
// =====================================================
|
||||
// PHASE 3: CALM AFTERMATH (26s - 45s)
|
||||
// Title holds, debris floats gently, everything settles
|
||||
// =====================================================
|
||||
|
||||
var afterTime = 26;
|
||||
|
||||
// HR accent slowly expands further then fades slightly
|
||||
tl.to(hrAccent, { width: 900, opacity: 0.5, duration: 8, ease: "driftSlow" }, afterTime);
|
||||
|
||||
// Floating debris drifts in
|
||||
debrisEls.forEach(function(d, i) {
|
||||
var startDelay = afterTime + (i * 0.4);
|
||||
var floatDuration = 10 + Math.random() * 8;
|
||||
var driftX = (Math.random() - 0.5) * 150;
|
||||
var driftY = -30 - Math.random() * 60;
|
||||
|
||||
tl.to(d, {
|
||||
opacity: d.querySelector(".red") ? 0.4 : 0.25,
|
||||
duration: 2,
|
||||
ease: "power1.inOut"
|
||||
}, startDelay);
|
||||
|
||||
tl.to(d, {
|
||||
x: driftX,
|
||||
y: driftY,
|
||||
rotation: (Math.random() - 0.5) * 60,
|
||||
duration: floatDuration,
|
||||
ease: "sine.inOut"
|
||||
}, startDelay);
|
||||
});
|
||||
|
||||
// Title glow breathes gently
|
||||
tl.to(titleMain, {
|
||||
textShadow: "0 0 50px rgba(200,30,30,0.6), 0 0 100px rgba(200,30,30,0.3)",
|
||||
duration: 4,
|
||||
ease: "sine.inOut",
|
||||
yoyo: true,
|
||||
repeat: 2
|
||||
}, afterTime + 1);
|
||||
|
||||
// Red ambient glow pulses slowly
|
||||
tl.to(glowRed, {
|
||||
opacity: 0.2,
|
||||
scale: 1.8,
|
||||
duration: 6,
|
||||
ease: "sine.inOut",
|
||||
yoyo: true,
|
||||
repeat: 1
|
||||
}, afterTime + 2);
|
||||
|
||||
// Vignette slightly deepens
|
||||
tl.to(vignette, { opacity: 1.2, duration: 8, ease: "power1.inOut" }, afterTime + 3);
|
||||
|
||||
// Occasional subtle flicker on title chars
|
||||
tl.to(titleChars[2], { opacity: 0.5, duration: 0.04 }, afterTime + 5);
|
||||
tl.to(titleChars[2], { opacity: 1, duration: 0.08 }, afterTime + 5.06);
|
||||
tl.to(titleChars[6], { opacity: 0.4, duration: 0.03 }, afterTime + 8);
|
||||
tl.to(titleChars[6], { opacity: 1, duration: 0.1 }, afterTime + 8.05);
|
||||
|
||||
// Light streak whisper
|
||||
tl.to(lightStreakTop, { opacity: 0.15, duration: 0.5, ease: "power1.inOut" }, afterTime + 6);
|
||||
tl.to(lightStreakTop, { opacity: 0, duration: 2, ease: "power1.out" }, afterTime + 6.5);
|
||||
|
||||
// Final fade: everything gently dims as we approach 45s
|
||||
var fadeOutTime = 40;
|
||||
|
||||
tl.to(titleMain, { opacity: 0.7, duration: 5, ease: "power1.inOut" }, fadeOutTime);
|
||||
tl.to(subtitle, { opacity: 0.5, duration: 5, ease: "power1.inOut" }, fadeOutTime);
|
||||
tl.to(hrAccent, { opacity: 0.2, duration: 5, ease: "power1.inOut" }, fadeOutTime);
|
||||
tl.to(debrisEls, {
|
||||
opacity: 0,
|
||||
duration: 4,
|
||||
ease: "power1.inOut",
|
||||
stagger: { each: 0.1, from: "random" }
|
||||
}, fadeOutTime + 1);
|
||||
tl.to(glowRed, { opacity: 0.05, duration: 5, ease: "power1.inOut" }, fadeOutTime);
|
||||
tl.to(glowWhite, { opacity: 0, duration: 4, ease: "power1.inOut" }, fadeOutTime + 1);
|
||||
tl.to(scanlines, { opacity: 0.15, duration: 5, ease: "power1.inOut" }, fadeOutTime);
|
||||
|
||||
// Final hold to ~45s with gentle darkness
|
||||
tl.to({}, { duration: 0.5 }, 45);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
/Users/vanceingalls/src/hyperframes/examples/infographic-a
|
||||
@@ -0,0 +1 @@
|
||||
/Users/vanceingalls/src/hyperframes/examples/infographic-b
|
||||
@@ -0,0 +1 @@
|
||||
/Users/vanceingalls/src/hyperframes/examples/infographic-eval
|
||||
@@ -0,0 +1 @@
|
||||
/Users/vanceingalls/src/hyperframes/examples/metrics-no-skill
|
||||
@@ -0,0 +1 @@
|
||||
/Users/vanceingalls/src/hyperframes/examples/metrics-with-skill
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
@@ -0,0 +1 @@
|
||||
/Users/vanceingalls/src/hyperframes/examples/vignelli
|
||||
Reference in New Issue
Block a user