chore(deps,skills): bump Remotion 4.0.441→4.0.484; re-vendor HyperFrames skills at v0.7.17
## Remotion bump (mechanical, semver-safe)
remotion-composer: Remotion 4.0.441 → 4.0.484 (43 patch versions, all
within 4.0.x). Includes the seven core packages: remotion + @remotion/cli,
captions, google-fonts, media, player, transitions.
Smoke test: re-rendered the compound-snowball atelier composition through
the unchanged tool path; final_review status=pass, atelier checks clean.
Note: package.json also carries d3-geo@^3.1.1 forward — this line was
already in the working tree from prior unrelated WIP and is not part of
this bump. Removing it would leave package-lock mismatched, so it's
preserved as-is here; clean up separately when its consumer lands.
## HyperFrames skills re-vendor (0.4.2 → 0.7.17)
The runtime invoked by hyperframes_compose (`npx hyperframes`) was
already pulling 0.7.17 on every render, but the vendored skill docs the
agent reads were frozen at 0.4.2-era. This commit closes that gap.
Re-vendored from upstream commit 3351fb1a (tag v0.7.17, 2026-06-27):
Re-vendored core 4 (restructured upstream):
- hyperframes (slim entry; deep content moved to focused skills)
- hyperframes-cli (1 → 7 files; covers validate/inspect/snapshot/
benchmark/lambda natively, dropping the obsolete
OM-local validate patch)
- hyperframes-registry
- website-to-video (renamed upstream from website-to-hyperframes)
Newly vendored (8 strategic additions in 0.5–0.7):
- hyperframes-core composition contract (data-*/tracks/sub-comps)
- hyperframes-creative palette, type, narration, beat planning
- hyperframes-media TTS, BGM, SFX, transcription, captions, bg-remove
- hyperframes-animation all motion knowledge (rules, blueprints,
transitions, 7 runtime adapters)
- media-use agent Media OS (one `resolve` verb for
BGM/SFX/image/icon; project + global cache)
- motion-graphics short design-led motion patterns
- remotion-to-hyperframes migration guidance (directly relevant since
OpenMontage runs both runtimes)
- music-to-video beat-synced video using `hyperframes beats`
Intentionally NOT vendored (HF-workflow-specific; would compete with
OpenMontage pipeline routing): embedded-captions, faceless-explainer,
general-video, pr-to-video, product-launch-video, slideshow,
talking-head-recut. Re-evaluate per pipeline need.
PROVENANCE.md refreshed with the new vendor point + re-sync instructions.
## GSAP CDN pin
.agents/skills/hyperframes/SKILL.md: gsap@3.14.2 → gsap@3 (auto-latest 3.x
on jsdelivr; avoids future drift without breaking the API surface).
## Doctrine updates routing to new skill structure
- skills/INDEX.md — HyperFrames row expanded to enumerate the 12 vendored
skills and their roles.
- skills/meta/animation-runtime-selector.md — runtime decision matrix
updated for the rename (website-to-hyperframes → website-to-video) and
three new rows added: beat-synced music videos, Remotion→HF porting,
and the media-use resolve verb. The HyperFrames composition row in the
animation-library matrix split into four (core/creative/media/animation)
per the upstream skill structure.
- skills/core/hyperframes.md — Layer-2 routing skill rewritten to point at
the new focused skills and all website-to-hyperframes references renamed.
@@ -0,0 +1,197 @@
|
||||
---
|
||||
name: music-to-video
|
||||
description: "Use when the user has a music track (an audio file, or a video to pull audio from) and wants a beat-synced HyperFrames video, calm to hard-hitting. The music drives everything: one analyzer reads it once, the orchestrator lays out the frames and fills a per-frame plan, and one sub-agent builds each frame. Typography and templates are the floor — a complete video needs zero assets — but any images or videos the user supplies are cut into the frames on the same beat grid (beat-cut / ken-burns). The genre (lyric video, slideshow, kinetic promo) falls out of the per-frame choices; the pipeline never branches on it."
|
||||
---
|
||||
|
||||
# music-to-video — one music-grounded, beat-synced video workflow
|
||||
|
||||
Use this skill to turn a **music track** into a beat-synced HyperFrames video. You analyze the track once, lay out the frames, fill in a per-frame plan, and build each frame as a composition. The input is a music track plus optional user images or videos — there is **no narration and no website capture**. Typography and templates are the floor (a complete video needs zero assets); any media the user supplies is cut in on the same beat grid.
|
||||
|
||||
You are the **orchestrator**. Work in `videos/<project>/`. Run the steps in order and pass each **Gate** before moving on. Two steps need the user: **Step 3** (plan approval) and **Step 6** (render approval). Do every step yourself except **Step 4**, where you dispatch **one sub-agent per frame**. Keep design and motion rules out of this file — they live in `references/` and the `frame-worker` sub-agent.
|
||||
|
||||
`SKILL_DIR` = this skill directory. `PROJECT_DIR` = `videos/<project-name>/`.
|
||||
|
||||
Workflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze → `audiomap.json`; Step 2 skeleton → `STORYBOARD.md` (frames, groups `TBD`); Step 3 plan → complete `STORYBOARD.md` + `frame.md`; Step 4 build → `compositions/frames/NN-*.html`; Step 5 assemble → `index.html`; Step 6 render → `renders/video.mp4`.
|
||||
|
||||
## Two ideas that shape everything
|
||||
|
||||
- **One analyzer, and you trust it.** `analyze-beatgrid.py` is the only beat analyzer — never re-measure beats with another tool or by ear. Its energy / density / rolls / onsets / silences are always reliable. Its `bpm` and `beats_sec` are reliable **only when the music is genuinely rhythmic**; on calm music the grid is a metronome the tracker imposed, so pace by phrases and energy instead and never hard-cut to it. Deciding which case you're in is each frame's `pacing` (Step 2).
|
||||
- **One frame = one file; groups live inside.** Step 2 cuts the track into **frames**, and each frame becomes one composition file `compositions/frames/NN-<frame_id>.html`, built by one frame-worker. A frame can subdivide into **groups** (each a template or a motion-primitives combo). Extra density goes _inside_ a group, so **frame count tracks distinct treatments, not beats** — a fast track does not blow up the number of sub-agents.
|
||||
|
||||
---
|
||||
|
||||
## Step 0: Setup, BGM, and inputs
|
||||
|
||||
Goal: Establish the music source, create the HyperFrames project, and note any user-supplied media.
|
||||
|
||||
The **music is the spine** — establish one track before anything else. This skill is tuned for **fast, high-energy BGM**: a strong beat grid drives the cuts (calm tracks work, but pace by phrase rather than beat). If the user gave you audio — a music file, or a video to pull the audio from — use it. If not, generate one: choose the mood from the user's description (e.g. "driving synthwave", "trap beat", "upbeat corporate") and produce a track via `/hyperframes-media` (`references/bgm.md` — HeyGen retrieval when credentialed, else local Lyria / MusicGen; ElevenLabs or another generator also works). Before generating, run `npx hyperframes auth status` and **relay its output verbatim (don't paraphrase or rewrite it)** — it shows whether BGM comes from HeyGen or local MusicGen and, if not signed in, how to sign in. **If not signed in, STOP and wait for the user to choose — sign in, or continue offline with local MusicGen — before generating the track**; don't write keys into a per-repo `.env`. (In autonomous mode, note the status and continue offline.) See `/hyperframes-media` → Preflight for the canonical guidance. Either way the track lands at `assets/bgm.mp3`. Stage any user-supplied images or videos so frames can weave them in on the beat grid; otherwise typography carries the whole video.
|
||||
|
||||
Initialize only if `hyperframes.json` is missing. Name `<project>` from the brief in kebab-case, such as `midnight-drive-loop` — never a timestamp. `init` checks the installed skills against the latest on GitHub and updates the global set if any are out of date.
|
||||
|
||||
```bash
|
||||
npx hyperframes init "videos/<project>" --non-interactive --example=blank
|
||||
mkdir -p "$PROJECT_DIR/assets" "$PROJECT_DIR/renders"
|
||||
cp "<user-music>" "$PROJECT_DIR/assets/bgm.mp3" # extract from a video first if needed
|
||||
# only if the user gave you images/videos:
|
||||
node <SKILL_DIR>/scripts/stage-assets.mjs --from <dir> --hyperframes "$PROJECT_DIR" --into public
|
||||
```
|
||||
|
||||
The **brand** (font + palette) is chosen at Step 3, not here. Don't pick a genre or a track type up front — assets are just an optional ingredient, and the genre emerges from the per-frame choices.
|
||||
|
||||
**Gate:** `hyperframes.json` + `assets/bgm.mp3` exist; aspect / length / fps and (if any) the asset inventory are noted.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Analyze the music
|
||||
|
||||
Goal: Produce the one canonical timing analysis the whole video is built on.
|
||||
|
||||
`analyze-beatgrid.py` is the **only** beat analyzer — never re-measure beats with another tool or by ear. It reads the track once and writes `audiomap.json`: energy phases (level / density / feel), onsets + `onset_rate`, rolls, silences, `hard_stops`, `key_moments`, phrases, tempo / grid, and `audio.duration_sec`. It's deterministic — the same file always gives the same map. Most fields are reliable on any music; `bpm` and `beats_sec` are reliable only when the music is genuinely rhythmic, and judging that is the call you make at Step 2.
|
||||
|
||||
Prerequisites: Python 3 with `librosa`, `numpy`, and `soundfile` available. If import fails, install them into the active Python environment before running the analyzer:
|
||||
|
||||
```bash
|
||||
python3 -m pip install librosa numpy soundfile
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 <SKILL_DIR>/scripts/analyze-beatgrid.py "$PROJECT_DIR/assets/bgm.mp3" \
|
||||
-o "$PROJECT_DIR/audiomap.json" --print
|
||||
```
|
||||
|
||||
**Gate:** `audiomap.json` exists; `audio.duration_sec` is known.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Frame skeleton (structure only)
|
||||
|
||||
Goal: Read the music and lay out the frames — the skeleton of `STORYBOARD.md`.
|
||||
|
||||
Read [`references/frame-skeleton.md`](references/frame-skeleton.md). Turn `audiomap.json` into the **skeleton** of `STORYBOARD.md` yourself — there is no intermediate JSON. Cut the track into **frames** at real musical changes (`hard_stops`, SURGE / DROP `key_moments`, the edges of a roll, a stretch with no onsets, a big energy jump), snapping every boundary to an audiomap anchor. For each frame set `span_sec`, `pacing` (the verdict from Step 1's trust call — `beat_cut` when the grid is real, `phrase_flow` when it's a metronome imposed on calm music), `mood`, and a one-line `feel` (the plain music situation Step 3 matches a template against). Only classify and lay out here: leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank — no templates, copy, color, or fonts. Expect ~1–6 frames.
|
||||
|
||||
**Gate:** frames tile the track (first at 0, last at `duration_s`); each carries `span_sec` + `pacing` + `mood` + `feel`; every `### Groups` is `TBD`; no content anywhere.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Fill the plan (user-gated)
|
||||
|
||||
Goal: Turn the skeleton into an approved, complete `STORYBOARD.md`.
|
||||
|
||||
Read [`references/planning.md`](references/planning.md), [`storyboard-format.md`](references/storyboard-format.md), [`template-catalog.md`](references/template-catalog.md), [`motion-primitive-catalog.md`](references/motion-primitive-catalog.md), and [`montage.md`](references/montage.md) (only if the user supplied assets). Editing the same file in place, do two things:
|
||||
|
||||
1. **Pick the brand.** Choose one preset from `../hyperframes-creative/frame-presets/` using the table in `../hyperframes-creative/references/design-spec.md` (match the track's mood; **only its fonts and colors matter** — templates own composition). Copy it into `frame.md` **unmodified** and fill the frontmatter `style` (font + a ≤4–6 swatch palette) from it.
|
||||
2. **Fill every frame.** Decide its groups and give each a treatment: a matched template from the catalog (with bound params and real audiomap anchors), a free-compose from the primitive catalog, or an asset treatment that **obeys `pacing`**. Write the copy. You own WHAT (template / primitives + content + anchors); the frame-worker owns HOW — **never write millisecond tweens into the storyboard**.
|
||||
|
||||
```bash
|
||||
node <SKILL_DIR>/scripts/validate-plan.mjs --storyboard "$PROJECT_DIR/STORYBOARD.md" \
|
||||
--audiomap "$PROJECT_DIR/audiomap.json" --templates <SKILL_DIR>/references/templates
|
||||
```
|
||||
|
||||
Fix every `✗` (hard errors: duration mismatch, frames not tiling the track, a missing `src`); warnings are best-effort. Then show the user a frame-by-frame summary and iterate until they approve.
|
||||
|
||||
**Gate:** `frame.md` is a verbatim preset copy; `validate-plan.mjs` exits 0; the user approved the plan.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Build frames from the plan
|
||||
|
||||
Goal: Build every frame as a self-contained composition file.
|
||||
|
||||
Create `compositions/frames/`. Read [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md) and `../hyperframes-core/references/subagent-dispatch.md`. Dispatch **one frame-worker per frame**, in parallel where possible (otherwise in waves). Each worker gets exactly one frame and this context:
|
||||
|
||||
```text
|
||||
PROJECT_DIR: <abs path>
|
||||
frame_id: <NN-frame_id> # = the frame file stem, e.g. 02-f2; the composition id
|
||||
Your block: the `## Frame N — <frame_id>` block in PROJECT_DIR/STORYBOARD.md
|
||||
audiomap: PROJECT_DIR/audiomap.json
|
||||
frame.md: PROJECT_DIR/frame.md
|
||||
Materials: for each group, <SKILL_DIR>/references/templates/<id>/index.html (templates) and
|
||||
<SKILL_DIR>/references/motion-primitives/<id>/ (free); staged assets/ (asset groups)
|
||||
Contracts: ../hyperframes-core/references/sub-compositions.md + determinism-rules.md
|
||||
Canvas: <w>×<h> Pacing: <beat_cut|phrase_flow>
|
||||
Write to: PROJECT_DIR/compositions/frames/<frame_id>.html
|
||||
```
|
||||
|
||||
The worker forks the cited materials, converts every anchor to frame-local seconds (`local_t = track_t − span_sec[0]`), gates its groups with 0ms cuts, and writes one seek-safe frame file. **The worker never runs the `hyperframes` CLI** — those commands operate on the assembled project, which doesn't exist yet, so they'd report on the wrong files. The worker just writes to the contract and stops; you verify after assembly (Step 6). As each worker returns, you can confirm its file landed on disk.
|
||||
|
||||
**Gate:** every frame has its `compositions/frames/NN-*.html` on disk.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Assemble
|
||||
|
||||
Goal: Wire the built frames + BGM into the playable `index.html`.
|
||||
|
||||
`assemble-index.mjs` is deterministic — no subagent, no judgment. It references each frame file at its cumulative `data-start`, mounts `assets/bgm.mp3` on track 11, and hard-cuts frame → frame (frames tile the track with no gaps, so there is **no transition injector**).
|
||||
|
||||
```bash
|
||||
node <SKILL_DIR>/scripts/assemble-index.mjs --storyboard "$PROJECT_DIR/STORYBOARD.md" \
|
||||
--hyperframes "$PROJECT_DIR" --audiomap "$PROJECT_DIR/audiomap.json"
|
||||
```
|
||||
|
||||
Fix any `✗` it reports — a missing or blank frame file means that worker wrote a partial file; re-dispatch it (Step 4) and re-assemble.
|
||||
|
||||
**Gate:** `index.html` exists; total duration == `audiomap.audio.duration_sec`.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Verify and render
|
||||
|
||||
Goal: Verify the assembled video, get user approval, and render the final MP4.
|
||||
|
||||
Run the CLI on the **assembled project** — that's the correct unit (the per-frame workers couldn't run it). `lint` checks structure, `validate` runs headless Chrome (catching JS errors and missing assets), `inspect` snapshots frames.
|
||||
|
||||
```bash
|
||||
( cd "$PROJECT_DIR" && npx hyperframes lint . && npx hyperframes validate . && npx hyperframes inspect . )
|
||||
```
|
||||
|
||||
Inspect at `t=0`, each frame start, the strongest DROP / SURGE, every `hard_stops[].t`, and the final frame. On failure, make the **cheapest safe fix** yourself: edit the offending `compositions/frames/NN-*.html`. Never change duration or audio timing to hide a sync issue. Once the gates pass, pause for user review, then render only on approval:
|
||||
|
||||
```bash
|
||||
( cd "$PROJECT_DIR" && npx hyperframes render . --skill=music-to-video -q draft -o renders/video.mp4 --fps 30 )
|
||||
```
|
||||
|
||||
**Gate:** `lint` / `validate` / `inspect` passed; the user approved; `renders/video.mp4` exists with audio, duration == `audiomap.audio.duration_sec`. The final reply states the MP4 path and duration.
|
||||
|
||||
---
|
||||
|
||||
## Resume table
|
||||
|
||||
| You have | Continue from |
|
||||
| -------------------------- | ------------- |
|
||||
| `assets/bgm.mp3` only | Step 1 |
|
||||
| `audiomap.json` | Step 2 |
|
||||
| `STORYBOARD.md` (skeleton) | Step 3 |
|
||||
| `STORYBOARD.md` (complete) | Step 4 |
|
||||
| all frame files | Step 5 |
|
||||
| `index.html` | Step 6 |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Formats:** landscape `1920x1080` by default; portrait `1080x1920`; square `1080x1080`. Set the canvas once in the storyboard frontmatter (`canvas: { w, h, fps }`).
|
||||
|
||||
**Scripts** under `scripts/`: `analyze-beatgrid.py` (the one analyzer), `validate-plan.mjs` (plan check), `assemble-index.mjs` (index assembly), `stage-assets.mjs` (stage user media), `lib/storyboard.mjs` (vendored parser). Everything else is the `hyperframes` CLI.
|
||||
|
||||
| Read | When |
|
||||
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| [`references/frame-skeleton.md`](references/frame-skeleton.md) | Step 2: read the music, lay out the frames, set pacing |
|
||||
| [`references/planning.md`](references/planning.md) · [`storyboard-format.md`](references/storyboard-format.md) | Step 3: pick the brand, fill each frame, write the plan |
|
||||
| [`references/template-catalog.md`](references/template-catalog.md) | Step 3: pick a template per group |
|
||||
| [`references/motion-primitive-catalog.md`](references/motion-primitive-catalog.md) | Step 3/4: L0 recipes for free-compose |
|
||||
| [`references/montage.md`](references/montage.md) | Step 3/4: asset treatments (beat-cut / ken-burns) |
|
||||
| [`sub-agents/frame-worker.md`](sub-agents/frame-worker.md) | Step 4: dispatch + build one frame |
|
||||
| `../hyperframes-core/references/subagent-dispatch.md` | Step 4: dispatch sub-agents safely |
|
||||
| `../hyperframes-creative/references/design-spec.md` | Step 3: pick the preset (the brand) |
|
||||
|
||||
## Directory layout
|
||||
|
||||
```
|
||||
music-to-video/
|
||||
SKILL.md
|
||||
references/ frame-skeleton.md · planning.md · storyboard-format.md
|
||||
template-catalog.md · motion-primitive-catalog.md · montage.md
|
||||
templates/<id>/ { index.html (+ assets/ · program.json) } ← L1 catalog impls
|
||||
motion-primitives/<id>/ { index.html } (+ ../assets/gsap.min.js shared by recipes) ← L0 catalog impls
|
||||
scripts/ analyze-beatgrid.py · assemble-index.mjs · validate-plan.mjs · stage-assets.mjs · lib/storyboard.mjs
|
||||
sub-agents/ frame-worker.md ← the one subagent (one per frame)
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
# Frame skeleton (Step 2) — read the music, lay out the frames
|
||||
|
||||
At Step 2 **you (the orchestrator)** read `audiomap.json` and write the **skeleton** of
|
||||
`STORYBOARD.md` directly: cut the track into **frames** (one frame = one composition file =
|
||||
one scene), and for each frame set its **span**, its **pacing** (does this stretch want hard
|
||||
beat-cuts, or calm phrase/energy flow?), its **mood**, and a one-line **feel** note.
|
||||
|
||||
You **classify and lay out the spine only.** You do **not** pick templates, write copy, choose
|
||||
colors/fonts, or decide a frame's groups — those are Step 3 (the plan fills each frame in
|
||||
place). Leave every frame's `### Groups` as `TBD (Step 3)` and the frontmatter `style` blank.
|
||||
|
||||
There is **no intermediate JSON** — the skeleton _is_ the start of `STORYBOARD.md`. Step 3
|
||||
edits the same file.
|
||||
|
||||
## The trust boundary (read this first)
|
||||
|
||||
`audiomap.json` is one analyzer's output. Some fields are robust on **any** music; some are
|
||||
reliable only when the music is **actually rhythmic**. This decides each frame's `pacing`:
|
||||
|
||||
| Field | Trust |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `energy_phases[]` (level / energy / density / feel), `events[]` + `onset_rate`, `rolls[]` (and their **absence**), `silences[]`, `hard_stops[]`, `key_moments[]`, `phrases[]`, `audio.duration_sec` | **Always** — robust measurements |
|
||||
| `tempo.bpm`, `grid.beats_sec` / `downbeats_sec` **precision** | **Only when the music is rhythmic.** On calm / sparse material the beat grid is a metronome the tracker _imposes_ (often octave-doubled) — usually **more grid beats than real onsets**. Do **not** anchor cuts to it there. |
|
||||
|
||||
- **Grid is reliable** when: rolls present, and/or dense phases, and/or high `onset_rate` with a steady grid.
|
||||
- **Grid is fictional** when: `rolls`≈0, mostly `sparse` phases, low `onset_rate` → pace by `phrases[]` + `energy_phases[]`, not beats.
|
||||
|
||||
## How to lay out frames (run in order)
|
||||
|
||||
1. **Gestalt.** From `summary` / `tempo` / `audio.duration_sec` + the roll count, write the
|
||||
frontmatter `compositionId`, `duration_s` (== `audio.duration_sec`), `canvas`, and a
|
||||
one-line read of the track's density arc.
|
||||
2. **Cut into frames.** Walk `energy_phases[]` and split where the music genuinely changes
|
||||
state — at `hard_stops[]`, `SURGE` / `DROP` `key_moments`, the start/end of a `rolls[]` run,
|
||||
an **onset desert** (a long gap in `events[]`), or a big energy-level jump. Collapse adjacent
|
||||
phases that are one gesture. Expect **~1–6 frames**; a short clip may be one.
|
||||
**Snap every boundary to an audiomap anchor**, then re-snap to the nearest `beats_sec`
|
||||
(tolerance ≤ ½ beat) **only when the grid is reliable**; on calm material snap to
|
||||
`phrases[]` / `energy_phases[]` edges instead. Frames **tile the track** (first at 0, last
|
||||
at `duration_s`, no gaps/overlaps).
|
||||
3. **Per frame, set `pacing`** — the trust-boundary call:
|
||||
- **`beat_cut`** — genuinely rhythmic: a roll present, **or** dense, **or** (clearly high `onset_rate` **and** a steady grid). Hard cuts / per-onset reveals may anchor to beats.
|
||||
- **`phrase_flow`** — calm / sparse: `rolls`≈0, mostly sparse, low `onset_rate`. Do **not** anchor hard cuts to the grid; pace by `phrases[]` + the energy envelope (slow crossfades, long holds).
|
||||
4. **Per frame, tag `mood`** (1–3 of: `warm` · `dark` · `hype` · `elegant` · `glitch` ·
|
||||
`cinematic` · `playful` · `tense` · `dreamy` · `aggressive`) from `energy_phases[].feel` +
|
||||
energy + any genre cue in the brief/title.
|
||||
5. **Per frame, write a one-line `feel`** — the plain-language music situation Step 3 matches a
|
||||
template against (e.g. "accelerating onset stream into a held downbeat", "calm held pad, one
|
||||
onset desert", "fast sustained-fill roll, no readable message"). This is what the planner
|
||||
reads against the catalog's **Reach for it when** — keep it concrete, drawn from the robust
|
||||
fields, never invented.
|
||||
|
||||
## What the skeleton looks like
|
||||
|
||||
A valid `STORYBOARD.md` with the spine set and every frame's treatment left for Step 3
|
||||
(full syntax in [`storyboard-format.md`](storyboard-format.md)):
|
||||
|
||||
```markdown
|
||||
---
|
||||
compositionId: bgm
|
||||
duration_s: 30.0 # == audiomap.audio.duration_sec
|
||||
canvas: { w: 1920, h: 1080, fps: 30 }
|
||||
style: # blank — Step 3 fills it from the chosen frame.md preset
|
||||
build_notes: ["one paused timeline per frame", "no remote assets"]
|
||||
---
|
||||
|
||||
## Frame 1 — f1
|
||||
|
||||
- src: compositions/frames/01-f1.html
|
||||
- duration: 7.198s # = span length; assembler sums these for cumulative data-start
|
||||
- span_sec: [0.0, 7.198] # track seconds; frames tile the track
|
||||
- pacing: beat_cut
|
||||
- mood: [hype]
|
||||
- feel: accelerating onset stream building into a held downbeat
|
||||
|
||||
### Groups
|
||||
|
||||
- TBD (Step 3)
|
||||
|
||||
## Frame 2 — f2
|
||||
|
||||
- src: compositions/frames/02-f2.html
|
||||
- duration: 10.4s
|
||||
- span_sec: [7.198, 17.598]
|
||||
- pacing: phrase_flow
|
||||
- mood: [warm, cinematic]
|
||||
- feel: calm held pad, one long onset desert
|
||||
|
||||
### Groups
|
||||
|
||||
- TBD (Step 3)
|
||||
```
|
||||
|
||||
## Self-check
|
||||
|
||||
- `duration_s == audiomap.audio.duration_sec`; frames tile the track gap-free (first at 0, last at `duration_s`).
|
||||
- Every frame has `src` + `span_sec` + `duration` + `pacing` + `mood` + a one-line `feel`.
|
||||
- `pacing` was set from the **robust** fields (energy / density / rolls / onset_rate), never from `bpm` / `beats_sec` alone.
|
||||
- No frame boundary sits inside a `rolls[]` run or leaves a sub-1-bar fragment.
|
||||
- Every frame's `### Groups` is `TBD (Step 3)`; `style` is blank. **No template, copy, color, or font anywhere.**
|
||||
@@ -0,0 +1,58 @@
|
||||
# Asset treatments — weaving user media onto the beat spine
|
||||
|
||||
When the user supplies images/videos, a group can be an **asset treatment** instead of a
|
||||
typographic template/free-compose. Assets are an **additive ingredient on the same beat
|
||||
spine** — never a separate pipeline. Typography/templates stay the floor: if no asset fits a
|
||||
group, fall back to a template/free group (a complete video needs zero assets).
|
||||
|
||||
The planner (Step 3) picks the treatment and the clips + anchors (WHAT); the frame-worker
|
||||
realizes it inside the frame file (HOW). **Obey the frame's `pacing`.**
|
||||
|
||||
## The three treatments
|
||||
|
||||
### `beat_cut` — one clip per anchor (only on a `beat_cut` frame)
|
||||
|
||||
The asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor
|
||||
(the frame's beats/onsets from the audiomap). Each clip is a `class="clip"` element
|
||||
(`<img>` for a photo, **muted** `<video>` for a motion clip) placed at its anchor with
|
||||
`data-start`/`data-duration`/`data-track-index` per the core clip contract. Between clips,
|
||||
crossfade the outgoing content to `opacity:0` ending **at** the next anchor, then immediately
|
||||
**hard-kill** with a `tl.set(..., {opacity:0}, anchor)` — this pair is required (the
|
||||
`gsap_exit_missing_hard_kill` lint rule; non-linear seeking otherwise bleeds stale frames).
|
||||
Cut on the **strong** anchors; land a hero clip on a `key_moment`/downbeat.
|
||||
|
||||
### `ken_burns` — slow push on one clip (fits a `phrase_flow` frame)
|
||||
|
||||
For calm frames: one clip held over the span with a slow scale/translate push (e.g. scale
|
||||
1.0→1.08 + a small drift) eased across the whole `span_sec` — paced by the frame, not by
|
||||
beats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment
|
||||
when the beat grid is unreliable (calm music).
|
||||
|
||||
### `bg_under_text` — clip dimmed behind a template/free group
|
||||
|
||||
A full-bleed clip dimmed ~30–50% as the background of a group whose foreground is a template
|
||||
or free-compose typographic treatment. The text rides on the same anchors; the clip is the
|
||||
bed. Use when the user wants their footage present but the message must stay readable.
|
||||
|
||||
## Rules
|
||||
|
||||
- **`pacing` decides the treatment**: `beat_cut` only on a `beat_cut` frame; on a
|
||||
`phrase_flow` frame use `ken_burns` or a slow crossfade — **never** per-onset hard cuts on
|
||||
the (unreliable) calm grid.
|
||||
- **Clips are muted; the root owns audio.** Mount each `<video class="clip">` **muted**, as a
|
||||
direct child of the frame root (never nested in another timed element, or the renderer
|
||||
freezes it). The BGM is the only audio in v1.
|
||||
- **Crossfades animate `opacity`/`autoAlpha`**, never `visibility`/`display` on a `.clip`
|
||||
(the framework owns clip visibility — that trips `gsap_animates_clip_element`).
|
||||
- **Backgrounds dim ~30–50%** so any foreground text stays legible.
|
||||
- Anchors are **track seconds from `audiomap.json`**; the worker subtracts the frame start
|
||||
to get frame-local time.
|
||||
- Local staged assets only (`assets/` via `stage-assets.mjs`); never remote URLs.
|
||||
|
||||
## Deferred hook (not v1)
|
||||
|
||||
A clip that should play **its own sound** (interview cut, lyric clip) needs a sibling
|
||||
`<audio>` mounted at the **root** by the assembler, with the BGM ducked under it (timeline
|
||||
volume automation: `tl.to("#el-bgm",{volume:0.15,…},clipStart)` … `tl.to(…,{volume:0.9,…},
|
||||
clipEnd)`). The frame-worker mounts no audio. Keep clips muted in v1; wire clip-audio +
|
||||
ducking only when the user asks.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Motion-primitive catalog — the free-compose menu
|
||||
|
||||
The atomic layer: one anchor → one micro-move. When no template fits a group, free-compose by
|
||||
naming primitives from here. Scan **anchor** + **best span** + **what it does**, then pick the
|
||||
smallest set that carries the group.
|
||||
|
||||
## Timing & latency (applies to every primitive)
|
||||
|
||||
- **Hard hits are 0ms.** Cuts, palette flips, content swaps, freezes are `tl.set(...)` with no duration — the percussion _is_ the motion. Easing a hit kills it.
|
||||
- **Lead the anchor.** A move that must _land_ on a beat (a wipe covering the frame, a count-up locking, two blocks colliding) starts **~40–190ms early** so it completes ON the anchor. Reactive entrances (something appearing _because_ of the hit) fire 0–45ms after.
|
||||
- **Eased entrances: 300–500ms** (scale punch, slides, camera pushes). **Macro builds: 800–2000ms** spanning a whole roll / silence.
|
||||
- **Per-bar caps:** one accumulating element per hit (not a burst); a camera move at most once per phrase, never per beat; a dense flip/strobe system runs ≤2–3s.
|
||||
- **Tension-builds lock.** A count-up / sequential build / morph must _resolve on_ a downbeat or hard_stop, never trail off mid-bar.
|
||||
- **Best span means active motion.** The catalog's span guidance is not a license to stretch one primitive over a whole frame. If a free-composed group runs longer than the listed span, add a hold / bed / next primitive, or split the frame into another group at the next musical anchor.
|
||||
|
||||
## Catalog
|
||||
|
||||
| id | anchor | best span | what it does |
|
||||
| --------------------- | -------------------------------- | ----------------- | ------------------------------------------------------------------------- |
|
||||
| `hypercut-whip` | beat / hard_stop | 0.18-0.45s | fast whip-pan hard cut between frames |
|
||||
| `kinetic-letter-in` | downbeat / phrase | 0.4-1.2s | per-letter kinetic entrance |
|
||||
| `braam-punch` | drop / surge | 0.2-0.9s active | big impact: scale + weight slam |
|
||||
| `chromatic-split` | snare / glitch / surge | 0.1-0.6s | RGB channel split / glitch on a word |
|
||||
| `mask-reveal` | section_start / downbeat | 0.5-1.2s | clip-path mask wipe reveal |
|
||||
| `screen-shake` | drop / crash / kick | 0.1-0.5s | camera / screen shake jitter |
|
||||
| `binary-decrypt` | roll / build | 0.8-2.5s | scramble→decode text (binary → word) |
|
||||
| `dolly-zoom` | phrase / build | 1.2-2.5s | vertigo dolly-zoom (scale vs perspective) |
|
||||
| `iris-open` | section_start / reveal | 0.6-1.2s | circular iris-open reveal |
|
||||
| `electric-arc` | accent / glitch | 0.1-0.6s | electric arc / lightning accent |
|
||||
| `neon-flicker` | hold / texture | 0.5-2.5s | neon-sign flicker |
|
||||
| `chrome-sweep` | downbeat / reveal | 0.6-1.4s | metallic specular sweep across text |
|
||||
| `slot-machine-reveal` | roll → downbeat | 0.8-2.0s | slot-machine spin-to-land character reveal |
|
||||
| `liquid-morph` | phrase / transition | 1.0-2.5s | liquid / blob morph |
|
||||
| `gooey-metaball` | build / drop | 1.5-3.0s | gooey metaball merge field |
|
||||
| `3d-card-flip` | downbeat / swap | 0.8-1.6s | 3D card flip (rotateY) |
|
||||
| `crash-zoom-in` | drop / surge | 0.2-0.8s | violent crash zoom-in |
|
||||
| `spotlight-sweep` | reveal / hold | 0.8-2.0s | spotlight / gradient sweep over text |
|
||||
| `outline-to-fill` | downbeat / reveal | 0.8-1.8s | stroke outline → solid fill |
|
||||
| `counting-punch` | roll → downbeat | 1.0-2.5s | number count-up that punches & locks |
|
||||
| `particle-burst` | drop / crash | 0.2-1.2s | particle explosion burst |
|
||||
| `radial-burst-lines` | drop / surge | 0.2-0.8s | radial speed-lines burst |
|
||||
| `pixel-dissolve` | transition / hard_stop | 0.5-1.5s | pixelated dissolve |
|
||||
| `datamosh-smear` | glitch / transition | 0.4-1.2s | datamosh / motion smear |
|
||||
| `text-wave-distort` | hold / texture | 1.0-2.5s | wavy text distortion |
|
||||
| `bg-flow-field` | energy / whole span (bed) | 4-12s bed | generative curl-noise background bed; compose any foreground move over it |
|
||||
| `blur-resolve` | stop / final hold | 0.7-2.0s | blur-in to crisp focus, then blur-out on the cut |
|
||||
| `chromatic-pressure` | snare / glitch | 0.1-0.5s | RGB split / digital tension on a transient |
|
||||
| `color-grid-shuffle` | onset | 0ms hits; ≤2s run | grid of cells recolored by a deterministic index per onset |
|
||||
| `content-swap` | beat | 0ms hits; ≤3s run | 0ms swap of stacked nodes: the workhorse percussive move |
|
||||
| `directional-fill` | beat / reveal | 0.3-1.0s each | directional wipe-fill (scaleX) sweeping across bars |
|
||||
| `flash-cut` | drop / crash | 0-0.6s | full-frame flash masking a word / color state change |
|
||||
| `freeze-hold` | hard_stop | 0ms in; 0.5-2s | freeze the moving system and hold it |
|
||||
| `hard-cut` | beat / hard_stop | 0ms in; 0.3-2s | sample-accurate color-block + word cut |
|
||||
| `mosaic-pack` | beat / build | 1.5-3.5s | scattered tiles fly in and pack into a grid |
|
||||
| `negative-space-hold` | silence / hard_stop / final hold | 1-6s hold | kill busy layers, hold one readable mark in empty space |
|
||||
| `overlay-pop` | accent | 0.2-0.6s in | badge / lower-third overlay pops in over a base |
|
||||
| `palette-flip` | section change | 0ms flip; 0.5-4s | same layout re-skins via 0ms palette-variable flips |
|
||||
| `staggered-exit` | phrase / transition | 0.4-1.2s | ordered cascade-out clearing the frame |
|
||||
| `staggered-reveal` | build | 0.8-2.5s | ordered cascade-in of a stack / list |
|
||||
| `system-replace` | drop / regime change | 0ms cut | hard-cut the entire visual system, then boot the new one |
|
||||
| `text-spectral-rays` | phrase / sweep (hero text) | 2.5-5s | volumetric light-rays cast by a wordmark toward a sweeping light cursor |
|
||||
| `tile-mosaic` | build / reveal | 1.5-3.5s | grid of tiles revealed in a diagonal sweep, assembling a poster |
|
||||
| `typewriter-reveal` | roll / build | 1.0-3.0s | character / word type-on with caret |
|
||||
| `value-counter` | roll → downbeat | 1.0-2.5s | count-up that locks on a downbeat / hard_stop |
|
||||
| `word-grid-burst` | onsets → downbeat | 1.8-3.2s | grid of words revealed per onset, refocus one on a downbeat |
|
||||
|
||||
## How to combine
|
||||
|
||||
- One dominant system per group; layer at most one texture primitive over one structural primitive.
|
||||
- Structure on strong beats (cuts, camera, `system-replace` → downbeat / phrase / section_start); texture on weak / syncopated hits (`content-swap`, typewriter letters, chromatic accents).
|
||||
- A roll is an accumulation container — build during it, hard-cut to a clean layout on the downbeat that ends it.
|
||||
- `drop` ≠ `downbeat`: a downbeat is a cut within the regime; a drop is a regime change (`system-replace`, total clear, element-count jump).
|
||||
- Let silence remove density (`negative-space-hold`).
|
||||
- Background beds are a layer, not a move: one bed at a time, under foreground primitives.
|
||||
- `text-spectral-rays` is the hero wordmark treatment; do not stack another visible copy of the same word on top.
|
||||
@@ -0,0 +1,154 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>18 · 3d_card_flip</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.card-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
perspective: 2400px;
|
||||
}
|
||||
.card {
|
||||
position: relative;
|
||||
width: 1000px;
|
||||
height: 580px;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
}
|
||||
.face {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
backface-visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
font-size: 200px;
|
||||
letter-spacing: -0.04em;
|
||||
border-radius: 24px;
|
||||
box-shadow:
|
||||
0 40px 80px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.08) inset;
|
||||
}
|
||||
.face.front {
|
||||
background: linear-gradient(135deg, #1e1b3a 0%, #0a0a0f 100%);
|
||||
color: var(--hg-fg);
|
||||
}
|
||||
.face.back {
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 60%, #d946ef 100%);
|
||||
color: var(--hg-fg);
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
.face .meta {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
left: 40px;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.16em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.card-shadow {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 22%;
|
||||
width: 1000px;
|
||||
height: 80px;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.55) 0%, transparent 70%);
|
||||
transform: translateX(-50%);
|
||||
filter: blur(12px);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="card-shadow"></div>
|
||||
<div class="card-stage">
|
||||
<div class="card" id="card">
|
||||
<div class="face front">
|
||||
<div class="meta">v4 · CURRENT</div>
|
||||
PRODUCT
|
||||
</div>
|
||||
<div class="face back">
|
||||
<div class="meta">v5 · LAUNCH</div>
|
||||
v5.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.fromTo(
|
||||
"#card",
|
||||
{ rotateY: 0, scale: 0.92, opacity: 0 },
|
||||
{ rotateY: 0, scale: 1, opacity: 1, duration: 0.45, ease: "power3.out" },
|
||||
0.15,
|
||||
);
|
||||
|
||||
// Hold then flip
|
||||
tl.to("#card", { rotateY: 180, duration: 1.0, ease: "power3.inOut" }, 0.7);
|
||||
|
||||
// Shadow squishes mid-flip
|
||||
tl.to(
|
||||
".card-shadow",
|
||||
{ scaleX: 0.6, opacity: 0.4, duration: 0.5, ease: "power2.inOut" },
|
||||
0.7,
|
||||
);
|
||||
tl.to(".card-shadow", { scaleX: 1, opacity: 0.7, duration: 0.5, ease: "power2.inOut" }, 1.2);
|
||||
|
||||
// Tiny landing settle
|
||||
tl.to("#card", { scale: 1.02, duration: 0.12, ease: "power2.out" }, 1.68);
|
||||
tl.to("#card", { scale: 1, duration: 0.3, ease: "elastic.out(1, 0.5)" }, 1.8);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,326 @@
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-variables='[
|
||||
{"id":"bgColor", "type":"color", "label":"Page background", "default":"#050507"},
|
||||
{"id":"colorLow", "type":"color", "label":"Flow — shadow tone", "default":"#0a1430"},
|
||||
{"id":"colorMid", "type":"color", "label":"Flow — mid tone", "default":"#6a6f9e"},
|
||||
{"id":"colorHigh", "type":"color", "label":"Flow — highlight tone", "default":"#e8f0ff"},
|
||||
{"id":"accentColor","type":"color", "label":"Core / glow accent", "default":"#aee4ff"},
|
||||
{"id":"flowSpeed", "type":"number", "label":"Flow speed", "default":1, "min":0.1, "max":3, "step":0.1}
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>bg_flow_field</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #050507;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.field {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: block;
|
||||
}
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 48px;
|
||||
bottom: 40px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="4" data-track-index="1">
|
||||
<canvas class="field" width="1920" height="1080"></canvas>
|
||||
<div class="label">bg_flow_field</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// ── Variables (palette + speed); falls back to defaults standalone ──
|
||||
const defaults = {
|
||||
bgColor: "#050507",
|
||||
colorLow: "#0a1430",
|
||||
colorMid: "#6a6f9e",
|
||||
colorHigh: "#e8f0ff",
|
||||
accentColor: "#aee4ff",
|
||||
flowSpeed: 1,
|
||||
};
|
||||
const vars = Object.assign(
|
||||
{},
|
||||
defaults,
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {},
|
||||
);
|
||||
|
||||
// sRGB hex -> linear-light [r,g,b] (shader works in linear, gamma-encodes at end)
|
||||
function hexLin(hex) {
|
||||
let h = String(hex).replace("#", "").trim();
|
||||
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
||||
const r = parseInt(h.substr(0, 2), 16) / 255;
|
||||
const g = parseInt(h.substr(2, 2), 16) / 255;
|
||||
const b = parseInt(h.substr(4, 2), 16) / 255;
|
||||
const lin = (c) => Math.pow(c, 2.2);
|
||||
return [lin(r), lin(g), lin(b)];
|
||||
}
|
||||
|
||||
const canvas = document.querySelector(".field");
|
||||
const gl = canvas.getContext("webgl", {
|
||||
antialias: false,
|
||||
alpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
const VERT = [
|
||||
"attribute vec2 position;",
|
||||
"varying vec2 vUv;",
|
||||
"void main(){",
|
||||
" vUv = position * 0.5 + 0.5;",
|
||||
" gl_Position = vec4(position, 0.0, 1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
// Curl-noise flow field — ported verbatim from the held-message-living-field
|
||||
// template's "living field" background. All visible state comes from uniforms.
|
||||
const FRAG = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vUv;",
|
||||
"uniform float uTime;",
|
||||
"uniform float uPhase;",
|
||||
"uniform float uReveal;",
|
||||
"uniform float uChromAb;",
|
||||
"uniform float uVignette;",
|
||||
"uniform float uIntensity;",
|
||||
"uniform float uSpeed;",
|
||||
"uniform vec2 uResolution;",
|
||||
"uniform vec3 uColLow;",
|
||||
"uniform vec3 uColMid;",
|
||||
"uniform vec3 uColHigh;",
|
||||
"uniform vec3 uAccent;",
|
||||
"float hash21(vec2 p){",
|
||||
" p = fract(p * vec2(123.34, 456.21));",
|
||||
" p += dot(p, p + 45.32);",
|
||||
" return fract(p.x * p.y);",
|
||||
"}",
|
||||
"float noise2(vec2 p){",
|
||||
" vec2 i = floor(p);",
|
||||
" vec2 f = fract(p);",
|
||||
" vec2 u = f * f * (3.0 - 2.0 * f);",
|
||||
" float a = hash21(i);",
|
||||
" float b = hash21(i + vec2(1.0, 0.0));",
|
||||
" float c = hash21(i + vec2(0.0, 1.0));",
|
||||
" float d = hash21(i + vec2(1.0, 1.0));",
|
||||
" return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);",
|
||||
"}",
|
||||
"float fbm(vec2 p){",
|
||||
" float v = 0.0;",
|
||||
" float a = 0.5;",
|
||||
" for(int i=0;i<5;i++){",
|
||||
" v += a * noise2(p);",
|
||||
" p *= 2.02;",
|
||||
" a *= 0.5;",
|
||||
" }",
|
||||
" return v;",
|
||||
"}",
|
||||
"vec2 curl(vec2 p, float t){",
|
||||
" float e = 0.6;",
|
||||
" float n1 = fbm(p + vec2(0.0, e) + t*0.18);",
|
||||
" float n2 = fbm(p + vec2(0.0, -e) + t*0.18);",
|
||||
" float n3 = fbm(p + vec2( e, 0.0) + t*0.18);",
|
||||
" float n4 = fbm(p + vec2(-e, 0.0) + t*0.18);",
|
||||
" return vec2(n1 - n2, -(n3 - n4));",
|
||||
"}",
|
||||
"vec3 palette(float t){",
|
||||
" t = clamp(t, 0.0, 1.0);",
|
||||
" vec3 lowMid = mix(uColLow, uColMid, smoothstep(0.0, 0.5, t));",
|
||||
" return mix(lowMid, uColHigh, smoothstep(0.5, 1.0, t));",
|
||||
"}",
|
||||
"vec3 sampleField(vec2 uv, float t, float energy){",
|
||||
" vec2 p = (uv - 0.5) * vec2(uResolution.x/uResolution.y, 1.0) * 2.4;",
|
||||
" vec2 c = curl(p * 0.9, t);",
|
||||
" vec2 q = p + c * (0.45 + 0.55 * energy);",
|
||||
" float flow = fbm(q * 1.6 + vec2(t * 0.11, -t * 0.07));",
|
||||
" float bands = sin(flow * 14.0 + t * 1.1);",
|
||||
" bands = smoothstep(0.0, 1.0, 0.5 + 0.5 * bands);",
|
||||
" float r = length(p);",
|
||||
" float core = smoothstep(1.5, 0.05, r) * (0.6 + 0.4 * energy);",
|
||||
" float glow = exp(-r * 1.6) * (0.55 + 0.45 * energy);",
|
||||
" float k = clamp(flow * 0.85 + 0.15 * bands + 0.25 * core + 0.6 * glow, 0.0, 1.4);",
|
||||
" vec3 col = palette(k * 0.9 + 0.1 * sin(t * 0.4));",
|
||||
" col *= 0.7 + 0.6 * energy;",
|
||||
" col += uAccent * core * 0.55;",
|
||||
" return col;",
|
||||
"}",
|
||||
"void main(){",
|
||||
" vec2 uv = vUv;",
|
||||
" float t = uTime * uSpeed;",
|
||||
" float e = uIntensity;",
|
||||
" vec2 dir = normalize(uv - 0.5 + 1e-6);",
|
||||
" float ca = uChromAb * (0.004 + 0.012 * length(uv - 0.5));",
|
||||
" vec3 cR = sampleField(uv + dir * ca, t, e);",
|
||||
" vec3 cG = sampleField(uv, t, e);",
|
||||
" vec3 cB = sampleField(uv - dir * ca, t, e);",
|
||||
" vec3 col = vec3(cR.r, cG.g, cB.b);",
|
||||
" float lum = dot(col, vec3(0.299, 0.587, 0.114));",
|
||||
" col = mix(vec3(lum), col, 1.05);",
|
||||
" float rev = uReveal;",
|
||||
" float sweep = smoothstep(0.0, 0.6, rev) * (1.0 - smoothstep(0.6, 1.0, rev));",
|
||||
" col += uColHigh * sweep * 0.35;",
|
||||
" vec3 phaseTint = mix(vec3(0.85, 0.92, 1.05), vec3(1.05, 0.95, 0.85), uPhase);",
|
||||
" col *= phaseTint;",
|
||||
" float v = length(uv - 0.5);",
|
||||
" col *= 1.0 - smoothstep(0.45, 0.95, v) * uVignette;",
|
||||
" col = col / (col + 1.0);",
|
||||
" col = pow(col, vec3(1.0/2.2));",
|
||||
" gl_FragColor = vec4(col, 1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
function compile(type, src) {
|
||||
const s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
return s;
|
||||
}
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
|
||||
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
|
||||
gl.linkProgram(prog);
|
||||
gl.useProgram(prog);
|
||||
|
||||
// full-screen triangle
|
||||
const buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
||||
const posLoc = gl.getAttribLocation(prog, "position");
|
||||
gl.enableVertexAttribArray(posLoc);
|
||||
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.viewport(0, 0, 1920, 1080);
|
||||
|
||||
const L = (n) => gl.getUniformLocation(prog, n);
|
||||
const uLoc = {
|
||||
uTime: L("uTime"),
|
||||
uPhase: L("uPhase"),
|
||||
uReveal: L("uReveal"),
|
||||
uChromAb: L("uChromAb"),
|
||||
uVignette: L("uVignette"),
|
||||
uIntensity: L("uIntensity"),
|
||||
uSpeed: L("uSpeed"),
|
||||
uResolution: L("uResolution"),
|
||||
uColLow: L("uColLow"),
|
||||
uColMid: L("uColMid"),
|
||||
uColHigh: L("uColHigh"),
|
||||
uAccent: L("uAccent"),
|
||||
};
|
||||
|
||||
// animated uniform state (GSAP tweens these .value fields)
|
||||
const u = {
|
||||
uTime: { value: 0 },
|
||||
uPhase: { value: 0.1 },
|
||||
uReveal: { value: 0 },
|
||||
uChromAb: { value: 0 },
|
||||
uVignette: { value: 0 },
|
||||
uIntensity: { value: 0.3 },
|
||||
};
|
||||
const speed = Number(vars.flowSpeed) || 1;
|
||||
const cLow = hexLin(vars.colorLow);
|
||||
const cMid = hexLin(vars.colorMid);
|
||||
const cHigh = hexLin(vars.colorHigh);
|
||||
const cAcc = hexLin(vars.accentColor);
|
||||
|
||||
function render() {
|
||||
gl.uniform1f(uLoc.uTime, u.uTime.value);
|
||||
gl.uniform1f(uLoc.uPhase, u.uPhase.value);
|
||||
gl.uniform1f(uLoc.uReveal, u.uReveal.value);
|
||||
gl.uniform1f(uLoc.uChromAb, u.uChromAb.value);
|
||||
gl.uniform1f(uLoc.uVignette, u.uVignette.value);
|
||||
gl.uniform1f(uLoc.uIntensity, u.uIntensity.value);
|
||||
gl.uniform1f(uLoc.uSpeed, speed);
|
||||
gl.uniform2f(uLoc.uResolution, 1920, 1080);
|
||||
gl.uniform3fv(uLoc.uColLow, cLow);
|
||||
gl.uniform3fv(uLoc.uColMid, cMid);
|
||||
gl.uniform3fv(uLoc.uColHigh, cHigh);
|
||||
gl.uniform3fv(uLoc.uAccent, cAcc);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
}
|
||||
render(); // initial frame so a seek to 0 / first paint is correct
|
||||
|
||||
// ── Energy-envelope drive (the "living field" breath): all repaint is via
|
||||
// onUpdate, so the field is fully determined by the timeline position — no
|
||||
// requestAnimationFrame, seek-safe. ──
|
||||
tl.eventCallback("onUpdate", render);
|
||||
|
||||
tl.to(u.uTime, { value: 4, duration: 4, ease: "none" }, 0);
|
||||
|
||||
// energy: settle-in → breathe → SURGE → settle dark for the hold
|
||||
tl.to(u.uIntensity, { value: 0.6, duration: 1.2, ease: "sine.inOut" }, 0);
|
||||
tl.to(u.uIntensity, { value: 1.0, duration: 0.7, ease: "power2.in" }, 1.3);
|
||||
tl.to(u.uIntensity, { value: 0.35, duration: 2.0, ease: "power2.out" }, 2.0);
|
||||
|
||||
// chromatic light-leak: light at entry, spikes on the surge, then settles
|
||||
tl.fromTo(u.uChromAb, { value: 0 }, { value: 0.4, duration: 1.2, ease: "sine.in" }, 0);
|
||||
tl.to(u.uChromAb, { value: 1.4, duration: 0.5, ease: "power3.in" }, 1.4);
|
||||
tl.to(u.uChromAb, { value: 0.2, duration: 1.6, ease: "power2.out" }, 1.9);
|
||||
|
||||
// a brief highlight sweep across the surge
|
||||
tl.fromTo(u.uReveal, { value: 0 }, { value: 1, duration: 0.7, ease: "none" }, 1.3);
|
||||
tl.set(u.uReveal, { value: 0 }, 2.0);
|
||||
|
||||
// colour-temperature drift
|
||||
tl.to(u.uPhase, { value: 0.6, duration: 2.2, ease: "sine.inOut" }, 0.6);
|
||||
|
||||
// black mask settles the field to near-black + central glow for the hold
|
||||
tl.to(u.uVignette, { value: 0.85, duration: 1.6, ease: "power2.out" }, 1.9);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>08 · binary_decrypt</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-cyan: #2afff0;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.crypt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-weight: 700;
|
||||
font-size: 200px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--hg-cyan);
|
||||
text-shadow: 0 0 30px rgba(42, 255, 240, 0.5);
|
||||
}
|
||||
.crypt .char {
|
||||
display: inline-block;
|
||||
min-width: 1ch;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="crypt">
|
||||
<span class="char" data-target="D">_</span><span class="char" data-target="E">_</span
|
||||
><span class="char" data-target="C">_</span><span class="char" data-target="R">_</span
|
||||
><span class="char" data-target="Y">_</span><span class="char" data-target="P">_</span
|
||||
><span class="char" data-target="T">_</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const SCRAMBLE = "01█▓▒░▀▄■◆◇▪▫";
|
||||
function scrambleChar(charIdx, phase) {
|
||||
const tick = Math.floor(phase * 60);
|
||||
const i = (charIdx * 7 + tick * 13 + 3) % SCRAMBLE.length;
|
||||
return SCRAMBLE.charAt(i);
|
||||
}
|
||||
const cryptState = [{ p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }];
|
||||
cryptState.forEach((state, idx) => {
|
||||
tl.to(
|
||||
state,
|
||||
{
|
||||
p: 1,
|
||||
duration: 0.85,
|
||||
delay: idx * 0.05,
|
||||
ease: "power2.out",
|
||||
onUpdate: function () {
|
||||
const spans = document.querySelectorAll(".crypt .char");
|
||||
const span = spans[idx];
|
||||
if (!span) return;
|
||||
const target = span.getAttribute("data-target");
|
||||
if (state.p >= 0.72) {
|
||||
span.textContent = target;
|
||||
span.style.color = "var(--hg-fg)";
|
||||
span.style.textShadow = "0 0 30px rgba(168, 85, 247, 0.8)";
|
||||
} else {
|
||||
span.textContent = scrambleChar(idx, state.p);
|
||||
}
|
||||
},
|
||||
},
|
||||
0.15,
|
||||
);
|
||||
});
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,95 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>blur_resolve</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 320px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
will-change: filter, transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="hero" id="hero">FOCUS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Resolve: heavy blur + scaled-up + dim settles into perfect crisp focus.
|
||||
tl.fromTo(
|
||||
"#hero",
|
||||
{ filter: "blur(36px)", scale: 1.08, opacity: 0.25 },
|
||||
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.7, ease: "power2.out" },
|
||||
0.05,
|
||||
);
|
||||
|
||||
// Hold crisp through the middle (no keys needed — state persists to ~1.5).
|
||||
|
||||
// Blur-out near the end: brief defocus + dim, a cut-away that loops seamlessly.
|
||||
tl.to(
|
||||
"#hero",
|
||||
{ filter: "blur(24px)", opacity: 0.25, duration: 0.25, ease: "power2.in" },
|
||||
1.7,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,135 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>03 · braam_punch</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
will-change: transform, text-shadow;
|
||||
}
|
||||
.glow-ring {
|
||||
position: absolute;
|
||||
width: 900px;
|
||||
height: 900px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(124, 58, 237, 0.55) 0%, rgba(124, 58, 237, 0) 65%);
|
||||
filter: blur(20px);
|
||||
will-change: transform, opacity;
|
||||
opacity: 0;
|
||||
}
|
||||
.flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #fff;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="glow-ring"></div>
|
||||
<div class="hero" id="hero">BRAAM</div>
|
||||
</div>
|
||||
<div class="flash"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set("#hero", { scale: 0.92, opacity: 0 }, 0);
|
||||
tl.set(".glow-ring", { scale: 0.6, opacity: 0 }, 0);
|
||||
tl.to("#hero", { opacity: 1, duration: 0.18, ease: "power2.out" }, 0.05);
|
||||
tl.to(".glow-ring", { scale: 1.2, opacity: 1, duration: 0.45, ease: "power3.out" }, 0.05);
|
||||
tl.to(
|
||||
"#hero",
|
||||
{
|
||||
scale: 1.18,
|
||||
textShadow: "0 0 60px rgba(168,85,247,0.9)",
|
||||
duration: 0.12,
|
||||
ease: "power4.out",
|
||||
},
|
||||
0.25,
|
||||
);
|
||||
tl.to(
|
||||
"#hero",
|
||||
{
|
||||
scale: 1.0,
|
||||
textShadow: "0 0 0px rgba(168,85,247,0)",
|
||||
duration: 0.7,
|
||||
ease: "elastic.out(1, 0.5)",
|
||||
},
|
||||
0.37,
|
||||
);
|
||||
tl.fromTo(
|
||||
".flash",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 0.85, duration: 0.06, ease: "power4.out" },
|
||||
0.25,
|
||||
);
|
||||
tl.to(".flash", { opacity: 0, duration: 0.35, ease: "power3.in" }, 0.31);
|
||||
tl.to(
|
||||
".glow-ring",
|
||||
{ scale: 1.45, opacity: 0.4, duration: 1.0, ease: "power2.out", overwrite: "auto" },
|
||||
0.55,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>04 · chromatic_split</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.stack {
|
||||
position: relative;
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mix-blend-mode: screen;
|
||||
will-change: transform;
|
||||
}
|
||||
.layer.r {
|
||||
color: #ff2a4a;
|
||||
}
|
||||
.layer.g {
|
||||
color: #2afff0;
|
||||
}
|
||||
.layer.b {
|
||||
color: #b56dff;
|
||||
}
|
||||
.spacer {
|
||||
color: transparent;
|
||||
}
|
||||
.scanlines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0) 0px,
|
||||
rgba(255, 255, 255, 0) 3px,
|
||||
rgba(255, 255, 255, 0.04) 3px,
|
||||
rgba(255, 255, 255, 0.04) 4px
|
||||
);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="stack">
|
||||
<span class="spacer">GLITCH</span>
|
||||
<span class="layer r">GLITCH</span>
|
||||
<span class="layer g">GLITCH</span>
|
||||
<span class="layer b">GLITCH</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scanlines"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.fromTo(
|
||||
".stack",
|
||||
{ opacity: 0, scale: 0.95 },
|
||||
{ opacity: 1, scale: 1, duration: 0.25, ease: "power2.out" },
|
||||
0.05,
|
||||
);
|
||||
tl.set(".layer.r", { x: -2 }, 0);
|
||||
tl.set(".layer.g", { x: 2 }, 0);
|
||||
tl.set(".layer.b", { x: 0 }, 0);
|
||||
tl.to(".layer.r", { x: -14, duration: 0.06, ease: "steps(1)" }, 0.35);
|
||||
tl.to(".layer.g", { x: 14, duration: 0.06, ease: "steps(1)" }, 0.35);
|
||||
tl.to(".layer.r", { x: -4, duration: 0.06, ease: "steps(1)" }, 0.5);
|
||||
tl.to(".layer.g", { x: 4, duration: 0.06, ease: "steps(1)" }, 0.5);
|
||||
tl.to(".layer.r", { x: -22, duration: 0.04, ease: "steps(1)" }, 0.85);
|
||||
tl.to(".layer.g", { x: 22, duration: 0.04, ease: "steps(1)" }, 0.85);
|
||||
tl.to(".layer.b", { x: 6, duration: 0.04, ease: "steps(1)" }, 0.85);
|
||||
tl.to(".layer.r", { x: -6, duration: 0.04, ease: "steps(1)" }, 0.97);
|
||||
tl.to(".layer.g", { x: 6, duration: 0.04, ease: "steps(1)" }, 0.97);
|
||||
tl.to(".layer.b", { x: -2, duration: 0.04, ease: "steps(1)" }, 0.97);
|
||||
tl.to(".layer.r", { x: -3, duration: 0.4, ease: "power2.out" }, 1.2);
|
||||
tl.to(".layer.g", { x: 3, duration: 0.4, ease: "power2.out" }, 1.2);
|
||||
tl.to(".layer.b", { x: 0, duration: 0.4, ease: "power2.out" }, 1.2);
|
||||
tl.fromTo(
|
||||
".scanlines",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 0.85, duration: 0.06, ease: "steps(1)" },
|
||||
0.35,
|
||||
);
|
||||
tl.to(".scanlines", { opacity: 0.25, duration: 0.5, ease: "power2.out" }, 1.0);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>13 · chrome_sweep</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.chrome {
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
background: linear-gradient(
|
||||
110deg,
|
||||
#404048 0%,
|
||||
#b4b4be 20%,
|
||||
#ffffff 45%,
|
||||
#ffffff 50%,
|
||||
#d8d8e0 60%,
|
||||
#5a5a64 85%,
|
||||
#ffffff 100%
|
||||
);
|
||||
background-size: 250% 100%;
|
||||
background-position: 200% 0;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
will-change: background-position;
|
||||
filter: drop-shadow(0 6px 0 rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage"><div class="chrome">CHROME</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.fromTo(
|
||||
".chrome",
|
||||
{ opacity: 0, scale: 0.96 },
|
||||
{ opacity: 1, scale: 1, duration: 0.3, ease: "power3.out" },
|
||||
0.2,
|
||||
);
|
||||
tl.fromTo(
|
||||
".chrome",
|
||||
{ backgroundPosition: "200% 0" },
|
||||
{ backgroundPosition: "-100% 0", duration: 1.4, ease: "power2.inOut" },
|
||||
0.3,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,154 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>22 · counting_punch</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-cyan: #2afff0;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
}
|
||||
.counter {
|
||||
font-weight: 900;
|
||||
font-size: 360px;
|
||||
letter-spacing: -0.04em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--hg-fg);
|
||||
will-change: transform, text-shadow;
|
||||
line-height: 1;
|
||||
}
|
||||
.caption {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 28px;
|
||||
color: var(--hg-dim);
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
}
|
||||
.underline {
|
||||
height: 4px;
|
||||
background: var(--hg-violet-2);
|
||||
width: 0;
|
||||
box-shadow: 0 0 20px var(--hg-violet);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="counter" id="counter">0</div>
|
||||
<div class="underline" id="underline"></div>
|
||||
<div class="caption" id="caption">USERS THIS WEEK</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const TARGET = 12847;
|
||||
const counter = { v: 0 };
|
||||
let lastShown = 0;
|
||||
|
||||
function format(n) {
|
||||
return Math.floor(n).toLocaleString("en-US");
|
||||
}
|
||||
|
||||
tl.to(
|
||||
counter,
|
||||
{
|
||||
v: TARGET,
|
||||
duration: 1.15,
|
||||
ease: "expo.out",
|
||||
onUpdate: function () {
|
||||
const el = document.getElementById("counter");
|
||||
if (!el) return;
|
||||
const cur = Math.floor(counter.v);
|
||||
if (cur !== lastShown) {
|
||||
el.textContent = format(cur);
|
||||
// tiny scale tick on each digit change
|
||||
el.style.transform = "scale(1.045)";
|
||||
el.style.textShadow = "0 0 40px rgba(168, 85, 247, 0.6)";
|
||||
// browser will paint, then this resets via the next onUpdate
|
||||
}
|
||||
lastShown = cur;
|
||||
},
|
||||
},
|
||||
0.15,
|
||||
);
|
||||
|
||||
// Big settle punch at landing
|
||||
tl.to(
|
||||
"#counter",
|
||||
{
|
||||
scale: 1.12,
|
||||
textShadow: "0 0 80px rgba(168, 85, 247, 0.9)",
|
||||
duration: 0.1,
|
||||
ease: "power4.out",
|
||||
},
|
||||
1.3,
|
||||
);
|
||||
tl.to(
|
||||
"#counter",
|
||||
{
|
||||
scale: 1.0,
|
||||
textShadow: "0 0 20px rgba(168, 85, 247, 0.4)",
|
||||
duration: 0.6,
|
||||
ease: "elastic.out(1, 0.55)",
|
||||
},
|
||||
1.4,
|
||||
);
|
||||
|
||||
// Underline draws
|
||||
tl.to("#underline", { width: 420, duration: 0.6, ease: "expo.out" }, 1.35);
|
||||
tl.to("#caption", { opacity: 1, duration: 0.5, ease: "power2.out" }, 1.55);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,155 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>19 · crash_zoom_in</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.zoom-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
will-change: transform;
|
||||
}
|
||||
.radial-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
rgba(168, 85, 247, 0.18) 0%,
|
||||
transparent 35%,
|
||||
rgba(0, 0, 0, 0.6) 80%
|
||||
);
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.speed-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-radial-gradient(
|
||||
circle at 50% 50%,
|
||||
transparent 0px,
|
||||
transparent 6px,
|
||||
rgba(255, 255, 255, 0.08) 7px,
|
||||
transparent 8px
|
||||
);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 360px;
|
||||
letter-spacing: -0.05em;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="radial-bg"></div>
|
||||
<div class="speed-lines"></div>
|
||||
<div class="zoom-stage"><div class="hero">CRASH</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Hold black for tension
|
||||
tl.set(".hero", { scale: 0.03, filter: "blur(20px)", opacity: 0 }, 0);
|
||||
|
||||
const CRASH = 0.45;
|
||||
|
||||
// The crash itself
|
||||
tl.to(
|
||||
".hero",
|
||||
{ scale: 1.0, filter: "blur(0px)", opacity: 1, duration: 0.32, ease: "expo.in" },
|
||||
CRASH,
|
||||
);
|
||||
|
||||
// Background swoosh in same window
|
||||
tl.fromTo(
|
||||
".radial-bg",
|
||||
{ opacity: 0, scale: 0.4 },
|
||||
{ opacity: 1, scale: 1, duration: 0.32, ease: "expo.in" },
|
||||
CRASH,
|
||||
);
|
||||
tl.fromTo(
|
||||
".speed-lines",
|
||||
{ opacity: 0, scale: 0.2 },
|
||||
{ opacity: 1, scale: 1.4, duration: 0.32, ease: "expo.in" },
|
||||
CRASH,
|
||||
);
|
||||
|
||||
// Landing settle
|
||||
const LAND = CRASH + 0.32;
|
||||
tl.to(".hero", { scale: 0.96, duration: 0.08, ease: "power3.out" }, LAND);
|
||||
tl.to(".hero", { scale: 1.0, duration: 0.5, ease: "elastic.out(1, 0.55)" }, LAND + 0.08);
|
||||
|
||||
// Quick landing shake
|
||||
const sh = [9, -7, 5, -4, 2, -1];
|
||||
sh.forEach((amp, i) => {
|
||||
tl.to(
|
||||
".zoom-stage",
|
||||
{ x: amp, y: amp * 0.5 * (i % 2 ? -1 : 1), duration: 0.03, ease: "steps(1)" },
|
||||
LAND + i * 0.03,
|
||||
);
|
||||
});
|
||||
tl.to(
|
||||
".zoom-stage",
|
||||
{ x: 0, y: 0, duration: 0.1, ease: "power2.out" },
|
||||
LAND + sh.length * 0.03,
|
||||
);
|
||||
|
||||
// Speed lines fade
|
||||
tl.to(".speed-lines", { opacity: 0, duration: 0.4, ease: "power2.out" }, LAND + 0.05);
|
||||
// Radial settles
|
||||
tl.to(".radial-bg", { opacity: 0.5, duration: 0.5, ease: "power2.out" }, LAND);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,166 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>27 · datamosh_smear</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-cyan: #2afff0;
|
||||
--hg-magenta: #ff2a4a;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.smear-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.smear-hero {
|
||||
position: relative;
|
||||
font-weight: 900;
|
||||
font-size: 320px;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 0.95;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.smear-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mix-blend-mode: screen;
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
.smear-layer.l1 {
|
||||
color: #ff2a4a;
|
||||
}
|
||||
.smear-layer.l2 {
|
||||
color: #2afff0;
|
||||
}
|
||||
.smear-layer.l3 {
|
||||
color: #b56dff;
|
||||
}
|
||||
.smear-layer.l4 {
|
||||
color: #ffd166;
|
||||
}
|
||||
.smear-layer.l5 {
|
||||
color: #f5f5fa;
|
||||
}
|
||||
.spacer {
|
||||
color: transparent;
|
||||
}
|
||||
.scan-noise {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0) 0px,
|
||||
rgba(255, 255, 255, 0) 2px,
|
||||
rgba(255, 255, 255, 0.07) 3px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0) 0px,
|
||||
rgba(0, 0, 0, 0) 4px,
|
||||
rgba(0, 0, 0, 0.15) 5px
|
||||
);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="smear-stage">
|
||||
<div class="smear-hero">
|
||||
<span class="spacer">SMEAR</span>
|
||||
<span class="smear-layer l1">SMEAR</span>
|
||||
<span class="smear-layer l2">SMEAR</span>
|
||||
<span class="smear-layer l3">SMEAR</span>
|
||||
<span class="smear-layer l4">SMEAR</span>
|
||||
<span class="smear-layer l5">SMEAR</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scan-noise"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// All layers start far right, stagger-lag in, then settle near center
|
||||
const layers = [
|
||||
".smear-layer.l1",
|
||||
".smear-layer.l2",
|
||||
".smear-layer.l3",
|
||||
".smear-layer.l4",
|
||||
".smear-layer.l5",
|
||||
];
|
||||
|
||||
layers.forEach((sel, i) => {
|
||||
tl.fromTo(
|
||||
sel,
|
||||
{ x: 360, opacity: 0, filter: "blur(20px)" },
|
||||
{ x: 0, opacity: 0.85, filter: "blur(0px)", duration: 0.7, ease: "expo.out" },
|
||||
0.2 + i * 0.08,
|
||||
);
|
||||
});
|
||||
|
||||
// Final settle: pull layers into proper offset
|
||||
const finalX = [-6, -2, 0, 2, 4];
|
||||
layers.forEach((sel, i) => {
|
||||
tl.to(sel, { x: finalX[i], duration: 0.4, ease: "power2.out" }, 1.1);
|
||||
});
|
||||
|
||||
// Scan noise pulses
|
||||
tl.fromTo(
|
||||
".scan-noise",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.06, ease: "steps(1)" },
|
||||
0.2,
|
||||
);
|
||||
tl.to(".scan-noise", { opacity: 0.25, duration: 0.8, ease: "power2.out" }, 0.6);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>directional_fill</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 36px;
|
||||
}
|
||||
.bar {
|
||||
position: relative;
|
||||
width: 1040px;
|
||||
height: 188px;
|
||||
border: 3px solid var(--hg-dim);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 56px;
|
||||
}
|
||||
.fill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: scaleX(0);
|
||||
will-change: transform;
|
||||
}
|
||||
.fill.a {
|
||||
background: var(--hg-violet);
|
||||
transform-origin: left center;
|
||||
}
|
||||
.fill.b {
|
||||
background: var(--hg-violet-2);
|
||||
transform-origin: right center;
|
||||
}
|
||||
.fill.c {
|
||||
background: #2afff0;
|
||||
transform-origin: left center;
|
||||
}
|
||||
.label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-weight: 900;
|
||||
font-size: 96px;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1;
|
||||
color: var(--hg-fg);
|
||||
will-change: color;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="1.8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="1.8" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="bar">
|
||||
<div class="fill a"></div>
|
||||
<span class="label" id="l1">READY</span>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<div class="fill b"></div>
|
||||
<span class="label" id="l2">SET</span>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<div class="fill c"></div>
|
||||
<span class="label" id="l3">FILL</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Bar 1 — sweeps left -> right, landing on the beat at ~0.55s.
|
||||
tl.to(".fill.a", { scaleX: 1, duration: 0.35, ease: "power2.out" }, 0.2);
|
||||
tl.set("#l1", { color: "var(--hg-bg)" }, 0.55);
|
||||
|
||||
// Bar 2 — sweeps right -> left, landing at ~0.85s.
|
||||
tl.to(".fill.b", { scaleX: 1, duration: 0.35, ease: "power2.out" }, 0.5);
|
||||
tl.set("#l2", { color: "var(--hg-bg)" }, 0.85);
|
||||
|
||||
// Bar 3 — sweeps left -> right, landing at ~1.15s.
|
||||
tl.to(".fill.c", { scaleX: 1, duration: 0.35, ease: "power2.out" }, 0.8);
|
||||
tl.set("#l3", { color: "var(--hg-bg)" }, 1.15);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,137 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>09 · dolly_zoom</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.layers {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bg-far,
|
||||
.bg-mid,
|
||||
.bg-near {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
.bg-far {
|
||||
background:
|
||||
radial-gradient(2px 2px at 20% 30%, var(--hg-dim) 50%, transparent 51%),
|
||||
radial-gradient(2px 2px at 70% 80%, var(--hg-dim) 50%, transparent 51%),
|
||||
radial-gradient(1px 1px at 45% 60%, var(--hg-dim) 50%, transparent 51%),
|
||||
radial-gradient(1px 1px at 85% 25%, var(--hg-dim) 50%, transparent 51%),
|
||||
radial-gradient(2px 2px at 10% 75%, var(--hg-dim) 50%, transparent 51%),
|
||||
radial-gradient(1px 1px at 60% 15%, var(--hg-dim) 50%, transparent 51%);
|
||||
background-size: 400px 400px;
|
||||
filter: blur(2px);
|
||||
}
|
||||
.bg-mid {
|
||||
background:
|
||||
linear-gradient(90deg, transparent 49.7%, rgba(168, 85, 247, 0.25) 50%, transparent 50.3%)
|
||||
0 0 / 200px 200px,
|
||||
linear-gradient(
|
||||
180deg,
|
||||
transparent 49.7%,
|
||||
rgba(168, 85, 247, 0.15) 50%,
|
||||
transparent 50.3%
|
||||
)
|
||||
0 0 / 200px 200px;
|
||||
}
|
||||
.bg-near {
|
||||
background:
|
||||
radial-gradient(circle at 15% 50%, rgba(168, 85, 247, 0.4) 0%, transparent 25%),
|
||||
radial-gradient(circle at 85% 50%, rgba(124, 58, 237, 0.4) 0%, transparent 25%);
|
||||
}
|
||||
.subject {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
font-family: "Playfair Display", serif;
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-size: 180px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="layers">
|
||||
<div class="bg-far"></div>
|
||||
<div class="bg-mid"></div>
|
||||
<div class="bg-near"></div>
|
||||
</div>
|
||||
<div class="stage"><div class="subject">subject.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.fromTo(
|
||||
".bg-far",
|
||||
{ scale: 1, filter: "blur(2px)" },
|
||||
{ scale: 1.6, filter: "blur(4px)", duration: 1.8, ease: "power2.inOut" },
|
||||
0.1,
|
||||
);
|
||||
tl.fromTo(".bg-mid", { scale: 1 }, { scale: 1.4, duration: 1.8, ease: "power2.inOut" }, 0.1);
|
||||
tl.fromTo(".bg-near", { scale: 1 }, { scale: 1.2, duration: 1.8, ease: "power2.inOut" }, 0.1);
|
||||
tl.fromTo(
|
||||
".subject",
|
||||
{ opacity: 0, scale: 1.1 },
|
||||
{ opacity: 1, scale: 1, duration: 0.6, ease: "power3.out" },
|
||||
0.15,
|
||||
);
|
||||
tl.to(".layers", { skewY: -0.5, duration: 1.8, ease: "power2.inOut" }, 0.1);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,144 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>11 · electric_arc</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-cyan: #2afff0;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.arc-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.arc-path {
|
||||
fill: none;
|
||||
stroke: var(--hg-fg);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
filter: drop-shadow(0 0 8px var(--hg-cyan)) drop-shadow(0 0 16px var(--hg-cyan));
|
||||
stroke-dasharray: 1200;
|
||||
stroke-dashoffset: 1200;
|
||||
will-change: stroke-dashoffset, opacity;
|
||||
}
|
||||
.arc-flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 50% 50%, rgba(42, 255, 240, 0.3) 0%, transparent 40%);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.arc-label {
|
||||
font-weight: 900;
|
||||
font-size: 240px;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--hg-fg);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<svg class="arc-svg" viewBox="0 0 1920 1080" preserveAspectRatio="none">
|
||||
<path
|
||||
class="arc-path"
|
||||
id="arc1"
|
||||
d="M 200 540 L 380 480 L 440 580 L 620 500 L 720 600 L 880 510 L 960 540"
|
||||
/>
|
||||
<path
|
||||
class="arc-path"
|
||||
id="arc2"
|
||||
d="M 1720 540 L 1540 600 L 1480 500 L 1300 580 L 1200 480 L 1040 570 L 960 540"
|
||||
/>
|
||||
<path
|
||||
class="arc-path"
|
||||
id="arc3"
|
||||
d="M 960 200 L 920 360 L 1040 420 L 940 520 L 960 540"
|
||||
/>
|
||||
</svg>
|
||||
<div class="arc-flash"></div>
|
||||
<div class="arc-label">CHARGE</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.fromTo(
|
||||
".arc-label",
|
||||
{ opacity: 0, scale: 0.9 },
|
||||
{ opacity: 1, scale: 1, duration: 0.3, ease: "power3.out" },
|
||||
0.5,
|
||||
);
|
||||
const arcs = ["#arc1", "#arc2", "#arc3"];
|
||||
arcs.forEach((p, i) => {
|
||||
tl.fromTo(
|
||||
p,
|
||||
{ strokeDashoffset: 1200, opacity: 0 },
|
||||
{ strokeDashoffset: 0, opacity: 1, duration: 0.18, ease: "power4.out" },
|
||||
0.15 + i * 0.05,
|
||||
);
|
||||
});
|
||||
tl.to(arcs.join(","), { opacity: 0.3, duration: 0.04, ease: "steps(1)" }, 0.55);
|
||||
tl.to(arcs.join(","), { opacity: 1, duration: 0.04, ease: "steps(1)" }, 0.6);
|
||||
tl.to(arcs.join(","), { opacity: 0.5, duration: 0.04, ease: "steps(1)" }, 0.7);
|
||||
tl.to(arcs.join(","), { opacity: 1, duration: 0.04, ease: "steps(1)" }, 0.78);
|
||||
tl.fromTo(
|
||||
".arc-flash",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.06, ease: "steps(1)" },
|
||||
0.5,
|
||||
);
|
||||
tl.to(".arc-flash", { opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>flash_cut</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.word {
|
||||
position: absolute;
|
||||
font-weight: 900;
|
||||
font-size: 260px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
white-space: nowrap;
|
||||
color: var(--hg-fg);
|
||||
will-change: opacity;
|
||||
}
|
||||
.accent {
|
||||
position: absolute;
|
||||
bottom: 360px;
|
||||
width: 220px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--hg-violet);
|
||||
will-change: background, transform;
|
||||
}
|
||||
.flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #ffffff;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="1.8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="1.8" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="accent" id="accent"></div>
|
||||
<div class="word" id="w0">READY</div>
|
||||
<div class="word" id="w1">IMPACT</div>
|
||||
<div class="word" id="w2">DONE</div>
|
||||
</div>
|
||||
<div class="flash" id="flash"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Initial state: first word visible, others hidden, accent violet.
|
||||
tl.set("#w0", { opacity: 1 }, 0);
|
||||
tl.set("#w1", { opacity: 0 }, 0);
|
||||
tl.set("#w2", { opacity: 0 }, 0);
|
||||
tl.set("#accent", { backgroundColor: "#7c3aed", scaleX: 1 }, 0);
|
||||
tl.set("#flash", { opacity: 0 }, 0);
|
||||
|
||||
// Two hits; during each white frame, swap underlying word + accent colour.
|
||||
const hits = [
|
||||
{ t: 0.5, from: "#w0", to: "#w1", color: "#a855f7", scaleX: 1.45 },
|
||||
{ t: 1.2, from: "#w1", to: "#w2", color: "#f5f5fa", scaleX: 0.7 },
|
||||
];
|
||||
|
||||
hits.forEach((h) => {
|
||||
// Instant full-frame white flash on the hit.
|
||||
tl.set("#flash", { opacity: 1 }, h.t);
|
||||
// While white (~2 frames at 30fps), swap the revealed state.
|
||||
tl.set(h.from, { opacity: 0 }, h.t + 0.03);
|
||||
tl.set(h.to, { opacity: 1 }, h.t + 0.03);
|
||||
tl.set("#accent", { backgroundColor: h.color, scaleX: h.scaleX }, h.t + 0.03);
|
||||
// Snap the flash back out within ~3 frames.
|
||||
tl.set("#flash", { opacity: 0 }, h.t + 0.06);
|
||||
});
|
||||
|
||||
// Graceful loop: dip back to white at the very end and restore the start
|
||||
// state so the cycle re-enters cleanly on the next pass.
|
||||
tl.set("#flash", { opacity: 1 }, 1.74);
|
||||
tl.set("#w2", { opacity: 0 }, 1.77);
|
||||
tl.set("#w0", { opacity: 1 }, 1.77);
|
||||
tl.set("#accent", { backgroundColor: "#7c3aed", scaleX: 1 }, 1.77);
|
||||
tl.set("#flash", { opacity: 0 }, 1.8);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,241 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>17 · gooey_metaball</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-cyan: #2afff0;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.hero {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--hg-fg);
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.gooey-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
filter: url(#gooey-filter);
|
||||
}
|
||||
.ball {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: var(--hg-violet-2);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
will-change: transform;
|
||||
opacity: 0;
|
||||
}
|
||||
.ball.a {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
background: #7c3aed;
|
||||
}
|
||||
.ball.b {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
background: #a855f7;
|
||||
}
|
||||
.ball.c {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #d946ef;
|
||||
}
|
||||
.ball.d {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background: #8b5cf6;
|
||||
}
|
||||
.ball.e {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: #c084fc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<svg style="position: absolute; width: 0; height: 0">
|
||||
<defs>
|
||||
<filter id="gooey-filter">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="20" result="blur" />
|
||||
<feColorMatrix
|
||||
in="blur"
|
||||
mode="matrix"
|
||||
values="
|
||||
1 0 0 0 0
|
||||
0 1 0 0 0
|
||||
0 0 1 0 0
|
||||
0 0 0 22 -10"
|
||||
result="gooey"
|
||||
/>
|
||||
<feComposite in="SourceGraphic" in2="gooey" operator="atop" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="gooey-stage">
|
||||
<div class="ball a" id="ballA"></div>
|
||||
<div class="ball b" id="ballB"></div>
|
||||
<div class="ball c" id="ballC"></div>
|
||||
<div class="ball d" id="ballD"></div>
|
||||
<div class="ball e" id="ballE"></div>
|
||||
</div>
|
||||
|
||||
<div class="hero">GOOEY</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Initial: all balls collapsed at center
|
||||
["#ballA", "#ballB", "#ballC", "#ballD", "#ballE"].forEach((id) => {
|
||||
tl.set(id, { xPercent: -50, yPercent: -50, scale: 0, opacity: 0 }, 0);
|
||||
});
|
||||
// Spawn outward
|
||||
tl.to(
|
||||
"#ballA",
|
||||
{
|
||||
xPercent: -50 - 120,
|
||||
yPercent: -50 + 20,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
ease: "back.out(1.6)",
|
||||
},
|
||||
0.15,
|
||||
);
|
||||
tl.to(
|
||||
"#ballB",
|
||||
{
|
||||
xPercent: -50 + 100,
|
||||
yPercent: -50 + 50,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.55,
|
||||
ease: "back.out(1.6)",
|
||||
},
|
||||
0.2,
|
||||
);
|
||||
tl.to(
|
||||
"#ballC",
|
||||
{
|
||||
xPercent: -50 + 30,
|
||||
yPercent: -50 - 80,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.55,
|
||||
ease: "back.out(1.6)",
|
||||
},
|
||||
0.25,
|
||||
);
|
||||
tl.to(
|
||||
"#ballD",
|
||||
{
|
||||
xPercent: -50 - 50,
|
||||
yPercent: -50 - 30,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.55,
|
||||
ease: "back.out(1.6)",
|
||||
},
|
||||
0.3,
|
||||
);
|
||||
tl.to(
|
||||
"#ballE",
|
||||
{
|
||||
xPercent: -50 + 80,
|
||||
yPercent: -50 - 40,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.55,
|
||||
ease: "back.out(1.6)",
|
||||
},
|
||||
0.18,
|
||||
);
|
||||
|
||||
// Idle drift
|
||||
tl.to(
|
||||
"#ballA",
|
||||
{ xPercent: "+=20", yPercent: "+=12", duration: 1.0, ease: "sine.inOut" },
|
||||
0.7,
|
||||
);
|
||||
tl.to(
|
||||
"#ballB",
|
||||
{ xPercent: "-=18", yPercent: "-=10", duration: 1.0, ease: "sine.inOut" },
|
||||
0.7,
|
||||
);
|
||||
tl.to(
|
||||
"#ballC",
|
||||
{ xPercent: "-=15", yPercent: "+=20", duration: 1.0, ease: "sine.inOut" },
|
||||
0.7,
|
||||
);
|
||||
tl.to(
|
||||
"#ballD",
|
||||
{ xPercent: "+=25", yPercent: "+=8", duration: 1.0, ease: "sine.inOut" },
|
||||
0.7,
|
||||
);
|
||||
tl.to(
|
||||
"#ballE",
|
||||
{ xPercent: "-=22", yPercent: "+=18", duration: 1.0, ease: "sine.inOut" },
|
||||
0.7,
|
||||
);
|
||||
|
||||
// Reveal text behind
|
||||
tl.fromTo(
|
||||
".hero",
|
||||
{ opacity: 0, scale: 0.95 },
|
||||
{ opacity: 1, scale: 1, duration: 0.5, ease: "power3.out" },
|
||||
0.6,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>hard_cut</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.word {
|
||||
font-weight: 900;
|
||||
font-size: 340px;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 0.9;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage" id="stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Deterministic palette + word list. Variation is derived from index only.
|
||||
const BG = ["#7c3aed", "#0a0a0f", "#ff2a4a", "#2afff0", "#a855f7", "#f5f5fa", "#0a0a0f"];
|
||||
const FG = ["#f5f5fa", "#a855f7", "#0a0a0f", "#0a0a0f", "#0a0a0f", "#7c3aed", "#f5f5fa"];
|
||||
const WORDS = ["CUT", "NOW", "HARD", "SNAP", "STOP", "GO", "CUT"];
|
||||
const STEP = 0.28; // beat interval
|
||||
const N = WORDS.length;
|
||||
|
||||
// Build one full-frame panel per cut, all hidden but the first.
|
||||
const stage = document.getElementById("stage");
|
||||
for (let i = 0; i < N; i++) {
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "panel";
|
||||
panel.id = "p" + i;
|
||||
panel.style.background = BG[i];
|
||||
panel.style.opacity = i === 0 ? "1" : "0";
|
||||
const word = document.createElement("div");
|
||||
word.className = "word";
|
||||
word.style.color = FG[i];
|
||||
word.textContent = WORDS[i];
|
||||
panel.appendChild(word);
|
||||
stage.appendChild(panel);
|
||||
}
|
||||
|
||||
// Sample-accurate 0ms cuts: at each beat, hide current panel, show next.
|
||||
// tl.set has no duration and no ease — brutal instant switching.
|
||||
for (let i = 1; i < N; i++) {
|
||||
const t = i * STEP;
|
||||
tl.set("#p" + (i - 1), { opacity: 0 }, t);
|
||||
tl.set("#p" + i, { opacity: 1 }, t);
|
||||
}
|
||||
|
||||
// Loop gracefully: snap back to the first panel before the clip ends so a
|
||||
// re-entry starts clean on the same color/word it began with.
|
||||
tl.set("#p" + (N - 1), { opacity: 0 }, N * STEP);
|
||||
tl.set("#p0", { opacity: 1 }, N * STEP);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>01 · hypercut_whip</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage"><div class="hero" id="hero">HYPERCUT</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set("#hero", { x: 0, filter: "blur(0px)" }, 0);
|
||||
tl.to("#hero", { x: 1800, filter: "blur(30px)", duration: 0.18, ease: "power4.in" }, 1.65);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>10 · iris_open</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.iris-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
clip-path: circle(0% at 50% 50%);
|
||||
will-change: clip-path;
|
||||
background: linear-gradient(135deg, #1a0033 0%, #0a0a0f 60%);
|
||||
}
|
||||
.iris-stage .hero {
|
||||
font-family: "Playfair Display", serif;
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.iris-ring {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--hg-violet);
|
||||
box-shadow: 0 0 40px var(--hg-violet);
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="iris-stage">
|
||||
<div class="iris-ring" style="width: 200px; height: 200px"></div>
|
||||
<div class="hero">IRIS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(".iris-stage", { clipPath: "circle(0% at 50% 50%)" }, 0);
|
||||
tl.fromTo(
|
||||
".iris-ring",
|
||||
{ scale: 0, opacity: 0.9 },
|
||||
{ scale: 9, opacity: 0, duration: 0.85, ease: "expo.out" },
|
||||
0.2,
|
||||
);
|
||||
tl.to(
|
||||
".iris-stage",
|
||||
{ clipPath: "circle(75% at 50% 50%)", duration: 0.9, ease: "expo.out" },
|
||||
0.3,
|
||||
);
|
||||
tl.from(".iris-stage .hero", { y: 40, opacity: 0, duration: 0.6, ease: "power3.out" }, 0.6);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>02 · kinetic_letter_in</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.char {
|
||||
display: inline-block;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="hero" id="hero">
|
||||
<span class="char">K</span><span class="char">I</span><span class="char">N</span
|
||||
><span class="char">E</span><span class="char">T</span><span class="char">I</span
|
||||
><span class="char">C</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.from(
|
||||
".char",
|
||||
{ y: 80, opacity: 0, stagger: 0.045, ease: "back.out(2)", duration: 0.55 },
|
||||
0.05,
|
||||
);
|
||||
tl.to("#hero", { letterSpacing: "0.04em", duration: 0.6, ease: "power2.out" }, 0.7);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,131 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>16 · liquid_morph</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.blob-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 1100px;
|
||||
height: 800px;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 60%, #d946ef 100%);
|
||||
clip-path: polygon(
|
||||
50% 5%,
|
||||
65% 8%,
|
||||
78% 16%,
|
||||
88% 30%,
|
||||
92% 50%,
|
||||
88% 70%,
|
||||
78% 84%,
|
||||
65% 92%,
|
||||
50% 95%,
|
||||
35% 92%,
|
||||
22% 84%,
|
||||
12% 70%,
|
||||
8% 50%,
|
||||
12% 30%,
|
||||
22% 16%,
|
||||
35% 8%
|
||||
);
|
||||
will-change: clip-path, transform;
|
||||
filter: blur(0.5px);
|
||||
}
|
||||
.hero {
|
||||
font-family: "Playfair Display", serif;
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="blob-wrap"><div class="blob"></div></div>
|
||||
<div class="stage"><div class="hero">liquid</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const BLOB_A =
|
||||
"polygon(50% 5%, 65% 8%, 78% 16%, 88% 30%, 92% 50%, 88% 70%, 78% 84%, 65% 92%, 50% 95%, 35% 92%, 22% 84%, 12% 70%, 8% 50%, 12% 30%, 22% 16%, 35% 8%)";
|
||||
const BLOB_B =
|
||||
"polygon(38% 10%, 58% 4%, 75% 14%, 90% 28%, 95% 46%, 86% 60%, 80% 78%, 62% 88%, 46% 94%, 30% 86%, 16% 74%, 6% 58%, 10% 40%, 20% 22%, 30% 14%, 38% 10%)";
|
||||
const BLOB_C =
|
||||
"polygon(46% 8%, 62% 10%, 80% 20%, 92% 36%, 90% 52%, 92% 70%, 76% 86%, 58% 90%, 44% 94%, 28% 88%, 14% 76%, 10% 60%, 12% 44%, 18% 26%, 30% 12%, 38% 8%)";
|
||||
|
||||
tl.set(".blob", { clipPath: BLOB_A, scale: 0.95 }, 0);
|
||||
tl.to(".blob", { clipPath: BLOB_B, scale: 1.05, duration: 0.7, ease: "power2.inOut" }, 0.2);
|
||||
tl.to(".blob", { clipPath: BLOB_C, scale: 1.0, duration: 0.7, ease: "power2.inOut" }, 0.9);
|
||||
tl.fromTo(
|
||||
".hero",
|
||||
{ opacity: 0, y: 30 },
|
||||
{ opacity: 1, y: 0, duration: 0.6, ease: "power3.out" },
|
||||
0.4,
|
||||
);
|
||||
tl.to(".blob", { rotation: 6, duration: 1.5, ease: "sine.inOut" }, 0.2);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,109 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>05 · mask_reveal</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mask-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0 60px;
|
||||
}
|
||||
.mask-clip {
|
||||
clip-path: inset(0 100% 0 0);
|
||||
will-change: clip-path;
|
||||
}
|
||||
.sweep {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
background: var(--hg-violet);
|
||||
box-shadow:
|
||||
0 0 40px var(--hg-violet),
|
||||
0 0 80px var(--hg-violet);
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="mask-wrap">
|
||||
<div class="hero mask-clip">REVEAL</div>
|
||||
<div class="sweep"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(".mask-clip", { clipPath: "inset(0 100% 0 0)" }, 0);
|
||||
tl.set(".sweep", { x: 0, opacity: 0 }, 0);
|
||||
tl.to(".sweep", { opacity: 1, duration: 0.08, ease: "power2.out" }, 0.1);
|
||||
tl.to(".sweep", { x: 1300, duration: 0.75, ease: "expo.out" }, 0.15);
|
||||
tl.to(".mask-clip", { clipPath: "inset(0 0% 0 0)", duration: 0.75, ease: "expo.out" }, 0.18);
|
||||
tl.to(".sweep", { opacity: 0, duration: 0.3, ease: "power2.in" }, 0.85);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>mosaic_pack</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 200px);
|
||||
grid-template-rows: repeat(4, 200px);
|
||||
gap: 18px;
|
||||
}
|
||||
.tile {
|
||||
border-radius: 14px;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2.4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2.4" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="grid" id="grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Deterministic palette + grid build (6x4 = 24 cells).
|
||||
const PALETTE = ["#7c3aed", "#a855f7", "#f5f5fa", "#2a2a3a", "#b56dff", "#6b6b78"];
|
||||
const COLS = 6;
|
||||
const ROWS = 4;
|
||||
const COUNT = COLS * ROWS;
|
||||
const grid = document.getElementById("grid");
|
||||
const tiles = [];
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
tile.style.background = PALETTE[i % PALETTE.length];
|
||||
grid.appendChild(tile);
|
||||
tiles.push(tile);
|
||||
}
|
||||
|
||||
// Scatter each tile from a deterministic offset/rotation, then pack into place.
|
||||
// Land order ripples diagonally (col + row) for a deterministic "assembling" feel.
|
||||
const STEP = 0.07;
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
const col = i % COLS;
|
||||
const row = Math.floor(i / COLS);
|
||||
const dx = (((i * 53) % 200) - 100) * 9; // far-flung scatter X
|
||||
const dy = (((i * 89) % 200) - 100) * 7; // far-flung scatter Y
|
||||
const rot = ((i * 37) % 80) - 40; // deterministic tilt
|
||||
const order = col + row; // diagonal land sequence
|
||||
tl.fromTo(
|
||||
tiles[i],
|
||||
{ x: dx, y: dy, rotation: rot, scale: 0.6, autoAlpha: 0.15 },
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
scale: 1,
|
||||
autoAlpha: 1,
|
||||
duration: 0.6,
|
||||
ease: "back.out(1.6)",
|
||||
},
|
||||
0.1 + order * STEP,
|
||||
);
|
||||
}
|
||||
|
||||
// Settle the packed poster with a tiny breath so the loop seats cleanly.
|
||||
tl.to("#grid", { scale: 1.02, duration: 0.18, ease: "power2.out" }, 1.9);
|
||||
tl.to("#grid", { scale: 1, duration: 0.32, ease: "power2.inOut" }, 2.08);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,108 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>12 · neon_flicker</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.neon {
|
||||
font-family: "Playfair Display", serif;
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.02em;
|
||||
color: #ff64a0;
|
||||
text-shadow:
|
||||
0 0 8px #ff64a0,
|
||||
0 0 24px #ff64a0,
|
||||
0 0 60px #ff1a7a,
|
||||
0 0 120px #ff1a7a;
|
||||
will-change: opacity, text-shadow;
|
||||
}
|
||||
.neon-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(255, 26, 122, 0.12) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="neon-bg"></div>
|
||||
<div class="stage"><div class="neon">neon</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(".neon", { opacity: 0 }, 0);
|
||||
const neonPattern = [
|
||||
{ t: 0.2, o: 1 },
|
||||
{ t: 0.26, o: 0 },
|
||||
{ t: 0.3, o: 1 },
|
||||
{ t: 0.38, o: 0.3 },
|
||||
{ t: 0.42, o: 1 },
|
||||
{ t: 0.5, o: 0.5 },
|
||||
{ t: 0.55, o: 1 },
|
||||
];
|
||||
neonPattern.forEach((p) => {
|
||||
tl.to(".neon", { opacity: p.o, duration: 0.02, ease: "steps(1)" }, p.t);
|
||||
});
|
||||
tl.to(".neon-bg", { opacity: 1, duration: 0.4, ease: "power2.out" }, 0.5);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>21 · outline_to_fill</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.outline-stack {
|
||||
position: relative;
|
||||
font-family: "Playfair Display", serif;
|
||||
font-weight: 900;
|
||||
font-size: 320px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stroke-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: transparent;
|
||||
-webkit-text-stroke: 3px var(--hg-violet-2);
|
||||
text-shadow: 0 0 30px rgba(168, 85, 247, 0.5);
|
||||
will-change:
|
||||
opacity,
|
||||
-webkit-text-stroke;
|
||||
}
|
||||
.fill-layer {
|
||||
position: relative;
|
||||
color: var(--hg-fg);
|
||||
clip-path: inset(100% 0 0 0);
|
||||
will-change: clip-path;
|
||||
}
|
||||
.spacer {
|
||||
color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="outline-stack">
|
||||
<span class="spacer">Premium</span>
|
||||
<span class="stroke-layer">Premium</span>
|
||||
<span class="fill-layer">Premium</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Stroke draws in
|
||||
tl.fromTo(
|
||||
".stroke-layer",
|
||||
{ opacity: 0, scale: 1.04 },
|
||||
{ opacity: 1, scale: 1, duration: 0.45, ease: "power3.out" },
|
||||
0.15,
|
||||
);
|
||||
|
||||
// Fill rises from bottom
|
||||
tl.set(".fill-layer", { clipPath: "inset(100% 0 0 0)" }, 0);
|
||||
tl.to(".fill-layer", { clipPath: "inset(0% 0 0 0)", duration: 1.0, ease: "expo.out" }, 0.55);
|
||||
|
||||
// Stroke recedes once fill arrives
|
||||
tl.to(".stroke-layer", { opacity: 0.35, duration: 0.6, ease: "power2.out" }, 1.0);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>palette_flip</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.kicker {
|
||||
font-weight: 700;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.42em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.word {
|
||||
font-weight: 900;
|
||||
font-size: 300px;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 0.9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bars {
|
||||
display: flex;
|
||||
gap: 26px;
|
||||
margin-top: 56px;
|
||||
}
|
||||
.bar {
|
||||
width: 220px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.bar.b2 {
|
||||
width: 140px;
|
||||
}
|
||||
.bar.b3 {
|
||||
width: 90px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2.6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2.6" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="kicker" id="kicker">REGIME</div>
|
||||
<div class="word" id="word">REGIME</div>
|
||||
<div class="bars">
|
||||
<div class="bar b1"></div>
|
||||
<div class="bar b2"></div>
|
||||
<div class="bar b3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Four distinct palette regimes — same layout, re-skinned on each flip.
|
||||
const regimes = [
|
||||
{ bg: "#0a0a0f", fg: "#f5f5fa", accent: "#7c3aed", kicker: "#a855f7" }, // dark / violet
|
||||
{ bg: "#f4ead8", fg: "#1a1410", accent: "#ff5a47", kicker: "#c4452f" }, // cream / coral
|
||||
{ bg: "#08222b", fg: "#eafaf6", accent: "#1fd1b0", kicker: "#7fe3d2" }, // navy / teal
|
||||
{ bg: "#000000", fg: "#ffce3a", accent: "#ffce3a", kicker: "#7a6010" }, // black / amber
|
||||
];
|
||||
|
||||
const beat = 0.6;
|
||||
const scene = "#scene";
|
||||
const word = "#word";
|
||||
const kicker = "#kicker";
|
||||
const bars = ".bar";
|
||||
|
||||
// Instant 0ms flips on every beat — cycle through the regimes deterministically.
|
||||
// The motion IS the colour change; layout never moves.
|
||||
const flips = 5;
|
||||
for (let i = 0; i < flips; i++) {
|
||||
const p = regimes[i % regimes.length];
|
||||
const t = i * beat;
|
||||
tl.set(scene, { backgroundColor: p.bg }, t);
|
||||
tl.set(word, { color: p.fg }, t);
|
||||
tl.set(kicker, { color: p.kicker }, t);
|
||||
tl.set(bars, { backgroundColor: p.accent }, t);
|
||||
}
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>24 · particle_burst</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 320px;
|
||||
letter-spacing: -0.05em;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
will-change: transform, opacity;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.particle-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--hg-violet-2);
|
||||
box-shadow: 0 0 12px var(--hg-violet-2);
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="particle-stage" id="particles"></div>
|
||||
<div class="stage"><div class="hero">SPARK</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Build N particles deterministically
|
||||
const N = 36;
|
||||
const container = document.getElementById("particles");
|
||||
const particles = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
const p = document.createElement("div");
|
||||
p.className = "particle";
|
||||
// Vary size per particle for visual interest
|
||||
const sz = 4 + (i % 4) * 3;
|
||||
p.style.width = sz + "px";
|
||||
p.style.height = sz + "px";
|
||||
// Slight color variation
|
||||
const hue = ((i * 17) % 60) - 30;
|
||||
p.style.filter = `hue-rotate(${hue}deg)`;
|
||||
container.appendChild(p);
|
||||
particles.push(p);
|
||||
}
|
||||
|
||||
// Hero punch + flash entry
|
||||
tl.fromTo(
|
||||
".hero",
|
||||
{ scale: 0.85, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.3, ease: "power4.out" },
|
||||
0.3,
|
||||
);
|
||||
|
||||
// Particles burst out
|
||||
particles.forEach((p, i) => {
|
||||
const angle = (i / N) * Math.PI * 2 + i * 0.137; // golden-angle jitter
|
||||
const radius = 500 + (i % 5) * 90 + (i % 3) * 40;
|
||||
const tx = Math.cos(angle) * radius;
|
||||
const ty = Math.sin(angle) * radius;
|
||||
const dur = 1.1 + (i % 5) * 0.08;
|
||||
tl.fromTo(
|
||||
p,
|
||||
{ x: 0, y: 0, scale: 1, opacity: 1 },
|
||||
{ x: tx, y: ty, scale: 0, opacity: 0, duration: dur, ease: "expo.out" },
|
||||
0.35 + (i % 6) * 0.012,
|
||||
);
|
||||
});
|
||||
|
||||
// Hero settle wobble
|
||||
tl.to(".hero", { scale: 1.04, duration: 0.1, ease: "power3.out" }, 0.35);
|
||||
tl.to(".hero", { scale: 1.0, duration: 0.5, ease: "elastic.out(1, 0.5)" }, 0.45);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,121 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>26 · pixel_dissolve</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 320px;
|
||||
letter-spacing: -0.05em;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 60%, #d946ef 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
.grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
will-change: contents;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cell {
|
||||
background: var(--hg-bg);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage"><div class="hero">DISSOLVE</div></div>
|
||||
<div class="grid" id="grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const COLS = 24;
|
||||
const ROWS = 14;
|
||||
const grid = document.getElementById("grid");
|
||||
grid.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
|
||||
grid.style.gridTemplateRows = `repeat(${ROWS}, 1fr)`;
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < COLS * ROWS; i++) {
|
||||
const c = document.createElement("div");
|
||||
c.className = "cell";
|
||||
grid.appendChild(c);
|
||||
cells.push(c);
|
||||
}
|
||||
|
||||
// Deterministic shuffle: hash function based on i+j
|
||||
function hash(i) {
|
||||
return ((i * 9301 + 49297) % 233280) / 233280;
|
||||
}
|
||||
const order = cells.map((c, i) => ({ c, i, h: hash(i) })).sort((a, b) => a.h - b.h);
|
||||
|
||||
// All cells start visible (covering hero), fade out in shuffled order
|
||||
const TOTAL = 0.85;
|
||||
order.forEach((o, k) => {
|
||||
const t = 0.35 + (k / order.length) * TOTAL;
|
||||
tl.to(o.c, { opacity: 0, duration: 0.06, ease: "steps(1)" }, t);
|
||||
});
|
||||
|
||||
// After dissolve, gentle hero scale settle (it was always visible, now exposed)
|
||||
tl.fromTo(".hero", { scale: 1.03 }, { scale: 1.0, duration: 0.5, ease: "power2.out" }, 1.0);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,160 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>25 · radial_burst_lines</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 320px;
|
||||
letter-spacing: -0.05em;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
will-change: transform;
|
||||
}
|
||||
.burst-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.line {
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 180px;
|
||||
background: linear-gradient(to top, transparent 0%, var(--hg-fg) 50%, transparent 100%);
|
||||
transform-origin: bottom center;
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="burst-stage" id="bursts"></div>
|
||||
<div class="stage"><div class="hero">BAM</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Build 20 lines
|
||||
const N = 20;
|
||||
const container = document.getElementById("bursts");
|
||||
const lines = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
const l = document.createElement("div");
|
||||
l.className = "line";
|
||||
const angle = (i / N) * 360;
|
||||
const innerRadius = 220;
|
||||
// Rotate around center, push outward via transform-origin trick.
|
||||
// Easier: use translate then rotate
|
||||
l.style.transform = `rotate(${angle}deg) translateY(-${innerRadius}px) scaleY(0)`;
|
||||
container.appendChild(l);
|
||||
lines.push({ el: l, angle });
|
||||
}
|
||||
|
||||
// Hero punch
|
||||
tl.fromTo(
|
||||
".hero",
|
||||
{ scale: 0.7, opacity: 0 },
|
||||
{ scale: 1.1, opacity: 1, duration: 0.15, ease: "power4.out" },
|
||||
0.3,
|
||||
);
|
||||
tl.to(".hero", { scale: 1.0, duration: 0.4, ease: "elastic.out(1, 0.5)" }, 0.45);
|
||||
|
||||
// Burst lines expand
|
||||
lines.forEach((l, i) => {
|
||||
const innerRadius = 220 + (i % 3) * 30; // vary radius
|
||||
const len = 200 + (i % 4) * 40; // vary length
|
||||
const ang = l.angle;
|
||||
tl.set(
|
||||
l.el,
|
||||
{
|
||||
rotation: ang,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scaleY: 0,
|
||||
opacity: 0,
|
||||
transformOrigin: "center bottom",
|
||||
},
|
||||
0,
|
||||
);
|
||||
// GSAP transforms compose, so we set the rotation + translate via separate properties
|
||||
// Use a custom transform string approach
|
||||
tl.to(
|
||||
l.el,
|
||||
{
|
||||
scaleY: 1,
|
||||
opacity: 1,
|
||||
duration: 0.18,
|
||||
ease: "power4.out",
|
||||
onUpdate: function () {
|
||||
const p = this.progress();
|
||||
const t = -innerRadius - len * p * 0.2;
|
||||
l.el.style.transform = `rotate(${ang}deg) translateY(${t}px) scaleY(${p})`;
|
||||
l.el.style.opacity = (p > 0.7 ? 1 - (p - 0.7) / 0.3 : 1).toString();
|
||||
},
|
||||
},
|
||||
0.32 + (i % 5) * 0.018,
|
||||
);
|
||||
});
|
||||
|
||||
// Hold then fade
|
||||
tl.to(".line", { opacity: 0, duration: 0.4, ease: "power2.out" }, 0.9);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>07 · screen_shake</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shake-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
will-change: transform;
|
||||
}
|
||||
.impact-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 50% 50%, rgba(255, 42, 74, 0.18) 0%, transparent 60%);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="impact-bg"></div>
|
||||
<div class="shake-stage"><div class="hero">IMPACT</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.fromTo(
|
||||
".hero",
|
||||
{ scale: 0.6, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.15, ease: "power4.out" },
|
||||
0.3,
|
||||
);
|
||||
tl.fromTo(
|
||||
".impact-bg",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.06, ease: "steps(1)" },
|
||||
0.3,
|
||||
);
|
||||
tl.to(".impact-bg", { opacity: 0, duration: 0.6, ease: "power2.in" }, 0.4);
|
||||
|
||||
const shakeStart = 0.32;
|
||||
const shakeAmps = [28, -24, 20, -22, 16, -18, 14, -14, 10, -8, 6, -5, 3, -2];
|
||||
shakeAmps.forEach((amp, i) => {
|
||||
tl.to(
|
||||
".shake-stage",
|
||||
{ x: amp, y: amp * 0.7 * (i % 2 ? -1 : 1), duration: 0.03, ease: "steps(1)" },
|
||||
shakeStart + i * 0.03,
|
||||
);
|
||||
});
|
||||
tl.to(
|
||||
".shake-stage",
|
||||
{ x: 0, y: 0, duration: 0.1, ease: "power2.out" },
|
||||
shakeStart + shakeAmps.length * 0.03,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,130 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>15 · slot_machine_reveal</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.slot {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-weight: 700;
|
||||
font-size: 280px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--hg-fg);
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
.slot .digit {
|
||||
display: inline-block;
|
||||
min-width: 1ch;
|
||||
text-shadow: 0 0 40px rgba(168, 85, 247, 0.6);
|
||||
}
|
||||
.slot-label {
|
||||
position: absolute;
|
||||
bottom: 30%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 22px;
|
||||
color: var(--hg-dim);
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="slot">
|
||||
<span class="digit" data-target="5">0</span>
|
||||
<span class="digit" data-target=".">0</span>
|
||||
<span class="digit" data-target="5">0</span>
|
||||
<span class="digit" data-target="x">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slot-label">FINAL VERSION</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const DIGITS = "0123456789";
|
||||
const slotStates = [{ p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }];
|
||||
slotStates.forEach((state, idx) => {
|
||||
tl.to(
|
||||
state,
|
||||
{
|
||||
p: 1,
|
||||
duration: 1.0,
|
||||
delay: idx * 0.08,
|
||||
ease: "power3.out",
|
||||
onUpdate: function () {
|
||||
const spans = document.querySelectorAll(".slot .digit");
|
||||
const span = spans[idx];
|
||||
if (!span) return;
|
||||
const target = span.getAttribute("data-target");
|
||||
if (state.p >= 0.85) {
|
||||
span.textContent = target;
|
||||
span.style.color = "var(--hg-violet-2)";
|
||||
} else {
|
||||
const tick = Math.floor(state.p * 80 + idx * 3);
|
||||
span.textContent = DIGITS.charAt(tick % 10);
|
||||
}
|
||||
},
|
||||
},
|
||||
0.15,
|
||||
);
|
||||
});
|
||||
tl.to(".slot-label", { opacity: 1, duration: 0.5, ease: "power2.out" }, 1.2);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,137 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>20 · spotlight_sweep</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.dim-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #03030a;
|
||||
}
|
||||
.hero-bright,
|
||||
.hero-dim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Playfair Display", serif;
|
||||
font-weight: 900;
|
||||
font-size: 320px;
|
||||
letter-spacing: -0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hero-dim {
|
||||
color: rgba(245, 245, 250, 0.06);
|
||||
}
|
||||
.hero-bright {
|
||||
color: var(--hg-fg);
|
||||
text-shadow:
|
||||
0 0 30px rgba(168, 85, 247, 0.4),
|
||||
0 0 80px rgba(168, 85, 247, 0.2);
|
||||
-webkit-mask-image: radial-gradient(
|
||||
circle 360px at -400px 50%,
|
||||
#000 0%,
|
||||
rgba(0, 0, 0, 0.6) 60%,
|
||||
transparent 80%
|
||||
);
|
||||
mask-image: radial-gradient(
|
||||
circle 360px at -400px 50%,
|
||||
#000 0%,
|
||||
rgba(0, 0, 0, 0.6) 60%,
|
||||
transparent 80%
|
||||
);
|
||||
will-change: mask-image;
|
||||
}
|
||||
.spotlight-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
circle 380px at -400px 50%,
|
||||
rgba(168, 85, 247, 0.18) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
will-change: background;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="dim-layer"></div>
|
||||
<div class="hero-dim">DISCOVER</div>
|
||||
<div class="hero-bright" id="bright">DISCOVER</div>
|
||||
<div class="spotlight-glow" id="glow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Animate the spotlight x position via mask-image (use a proxy + onUpdate for smoothness)
|
||||
const state = { x: -400 };
|
||||
tl.to(
|
||||
state,
|
||||
{
|
||||
x: 2300,
|
||||
duration: 1.4,
|
||||
ease: "power2.inOut",
|
||||
onUpdate: function () {
|
||||
const m = `radial-gradient(circle 360px at ${state.x}px 50%, #000 0%, rgba(0,0,0,0.6) 60%, transparent 80%)`;
|
||||
const g = `radial-gradient(circle 380px at ${state.x}px 50%, rgba(168, 85, 247, 0.18) 0%, transparent 60%)`;
|
||||
const bright = document.getElementById("bright");
|
||||
const glow = document.getElementById("glow");
|
||||
if (bright) {
|
||||
bright.style.maskImage = m;
|
||||
bright.style.webkitMaskImage = m;
|
||||
}
|
||||
if (glow) glow.style.background = g;
|
||||
},
|
||||
},
|
||||
0.3,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>staggered_exit</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 320px);
|
||||
grid-template-rows: repeat(2, 320px);
|
||||
gap: 36px;
|
||||
}
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(135deg, var(--hg-violet), var(--hg-violet-2));
|
||||
color: var(--hg-fg);
|
||||
font-weight: 900;
|
||||
font-size: 72px;
|
||||
letter-spacing: -0.02em;
|
||||
box-shadow: 0 24px 60px rgba(124, 58, 237, 0.35);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="grid">
|
||||
<div class="card">NORTH</div>
|
||||
<div class="card">EAST</div>
|
||||
<div class="card">SOUTH</div>
|
||||
<div class="card">WEST</div>
|
||||
<div class="card">PEAK</div>
|
||||
<div class="card">EDGE</div>
|
||||
<div class="card">CORE</div>
|
||||
<div class="card">FLUX</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const cards = gsap.utils.toArray(".card");
|
||||
const hold = 0.4; // fully visible hold before the cascade begins
|
||||
const step = 0.11; // per-element offset
|
||||
const exitDur = 0.45;
|
||||
|
||||
// start fully visible
|
||||
tl.set(cards, { autoAlpha: 1, y: 0 }, 0);
|
||||
|
||||
// ordered exit: each card slides down + fades, one after another
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
tl.to(
|
||||
cards[i],
|
||||
{ y: 80, autoAlpha: 0, duration: exitDur, ease: "power3.in" },
|
||||
hold + i * step,
|
||||
);
|
||||
}
|
||||
|
||||
// snap all back to the visible start state so the next loop begins full
|
||||
tl.set(cards, { autoAlpha: 1, y: 0 }, 1.95);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
# Using `text-spectral-rays` — it OWNS its wordmark
|
||||
|
||||
`text-spectral-rays` is a **self-contained WebGL hero-text renderer**. From ONE rasterized
|
||||
glyph mask it draws **both** the solid wordmark **and** the spectral rays that emanate from
|
||||
it. Letters and rays share the same mask, so they are always perfectly registered.
|
||||
|
||||
## The one trap: never give the word a second source
|
||||
|
||||
The ghost / doubled-wordmark artifact comes from splitting the word across two sources:
|
||||
|
||||
- ❌ **Wrong** — use the shader as a "rays-only background" and draw the visible letters
|
||||
with a **separate DOM element** (or stack a second text move like `content_swap` /
|
||||
`chromatic_pressure` on the same word). The DOM font (e.g. Inter) and the shader's raster
|
||||
font (Arial Black / Impact fallback) differ in width, shape, and position, so the ray
|
||||
edges never line up with the DOM letters → a misregistered ghost. **Deleting the shader's
|
||||
letter terms does NOT fix it** — the ray mask itself is still the second, misaligned copy
|
||||
of the word.
|
||||
|
||||
- ✅ **Right** — let the shader render the wordmark. There is exactly ONE source, so a
|
||||
ghost is structurally impossible.
|
||||
|
||||
## Integrate in one pass
|
||||
|
||||
1. **It IS the wordmark.** Hide any DOM logo for that word (keep it only as an invisible
|
||||
layout spacer if a tagline/CTA below depends on its box). Never stack a discrete text
|
||||
move on the same word.
|
||||
2. **One timeline.** Merge its `progress` / `effectMix` state tweens onto the group's master
|
||||
timeline and repaint via `tl.eventCallback("onUpdate", render)` — no second timeline, no
|
||||
`requestAnimationFrame`.
|
||||
3. **Align the cursor to the word.** `mouse.y` must equal the mask's vertical center. If you
|
||||
move the rasterized word off frame-center (e.g. up, to leave room for a tagline), shift
|
||||
the cursor's `y` by the same amount — otherwise the rays cast at the wrong angle.
|
||||
4. **Local raster only.** Rasterize the word with a bundled / system font (no CDN font);
|
||||
upload the mask + colour canvases as textures.
|
||||
5. **Entrance.** Slam the whole canvas (autoAlpha + a scale punch, `transform-origin` on the
|
||||
word's optical center) on the hit; let `effectMix` bloom the rays just after. The solid
|
||||
letters are present the instant the canvas reveals.
|
||||
|
||||
## Pairs with
|
||||
|
||||
A background bed (`bg-flow-field`) or **separate** supporting elements (tagline, CTA, rule)
|
||||
— never a second treatment of its own word.
|
||||
@@ -0,0 +1,313 @@
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-variables='[
|
||||
{"id":"text", "type":"string", "label":"Wordmark", "default":"SPECTRAL"},
|
||||
{"id":"textColor", "type":"color", "label":"Wordmark fill", "default":"#f6ff7a"},
|
||||
{"id":"bgColor", "type":"color", "label":"Stage background", "default":"#000000"},
|
||||
{"id":"sweepSpeed", "type":"number", "label":"Light sweep speed", "default":1, "min":0.2, "max":3, "step":0.1}
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>text_spectral_rays</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #000000;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #000;
|
||||
}
|
||||
.vfx-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: block;
|
||||
}
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 48px;
|
||||
bottom: 40px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="4" data-track-index="1">
|
||||
<canvas class="vfx-canvas" width="1920" height="1080" aria-hidden="true"></canvas>
|
||||
<div class="label">text_spectral_rays</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
(function () {
|
||||
const W = 1920;
|
||||
const H = 1080;
|
||||
|
||||
const defaults = {
|
||||
text: "SPECTRAL",
|
||||
textColor: "#f6ff7a",
|
||||
bgColor: "#000000",
|
||||
sweepSpeed: 1,
|
||||
};
|
||||
const vars = Object.assign(
|
||||
{},
|
||||
defaults,
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {},
|
||||
);
|
||||
|
||||
const canvas = document.querySelector(".vfx-canvas");
|
||||
const gl = canvas.getContext("webgl", {
|
||||
premultipliedAlpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
// animated state — GSAP tweens these; mouse (the light cursor) is derived
|
||||
const state = { progress: 0, effectMix: 0 };
|
||||
const mouse = { x: 0, y: H * 0.5 };
|
||||
const speed = Number(vars.sweepSpeed) || 1;
|
||||
|
||||
// ── Rasterize the wordmark into a white MASK + a coloured SRC canvas ──
|
||||
function drawWord(ctx, fill) {
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
let fs = 340;
|
||||
const stack = "900 " + fs + 'px "Arial Black", Impact, "Inter", system-ui, sans-serif';
|
||||
ctx.font = stack;
|
||||
const maxW = 1600;
|
||||
const measured = ctx.measureText(vars.text).width || 1;
|
||||
if (measured > maxW) {
|
||||
fs = Math.floor((fs * maxW) / measured);
|
||||
ctx.font = "900 " + fs + 'px "Arial Black", Impact, "Inter", system-ui, sans-serif';
|
||||
}
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = fill;
|
||||
ctx.fillText(vars.text, W * 0.5, H * 0.5);
|
||||
}
|
||||
const maskCanvas = document.createElement("canvas");
|
||||
maskCanvas.width = W;
|
||||
maskCanvas.height = H;
|
||||
drawWord(maskCanvas.getContext("2d"), "#ffffff");
|
||||
const colorCanvas = document.createElement("canvas");
|
||||
colorCanvas.width = W;
|
||||
colorCanvas.height = H;
|
||||
drawWord(colorCanvas.getContext("2d"), vars.textColor);
|
||||
|
||||
// ── WebGL fallback: flat fill (keeps the timeline registered) ──
|
||||
if (!gl) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.fillStyle = vars.bgColor;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
tl.to(state, { progress: 1, duration: 3.6, ease: "none" }, 0.2);
|
||||
window.__timelines["main"] = tl;
|
||||
return;
|
||||
}
|
||||
|
||||
const VERT = [
|
||||
"attribute vec2 position;",
|
||||
"varying vec2 vUv;",
|
||||
"void main(){",
|
||||
" vUv = position * 0.5 + 0.5;",
|
||||
" gl_Position = vec4(position, 0.0, 1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
// Volumetric spectral light-rays cast by the text mask toward a moving
|
||||
// light cursor — ported from the vfx-text-cursor golden sample, made
|
||||
// generic (reads any text mask/colour). All state from uniforms.
|
||||
const FRAG = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vUv;",
|
||||
"uniform vec2 resolution;",
|
||||
"uniform vec2 mouse;",
|
||||
"uniform float maskY;",
|
||||
"uniform float maskScale;",
|
||||
"uniform float effectMix;",
|
||||
"uniform sampler2D src;",
|
||||
"uniform sampler2D colorSrc;",
|
||||
"#define PI 3.141593",
|
||||
"#define SAMPLES 112.0",
|
||||
"float hash(vec2 p){ return fract(sin(dot(p, vec2(489.0,589.0))) * 492.0) * 2.0 - 1.0; }",
|
||||
"float hash(vec3 p){ return fract(sin(dot(p, vec3(489.0,589.0,58.0))) * 492.0) * 2.0 - 1.0; }",
|
||||
"vec2 hash2(vec3 p){ return vec2(hash(p), hash(p + 1.0)); }",
|
||||
"vec4 readMask(vec2 uv){ if(uv.x<0.0||uv.x>1.0||uv.y<0.0||uv.y>1.0){return vec4(0.0);} return texture2D(src, uv); }",
|
||||
"vec4 readColor(vec2 uv){ if(uv.x<0.0||uv.x>1.0||uv.y<0.0||uv.y>1.0){return vec4(0.0);} return texture2D(colorSrc, uv); }",
|
||||
"vec3 spectrum(float x){ return cos((x - vec3(0.0,0.5,1.0)) * vec3(0.6,1.0,0.5) * PI); }",
|
||||
"void main(){",
|
||||
" vec2 uv = vUv;",
|
||||
" vec2 center = vec2(0.5, 0.5);",
|
||||
" vec2 maskUv = (uv - center) / maskScale + center + vec2(0.0, maskY / resolution.y);",
|
||||
" float mask = readMask(maskUv).r;",
|
||||
" vec4 col0 = readColor(maskUv);",
|
||||
" vec2 p = uv * 2.0 - 1.0;",
|
||||
" p.x *= resolution.x / resolution.y;",
|
||||
" vec2 mp = mouse / resolution;",
|
||||
" mp.y = 1.0 - mp.y;",
|
||||
" mp = mp * 2.0 - 1.0;",
|
||||
" mp.x *= resolution.x / resolution.y;",
|
||||
" vec2 rp = p;",
|
||||
" vec2 d = (mp - p) / SAMPLES;",
|
||||
" float acc = 0.0;",
|
||||
" for(float i = 0.0; i < SAMPLES; i++){",
|
||||
" rp += d;",
|
||||
" rp += hash2(vec3(rp, i)) * 0.5 / SAMPLES;",
|
||||
" vec2 uv2 = rp;",
|
||||
" uv2.x /= resolution.x / resolution.y;",
|
||||
" uv2 = uv2 * 0.5 + 0.5;",
|
||||
" vec2 rayUv = (uv2 - center) / maskScale + center + vec2(0.0, maskY / resolution.y);",
|
||||
" acc += readMask(rayUv).r / SAMPLES;",
|
||||
" }",
|
||||
" vec4 c = vec4(0.0, 0.0, 0.0, 1.0);",
|
||||
" c -= acc * 0.24;",
|
||||
" c += vec4(spectrum(cos(acc * 4.9)), 1.0) * acc * 3.65;",
|
||||
" c.rgb = max(c.rgb - 0.035, 0.0);",
|
||||
" c.rgb *= 1.16;",
|
||||
" float cursorX = mouse.x / resolution.x;",
|
||||
" float sweep = exp(-pow((uv.x - cursorX) * 6.2, 2.0));",
|
||||
" float textProximity = smoothstep(0.006, 0.34, acc);",
|
||||
" float longRay = smoothstep(0.004, 0.18, acc);",
|
||||
" vec3 sweepColor = vec3(0.14, 0.65, 0.85) + spectrum(cursorX * 1.8) * 0.28;",
|
||||
" c.rgb += sweepColor * sweep * (0.34 + textProximity * 2.35);",
|
||||
" c.rgb += spectrum(cursorX + acc * 2.0) * longRay * 0.58;",
|
||||
" c.rgb += col0.rgb * acc * 1.18;",
|
||||
" c.rgb += col0.rgb * sweep * mask * 0.92;",
|
||||
" c.rgb *= effectMix;",
|
||||
" c.rgb = mix(c.rgb, col0.rgb, smoothstep(0.02, 0.75, col0.a));",
|
||||
" c.rgb += vec3(1.0) * smoothstep(0.04, 0.38, mask) * 0.12 * effectMix;",
|
||||
" float grain = hash(vec3(uv.xyy + mouse.x * 0.0007));",
|
||||
" float dust = smoothstep(0.02, 0.38, acc) + smoothstep(0.03, 0.22, mask);",
|
||||
" c.rgb += grain * 0.07 * (0.42 + dust) * effectMix;",
|
||||
" c.rgb += spectrum(grain + cursorX) * abs(grain) * 0.045 * smoothstep(0.02, 0.5, acc) * effectMix;",
|
||||
" c.a = 1.0;",
|
||||
" gl_FragColor = c;",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
function compile(type, source) {
|
||||
const s = gl.createShader(type);
|
||||
gl.shaderSource(s, source);
|
||||
gl.compileShader(s);
|
||||
return s;
|
||||
}
|
||||
const program = gl.createProgram();
|
||||
gl.attachShader(program, compile(gl.VERTEX_SHADER, VERT));
|
||||
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAG));
|
||||
gl.linkProgram(program);
|
||||
gl.useProgram(program);
|
||||
|
||||
function createTexture(source) {
|
||||
const t = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, t);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
return t;
|
||||
}
|
||||
const maskTex = createTexture(maskCanvas);
|
||||
const colorTex = createTexture(colorCanvas);
|
||||
|
||||
const buffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
const posLoc = gl.getAttribLocation(program, "position");
|
||||
const L = (n) => gl.getUniformLocation(program, n);
|
||||
const uRes = L("resolution");
|
||||
const uMouse = L("mouse");
|
||||
const uMaskY = L("maskY");
|
||||
const uMaskScale = L("maskScale");
|
||||
const uMix = L("effectMix");
|
||||
const uSrc = L("src");
|
||||
const uColorSrc = L("colorSrc");
|
||||
|
||||
function render() {
|
||||
mouse.x = state.progress * W;
|
||||
mouse.y = H * 0.5;
|
||||
gl.viewport(0, 0, W, H);
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.useProgram(program);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
gl.enableVertexAttribArray(posLoc);
|
||||
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, maskTex);
|
||||
gl.uniform1i(uSrc, 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, colorTex);
|
||||
gl.uniform1i(uColorSrc, 1);
|
||||
gl.uniform2f(uRes, W, H);
|
||||
gl.uniform2f(uMouse, mouse.x, mouse.y);
|
||||
gl.uniform1f(uMaskY, 0);
|
||||
gl.uniform1f(uMaskScale, 1);
|
||||
gl.uniform1f(uMix, state.effectMix);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
render(); // initial frame (seek-to-0 correctness)
|
||||
|
||||
// All repaint via the timeline's onUpdate — fully seek-safe, no rAF.
|
||||
tl.eventCallback("onUpdate", render);
|
||||
|
||||
// effect punches in, the light cursor sweeps L→R across the wordmark,
|
||||
// then fades for a clean loop.
|
||||
tl.set(state, { progress: 0, effectMix: 0 }, 0);
|
||||
tl.to(state, { effectMix: 1, duration: 0.45, ease: "power3.out" }, 0.15);
|
||||
tl.to(state, { progress: 1, duration: 3.4 / speed, ease: "none" }, 0.2);
|
||||
tl.to(state, { effectMix: 0, duration: 0.4, ease: "power2.in" }, 3.6);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,136 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>28 · text_wave_distort</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-cyan: #2afff0;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.wave-hero {
|
||||
font-family: "Playfair Display", serif;
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-size: 280px;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 0 40px rgba(168, 85, 247, 0.5);
|
||||
}
|
||||
.wave-hero .char {
|
||||
display: inline-block;
|
||||
will-change: transform;
|
||||
}
|
||||
/* per-char solid colors marching across a violet→cyan ramp */
|
||||
.wave-hero .char:nth-child(1) {
|
||||
color: #7c3aed;
|
||||
}
|
||||
.wave-hero .char:nth-child(2) {
|
||||
color: #a855f7;
|
||||
}
|
||||
.wave-hero .char:nth-child(3) {
|
||||
color: #d946ef;
|
||||
}
|
||||
.wave-hero .char:nth-child(4) {
|
||||
color: #ec4899;
|
||||
}
|
||||
.wave-hero .char:nth-child(5) {
|
||||
color: #2afff0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="wave-hero" id="hero">
|
||||
<span class="char">f</span><span class="char">l</span><span class="char">u</span
|
||||
><span class="char">i</span><span class="char">d</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Explicit fromTo entry — safer than .from() in paused/seek mode
|
||||
tl.fromTo(
|
||||
"#hero .char",
|
||||
{ y: 60, opacity: 0 },
|
||||
{ y: 0, opacity: 1, stagger: 0.06, ease: "back.out(1.8)", duration: 0.5 },
|
||||
0.1,
|
||||
);
|
||||
|
||||
// Continuous wave: phase 0→4 cycles over 1.5s, amplitude envelope rise→settle
|
||||
const state = { phase: 0 };
|
||||
tl.to(
|
||||
state,
|
||||
{
|
||||
phase: 4,
|
||||
duration: 1.5,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const chars = document.querySelectorAll("#hero .char");
|
||||
const p = state.phase / 4;
|
||||
let amp;
|
||||
if (p < 0.35) amp = (p / 0.35) * 24;
|
||||
else if (p < 0.7)
|
||||
amp = 24 - ((p - 0.35) / 0.35) * 12; // 24 → 12
|
||||
else amp = 12 - ((p - 0.7) / 0.3) * 4; // 12 → 8
|
||||
chars.forEach((c, i) => {
|
||||
const y = amp * Math.sin(state.phase * Math.PI * 2 + i * 0.45);
|
||||
// Use gsap.set so it cooperates with GSAP's transform tracking
|
||||
gsap.set(c, { y });
|
||||
});
|
||||
},
|
||||
},
|
||||
0.65,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,144 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>tile_mosaic</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mosaic {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 6px;
|
||||
width: 1440px;
|
||||
height: 960px;
|
||||
}
|
||||
.tile {
|
||||
border-radius: 4px;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.title {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
font-size: 220px;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--hg-fg);
|
||||
text-shadow: 0 8px 40px rgba(0, 0, 0, 0.55);
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2.4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2.4" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="mosaic" id="mosaic"></div>
|
||||
<div class="title" id="title">MOSAIC</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const COLS = 6;
|
||||
const ROWS = 4;
|
||||
const STEP = 0.06;
|
||||
// Deterministic palette; tile colour chosen by index, no randomness.
|
||||
const PALETTE = [
|
||||
"#7c3aed",
|
||||
"#a855f7",
|
||||
"#6d28d9",
|
||||
"#c084fc",
|
||||
"#5b21b6",
|
||||
"#8b5cf6",
|
||||
"#9333ea",
|
||||
"#4c1d95",
|
||||
];
|
||||
|
||||
const mosaic = document.getElementById("mosaic");
|
||||
const tiles = [];
|
||||
for (let i = 0; i < COLS * ROWS; i++) {
|
||||
const row = Math.floor(i / COLS);
|
||||
const col = i % COLS;
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "tile";
|
||||
const a = PALETTE[(row + col) % PALETTE.length];
|
||||
const b = PALETTE[(row + col + 3) % PALETTE.length];
|
||||
cell.style.background = "linear-gradient(135deg, " + a + " 0%, " + b + " 100%)";
|
||||
mosaic.appendChild(cell);
|
||||
tiles.push({ el: cell, diag: row + col });
|
||||
}
|
||||
|
||||
// Diagonal sweep: reveal order governed by (row + col), explicit per-tile timing.
|
||||
for (const t of tiles) {
|
||||
tl.fromTo(
|
||||
t.el,
|
||||
{ autoAlpha: 0, scale: 0.6 },
|
||||
{ autoAlpha: 1, scale: 1, duration: 0.4, ease: "back.out(1.7)" },
|
||||
t.diag * STEP,
|
||||
);
|
||||
}
|
||||
|
||||
// Title settles over the finished, dense mosaic.
|
||||
tl.fromTo(
|
||||
"#title",
|
||||
{ autoAlpha: 0, scale: 1.12 },
|
||||
{ autoAlpha: 1, scale: 1, duration: 0.45, ease: "power2.out" },
|
||||
1.7,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>typewriter_reveal</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-family: "JetBrains Mono", ui-monospace, monospace;
|
||||
font-weight: 800;
|
||||
font-size: 200px;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 0.95;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.char {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.char.space {
|
||||
width: 0.4em;
|
||||
}
|
||||
.caret {
|
||||
display: inline-block;
|
||||
width: 0.09em;
|
||||
height: 0.86em;
|
||||
margin-left: 0.06em;
|
||||
background: var(--hg-violet-2);
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2.4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2.4" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="line" id="line">
|
||||
<span class="char">S</span><span class="char">Y</span><span class="char">S</span
|
||||
><span class="char">T</span><span class="char">E</span><span class="char">M</span
|
||||
><span class="char space"> </span><span class="char">O</span
|
||||
><span class="char">N</span><span class="char">L</span><span class="char">I</span
|
||||
><span class="char">N</span><span class="char">E</span
|
||||
><span class="caret" id="caret"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const chars = gsap.utils.toArray(".char");
|
||||
const step = 0.07;
|
||||
|
||||
// Explicit per-character reveal: one set() per char at time i * step.
|
||||
chars.forEach((char, i) => {
|
||||
tl.set(char, { opacity: 1 }, i * step);
|
||||
});
|
||||
|
||||
const lastT = (chars.length - 1) * step;
|
||||
|
||||
// Caret rides just past the last revealed char and blinks a finite number
|
||||
// of times (no repeat:-1), then the phrase locks crisp.
|
||||
tl.set("#caret", { opacity: 1 }, 0);
|
||||
tl.to(
|
||||
"#caret",
|
||||
{ opacity: 0, duration: 0.09, ease: "steps(1)", repeat: 8, yoyo: true },
|
||||
lastT + step,
|
||||
);
|
||||
tl.set("#caret", { opacity: 0 }, 2.32);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,126 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>word_grid_burst</title>
|
||||
<script src="../assets/gsap.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--hg-violet: #7c3aed;
|
||||
--hg-violet-2: #a855f7;
|
||||
--hg-bg: #0a0a0f;
|
||||
--hg-fg: #f5f5fa;
|
||||
--hg-dim: #6b6b78;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--hg-bg);
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--hg-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
padding: 140px 120px;
|
||||
gap: 24px;
|
||||
}
|
||||
.cell {
|
||||
font-weight: 900;
|
||||
font-size: 92px;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 0.95;
|
||||
color: var(--hg-fg);
|
||||
white-space: nowrap;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2.8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2.8" data-track-index="1">
|
||||
<div class="stage">
|
||||
<div class="cell">PULSE</div>
|
||||
<div class="cell">FORGE</div>
|
||||
<div class="cell">SURGE</div>
|
||||
<div class="cell">VOLT</div>
|
||||
<div class="cell">CORE</div>
|
||||
<div class="cell">FLUX</div>
|
||||
<div class="cell">SPARK</div>
|
||||
<div class="cell">DRIVE</div>
|
||||
<div class="cell">BLAZE</div>
|
||||
<div class="cell">WAVE</div>
|
||||
<div class="cell">SHIFT</div>
|
||||
<div class="cell">BOLT</div>
|
||||
<div class="cell">RUSH</div>
|
||||
<div class="cell">PEAK</div>
|
||||
<div class="cell">IGNITE</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Deterministic reveal order: scatter sequence across the 15-cell grid.
|
||||
const order = [7, 0, 12, 4, 9, 2, 14, 6, 11, 1, 8, 3, 13, 5, 10];
|
||||
const cells = gsap.utils.toArray(".cell");
|
||||
const step = 0.13;
|
||||
const FOCUS = 7; // chosen focal cell ("DRIVE")
|
||||
|
||||
// Each cell pops in on its own "onset" — explicit per-cell timing.
|
||||
order.forEach((idx, i) => {
|
||||
tl.fromTo(
|
||||
cells[idx],
|
||||
{ autoAlpha: 0, scale: 0.8 },
|
||||
{ autoAlpha: 1, scale: 1, duration: 0.22, ease: "back.out(1.8)" },
|
||||
i * step,
|
||||
);
|
||||
});
|
||||
|
||||
// Downbeat refocus (~2.3s): dim the whole grid, pop the focal word.
|
||||
tl.to(".cell", { autoAlpha: 0.25, duration: 0.18, ease: "power2.out" }, 2.3);
|
||||
tl.to(
|
||||
cells[FOCUS],
|
||||
{
|
||||
autoAlpha: 1,
|
||||
scale: 1.25,
|
||||
color: "var(--hg-violet-2)",
|
||||
duration: 0.22,
|
||||
ease: "back.out(2)",
|
||||
},
|
||||
2.32,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
# Planning (Step 3) — pick the brand, fill every frame
|
||||
|
||||
At Step 3 **you (the orchestrator)** turn the Step-2 skeleton into a complete, approved
|
||||
`STORYBOARD.md`. You edit the **same file** in place: pick the brand spine, then for each
|
||||
frame decide its **groups**, give each group a treatment, bind real beat anchors, and write
|
||||
the copy.
|
||||
|
||||
Your mantra: **music is the spine; a template is a head start, not a cage; typography is the
|
||||
floor and assets are an optional ingredient on the same beat grid.**
|
||||
|
||||
**You own WHAT, not HOW.** You name the template / primitives, the content, the brand, the
|
||||
anchor seconds, and the intent. The frame-worker (Step 4) decides HOW — micro-timing,
|
||||
realization, intra-frame cuts. **Never write millisecond tweens into the storyboard.**
|
||||
|
||||
## Inputs
|
||||
|
||||
- The Step-2 skeleton already in `STORYBOARD.md` — frames with `span_sec` + `pacing` + `mood` + `feel`.
|
||||
- `audiomap.json` — timing truth; read the real anchor seconds inside each frame's span.
|
||||
- [`template-catalog.md`](template-catalog.md) — the template selection menu.
|
||||
- [`motion-primitive-catalog.md`](motion-primitive-catalog.md) — the free-compose menu (L0 recipes).
|
||||
- [`montage.md`](montage.md) — asset treatments (only if the user supplied images/videos).
|
||||
- User brief / supplied copy — topic, mood, exact words to keep.
|
||||
|
||||
## Step A — pick the brand spine (one preset, unmodified)
|
||||
|
||||
The whole video shares one type family + palette. Pick **one ready-made preset** from
|
||||
`../hyperframes-creative/frame-presets/` using the preset table in
|
||||
`../hyperframes-creative/references/design-spec.md` — choose by the track's mood + the brief,
|
||||
and **only its fonts + colors matter** (templates own composition + motion; the preset only
|
||||
sets the look). Copy it in **unmodified**:
|
||||
|
||||
```bash
|
||||
cp ../hyperframes-creative/frame-presets/<preset>/FRAME.md "$PROJECT_DIR/frame.md"
|
||||
```
|
||||
|
||||
Then fill the storyboard frontmatter `style` from it: the `font` from its `typography:` and a
|
||||
≤4–6 swatch `palette` from its `colors:`. **Quote the hex / family verbatim — never invent or
|
||||
round.** Every group's palette params draw from this one palette; that unity is what makes
|
||||
different templates read as one piece.
|
||||
|
||||
## Step B — per frame, decide its groups
|
||||
|
||||
A frame is usually **one group** (one template or one free composition spanning the frame).
|
||||
Subdivide into 2+ groups **only when a single treatment can't cover the frame** — e.g. a busy
|
||||
opener plus a closing lockup. When you split, cut at a **real audiomap anchor** inside the
|
||||
frame's span (a `key_moment` / `phrase` edge / onset-cluster gap), **never inside a `rolls[]`
|
||||
run**, and keep every group **≥ ~1 bar**. Density does **not** force more groups — a dense
|
||||
frame is usually ONE group whose template absorbs the density internally (a meta-template like
|
||||
`poster-tile-mosaic`). **Group count tracks distinct treatments, not beats.**
|
||||
|
||||
## Step C — per group, pick a treatment (exactly one of three)
|
||||
|
||||
### A. Match a template
|
||||
|
||||
Read [`template-catalog.md`](template-catalog.md). Match the group's `feel` + `mood` + `pacing`
|
||||
to a template's **Reach for it when**; take the closest fit. Then bind it:
|
||||
|
||||
- Fill `params` (keys from the catalog entry) — your copy into text slots, palette from the brand spine, `duration` = the group's span length.
|
||||
- Fill `role_bindings` with this group's **real anchor seconds** read from `audiomap.json` over its span (not example times).
|
||||
- If the template's natural stop and the group's span end disagree, snap to the nearest anchor.
|
||||
|
||||
### B. Free-compose (no template fits)
|
||||
|
||||
Write a `free_design` — one visual thesis from [`motion-primitive-catalog.md`](motion-primitive-catalog.md)
|
||||
(a dominant system + the named L0 primitives + a density topology) + `anchors` (the real
|
||||
beat / onset seconds the moves ride). Free-compose is a **first-class** choice, written as
|
||||
carefully as a matched group — never a failure.
|
||||
|
||||
### C. Asset treatment (only when the user supplied assets and they fit)
|
||||
|
||||
Make it an `asset` group ([`montage.md`](montage.md)). **Obey `pacing`:** on a `beat_cut`
|
||||
frame use `beat_cut` (one clip per anchor) or `bg_under_text`; on a `phrase_flow` frame use
|
||||
`ken_burns` or a slow crossfade — **never** per-onset hard cuts. Assets are additive: if none
|
||||
fits a group, fall back to template / free (typography is the floor — a complete video needs
|
||||
no assets).
|
||||
|
||||
## Copy (you own the words)
|
||||
|
||||
- Keep exact user words; else invent with taste, on the brief's mood.
|
||||
- **Message vs texture:** a readable word holds ≥1 beat (headline 3–8, sentence 4–10), stable + focal; a word held <1 beat is texture (strobe / grid / ticks). Never force a message onto a sub-beat — demote it to texture.
|
||||
- Place copy into the template's text params, onto a free group's anchors, or as an asset group's `overlay_copy`. Declare the anchor + accumulate / stagger intent; leave micro-timing to the worker.
|
||||
- A closing logo / CTA lands on the final hit / hard stop and holds through trailing silence.
|
||||
|
||||
## Transitions (you do not emit them)
|
||||
|
||||
Everything is a **0ms hard cut** for now. **frame → frame** is owned by the assembler
|
||||
(back-to-back files); adjacent `span_sec` already imply the cut. **group → group inside a
|
||||
frame** is owned by the worker on its frame timeline; you only set each group's `span_sec`.
|
||||
|
||||
## Write + validate
|
||||
|
||||
Complete `STORYBOARD.md` ([`storyboard-format.md`](storyboard-format.md)), then run
|
||||
`node scripts/validate-plan.mjs` and fix every `✗`. Show the user a frame-by-frame summary and
|
||||
iterate until approved.
|
||||
|
||||
## Self-check
|
||||
|
||||
- `frame.md` is a verbatim copy of one preset; frontmatter `style.font` / `style.palette` are drawn from it (exact values).
|
||||
- Every frame became ≥1 group; groups tile the frame span in order; no group < ~1 bar; no group boundary inside a `rolls[]` run.
|
||||
- Each group is exactly one of template / free_design / asset.
|
||||
- Template `params` keys match the catalog entry; `role_bindings` / `anchors` use real audiomap seconds.
|
||||
- Asset treatments obey `pacing` (no `beat_cut` on a `phrase_flow` frame).
|
||||
- Every group's palette draws from the one brand palette.
|
||||
- `duration_s == audiomap.audio.duration_sec`; `validate-plan.mjs` passes.
|
||||
@@ -0,0 +1,94 @@
|
||||
# STORYBOARD.md format — frames → groups
|
||||
|
||||
`STORYBOARD.md` is the single reviewable plan the user approves at Step 3 and the **manual**
|
||||
each frame-worker follows at Step 4. It is **hierarchical**: one block per **frame**, written
|
||||
as a `## Frame N — <frame_id>` heading (the parser recognizes `Frame`). **One frame = one
|
||||
scene = one composition file.** Inside each frame block are its **groups** (the treatment
|
||||
units). The assembler reads the frame level (`duration` → `data-start`, `src`); the worker
|
||||
reads its own frame block. A frame's **composition id = its `src` file stem**
|
||||
(`compositions/frames/01-f1.html` → `01-f1`), which the worker uses as `data-composition-id`
|
||||
and the `window.__timelines` key.
|
||||
|
||||
Step 2 writes the skeleton (frame fields, groups `TBD`); Step 3 fills the groups + brand. It
|
||||
is a build spec, not code — the planner writes WHAT, the worker decides HOW. **Never write
|
||||
millisecond tweens here.**
|
||||
|
||||
## File shape
|
||||
|
||||
YAML frontmatter (the video-wide spine) + one `## Frame N — <frame_id>` block per frame.
|
||||
|
||||
```markdown
|
||||
---
|
||||
compositionId: bgm
|
||||
duration_s: 30.0 # == audiomap.audio.duration_sec, exactly
|
||||
canvas: { w: 1920, h: 1080, fps: 30 }
|
||||
style: # brand spine — from the chosen frame.md preset (Step 3)
|
||||
font: "EB Garamond / Inter / JetBrains Mono" # the preset's typography, verbatim
|
||||
palette: ["#FAF9F5", "#141413", "#CC785C", "#181715"] # ≤4–6 swatches from the preset's colors
|
||||
assets: false # false, or a note like "assets/ has 6 user photos"
|
||||
build_notes: ["one paused timeline per frame", "no remote assets"]
|
||||
avoid: ["generic slideshow", "tiny unreadable hero text"]
|
||||
---
|
||||
|
||||
## Frame 1 — f1
|
||||
|
||||
- src: compositions/frames/01-f1.html # worker writes here; assembler refs it; stem (01-f1) = composition id
|
||||
- duration: 7.198s # = span length; the assembler reads this for cumulative data-start
|
||||
- span_sec: [0.0, 7.198] # track seconds; frames tile the track
|
||||
- pacing: beat_cut # beat_cut | phrase_flow (from the skeleton; obey it)
|
||||
- mood: [hype]
|
||||
- feel: accelerating onset stream into a held downbeat
|
||||
|
||||
### Groups
|
||||
|
||||
- **g1** — template: `intro-kinetic-cascade`
|
||||
- span_sec: [0.0, 4.017] # frame-LOCAL build is 0-based; these are TRACK seconds (worker subtracts frame start)
|
||||
- params: { theme: "light", icon: "bolt", phrases: "[…]", climax: "{…}" }
|
||||
- role_bindings: { phrase: { times: [0.14, 0.55, 0.87] }, climax: { in: 3.79, iconAt: 4.9 } }
|
||||
- copy: "GROWTH THROUGH CREATIVITY"
|
||||
- **g2** — free_design
|
||||
- span_sec: [4.017, 7.198]
|
||||
- free_design: { dominant_system: "per-onset typography", primitives: ["content-swap", "braam-punch"], density_topology: "accumulate" }
|
||||
- anchors: [4.10, 4.80, 5.50, 6.20] # onset seconds the reveals ride (from audiomap)
|
||||
- copy: ["BUILD", "SHIP", "REPEAT"]
|
||||
|
||||
## Frame 2 — f2
|
||||
|
||||
…
|
||||
```
|
||||
|
||||
## Frame block — required fields
|
||||
|
||||
| field | meaning |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| heading `## Frame N — <frame_id>` | `frame_id` matches the `src` stem; `N` = the 1-based index. |
|
||||
| `src` | `compositions/frames/NN-<frame_id>.html` — where the worker writes; the assembler references it. Stem = `data-composition-id`. |
|
||||
| `duration` | the frame span length in seconds (e.g. `7.198s`) — **required**; the assembler sums these for cumulative `data-start`. |
|
||||
| `span_sec` | `[start, end]` track seconds. `duration = end − start`. |
|
||||
| `pacing` | `beat_cut` \| `phrase_flow` — from the skeleton; the worker must obey (no hard-cut on `phrase_flow`). |
|
||||
| `mood`, `feel` | from the skeleton; tone + the one-line music situation the planner matched against. |
|
||||
| `### Groups` list | ≥1 group; groups tile the frame span in order. |
|
||||
|
||||
## Group entry — exactly one of three kinds
|
||||
|
||||
Every group is **template** OR **free_design** OR **asset** — never two, never none. All three
|
||||
carry `span_sec` (track seconds, tiling the frame) and may carry `copy`.
|
||||
|
||||
- **template** — `template: <catalog id>` + `params` (keys from the catalog entry) + `role_bindings` (real audiomap anchor seconds) + `copy`.
|
||||
- **free_design** — `free_design: { dominant_system, primitives: [catalog ids], density_topology }` + `anchors` (real beat / onset seconds) + `copy`.
|
||||
- **asset** — `asset: { treatment, clips: [public/…], anchors?, overlay_copy? }`. `treatment` ∈ `beat_cut` (one clip per anchor — only on a `beat_cut` frame) | `ken_burns` (slow push — fits `phrase_flow`) | `bg_under_text` (clip dimmed behind a template / free group). See [`montage.md`](montage.md).
|
||||
|
||||
## Rules
|
||||
|
||||
- Frames tile the track (gap-free, first at 0, last at `duration_s`); a frame's groups tile its span; no group < ~1 bar; no group boundary inside a `rolls[]` run.
|
||||
- `params` keys come from the template's [`template-catalog.md`](template-catalog.md) entry.
|
||||
- All anchor seconds are **track seconds from `audiomap.json`** — the worker converts to frame-local by subtracting the frame start.
|
||||
- A `phrase_flow` frame MUST NOT use `beat_cut` asset treatment or per-onset hard cuts.
|
||||
- The brand `style` is set once; every group's palette draws from it.
|
||||
- Reviewable prose-plus-data; keep it scannable. No GSAP, no millisecond timing.
|
||||
|
||||
## Self-check (the planner runs `validate-plan.mjs`)
|
||||
|
||||
- frontmatter has `compositionId`, `duration_s` (== audiomap), `canvas`, `style`.
|
||||
- every frame has `span_sec` + `src` + positive `duration` + `pacing` + ≥1 group; frames tile the track.
|
||||
- every group is exactly one of template / free_design / asset; template ids exist in the catalog; anchors are real audiomap seconds; `phrase_flow` frames have no `beat_cut`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Template catalog — the selection menu (Step 3)
|
||||
|
||||
Reverse-engineered, asset-free **group templates**. This file is the **only** thing the
|
||||
planner reads to pick one — you do **not** open the template's `index.html` to choose. One
|
||||
template realizes **one group** of a frame (a frame may stack several groups; the brand spine
|
||||
unifies them).
|
||||
|
||||
**How to pick.** For each group, read its frame's `pacing` + `mood` + the one-line music
|
||||
situation from the skeleton, then scan **Reach for it when** + **Best span** below and take the
|
||||
closest fit. If nothing fits, **free-compose** from
|
||||
[`motion-primitive-catalog.md`](motion-primitive-catalog.md) — that is a first-class choice, not a
|
||||
failure.
|
||||
|
||||
**Brand comes from `frame.md`, not the template.** Templates ship their own demo palette
|
||||
(`theme` / `palette` / color params). Fill those params from the project's `frame.md` (the
|
||||
chosen preset's colors + fonts) so every group reads as one piece — the template supplies the
|
||||
**motion + layout**, the preset supplies the **look**.
|
||||
|
||||
**Params** are listed so you can fill content slots in the storyboard. Exact semantics +
|
||||
defaults live in each template's `index.html` (`data-composition-variables`) — the frame-worker
|
||||
reads those at build time; you only name the values.
|
||||
|
||||
Pacing tag: every template below is **beat_cut** except `held-message-living-field`
|
||||
(**phrase_flow**). Never put a beat_cut template on a phrase_flow frame.
|
||||
|
||||
## Group duration discipline
|
||||
|
||||
Template choice is per **group**, not per frame. A frame longer than a template's **Best span**
|
||||
should usually split into multiple groups at real audiomap anchors (`SURGE`, `DROP`, roll edge,
|
||||
hard_stop, phrase edge) instead of stretching one template across the whole frame.
|
||||
|
||||
- **Best span** below is the active treatment span: the part where the template's system is doing
|
||||
meaningful motion. A short readable hold at the end is fine; a long empty tail means pick another
|
||||
group.
|
||||
- **Frames over ~6s** usually need 2+ groups. Beat-cut exceptions are rich programs with real
|
||||
sub-phases (`poster-tile-mosaic`, sometimes `card-flyby`); phrase-flow exceptions use
|
||||
`held-message-living-field`.
|
||||
- **Roll templates are one-roll tools.** If a frame has two rolls or a drop between rolls, split it
|
||||
into two groups.
|
||||
- Do not extend by slowing every tween. Preserve the template's motion feel, then fill extra time
|
||||
with a hold / palette change / new group.
|
||||
|
||||
---
|
||||
|
||||
### card-flyby
|
||||
|
||||
- **What** — a depth column of cards rolls forward through perspective; each landing beat tumbles the next card into the front slot with a solid colored wipe, the old front falls toward camera, dwells shrink card-to-card so the deck accelerates into a held final card.
|
||||
- **Reach for it when** — a stream of discrete onsets that **accelerate** (gaps shrinking / a build into a downbeat) and you want to flash a **sequence of items** — titles, projects, posters, tiles — one per hit, climaxing on a held card.
|
||||
- **Best span** — **4-6.5s** for 4-7 landings plus a short final hold; split at the next downbeat if it wants to run **>7s**.
|
||||
- **Params** — `theme`, `bgColor`, `cards`, `landings`, `yaw`
|
||||
|
||||
### held-message-living-field · phrase_flow
|
||||
|
||||
- **What** — a readable mark (logo / word / title) held dead still over a soft, color-shifting blurred field; only the field breathes.
|
||||
- **Reach for it when** — a **calm / sparse** stretch with an onset desert — energy present but few or no onsets (a held pad or riser); you have one word or mark to hold and let breathe.
|
||||
- **Best span** — **6-16s**; this is the long-group exception. Under 4s feels underdeveloped; over ~20s needs a state change or another group.
|
||||
- **Params** — `markText`, `titleText`, `tagText`, `palette`, `flowSpeed`, `duration`
|
||||
|
||||
### held-text-strobe-burst
|
||||
|
||||
- **What** — a dead-still word whose letters flip through texture-filled frames (texture-clipped fill + per-frame tint + bg color) every ~3 frames, in short bursts pinned to a roll.
|
||||
- **Reach for it when** — a **dense, hard-hitting roll / fill** and a single word you want to strobe through textures for a few bars. (Ships texture-mask PNGs under `assets/`.)
|
||||
- **Best span** — **1.2-3.5s**; strobe fatigue starts fast, so cap at ~4s and cut to a cleaner system.
|
||||
- **Params** — `markText`, `fontStyle`, `markScale`, `idleColor`, `idleInk`, `frames`, `strobePlan`, `decor`, `duration`
|
||||
|
||||
### intro-kinetic-cascade
|
||||
|
||||
- **What** — a line laid out as a sequence of big editorial **phrases** (each a stacked poster with one enlarged hero word), revealed word-by-word on its anchors, hard cut between phrases, climaxing on a phrase that slides in with a swappable ringing **icon** (bell / cursor / sparkle / emoji / SVG).
|
||||
- **Reach for it when** — an **intro / opening statement**: a short line to land word-by-word as big type, climaxing on one keyword + an icon. Medium-or-more energy, steady grid.
|
||||
- **Best span** — **3.5-7s** for 2-4 phrase beats; if the statement needs more time, make the next clause a new group.
|
||||
- **Params** — `theme`, `icon`, `phrases`, `climax`
|
||||
|
||||
### logo-split-lockup-pulse
|
||||
|
||||
- **What** — a two-part mark joined at center splits left↔right to open a gap, grows a center word-lockup one word per onset (key word lands on the downbeat surge), snap-closes on a hit, then pulses with the beat.
|
||||
- **Reach for it when** — a short **logo / brand sting** (not a typed sentence): fast dense onsets + a sustained roll bed to pulse on, with a left/right bracketing mark.
|
||||
- **Best span** — **2-4s**; at **>4.5s** it reads like a sting stretched too long. Follow with a separate held-lockup / next idea group.
|
||||
- **Params** — `bgColor`, `markColor`, `textColor`, `leftMark`, `rightMark`, `word1`, `word2`, `word3`, `word4`
|
||||
|
||||
### poster-tile-mosaic
|
||||
|
||||
- **What** — a packed **mosaic** of different-sized colored tiles that tessellate to fill the frame (no overlap), driven by interchangeable beat-synced operations: staggered enter/exit, locked global recolor, snake-fill + overlay.
|
||||
- **Reach for it when** — many discrete, individually-placeable onsets (a hit for every tile move) with distinct sub-phases you want articulated differently (accumulate → recolor-on-roll → fill-then-drop). A dense section best held as **one** rich tile program rather than split.
|
||||
- **Best span** — **4-7s**, up to **8s** only when the program has clear sub-phases. If the music changes regime, split even if the tile system could continue.
|
||||
- **Params** — `bgColor`, `tiles`, `bands`, `gap`, `showText`, `labels`, `program`
|
||||
|
||||
### roll-flipbook-word-cycle
|
||||
|
||||
- **What** — a hi-hat roll drives a centred word that flips every 16th-note through a word list; optionally the flicker resolves and locks into a final phrase.
|
||||
- **Reach for it when** — a **fast sustained-fill roll** (hundreds of hits/min, ~16th-note) with no single readable message — fill the roll with a rapidly-cycling word flipbook.
|
||||
- **Best span** — **1.2-3.8s**, one roll into one resolve. Two rolls, or a drop between rolls, means two groups.
|
||||
- **Params** — `bgColor`, `textColor`, `accentColor`, `flipWords`, `resolveText`, `periodChar`
|
||||
|
||||
### split-anchor-word-slot
|
||||
|
||||
- **What** — a held left anchor column of fixed-word rows beside a torn-paper word-slot box on the right, driven by beat-synced operators: anchor lock-in, slot word-group cycle (in/out + per-line color), full-scene background flip, per-beat jitter, box-zoom exit wipe. Row count + number of flips are data.
|
||||
- **Reach for it when** — a short section with a **held idea** (a brand / name to anchor on the left) **and** a stream of onsets popping separate words on the right, plus a dense run to ride a shake on and a strong downbeat to wipe out into.
|
||||
- **Best span** — **3-6s**; above ~6.5s the fixed anchor goes stale unless the right slot enters a new group/program.
|
||||
- **Params** — `bgColor`, `anchors`, `theme`, `showText`, `program`
|
||||
|
||||
### typewriter-phrase-keyword-shuffle
|
||||
|
||||
- **What** — words type in one-per-onset to spell a phrase, then one keyword cycles typefaces on the beat while everything else holds dead still.
|
||||
- **Reach for it when** — a steady grid with a **continuous onset stream** (no desert): a phrase to type out, then a keyword to shuffle. The inverse of `held-message-living-field` (which wants an onset desert).
|
||||
- **Best span** — **2.5-5s**; if the phrase cannot type and shuffle inside ~5s, reduce words or split the sentence across groups.
|
||||
- **Params** — `bgColor`, `textColor`, `accentColor`, `lead1`, `lead2`, `lead3`, `keyword`, `periodChar`
|
||||
@@ -0,0 +1,527 @@
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-variables='[
|
||||
{"id":"theme", "type":"string", "label":"Palette theme — aurora | ember | mono | slate", "default":"aurora"},
|
||||
{"id":"bgColor", "type":"color", "label":"Stage background override (empty = theme bg)", "default":""},
|
||||
{"id":"cards", "type":"string", "label":"Deck JSON — array of {tag,title,meta,color?}", "default":""},
|
||||
{"id":"landings", "type":"string", "label":"Landing beats JSON — abs seconds, one per card", "default":""},
|
||||
{"id":"yaw", "type":"number", "label":"Scene yaw (deg, side tilt of the whole deck)", "default":-12}
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Card Flyby · accelerating 3D deck cascade — reference impl</title>
|
||||
<script src="../../motion-primitives/assets/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #06070d;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: radial-gradient(1300px 820px at 50% 52%, #0d1018 0%, #06070d 70%, #04050a 100%);
|
||||
overflow: hidden;
|
||||
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
/* .stage carries the perspective; .scene carries the static yaw. The cards' own
|
||||
rotateX drives the tumble-forward — the scene only holds the side tilt. */
|
||||
.stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
perspective: 2400px;
|
||||
perspective-origin: 50% 55%;
|
||||
}
|
||||
.scene {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateY(-12deg);
|
||||
}
|
||||
.card {
|
||||
position: absolute;
|
||||
left: calc(50% - 700px);
|
||||
top: calc(50% - 395px);
|
||||
width: 1400px;
|
||||
height: 790px;
|
||||
border-radius: 32px;
|
||||
will-change: transform, opacity;
|
||||
--wipe: 0;
|
||||
--color: #444;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
/* Wireframe (always present, opacity tracks the slot). */
|
||||
.wire {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 28px;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.42);
|
||||
box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.012);
|
||||
overflow: hidden;
|
||||
}
|
||||
.wire .wire-label {
|
||||
position: absolute;
|
||||
left: 68px;
|
||||
top: 60px;
|
||||
font-size: 26px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.wire .wire-corner {
|
||||
position: absolute;
|
||||
right: 68px;
|
||||
top: 60px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.32);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.wire .wire-bar {
|
||||
position: absolute;
|
||||
left: 68px;
|
||||
right: 68px;
|
||||
bottom: 68px;
|
||||
height: 1.5px;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
/* Solid colored face — revealed via an angled clip-path wipe driven by --wipe.
|
||||
Polygon sweeps upper-left → lower-right at ~72°. wipe=0 collapsed off-element,
|
||||
wipe=1 covers the full card. */
|
||||
.solid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 28px;
|
||||
/* gentle top→bottom tonal gradient (theme-derived shades) for depth */
|
||||
background: linear-gradient(
|
||||
158deg,
|
||||
var(--c1, var(--color)) 0%,
|
||||
var(--c2, var(--color)) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 34px 100px rgba(0, 0, 0, 0.55),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.14);
|
||||
overflow: hidden;
|
||||
/* the title is the hero — centered in the card */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 90px 104px;
|
||||
clip-path: polygon(
|
||||
0% 0%,
|
||||
calc(var(--wipe) * 200% - 30%) 0%,
|
||||
calc(var(--wipe) * 200% - 80%) 100%,
|
||||
0% 100%
|
||||
);
|
||||
}
|
||||
.solid::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.13) 0%,
|
||||
rgba(255, 255, 255, 0.03) 40%,
|
||||
rgba(0, 0, 0, 0.14) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
.solid .title {
|
||||
font-size: 184px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 0.92;
|
||||
color: var(--ink, #ffffff);
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.solid .play {
|
||||
position: absolute;
|
||||
right: 68px;
|
||||
bottom: 70px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.solid .play::after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 22px solid #ffffff;
|
||||
border-top: 14px solid transparent;
|
||||
border-bottom: 14px solid transparent;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- duration 6.0s = a 6-card accelerating cascade (1 card lands per beat, dwells shrinking) -->
|
||||
<div
|
||||
data-hf-id="hf-cf-root"
|
||||
id="root"
|
||||
data-composition-id="card-flyby"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="6.0"
|
||||
data-root="true"
|
||||
>
|
||||
<div data-hf-id="hf-cf-stage" class="stage">
|
||||
<div data-hf-id="hf-cf-scene" class="scene" id="scene"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// CARD FLYBY · accelerating 3D deck cascade
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// A depth column of N cards rolls FORWARD. On each landing beat the next
|
||||
// card tumbles up into the front slot (s0) and WIPES in its solid colored
|
||||
// face (angled clip-path), the previous front card tumbles forward + falls
|
||||
// down toward the camera on exit, and every card behind advances one slot.
|
||||
// Dwells shrink card-to-card → the deck reads as an accelerating rush into
|
||||
// a held final card. Asset-free: wireframe + solid CSS cards, system fonts.
|
||||
//
|
||||
// COMPOSE BY DATA — `cards` (the deck) and `landings` (one beat per card)
|
||||
// are the two data surfaces. Card count is data: 3/6/9 cards all "just
|
||||
// work"; landings default to an accelerating schedule derived from count.
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
// force3D is set per card tween/set below (NOT a global default — a global
|
||||
// would leak onto the plain-object padding tween and warn). It keeps a 3D
|
||||
// matrix on every transform so cards never collapse to 2D: a card whose
|
||||
// rotX/Y/Z all hit 0 (the front s0 pose) would otherwise drop out of the 3D
|
||||
// stacking context and paint in DOM order, letting back cards render on top.
|
||||
|
||||
// ── 1. Variables ──────────────────────────────────────────────────────
|
||||
var defaults = { theme: "aurora", bgColor: "", cards: "", landings: "", yaw: -12 };
|
||||
var vars = Object.assign(
|
||||
{},
|
||||
defaults,
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {},
|
||||
);
|
||||
|
||||
// ── Palette themes — each is a COHESIVE ramp (one temperature/saturation
|
||||
// family) so any card count reads as a designed set. card.color still
|
||||
// overrides per card. White-on-card stays AA at the card type sizes. ──
|
||||
var THEMES = {
|
||||
// cool jewel arc: teal → ocean → indigo → violet → plum
|
||||
aurora: {
|
||||
bg: "#06060e",
|
||||
ink: "#ffffff",
|
||||
colors: ["#0e5f63", "#15517e", "#283f86", "#4a3a83", "#6a3380", "#8a2e63"],
|
||||
},
|
||||
// warm sunset arc: wine → brick → rust → amber → ochre → bronze
|
||||
ember: {
|
||||
bg: "#0e0805",
|
||||
ink: "#fff6ee",
|
||||
colors: ["#5c2030", "#7e2828", "#9c3b1c", "#a9551a", "#8a5a1c", "#5f4626"],
|
||||
},
|
||||
// sleek monochrome blue staircase (dark → mid)
|
||||
mono: {
|
||||
bg: "#060912",
|
||||
ink: "#ffffff",
|
||||
colors: ["#11243f", "#16314f", "#1b3d63", "#214a78", "#27568d", "#2d63a2"],
|
||||
},
|
||||
// muted cool slate — understated / editorial
|
||||
slate: {
|
||||
bg: "#0b0e12",
|
||||
ink: "#eef2f6",
|
||||
colors: ["#232a33", "#2b3540", "#33414e", "#3b4c5c", "#43576a", "#4b6278"],
|
||||
},
|
||||
};
|
||||
var TH = THEMES[String(vars.theme || "").trim()] || THEMES.aurora;
|
||||
|
||||
// tiny hex helpers (deterministic, no deps) — derive shades + soft ink.
|
||||
function parseHex(h) {
|
||||
h = String(h || "")
|
||||
.trim()
|
||||
.replace(/^#/, "");
|
||||
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
||||
if (h.length !== 6) return null;
|
||||
var n = parseInt(h, 16);
|
||||
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
||||
}
|
||||
function clamp8(v) {
|
||||
return Math.max(0, Math.min(255, Math.round(v)));
|
||||
}
|
||||
function toHex(c) {
|
||||
return (
|
||||
"#" +
|
||||
[c.r, c.g, c.b]
|
||||
.map(function (v) {
|
||||
return clamp8(v).toString(16).padStart(2, "0");
|
||||
})
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
function mix(hex, t, amt) {
|
||||
var c = parseHex(hex);
|
||||
if (!c) return hex;
|
||||
return toHex({
|
||||
r: c.r + (t.r - c.r) * amt,
|
||||
g: c.g + (t.g - c.g) * amt,
|
||||
b: c.b + (t.b - c.b) * amt,
|
||||
});
|
||||
}
|
||||
var BLACK = { r: 0, g: 0, b: 0 },
|
||||
WHITE = { r: 255, g: 255, b: 255 };
|
||||
function darken(hex, amt) {
|
||||
return mix(hex, BLACK, amt);
|
||||
}
|
||||
function lighten(hex, amt) {
|
||||
return mix(hex, WHITE, amt);
|
||||
}
|
||||
|
||||
var DEFAULT_CARDS = [
|
||||
{ title: "FADEGLOW" },
|
||||
{ title: "SAUL BASS" },
|
||||
{ title: "EULER" },
|
||||
{ title: "HERMÈS" },
|
||||
{ title: "DOSSIER" },
|
||||
{ title: "BLACK HOLES" },
|
||||
];
|
||||
// landings tuned by hand on the sandbox: accelerating dwells, final card holds ~1.6s.
|
||||
var DEFAULT_LANDINGS = [0.4, 1.7, 2.7, 3.45, 4.0, 4.4];
|
||||
|
||||
var DUR = 6.0; // matches data-duration
|
||||
var YAW = Number(vars.yaw);
|
||||
if (!isFinite(YAW)) YAW = -12;
|
||||
|
||||
function parseJSON(v) {
|
||||
if (!v) return null;
|
||||
if (typeof v !== "string") return v;
|
||||
try {
|
||||
return JSON.parse(v);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var CARDS = parseJSON(vars.cards);
|
||||
if (!CARDS || !CARDS.length) CARDS = DEFAULT_CARDS;
|
||||
var N = CARDS.length;
|
||||
|
||||
// ── 2. Landing schedule (accelerating cascade) ─────────────────────────
|
||||
// gaps between landings shrink geometrically; the final card lands early
|
||||
// enough to hold a beat before the cut.
|
||||
function genLandings(n, total) {
|
||||
var start = 0.4;
|
||||
var hold = Math.min(1.6, total * 0.27);
|
||||
var end = Math.max(start + 0.3, total - hold);
|
||||
if (n <= 1) return [start];
|
||||
var ratio = 0.78,
|
||||
weights = [],
|
||||
sum = 0,
|
||||
i;
|
||||
for (i = 0; i < n - 1; i++) {
|
||||
var w = Math.pow(ratio, i);
|
||||
weights.push(w);
|
||||
sum += w;
|
||||
}
|
||||
var span = end - start,
|
||||
acc = start,
|
||||
times = [start];
|
||||
for (i = 0; i < n - 1; i++) {
|
||||
acc += (span * weights[i]) / sum;
|
||||
times.push(acc);
|
||||
}
|
||||
return times;
|
||||
}
|
||||
var LANDED = parseJSON(vars.landings);
|
||||
if (!LANDED || LANDED.length !== N) {
|
||||
LANDED = N === DEFAULT_CARDS.length ? DEFAULT_LANDINGS.slice() : genLandings(N, DUR);
|
||||
}
|
||||
// per-card transition duration derived from the gap into it (faster as we accelerate).
|
||||
function transDur(i) {
|
||||
var gap = i === 0 ? (N > 1 ? LANDED[1] - LANDED[0] : 0.4) : LANDED[i] - LANDED[i - 1];
|
||||
return Math.max(0.12, Math.min(0.42, gap * 0.32));
|
||||
}
|
||||
|
||||
// ── 3. Build the deck DOM from data ────────────────────────────────────
|
||||
var scene = document.getElementById("scene");
|
||||
scene.style.transform = "rotateY(" + YAW + "deg)";
|
||||
// bg: explicit bgColor override wins; else the theme's field color. The radial
|
||||
// lifts the centre slightly and sinks the edges, both derived from the bg.
|
||||
var BG = vars.bgColor && String(vars.bgColor).trim() ? vars.bgColor : TH.bg;
|
||||
document.documentElement.style.background = BG;
|
||||
document.body.style.background = BG;
|
||||
var rootEl = document.getElementById("root");
|
||||
rootEl.style.background =
|
||||
"radial-gradient(1300px 820px at 50% 52%, " +
|
||||
lighten(BG, 0.1) +
|
||||
" 0%, " +
|
||||
BG +
|
||||
" 70%, " +
|
||||
darken(BG, 0.4) +
|
||||
" 100%)";
|
||||
|
||||
function el(cls, parent) {
|
||||
var d = document.createElement("div");
|
||||
d.className = cls;
|
||||
if (parent) parent.appendChild(d);
|
||||
return d;
|
||||
}
|
||||
var cardEls = [];
|
||||
for (var i = 0; i < N; i++) {
|
||||
var c = CARDS[i] || {};
|
||||
var card = el("card", scene);
|
||||
card.id = "card-" + i;
|
||||
// color: per-card override, else cycle the cohesive theme ramp. A gentle
|
||||
// top→bottom shade pair (--c1/--c2) gives each face tonal depth.
|
||||
var col = c.color || TH.colors[i % TH.colors.length];
|
||||
card.style.setProperty("--color", col);
|
||||
card.style.setProperty("--c1", lighten(col, 0.05));
|
||||
card.style.setProperty("--c2", darken(col, 0.22));
|
||||
card.style.setProperty("--ink", TH.ink);
|
||||
|
||||
var wire = el("wire", card);
|
||||
var label = el("wire-label", wire);
|
||||
label.textContent = String(i + 1).padStart(3, "0");
|
||||
el("wire-corner", wire);
|
||||
el("wire-bar", wire);
|
||||
|
||||
var solid = el("solid", card);
|
||||
var title = el("title", solid);
|
||||
title.textContent = c.title || "";
|
||||
el("play", solid);
|
||||
|
||||
cardEls.push(card);
|
||||
}
|
||||
|
||||
// ── 4. SLOT poses ───────────────────────────────────────────────────────
|
||||
// rotX is the key. Cards behind tilt BACK (top edge away, +rotX); the front
|
||||
// is upright (rotX 0); on exit a card continues forward into a face-down
|
||||
// tumble (-rotX) while dropping down + toward camera. Depth from perspective
|
||||
// (Z) + the cards' rotateX, not a camera move.
|
||||
var SLOT = {
|
||||
far: { x: 360, y: 380, z: -1080, rotX: 84, scale: 0.74, opacity: 0, zi: 60 },
|
||||
s3: { x: 280, y: 290, z: -820, rotX: 66, scale: 0.8, opacity: 0.32, zi: 70 },
|
||||
s2: { x: 195, y: 200, z: -560, rotX: 46, scale: 0.86, opacity: 0.58, zi: 80 },
|
||||
s1: { x: 105, y: 105, z: -290, rotX: 22, scale: 0.93, opacity: 0.85, zi: 90 },
|
||||
s0: { x: 0, y: 0, z: 0, rotX: 0, scale: 1.0, opacity: 1.0, zi: 100 },
|
||||
exit: { x: -90, y: 620, z: 420, rotX: -88, scale: 1.1, opacity: 0, zi: 110 },
|
||||
};
|
||||
// pose for a card whose index is `rel` slots behind the current front card
|
||||
// (rel 0 = front, +ve = behind, -ve = already exiting / gone).
|
||||
function poseForRel(rel) {
|
||||
if (rel <= -1) return SLOT.exit;
|
||||
if (rel === 0) return SLOT.s0;
|
||||
if (rel === 1) return SLOT.s1;
|
||||
if (rel === 2) return SLOT.s2;
|
||||
if (rel === 3) return SLOT.s3;
|
||||
return SLOT.far;
|
||||
}
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
function poseTween(idx, slot, t, dur, ease) {
|
||||
// z-index is set instantly at the swap start so paint order matches the
|
||||
// slot — CSS can't reliably depth-sort non-intersecting planes.
|
||||
tl.set("#card-" + idx, { zIndex: slot.zi }, t);
|
||||
tl.to(
|
||||
"#card-" + idx,
|
||||
{
|
||||
x: slot.x,
|
||||
y: slot.y,
|
||||
z: slot.z,
|
||||
rotationX: slot.rotX,
|
||||
scale: slot.scale,
|
||||
opacity: slot.opacity,
|
||||
force3D: true,
|
||||
duration: dur,
|
||||
ease: ease || "power2.inOut",
|
||||
},
|
||||
t,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 5. Initial pose (front index = -1: nothing landed yet) ─────────────
|
||||
// card j sits at rel = j + 1, so card0 starts at s1, card1 at s2, etc. Apply
|
||||
// via gsap.set (so the paused render at t=0 shows the staged stack) AND via
|
||||
// tl.set at 0 (so scrubbing back to 0 restores it).
|
||||
for (var j = 0; j < N; j++) {
|
||||
var s = poseForRel(j + 1);
|
||||
var props = {
|
||||
x: s.x,
|
||||
y: s.y,
|
||||
z: s.z,
|
||||
rotationX: s.rotX,
|
||||
scale: s.scale,
|
||||
opacity: s.opacity,
|
||||
zIndex: s.zi,
|
||||
force3D: true,
|
||||
"--wipe": 0,
|
||||
};
|
||||
gsap.set("#card-" + j, props);
|
||||
tl.set("#card-" + j, props, 0);
|
||||
}
|
||||
|
||||
// ── 6. Per-landing orchestration ───────────────────────────────────────
|
||||
// On landing i, card i becomes front. Tween the live window [i-1 .. i+4]:
|
||||
// i-1 tumbles to exit, i rolls into s0 (+ wipe), the rest advance toward
|
||||
// the front. Cards outside the window are already at rest (far / gone).
|
||||
for (var L = 0; L < N; L++) {
|
||||
var dur = transDur(L);
|
||||
var t = Math.max(0, LANDED[L] - dur);
|
||||
|
||||
for (var k = L - 1; k <= L + 4; k++) {
|
||||
if (k < 0 || k >= N) continue;
|
||||
var rel = k - L;
|
||||
var ease = rel === 0 ? "power3.out" : rel < 0 ? "power2.in" : "power2.inOut";
|
||||
poseTween(k, poseForRel(rel), t, dur, ease);
|
||||
}
|
||||
|
||||
// the landing card wipes in its solid face as it rolls up.
|
||||
tl.to(
|
||||
"#card-" + L,
|
||||
{
|
||||
"--wipe": 1,
|
||||
duration: dur * 0.85,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
t + dur * 0.15,
|
||||
);
|
||||
}
|
||||
|
||||
// pad the timeline so duration() >= data-duration.
|
||||
tl.to({}, { duration: DUR }, 0);
|
||||
tl.seek(0);
|
||||
|
||||
window.__timelines["card-flyby"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"_": "Default card-flyby program. `theme` picks a cohesive palette; `cards` is the deck (each card = a big centered title; `color` is OPTIONAL — omit it and the card takes the next color from the theme ramp); `landings` is one onset per card (accelerating cadence; omit to auto-derive). Pass these as the matching composition-variables; empty vars fall back to these values.",
|
||||
"theme": "aurora",
|
||||
"cards": [
|
||||
{ "title": "FADEGLOW" },
|
||||
{ "title": "SAUL BASS" },
|
||||
{ "title": "EULER" },
|
||||
{ "title": "HERMÈS" },
|
||||
{ "title": "DOSSIER" },
|
||||
{ "title": "BLACK HOLES" }
|
||||
],
|
||||
"landings": [0.4, 1.7, 2.7, 3.45, 4.0, 4.4]
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-variables='[
|
||||
{"id":"bgColor", "type":"color", "label":"Page background", "default":"#050507"},
|
||||
{"id":"colorLow", "type":"color", "label":"Flow — shadow tone", "default":"#0a1430"},
|
||||
{"id":"colorMid", "type":"color", "label":"Flow — mid tone", "default":"#6a6f9e"},
|
||||
{"id":"colorHigh", "type":"color", "label":"Flow — highlight tone", "default":"#e8f0ff"},
|
||||
{"id":"accentColor","type":"color", "label":"Core / glow accent", "default":"#aee4ff"},
|
||||
{"id":"markText", "type":"string", "label":"Monogram", "default":"HF"},
|
||||
{"id":"titleText", "type":"string", "label":"Title", "default":"HYPERFRAMES"},
|
||||
{"id":"tagText", "type":"string", "label":"Tagline", "default":"HTML · SHADERS · MP4"},
|
||||
{"id":"flowSpeed", "type":"number", "label":"Flow speed", "default":1, "min":0.1, "max":3, "step":0.1}
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>WebGL Textures Playground — Template</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
|
||||
<script src="../../motion-primitives/assets/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* --bg / --mark-top / --mark-bottom / --line are set from variables in JS */
|
||||
html,
|
||||
body {
|
||||
background: var(--bg, #050507);
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg, #050507);
|
||||
}
|
||||
.sf-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.sf-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: block;
|
||||
}
|
||||
.sf-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 28px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sf-mark {
|
||||
font-size: 280px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -18px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
opacity: 0;
|
||||
filter: drop-shadow(0 0 60px rgba(180, 220, 255, 0.4));
|
||||
}
|
||||
.sf-mark-letter {
|
||||
display: inline-block;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--mark-top, #ffffff) 0%,
|
||||
var(--mark-bottom, #8aa9ff) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.sf-rule {
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--line, rgba(255, 255, 255, 0.9)) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0;
|
||||
}
|
||||
.sf-title {
|
||||
font-size: 64px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 18px;
|
||||
opacity: 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.sf-tag {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 8px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
opacity: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.sf-grain {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.06;
|
||||
mix-blend-mode: overlay;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
data-hf-id="hf-a0d4"
|
||||
id="root"
|
||||
data-composition-id="webgl-textures-playground"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-root="true"
|
||||
>
|
||||
<div data-hf-id="hf-clsq" class="sf-stage">
|
||||
<canvas data-hf-id="hf-yk51" class="sf-canvas" width="1920" height="1080"></canvas>
|
||||
<div data-hf-id="hf-7z0g" class="sf-overlay">
|
||||
<div data-hf-id="hf-yo0z" class="sf-mark"><!-- letters injected from markText --></div>
|
||||
<div data-hf-id="hf-nig2" class="sf-rule"></div>
|
||||
<div data-hf-id="hf-gjx6" class="sf-title"></div>
|
||||
<div data-hf-id="hf-e7y8" class="sf-tag"></div>
|
||||
</div>
|
||||
<div data-hf-id="hf-ku55" class="sf-grain"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// ── 1. Resolve variables (read ONCE during init) ──────────────────
|
||||
var defaults = {
|
||||
bgColor: "#050507",
|
||||
colorLow: "#0a1430",
|
||||
colorMid: "#6a6f9e",
|
||||
colorHigh: "#e8f0ff",
|
||||
accentColor: "#aee4ff",
|
||||
markText: "HF",
|
||||
titleText: "HYPERFRAMES",
|
||||
tagText: "HTML · SHADERS · MP4",
|
||||
flowSpeed: 1,
|
||||
};
|
||||
var vars = Object.assign(
|
||||
{},
|
||||
defaults,
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {},
|
||||
);
|
||||
|
||||
// sRGB hex -> linear-light vec3 (the shader works in linear space and
|
||||
// gamma-encodes at the end, so converting here makes the picked color
|
||||
// land close to what a color picker shows).
|
||||
function hexToLinear(hex) {
|
||||
var h = String(hex).replace("#", "").trim();
|
||||
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
||||
var r = parseInt(h.substr(0, 2), 16) / 255;
|
||||
var g = parseInt(h.substr(2, 2), 16) / 255;
|
||||
var b = parseInt(h.substr(4, 2), 16) / 255;
|
||||
var lin = function (c) {
|
||||
return Math.pow(c, 2.2);
|
||||
};
|
||||
return new THREE.Vector3(lin(r), lin(g), lin(b));
|
||||
}
|
||||
|
||||
var root = document.getElementById("root");
|
||||
var canvas = root.querySelector(".sf-canvas");
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
|
||||
// ── 2. Apply text + CSS-driven colors ─────────────────────────────
|
||||
var docEl = document.documentElement;
|
||||
docEl.style.setProperty("--bg", vars.bgColor);
|
||||
docEl.style.setProperty("--mark-top", vars.colorHigh);
|
||||
docEl.style.setProperty("--mark-bottom", vars.colorMid);
|
||||
docEl.style.setProperty("--line", vars.colorHigh);
|
||||
|
||||
var markEl = root.querySelector(".sf-mark");
|
||||
markEl.innerHTML = "";
|
||||
String(vars.markText)
|
||||
.split("")
|
||||
.forEach(function (ch) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "sf-mark-letter";
|
||||
span.textContent = ch;
|
||||
markEl.appendChild(span);
|
||||
});
|
||||
root.querySelector(".sf-title").textContent = vars.titleText;
|
||||
root.querySelector(".sf-tag").textContent = vars.tagText;
|
||||
|
||||
// ── 3. WebGL flow field ───────────────────────────────────────────
|
||||
var renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
antialias: false,
|
||||
alpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setSize(W, H, false);
|
||||
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
var uniforms = {
|
||||
uTime: { value: 0.0 },
|
||||
uPhase: { value: 0.0 },
|
||||
uReveal: { value: 0.0 },
|
||||
uChromAb: { value: 0.0 },
|
||||
uVignette: { value: 0.0 },
|
||||
uIntensity: { value: 0.0 },
|
||||
uResolution: { value: new THREE.Vector2(W, H) },
|
||||
uSpeed: { value: Number(vars.flowSpeed) || 1.0 },
|
||||
// ── COLOR CONTROLS (driven by the composition variables) ──
|
||||
uColLow: { value: hexToLinear(vars.colorLow) },
|
||||
uColMid: { value: hexToLinear(vars.colorMid) },
|
||||
uColHigh: { value: hexToLinear(vars.colorHigh) },
|
||||
uAccent: { value: hexToLinear(vars.accentColor) },
|
||||
};
|
||||
|
||||
var vert = [
|
||||
"varying vec2 vUv;",
|
||||
"void main(){",
|
||||
" vUv = uv;",
|
||||
" gl_Position = vec4(position.xy, 0.0, 1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
var frag = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vUv;",
|
||||
"uniform float uTime;",
|
||||
"uniform float uPhase;",
|
||||
"uniform float uReveal;",
|
||||
"uniform float uChromAb;",
|
||||
"uniform float uVignette;",
|
||||
"uniform float uIntensity;",
|
||||
"uniform float uSpeed;",
|
||||
"uniform vec2 uResolution;",
|
||||
"uniform vec3 uColLow;",
|
||||
"uniform vec3 uColMid;",
|
||||
"uniform vec3 uColHigh;",
|
||||
"uniform vec3 uAccent;",
|
||||
|
||||
"float hash21(vec2 p){",
|
||||
" p = fract(p * vec2(123.34, 456.21));",
|
||||
" p += dot(p, p + 45.32);",
|
||||
" return fract(p.x * p.y);",
|
||||
"}",
|
||||
|
||||
"float noise2(vec2 p){",
|
||||
" vec2 i = floor(p);",
|
||||
" vec2 f = fract(p);",
|
||||
" vec2 u = f * f * (3.0 - 2.0 * f);",
|
||||
" float a = hash21(i);",
|
||||
" float b = hash21(i + vec2(1.0, 0.0));",
|
||||
" float c = hash21(i + vec2(0.0, 1.0));",
|
||||
" float d = hash21(i + vec2(1.0, 1.0));",
|
||||
" return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);",
|
||||
"}",
|
||||
|
||||
"float fbm(vec2 p){",
|
||||
" float v = 0.0;",
|
||||
" float a = 0.5;",
|
||||
" for(int i=0;i<5;i++){",
|
||||
" v += a * noise2(p);",
|
||||
" p *= 2.02;",
|
||||
" a *= 0.5;",
|
||||
" }",
|
||||
" return v;",
|
||||
"}",
|
||||
|
||||
"vec2 curl(vec2 p, float t){",
|
||||
" float e = 0.6;",
|
||||
" float n1 = fbm(p + vec2(0.0, e) + t*0.18);",
|
||||
" float n2 = fbm(p + vec2(0.0, -e) + t*0.18);",
|
||||
" float n3 = fbm(p + vec2( e, 0.0) + t*0.18);",
|
||||
" float n4 = fbm(p + vec2(-e, 0.0) + t*0.18);",
|
||||
" return vec2(n1 - n2, -(n3 - n4));",
|
||||
"}",
|
||||
|
||||
// 3-stop gradient palette built from the color variables.
|
||||
// t≈0 -> shadow, t≈0.5 -> mid, t≈1 -> highlight.
|
||||
"vec3 palette(float t){",
|
||||
" t = clamp(t, 0.0, 1.0);",
|
||||
" vec3 lowMid = mix(uColLow, uColMid, smoothstep(0.0, 0.5, t));",
|
||||
" return mix(lowMid, uColHigh, smoothstep(0.5, 1.0, t));",
|
||||
"}",
|
||||
|
||||
"vec3 sampleField(vec2 uv, float t, float energy){",
|
||||
" vec2 p = (uv - 0.5) * vec2(uResolution.x/uResolution.y, 1.0) * 2.4;",
|
||||
" vec2 c = curl(p * 0.9, t);",
|
||||
" vec2 q = p + c * (0.45 + 0.55 * energy);",
|
||||
|
||||
" float flow = fbm(q * 1.6 + vec2(t * 0.11, -t * 0.07));",
|
||||
" float bands = sin(flow * 14.0 + t * 1.1);",
|
||||
" bands = smoothstep(0.0, 1.0, 0.5 + 0.5 * bands);",
|
||||
|
||||
" float r = length(p);",
|
||||
" float core = smoothstep(1.5, 0.05, r) * (0.6 + 0.4 * energy);",
|
||||
" float glow = exp(-r * 1.6) * (0.55 + 0.45 * energy);",
|
||||
|
||||
" float k = clamp(flow * 0.85 + 0.15 * bands + 0.25 * core + 0.6 * glow, 0.0, 1.4);",
|
||||
" vec3 col = palette(k * 0.9 + 0.1 * sin(t * 0.4));",
|
||||
" col *= 0.7 + 0.6 * energy;",
|
||||
" col += uAccent * core * 0.55;",
|
||||
" return col;",
|
||||
"}",
|
||||
|
||||
"void main(){",
|
||||
" vec2 uv = vUv;",
|
||||
" float t = uTime * uSpeed;",
|
||||
" float e = uIntensity;",
|
||||
|
||||
" vec2 dir = normalize(uv - 0.5 + 1e-6);",
|
||||
" float ca = uChromAb * (0.004 + 0.012 * length(uv - 0.5));",
|
||||
" vec3 cR = sampleField(uv + dir * ca, t, e);",
|
||||
" vec3 cG = sampleField(uv, t, e);",
|
||||
" vec3 cB = sampleField(uv - dir * ca, t, e);",
|
||||
" vec3 col = vec3(cR.r, cG.g, cB.b);",
|
||||
|
||||
" float lum = dot(col, vec3(0.299, 0.587, 0.114));",
|
||||
" col = mix(vec3(lum), col, 1.05);",
|
||||
|
||||
" float rev = uReveal;",
|
||||
" float sweep = smoothstep(0.0, 0.6, rev) * (1.0 - smoothstep(0.6, 1.0, rev));",
|
||||
" col += uColHigh * sweep * 0.35;",
|
||||
|
||||
" vec3 phaseTint = mix(vec3(0.85, 0.92, 1.05), vec3(1.05, 0.95, 0.85), uPhase);",
|
||||
" col *= phaseTint;",
|
||||
|
||||
" float v = length(uv - 0.5);",
|
||||
" col *= 1.0 - smoothstep(0.45, 0.95, v) * uVignette;",
|
||||
|
||||
" col = col / (col + 1.0);",
|
||||
" col = pow(col, vec3(1.0/2.2));",
|
||||
|
||||
" gl_FragColor = vec4(col, 1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
var mat = new THREE.ShaderMaterial({
|
||||
uniforms: uniforms,
|
||||
vertexShader: vert,
|
||||
fragmentShader: frag,
|
||||
});
|
||||
var geo = new THREE.PlaneGeometry(2, 2);
|
||||
var mesh = new THREE.Mesh(geo, mat);
|
||||
scene.add(mesh);
|
||||
|
||||
function renderFrame() {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
renderFrame();
|
||||
|
||||
// Continuous loop: ensures the WebGL backbuffer always has fresh
|
||||
// pixels when puppeteer takes a screenshot. All visible state comes
|
||||
// from uniforms driven by the GSAP timeline, so output is fully
|
||||
// determined by the timeline position (deterministic across renders).
|
||||
function loop() {
|
||||
renderFrame();
|
||||
window.requestAnimationFrame(loop);
|
||||
}
|
||||
window.requestAnimationFrame(loop);
|
||||
|
||||
// ── 4. Deterministic timeline (unchanged motion design) ───────────
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: renderFrame,
|
||||
});
|
||||
|
||||
tl.to(uniforms.uTime, { value: 12.0, duration: 12, ease: "none" }, 0);
|
||||
|
||||
tl.to(uniforms.uIntensity, { value: 0.55, duration: 3.0, ease: "sine.inOut" }, 0);
|
||||
tl.to(uniforms.uIntensity, { value: 1.0, duration: 1.2, ease: "power2.in" }, 3.0);
|
||||
tl.to(uniforms.uIntensity, { value: 0.55, duration: 3.0, ease: "power2.out" }, 4.2);
|
||||
tl.to(uniforms.uIntensity, { value: 0.3, duration: 4.8, ease: "sine.inOut" }, 7.2);
|
||||
|
||||
tl.fromTo(
|
||||
uniforms.uChromAb,
|
||||
{ value: 0.0 },
|
||||
{ value: 0.4, duration: 3.0, ease: "sine.in" },
|
||||
0,
|
||||
);
|
||||
tl.to(uniforms.uChromAb, { value: 1.6, duration: 1.0, ease: "power3.in" }, 3.0);
|
||||
tl.to(uniforms.uChromAb, { value: 0.25, duration: 1.5, ease: "power2.out" }, 4.0);
|
||||
tl.to(uniforms.uChromAb, { value: 0.18, duration: 6.5, ease: "sine.inOut" }, 5.5);
|
||||
|
||||
tl.fromTo(
|
||||
uniforms.uReveal,
|
||||
{ value: 0.0 },
|
||||
{ value: 1.0, duration: 1.2, ease: "none" },
|
||||
3.4,
|
||||
);
|
||||
tl.set(uniforms.uReveal, { value: 0.0 }, 4.6);
|
||||
|
||||
tl.to(uniforms.uPhase, { value: 0.6, duration: 5.0, ease: "sine.inOut" }, 1.5);
|
||||
tl.to(uniforms.uPhase, { value: 0.15, duration: 5.5, ease: "sine.inOut" }, 6.5);
|
||||
|
||||
tl.to(uniforms.uVignette, { value: 0.85, duration: 3.0, ease: "power2.out" }, 4.2);
|
||||
|
||||
var mark = root.querySelector(".sf-mark");
|
||||
var letters = root.querySelectorAll(".sf-mark-letter");
|
||||
var rule = root.querySelector(".sf-rule");
|
||||
var title = root.querySelector(".sf-title");
|
||||
var tag = root.querySelector(".sf-tag");
|
||||
|
||||
tl.fromTo(
|
||||
mark,
|
||||
{
|
||||
opacity: 0,
|
||||
scale: 1.18,
|
||||
filter: "blur(24px) drop-shadow(0 0 60px rgba(180,220,255,0.4))",
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1.0,
|
||||
filter: "blur(0px) drop-shadow(0 0 80px rgba(180,220,255,0.55))",
|
||||
duration: 0.9,
|
||||
ease: "power3.out",
|
||||
},
|
||||
3.6,
|
||||
);
|
||||
tl.fromTo(
|
||||
letters,
|
||||
{ y: 80 },
|
||||
{ y: 0, duration: 1.0, ease: "expo.out", stagger: 0.08 },
|
||||
3.6,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
rule,
|
||||
{ width: 0, opacity: 0 },
|
||||
{ width: 360, opacity: 0.85, duration: 0.8, ease: "power2.out" },
|
||||
5.1,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
title,
|
||||
{ opacity: 0, clipPath: "inset(0 100% 0 0)" },
|
||||
{ opacity: 1, clipPath: "inset(0 0% 0 0)", duration: 0.95, ease: "power3.out" },
|
||||
5.3,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
tag,
|
||||
{ opacity: 0, y: 12 },
|
||||
{ opacity: 0.7, y: 0, duration: 0.8, ease: "sine.out" },
|
||||
6.0,
|
||||
);
|
||||
|
||||
tl.to(mark, { scale: 1.04, duration: 5.0, ease: "sine.inOut" }, 7.0);
|
||||
|
||||
tl.to([mark, rule, title, tag], { opacity: 0, duration: 0.6, ease: "power1.in" }, 11.4);
|
||||
|
||||
window.__timelines["webgl-textures-playground"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 39 KiB |