This file provides guidance to Claude Code when working with this repository. See .claude/skills/ for detailed instructions on generating rustmotion scenarios.
Tout JSON de scénario généré doit être validé avec rustmotion validate avant d'être présenté à l'utilisateur. Le validateur fait deux passes : schema et geometry (détection de débordement viewport). Les deux doivent passer.
Aucun contenu textuel ne doit dépasser du device. Quatre propriétés contrôlent ce comportement :
style.white-space(defaultnormal, donc wrap actif) surtext: le texte wrap sur la largeur du parent par défaut.white-space: "nowrap"(ou"pre") est légitime uniquement si unmax-widthfini +font-sizeraisonnable garantissent que la ligne tient. Le validateur émetunwrappable_text_overflowsinon. Il n'existe pas de champstyle.wrap— c'est un vocabulaire hérité de l'ancien modèle de style, supprimé deCssStyle. Voir rules/geometry-safety.md.auto_scroll(defaulttrue) surcodeblocketterminal: quand le contenu dépasse la hauteur dusize, le moteur scrolle (clip + translate) sans réduire lafont-size.auto_scroll: false→auto_scroll_disabled_overflow.style.text-autofit(default absent) surtextetgradient_text: réduit lafont-sizejusqu'à ce que le contenu tienne dans sa boîte. À réserver au texte piloté par des données, dont on ne peut pas connaître la longueur à l'avance — pas pour compenser une mise en page qu'on peut simplement dimensionner. Le rétrécissement s'arrête à un plancher de lisibilité calibré ; si ça ne suffit pas, la violation est toujours signalée. Seuls ces deux composants l'implémentent : le déclarer ailleurs est inerte.style.overflow(defaultvisible) sur les conteneurs : sémantique CSS.hiddenclippe au bord du parent. Le validateur ne se plaint que si le contenu sort du viewport, pas d'un parentvisible.
marquee et cursor sont exemptés (leur rôle est de bleed).
CLI :
rustmotion validate -f file.json— schema + geometry--fix— auto-fix sûr :auto_scroll: truesurauto_scroll_disabled_overflow, retrait destyle.white-spacesurunwrappable_text_overflow(retour au wrapping), ettext-autofit: truesurcontent_overflows_boxpourtext/gradient_text. Les débordements de viewport restent non corrigés : ils demandent un arbitrage de mise en page.--fixrefuse d'écrire sur un scénario templaté, utilisantinclude, ou utilisantfor-each/use— les index de chemin ne correspondraient plus à la source.--report r.json— rapport JSON--strict-anim— vérification frame par frame ; ajoute la détectionanimated_text_overflow(transform animé qui sort du viewport à un instant échantillonné). L'échantillonnage s'arrête àscene.freeze_at, puisque rien n'est rendu au-delà.--strict-attrs— promeut en erreurs les attributs inconnus (détection schéma + did-you-mean, activée par défaut en warnings)--lenient— warnings au lieu d'errors
- ffmpeg est auto-détecté et utilisé par défaut (10-bit H.264, meilleure qualité sur les gradients sombres)
--hardware-accelerationsondeffmpeg -encoderset bascule sur VideoToolbox/NVENC/QSV/AMF si la machine en offre un. Indisponible → message explicite et repli logiciel, jamais de bascule silencieuse. Le CRF n'a pas de sens sur la plupart des encodeurs matériels : le passer avec l'accélération produit un avertissement.--frames a-brend une plage de frames en segment autonome, avec sa tranche d'audio (les pistes ne repartent pas de zéro).rustmotion concat seg1.mp4 seg2.mp4 -o out.mp4les recolle via le concat demuxer de ffmpeg. C'est la brique d'un rendu distribué.- Sans ffmpeg, le fallback openh264 intégré encode en 8-bit
- Pour les vidéos avec des gradients sombres, recommander
--codec prorespour une qualité maximale
Ne duplique pas un sous-arbre. Si dix cartes ne diffèrent que par leurs données, écris-en une et itère — c'est le mode d'échec le plus fréquent de la génération, chaque copie étant une occasion de diverger.
"components": { "stat_card": { "params": { "label": { "type": "string" } }, "template": { … } } },
"children": [{
"for-each": [ { "label": "Revenue" }, { "label": "Users" } ],
"template": { "use": "stat_card", "props": { "label": "$label" } }
}]Chaque élément du for-each lie ses champs directement ($label), plus $index et $item. params a la forme de config ; omettre default rend le paramètre requis. La clé d'overrides est props, pas config — ce nom-là est réservé et serait sauté par la substitution.
components est local au fichier qui le déclare. On peut itérer sur un tableau venu d'une variable ; on ne peut pas instancier un composant défini dans un fichier inclus. Toute erreur — cycle, tableau manquant, composant inconnu, paramètre absent — est nommée et située. Voir rules/templates-and-iteration.md.
--fixrefuse de réécrire un scénario qui utilise ces directives : les index de chemin ne correspondent plus à la source.validatefonctionne normalement, sur l'arbre expansé.
Un scénario est soit une liste plate scenes (racine) — implicitement enveloppée dans une seule vue slide — soit un composition: [...] explicite, un tableau de vues typées "slide" ou "world". Les deux sont mutuellement exclusifs (CompositionAndScenesConflict si les deux sont présents).
Dans une vue slide, les transition entre scènes sont des composites pixel de deux frame-buffers déjà rendus (fade, wipe, zoom, flip, iris, slide…) : aucun élément ne survit à la coupe, seuls les pixels sont mélangés.
La vue world est le seul mécanisme qui produit une continuité réelle entre beats : une caméra virtuelle se déplace en continu à travers un espace 2D où chaque scène occupe une position (world-position), avec un fondu de recouvrement pendant le pan au lieu d'une coupe. C'est la brique à utiliser pour une vidéo qui doit se lire comme un plan continu, sans limite de scène perceptible. Voir rules/world-view.md pour le modèle de coordonnées (le piège world-position = waypoint caméra, pas origine de scène), la recette du halo ambiant en view.background, et un exemple multi-beat validé.
Piège de casing à connaître : world-position (scène) est en kebab-case, alors que son voisin freeze_at (même struct Scene) est en snake_case. Vraie inconsistance du schéma, pas une faute de frappe — copier la casse telle quelle.
text, shape, image, icon, svg, video, gif, caption, rich_text, gradient_text
card, flex, grid, div (alias de container), container, positioned
div= layout pur sans décoration visuelle (HTML<div>).card= même chose mais avec fond/border-radius/ombre attendus.
chart— 12 types: bar, line, pie, donut, horizontal_bar, area, stacked_bar, radar, scatter, radial_bar, funnel, waterfall. Supporte axes/grilles/labels.gauge— jauge semi-circulaire pour KPIssparkline— mini-chart inline sans axesstat— carte KPI composite (valeur + label + tendance + sparkline)heatmap— grille colorée type GitHub contributionstreemap— rectangles proportionnels (slice-and-dice)dot_map— carte mondiale en dot-pattern avec points de données, pulse, lat/lngprogress— barre linéaire ou circulairecounter— compteur animé (standalone uniquement, pas dans les cards)table— tableau avec column_widths, column_align, cell_padding, show_borders
badge— pill avec icon, dot indicator, pulse animation, count badgeavatar/avatar_group— avatar circulaire / groupe empilé avec "+N"switch— toggle animé on/off avec toggle_atslider— curseur horizontal animé avec animate_to/animate_atrating— étoiles avec remplissage partiel animékbd— touche clavier visuelle (effet 3D)tooltip— label flottant avec flèche directionnellenotification— toast fade-in/out avec stack push (info/success/warning/error)pill_nav— tabs avec pill indicator animé entre ongletslist— liste bullet/numbered/checklist avec icônesstepper— étapes numérotées connectées avec progression animéecomparison— vue avant/après avec divider animécountdown— timer digital flip-clock stylemarquee— texte défilant continuskeleton— placeholder de chargement avec shimmer (rectangle/circle/text)tag_cloud— nuage de mots avec tailles pondéréescallout— bulle avec flèchedivider— séparateur visuel
codeblock— code syntax-highlighted avec reveal, diff mode (diff: true), state transitionsterminal— terminal avec chrome macOS, reveal typewriter + curseur clignotant
arrow, connector, timeline, line
Pour faire suivre une trajectoire à un composant, utilise l'effet d'animation
motion_path(données de chemin SVG, orientation optionnelle selon la tangente) plutôt que d'empiler destranslate. Voir rules/motion-path.md.
mockup, lottie, cursor, particle, qr_code
waveform— visualisation d'onde audio réactive au volume de la pisteaudio_spectrum— barres de spectre audio réactives (FFT)
Voir rules/audio-reactive.md pour lier un composant à une piste
audioviastyle.audio-reactive.
Le moteur utilise un pipeline box_tree → layout_pass → paint_pass inspiré des navigateurs web :
- box_tree (
box_builder.rs) — construit un arbre deBoxNode { css: CssStyle, children, intrinsic }depuis les composants JSON résolus - layout_pass (
engine/layout_pass.rs) — orchestre taffy pour calculer lesBoxLayout { x, y, width, height }de chaque nœud. Les feuilles avec unIntrinsicMeasure(texte, image, codeblock) sont mesurées via unemeasure_fn. - paint_pass (
engine/paint_pass.rs) — descend l'arbre, applique transform/opacity, peint les décorations (background, border, shadow), délègue auPainterdu composant pour le contenu.
Chaque composant implémente le trait Painter :
pub trait Painter {
fn paint_content(&self, canvas: &Canvas, layout: &BoxLayout, props: &AnimatedProperties, ctx: &PaintCtx);
fn intrinsic_size(&self, available: AvailableSize, ctx: &MeasureCtx) -> Option<(f32, f32)> { None }
}PaintCtx contient : time, scene_duration, fps, frame_index, video_width, video_height, stagger_offset.
crates/
├── rustmotion-core/src/
│ ├── css/ # Modèle CSS
│ │ ├── style.rs # CssStyle (propriétés CSS kebab-case)
│ │ ├── units.rs # Length, LengthPercentage (px, %, em, rem, vw, vh)
│ │ ├── cascade.rs # Héritage color/font-* parent → enfant
│ │ ├── taffy_bridge.rs # CssStyle → taffy::Style
│ │ └── animation.rs # Résolution des animations → override CssStyle
│ ├── engine/
│ │ ├── box_tree.rs # BoxNode, BoxKind, IntrinsicMeasure
│ │ ├── layout_pass.rs # Orchestration taffy, BoxLayout résultant
│ │ ├── paint_pass.rs # Walk top-down, décorations, dispatch Painter
│ │ ├── animator.rs # Résolution animations, easing, spring solver
│ │ ├── transition.rs # Transitions entre scènes
│ │ ├── renderer/ # Primitives Skia (colors, fonts, shapes, text)
│ │ └── text/cosmic.rs # Bridge cosmic-text — PAS branché sur le rendu réel
│ ├── schema/ # Modèles de données JSON
│ │ ├── scenario.rs # Scenario, ResolvedScenario, View, Scene, VideoConfig
│ │ ├── style.rs # Specialized types (CardBorder, CardShadow, Fill, etc.)
│ │ ├── background.rs # AnimatedBackground, BackgroundPreset
│ │ ├── animation.rs # EasingType, AnimationPreset, PresetConfig
│ │ ├── codeblock_types.rs # CodeblockChrome, CodeblockState
│ │ └── video.rs # AnimationEffect, Size, ShapeType, Stroke
│ └── traits/
│ ├── painter.rs # Painter trait + PaintCtx + AvailableSize + MeasureCtx
│ ├── animatable.rs # Animatable trait
│ ├── timed.rs # Timed trait + TimingConfig
│ └── styled.rs # Styled trait
│
├── rustmotion-components/src/
│ ├── lib.rs # Enum Component + dispatch (as_painter, as_animatable, etc.)
│ ├── box_builder.rs # build_scene() → BuiltScene (components + stagger_delays)
│ ├── intrinsic.rs # TextIntrinsic, BadgeIntrinsic, CounterIntrinsic, etc.
│ ├── legacy_dispatch.rs # LegacyPaintDispatcher (bridge NodeId → Painter)
│ ├── chart/ # 10 fichiers (mod + bar/line/pie/radar/scatter/radial/funnel/waterfall/axes)
│ └── *.rs # Un fichier par composant (impl Painter)
│
└── rustmotion-cli/src/
└── commands/ # validate, render, schema, info
- Créer
crates/rustmotion-components/src/mon_composant.rsavec struct serde +impl Painter(paint_content) - Ajouter
rustmotion_core::impl_traits!(MonComposant { Animatable => animation, Timed => timing, Styled => style }); - Ajouter le variant dans l'enum
Componentdanslib.rs - Ajouter les match arms dans les méthodes de dispatch (
as_painter,as_animatable,as_timed,as_styled) - Ajouter
pub mod mon_composant;etpub use mon_composant::MonComposant;danslib.rs - Si le composant a une taille fixe: la déclarer via apply_intrinsic_overrides dans box_builder.rs
- Si le composant mesure son propre contenu : ajouter
XxxIntrinsicdansintrinsic.rs
cargo test --workspace # ~200 tests (layout + serde round-trip + pixel regressions + smoke)
cargo check # Vérification compilation
rustmotion validate file.json # Validation scénario