Visual Design System — AI Weekly
Visual Design System — AI Weekly
The rationale behind assets/project-template/src/theme.ts. Read this before
changing any token; most visual bugs come from editing one scene instead of the
theme.
1. Design intent
"Broadcast telemetry." A calm, dark, high-contrast stage where exactly one element is allowed to shout: the accent color. Everything else recedes. This is what makes a 3-second card feel readable at a glance on a phone screen.
Three rules govern every frame:
- One accent —
#00D4FFmarks keywords, the active node, and nothing else. - Contrast hierarchy — white for claims,
#9AA7BDfor supporting detail,#2A3654for inactive structure. Never introduce a fourth text color. - Motion is directional — content enters from where the eye already is (headline from the left, meta from below), so movement reads as orientation.
2. Color
bgTop #14213D radial hotspot, top-left third
bgMid #0A0F1E dominant field
bgBottom #05080F vignette floor
text #FFFFFF headlines
textDim #9AA7BD summaries, sources
accent #00D4FF keywords, active node, CTA emphasis
nodeDim #2A3654 inactive timeline
The background is a single shared gradient (BG) reused by every scene. This
is deliberate: with hard cuts between scenes, an identical backdrop makes the
cut invisible, so the five cards read as one continuous program.
Why #00D4FF: at ~9:1 contrast on #0A0F1E it stays legible on a phone, and its
cyan hue is distinct from the warm emoji glyphs so the two never compete.
3. Typography
Font stack 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif — Inter for
Latin/numerals, PingFang SC for Chinese. Do not rely on emoji fonts beyond
the Apple Color Emoji set; pick glyphs inside that range or they render as tofu.
| Role | Size | Weight | Notes |
|---|---|---|---|
| Hero (intro) | 168 | 900 | Single line, overshoots to 1.08 then settles |
| News headline | 64 | 800 | max 2 lines, **kw** at accent + 800 |
| Summary | 34 | 400 | 1–2 lines, textDim |
| Meta (source·date) | 28 | 500 | bottom-fading, textDim with accent dot |
| Eyebrow | 30 | 700 | accent, letterSpacing: 6 |
| CTA | 130 | 900 | breathing scale |
Line height on headlines is 1.22 — CJK glyphs need the extra leading or multi-line headlines look cramped at this size. Do not tighten it.
4. Layout grid
0 230 360 1400 1920
│ timeline spine │ content column │ emoji │
- Timeline spine at x=230, nodes at y = 190/365/540/715/890.
- Text column:
left: 360,right: 520→ ~1040px usable. The reserved 520px is what guarantees the headline can never collide with the emoji. - Emoji: 300px circle at
right: 130, glyph 170px. - Safe margins: 120px horizontal, 80px vertical (keeps text clear of platform UI overlays on mobile re-crops).
Overflow rule: headline visual width ≤ 92 CJK units. The validator enforces this; exceeding it is a hard error, not a warning, because a 3-line headline would push the summary out of the safe area.
5. Motion vocabulary
Named springs in theme.ts (SPRING.hero / pop / glide) keep animation
declarative. Guidelines:
- Spring for entrances that should feel physical — hero drop, emoji pop.
- Interpolate for everything linear — opacity, slides, glow ramps.
- Always clamp both ends of
interpolate; unclamped values blow up outside the range and produce NaN transforms. - No CSS transitions or keyframes. All motion derives from
useCurrentFrame(). This is what makes rendering frame-accurate and reproducible — CSS animation timings drift between renders.
Per-card beat sheet (90 frames @ 30fps)
| Beat | Frames | Motion |
|---|---|---|
| node ignite | 0–24 | glow ramp 0→1 |
| headline | 0–18 | translateX(-140 → 0) + fade |
| summary | 8–26 | translateX(-90 → 0) + fade (staggered +8) |
| meta | 24–46 | translateY(34 → 0) + fade |
| emoji | 0–~20 | spring pop, scale 0→1 |
| fade out | 82–90 | opacity → 0 |
The stagger (0 / +8 / +24) creates a cascade that reads as deliberate pacing rather than everything appearing at once.
6. Frame budget arithmetic
intro(60) + N × news(90) + outro = TOTAL
For N=5 this is 60 + 450 + 90 = 600 frames = 20.0s exactly. The user's brief
(2 + 15 + 2) sums to 19s; the extra second is intentionally given to the outro
so the CTA breathing gets a full cycle and the declared duration matches the
real file. For N≠5, content.ts recomputes the outro rather than letting the
duration drift.
Always verify with ffprobe after rendering. render.sh does this
automatically and fails the job on any mismatch in width/height/duration.
7. Extending the system
- New variant / re-skin → edit
theme.tsonly. - New scene type → compose from existing tokens; never introduce raw hex or magic pixel values in a scene file.
- Aspect ratios (9:16, 1:1) → change
COMP.width/heightand re-deriveLAYOUT; keep the ratio ofcontentRightto width roughly constant.