Skip to content

Commit bbc981d

Browse files
committed
feat: enhance responsive device design
1 parent b494ff1 commit bbc981d

File tree

4 files changed

+334
-6
lines changed

4 files changed

+334
-6
lines changed

.claude/skills/rustmotion/SKILL.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,93 @@ rustmotion schema # Print JSON Schema
3636

3737
---
3838

39+
## Video Creation Wizard
40+
41+
When the user provides a **video idea or subject** (not a technical question), activate this guided wizard flow. Examples of triggers: "je veux créer une vidéo pour...", "make a video about...", "une vidéo de présentation de...", or any prompt describing video content to produce.
42+
43+
### Phase 1: Brief global
44+
45+
Ask the user **3-5 questions** using `AskUserQuestion` to understand the project. Ask them one at a time or grouped logically:
46+
47+
1. **Format & Device** — Portrait 9:16 / Mobile (1080×1920), Landscape 16:9 / Desktop (1920×1080), or Square 1:1 (1080×1080)? Accept aliases: "mobile"/"phone"/"story"/"reel"/"TikTok" → Mobile 9:16, "desktop"/"YouTube"/"presentation" → Desktop 16:9, "tablet"/"iPad" → Tablet. **The chosen device determines all component sizing** — see [rules/responsive-device-sizing.md](rules/responsive-device-sizing.md).
48+
2. **Target duration** — Short (15-30s), Medium (30-60s), or Long (60s+)?
49+
3. **Tone/style** — Corporate, Playful, Minimal, Tech/Dark, Colorful?
50+
4. **Key content** — What text, data, features, or CTA should appear?
51+
5. **Color palette** — Brand colors, or should we suggest one?
52+
53+
Skip questions where the answer is already obvious from context.
54+
55+
### Phase 2: Scene plan with component suggestions
56+
57+
Based on the brief, propose a **textual scene plan** that maps the user's ideas to concrete rustmotion components. Format:
58+
59+
```
60+
Scene 1 (3s) : Intro
61+
→ icon (lucide:rocket) + text (tagline) with char_scale_in
62+
→ animated-background radial gradient (#0f172a → #1e1b4b)
63+
→ particle stars for ambiance
64+
65+
Scene 2 (4s) : The Problem
66+
→ badge "Pain Point" + main text with char_fade_in (granularity: word)
67+
→ 3x card in row with icons (stagger 0.2s)
68+
→ dark red background with concentric_circles
69+
```
70+
71+
Each scene must include:
72+
- **Concrete components** (text, card, icon, shape, badge, counter, etc.)
73+
- **Recommended animations** (presets, char animations, glow, wiggle)
74+
- **Adapted background** (gradient, particles, concentric_circles)
75+
- **Suggested icons** (lucide:xxx, simple-icons:xxx)
76+
77+
**Idea → Component mapping table:**
78+
79+
| User's idea | Recommended components |
80+
|---|---|
81+
| Stats / numbers | `counter` (animated) + `card` |
82+
| Features / benefits | `card` grid + `icon` + `badge` |
83+
| Code / technical | `codeblock` + `terminal` |
84+
| Process / steps | `timeline` component |
85+
| Comparison | `flex` row with 2 `card` side by side |
86+
| Testimonial | `card` with `shape` circle (avatar) + `text` italic |
87+
| Pricing | `card` with `counter` + `text` |
88+
| Partner logos | `flex` row + `icon` (simple-icons:xxx) |
89+
| CTA / call to action | `badge` + glow + `particle` confetti |
90+
| Hero / intro | `text` with `char_scale_in` + main `icon` |
91+
| Transition / ambiance | `particle` stars/confetti + `animated-background` |
92+
93+
The user validates or adjusts the plan before proceeding.
94+
95+
### Phase 3: Iterative scene-by-scene construction
96+
97+
For each scene in the validated plan:
98+
1. Generate the JSON for the scene
99+
2. Add the scene to the global JSON file (named after the subject in kebab-case, e.g. `saas-analytics-presentation.json`)
100+
3. Validate with `rustmotion validate`
101+
4. Optionally propose a preview (`rustmotion render --frame N`) for visually complex scenes
102+
5. The user validates or requests adjustments
103+
6. Move to the next scene
104+
105+
**Important:** Always write incrementally. Never generate the entire video at once.
106+
107+
### Phase 4: Finalization
108+
109+
1. Assemble the complete JSON with all scenes
110+
2. Run final `rustmotion validate`
111+
3. Render with `rustmotion render -o output.mp4 --quiet`
112+
4. Suggest `--codec prores` for videos with dark gradients
113+
114+
### Design guidelines
115+
116+
- **Scene duration:** 3-5s per scene is the sweet spot. Intro/outro can be shorter (2-3s).
117+
- **Animation patterns:** Stagger entrances within a scene (0.1-0.3s delays). Use fade/slide transitions between scenes.
118+
- **Backgrounds:** Radial gradient for dark themes, concentric_circles for tech feel, particles for ambiance.
119+
- **Visual hierarchy:** Title (large font) → subtitle (medium) → body (smaller). Use color contrast to guide the eye.
120+
- **Consistency:** Keep the same color palette and animation style across all scenes.
121+
- **Pacing:** Alternate between dense scenes (multiple elements) and breathing scenes (single focal point).
122+
- **Device-aware sizing:** All component sizes (font-size, icon size, card width, padding, gaps) MUST be scaled to the target device. Use the Tailwind 4 type scale as reference, multiplied by the device factor (×3 for mobile, ×1.5 for desktop, ×2.5 for square). See [rules/responsive-device-sizing.md](rules/responsive-device-sizing.md). A title on mobile should be `text-4xl` equivalent = 108px, NOT 48px.
123+
124+
---
125+
39126
## Rules
40127

41128
Read individual rule files for detailed explanations, GOOD/BAD examples, and constraints:
@@ -59,6 +146,8 @@ Read individual rule files for detailed explanations, GOOD/BAD examples, and con
59146
- [rules/3d-perspective.md](rules/3d-perspective.md) - 3D perspective transforms with rotate_x, rotate_y, perspective keyframes
60147
- [rules/timeline-sequencing.md](rules/timeline-sequencing.md) - Timeline steps for multi-phase animations within a single scene
61148
- [rules/gradient-quality.md](rules/gradient-quality.md) - Gradient quality: linear color space, 10-bit encoding, ProRes for dark gradients
149+
- [rules/video-wizard.md](rules/video-wizard.md) - Video creation wizard: iterative scene-by-scene construction best practices
150+
- [rules/responsive-device-sizing.md](rules/responsive-device-sizing.md) - CRITICAL: Scale all sizes to target device using Tailwind 4 type scale (×3 mobile, ×1.5 desktop)
62151

63152
---
64153

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Rule: Responsive Device Sizing (Tailwind 4 based)
2+
3+
## Description
4+
When a user requests a video for a specific device class (mobile, tablet, desktop), all component dimensions must be scaled appropriately. rustmotion pixel values are **absolute render pixels**, NOT CSS logical pixels. A phone screen renders 1080px across ~375 CSS points — so everything is at ~3x density. A 48px font renders as ~16pt on screen, which is body text size, not a headline.
5+
6+
## Conversion formula
7+
8+
The scaling factor from rustmotion pixels to perceived CSS-like size:
9+
- **Mobile 1080×1920**: divide by 3 to get equivalent CSS px (e.g., 96px render = 32px CSS = `text-3xl`)
10+
- **Desktop 1920×1080**: divide by ~1.5 (e.g., 48px render = 32px CSS = `text-3xl`)
11+
- **Square 1080×1080**: divide by ~2.5
12+
13+
**Always think in Tailwind 4 type scale first, then multiply by the device factor.**
14+
15+
## Device aliases
16+
17+
| User says | Device class | Resolution |
18+
|---|---|---|
19+
| "mobile", "phone", "portrait", "story", "reel", "TikTok" | **Mobile 9:16** | 1080×1920 |
20+
| "desktop", "landscape", "YouTube", "presentation" | **Desktop 16:9** | 1920×1080 |
21+
| "tablet", "iPad" | **Tablet** | 1080×1440 or 1200×1600 |
22+
| "square", "Instagram", "LinkedIn" | **Square 1:1** | 1080×1080 |
23+
24+
## Tailwind 4 type scale → rustmotion pixels
25+
26+
Reference: Tailwind CSS default font sizes.
27+
28+
| Tailwind class | CSS size | Mobile (×3) | Desktop (×1.5) | Square (×2.5) | Usage |
29+
|---|---|---|---|---|---|
30+
| `text-sm` | 14px | 42 | 21 | 35 | Micro labels |
31+
| `text-base` | 16px | 48 | 24 | 40 | Small / label text |
32+
| `text-lg` | 18px | 54 | 27 | 45 | Card labels |
33+
| `text-xl` | 20px | 60 | 30 | 50 | Body text |
34+
| `text-2xl` | 24px | 72 | 36 | 60 | Large body |
35+
| `text-3xl` | 30px | 90 | 45 | 75 | Subtitle |
36+
| `text-4xl` | 36px | 108 | 54 | 90 | Title |
37+
| `text-5xl` | 48px | 144 | 72 | 120 | Big title |
38+
| `text-6xl` | 60px | 180 | 90 | 150 | Hero headline |
39+
| `text-7xl` | 72px | 216 | 108 | 180 | Impact counter |
40+
| `text-8xl` | 96px | 288 | 144 | 240 | Full-screen number |
41+
| `text-9xl` | 128px | 384 | 192 | 320 | Giant display |
42+
43+
## Tailwind 4 spacing scale → rustmotion pixels
44+
45+
Reference: Tailwind CSS default spacing (4px base unit).
46+
47+
| Tailwind | CSS size | Mobile (×3) | Desktop (×1.5) | Usage |
48+
|---|---|---|---|---|
49+
| `4` | 16px | 48 | 24 | Small gap |
50+
| `6` | 24px | 72 | 36 | Medium gap |
51+
| `8` | 32px | 96 | 48 | Card padding |
52+
| `10` | 40px | 120 | 60 | Large gap |
53+
| `12` | 48px | 144 | 72 | Section gap |
54+
| `16` | 64px | 192 | 96 | Scene layout gap |
55+
56+
## Complete sizing reference per device
57+
58+
### Mobile Portrait (1080×1920)
59+
60+
| Element | Size | Tailwind equivalent |
61+
|---|---|---|
62+
| **Title text** | font-size: 108–144 | `text-4xl` to `text-5xl` |
63+
| **Subtitle text** | font-size: 72–90 | `text-2xl` to `text-3xl` |
64+
| **Body text** | font-size: 54–72 | `text-lg` to `text-2xl` |
65+
| **Label / small text** | font-size: 42–48 | `text-sm` to `text-base` |
66+
| **Icon (hero/standalone)** | 160–200px | Large and impactful |
67+
| **Icon (in card)** | 72–96px | Clear and recognizable |
68+
| **Icon (inline/small)** | 48–60px | Chevrons, decorative |
69+
| **Card width** | 960–1020px | ~90% of viewport |
70+
| **Card padding** | 40–48px | Tailwind `p-10` to `p-12` |
71+
| **Card border-radius** | 28–36px | Generously rounded |
72+
| **Card row** | max 3 cols | More = unreadable |
73+
| **Counter** | font-size: 180–288 | `text-6xl` to `text-8xl` |
74+
| **Badge** | badge_size: "lg" + **font-size: 36–42** | Built-in "lg" = only 18px font (tiny on mobile!). MUST override with `style.font-size` |
75+
| **Terminal font-size** | 28–32px | Readable monospace |
76+
| **Terminal width** | 980–1020px | Near full width |
77+
| **Chart size** | width: 940, height: 400+ | Fill the space |
78+
| **Timeline width** | 940–980px | Near full width |
79+
| **Timeline node_radius** | 36–44px | Visible nodes |
80+
| **Timeline font_size** | 32–36px | Readable labels |
81+
| **Scene layout gap** | 48–72px | Tailwind `gap-12` to `gap-16` |
82+
| **CTA button** | width: 720–900px, padding: 36 | Thumb-friendly |
83+
| **max_width (text)** | 960–1000px | Almost full width |
84+
| **Glow radius** | 32–44px | Visible halo |
85+
| **Particle (halo) size_range** | {min: 60, max: 140} | Visible blobs |
86+
87+
### Desktop Landscape (1920×1080)
88+
89+
| Element | Size | Tailwind equivalent |
90+
|---|---|---|
91+
| **Title text** | font-size: 54–72 | `text-4xl` to `text-5xl` |
92+
| **Subtitle text** | font-size: 36–45 | `text-2xl` to `text-3xl` |
93+
| **Body text** | font-size: 24–30 | `text-base` to `text-lg` |
94+
| **Label / small text** | font-size: 21–24 | `text-sm` to `text-base` |
95+
| **Icon (hero)** | 72–96px | |
96+
| **Icon (in card)** | 40–56px | |
97+
| **Card width** | 800–1400px | 40-70% of viewport |
98+
| **Card padding** | 24–36px | |
99+
| **Counter** | font-size: 90–144 | |
100+
| **Badge** | badge_size: "md" | Default font-size OK for desktop |
101+
| **Terminal font-size** | 18–22px | |
102+
| **Scene layout gap** | 24–36px | |
103+
| **max_width (text)** | 800–1200px | |
104+
105+
### Square (1080×1080)
106+
107+
| Element | Size | Tailwind equivalent |
108+
|---|---|---|
109+
| **Title text** | font-size: 90–120 | `text-4xl` to `text-5xl` |
110+
| **Subtitle text** | font-size: 60–75 | `text-2xl` to `text-3xl` |
111+
| **Body text** | font-size: 40–50 | `text-base` to `text-lg` |
112+
| **Icon (hero)** | 120–150px | |
113+
| **Card width** | 920–1020px | ~90% of viewport |
114+
| **Counter** | font-size: 120–180 | |
115+
| **Badge** | badge_size: "lg" + **font-size: 32–36** | Override font-size for readability |
116+
| **Scene layout gap** | 36–48px | |
117+
118+
## Key principle
119+
120+
**Think in Tailwind classes first, then scale.** If your title should be `text-4xl` (36px CSS), multiply by 3 for mobile = 108px in rustmotion. This ensures consistent, readable results across devices.
121+
122+
## Layout adjustments per device
123+
124+
- **Mobile**: Prefer vertical stacking (column). Max 2–3 items per row. Cards should be nearly full-width (90%+). Use larger gaps. Break horizontal flows into stacked layouts. **CRITICAL: Add scene-level `padding` (48–60px) to prevent elements from touching screen edges.** All child widths (cards, timelines, terminals) must account for this padding: max child width = video width − 2 × scene padding. For 1080px with 48px padding → max child width = 984px.
125+
- **Desktop**: Can use horizontal rows with 3–5 items. Cards at 50–70% width. Tighter gaps. Scene padding optional (24px).
126+
- **Square**: Hybrid — 2–3 items per row, 85–95% width cards. Scene padding 36–48px.
127+
128+
## BAD: Using desktop sizes on mobile
129+
130+
```json
131+
{
132+
"type": "text",
133+
"content": "Title",
134+
"style": { "font-size": 48, "color": "#FFFFFF" }
135+
}
136+
```
137+
48px ÷ 3 = 16px CSS → `text-base` → body text, NOT a title!
138+
139+
## GOOD: Mobile-scaled title
140+
141+
```json
142+
{
143+
"type": "text",
144+
"content": "Title",
145+
"style": { "font-size": 108, "color": "#FFFFFF" }
146+
}
147+
```
148+
108px ÷ 3 = 36px CSS → `text-4xl` → proper title size!
149+
150+
## BAD: 4-column card layout on mobile
151+
152+
```json
153+
{
154+
"type": "card",
155+
"size": { "width": 920, "height": "auto" },
156+
"style": { "flex-direction": "row" },
157+
"children": [
158+
{ "size": { "width": 180 } },
159+
{ "size": { "width": 180 } },
160+
{ "size": { "width": 180 } },
161+
{ "size": { "width": 180 } }
162+
]
163+
}
164+
```
165+
4 × 180px = 60px CSS each → cramped, unreadable.
166+
167+
## GOOD: Stacked or 2-col on mobile
168+
169+
```json
170+
{
171+
"type": "card",
172+
"size": { "width": 1000, "height": "auto" },
173+
"style": { "flex-direction": "column", "gap": 24 },
174+
"children": [
175+
{
176+
"type": "card",
177+
"size": { "width": 1000, "height": "auto" },
178+
"style": { "flex-direction": "row", "align-items": "center", "gap": 24 },
179+
"children": [
180+
{ "type": "icon", "size": { "width": 72, "height": 72 } },
181+
{ "type": "text", "style": { "font-size": 48 } }
182+
]
183+
}
184+
]
185+
}
186+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Rule: Video Creation Wizard Best Practices
2+
3+
## Description
4+
Guidelines for the assisted video creation wizard flow.
5+
6+
## Rules
7+
8+
1. **Never generate the entire video at once.** Build scene by scene, validating each one before moving to the next. This lets the user course-correct early.
9+
10+
2. **Always validate each scene individually** with `rustmotion validate` before presenting it to the user. Fix any errors before asking for feedback.
11+
12+
3. **Propose alternatives when the user isn't satisfied.** If a scene doesn't match expectations, suggest 2-3 concrete variations (different layout, animation style, or component choice) rather than asking "what would you prefer?".
13+
14+
4. **Use presets over custom keyframes.** rustmotion has 39+ built-in animation presets. Always prefer them for consistency and reliability. Only use custom keyframes when no preset fits.
15+
16+
5. **Adapt style to the brief answers.** The tone/style chosen in Phase 1 should influence every decision:
17+
- **Corporate** → subtle animations (fade_in, slide_in_up), neutral backgrounds, clean layouts
18+
- **Playful** → bouncy presets (bounce_in, scale_in with overshoot), bright colors, particles
19+
- **Minimal** → few elements per scene, lots of whitespace, char animations only
20+
- **Tech/Dark** → dark gradients, concentric_circles, glow effects, monospace fonts
21+
- **Colorful** → multi-color gradients, confetti particles, varied icon colors
22+
23+
6. **Suggest previews for complex scenes.** When a scene has overlapping elements, custom positioning, or intricate layouts, render a single frame with `--frame` so the user can verify placement before moving on.
24+
25+
7. **Name files meaningfully.** Use kebab-case derived from the video subject (e.g., `product-launch-intro.json`, not `video.json` or `output.json`).
26+
27+
8. **Keep the conversation flowing.** After each scene validation, briefly recap progress ("Scene 3/6 done, next: feature showcase") to maintain context.
28+
29+
## BAD: Dumping everything at once
30+
```
31+
Here's your complete 6-scene video JSON...
32+
[500 lines of JSON]
33+
```
34+
35+
## GOOD: Iterative construction
36+
```
37+
Let's start with Scene 1 (Intro). Here's the JSON:
38+
[scene JSON]
39+
✓ Validated successfully.
40+
Want me to render a preview frame, or shall we move to Scene 2?
41+
```

src/components/badge.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ impl Badge {
7575
.unwrap_or_else(|| self.badge_size.params().0)
7676
}
7777

78+
/// Returns (h_padding, v_padding, icon_size) scaled proportionally
79+
/// to the resolved font size. If style.font_size overrides the default,
80+
/// padding and icon scale with it.
81+
fn resolved_params(&self) -> (f32, f32, f32) {
82+
let (default_fs, h_pad, v_pad, icon_size) = self.badge_size.params();
83+
let actual_fs = self.resolved_font_size();
84+
let ratio = actual_fs / default_fs;
85+
(h_pad * ratio, v_pad * ratio, icon_size * ratio)
86+
}
87+
7888
fn make_font(&self) -> skia_safe::Font {
7989
let fm = font_mgr();
8090
let font_style = skia_safe::FontStyle::normal();
@@ -91,13 +101,14 @@ impl Badge {
91101
}
92102

93103
fn measure_content(&self) -> (f32, f32) {
94-
let (_, h_pad, v_pad, icon_size) = self.badge_size.params();
104+
let (h_pad, v_pad, icon_size) = self.resolved_params();
95105
let font = self.make_font();
96106
let font_size = self.resolved_font_size();
97107

98108
let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size));
99109
let text_width = measure_text_with_fallback(&self.text, &font, &emoji_font, 0.0);
100-
let icon_gap = if self.icon.is_some() { 6.0 } else { 0.0 };
110+
let ratio = self.resolved_font_size() / self.badge_size.params().0;
111+
let icon_gap = if self.icon.is_some() { 6.0 * ratio } else { 0.0 };
101112
let icon_w = if self.icon.is_some() { icon_size } else { 0.0 };
102113

103114
let w = h_pad * 2.0 + text_width + icon_w + icon_gap;
@@ -116,7 +127,7 @@ impl Widget for Badge {
116127
_props: &crate::engine::animator::AnimatedProperties,
117128
) -> Result<()> {
118129
let color = self.style.background.as_deref().unwrap_or("#3B82F6");
119-
let (_, h_pad, _v_pad, icon_size) = self.badge_size.params();
130+
let (h_pad, _v_pad, icon_size) = self.resolved_params();
120131

121132
let w = layout.width;
122133
let h = layout.height;
@@ -150,8 +161,8 @@ impl Widget for Badge {
150161
color
151162
};
152163

153-
let icon_w = icon_size as u32;
154-
let icon_h = icon_size as u32;
164+
let icon_w = icon_size.round() as u32;
165+
let icon_h = icon_size.round() as u32;
155166
let cache_key = format!("icon:{}:{}:{}x{}", icon_id, icon_color, icon_w, icon_h);
156167

157168
let cache = asset_cache();
@@ -200,7 +211,8 @@ impl Widget for Badge {
200211
let dst = Rect::from_xywh(x_offset, icon_y, icon_size, icon_size);
201212
canvas.draw_image_rect(img, None, dst, &Paint::default());
202213

203-
x_offset += icon_size + 6.0;
214+
let ratio = self.resolved_font_size() / self.badge_size.params().0;
215+
x_offset += icon_size + 6.0 * ratio;
204216
}
205217

206218
// Text

0 commit comments

Comments
 (0)