Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions crates/rustmotion/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::env;
use std::fs;
use std::path::{Path, PathBuf};

fn collect_md_files(dir: &Path, out: &mut Vec<PathBuf>) {
fn collect_md_files(dir: &Path, out: &mut Vec<PathBuf>, directories: &mut Vec<PathBuf>) {
directories.push(dir.to_path_buf());

let mut entries: Vec<_> = fs::read_dir(dir)
.unwrap_or_else(|e| panic!("failed to read directory {}: {e}", dir.display()))
.filter_map(|e| e.ok())
Expand All @@ -12,7 +14,7 @@ fn collect_md_files(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in entries {
let path = entry.path();
if path.is_dir() {
collect_md_files(&path, out);
collect_md_files(&path, out, directories);
} else if path.extension().and_then(|e| e.to_str()) == Some("md") {
out.push(path);
}
Expand All @@ -25,7 +27,6 @@ fn main() {

let skills_root = manifest_dir.join("skills");

println!("cargo:rerun-if-changed={}", skills_root.display());
println!("cargo:rerun-if-changed=build.rs");

let skill_md = skills_root.join("SKILL.md");
Expand All @@ -37,11 +38,16 @@ fn main() {

let rules_dir = skills_root.join("rules");
let mut rule_files = Vec::new();
collect_md_files(&rules_dir, &mut rule_files);
let mut walked_directories = vec![skills_root.clone()];
collect_md_files(&rules_dir, &mut rule_files, &mut walked_directories);

let mut all_files = vec![skill_md];
all_files.extend(rule_files);

for watched in walked_directories.iter().chain(all_files.iter()) {
println!("cargo:rerun-if-changed={}", watched.display());
}

let mut generated = String::from("&[\n");
for path in &all_files {
let rel_path = Path::new(".claude/skills/rustmotion")
Expand Down
1 change: 1 addition & 0 deletions crates/rustmotion/skills/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ Read individual rule files for detailed explanations, GOOD/BAD examples, and con
- [rules/validate-json.md](rules/validate-json.md) - Always validate generated JSON with `rustmotion validate` before presenting
- [rules/geometry-safety.md](rules/geometry-safety.md) - Keep all content inside the viewport: `white-space`, `auto_scroll`, `overflow` semantics + violation kinds
- [rules/clip-path.md](rules/clip-path.md) - Non-rectangular masking: the six `clip-path` shapes, how their percentages resolve, and why `node-path` is not one of them yet
- [rules/overlapping-scenes.md](rules/overlapping-scenes.md) - Make an element outlive a cut: overlapping `at` windows composite instead of replacing, who supplies the background, and why `snap` never creates an overlap
- [rules/even-dimensions.md](rules/even-dimensions.md) - Use even width/height for H.264 encoding
- [rules/composition-recipes.md](rules/composition-recipes.md) - **Read this before reaching for a UI-widget component.** Composing KPI cards, pill rows, progress bars, and other former "frozen composition" shapes from primitives, `components`, and `for-each`
- [rules/templates-and-iteration.md](rules/templates-and-iteration.md) - `for-each`/`components`/`use` mechanics: bindings, param defaults, ordering of passes, named errors
Expand Down
66 changes: 66 additions & 0 deletions crates/rustmotion/skills/rules/overlapping-scenes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Scènes superposées : faire durer un élément à travers les coupes

Dans une vue `slide`, chaque scène est rendue seule et la `transition` mélange
deux images déjà finies : **rien ne survit à la coupe**. C'est ce qui fait lire une
vidéo comme une suite de diapositives plutôt que comme un plan continu.

En `timing: "v2"`, deux scènes dont les fenêtres se recouvrent ne se remplacent
plus — elles se **composent**. Une maquette qui monte au beat 3 et reste à l'écran
pendant que six libellés se succèdent par-dessus s'écrit comme une scène longue et
six scènes courtes :

```json
"timing": "v2",
"bpm": 120,
"composition": [{ "type": "slide", "scenes": [
{ "duration": 12.0, "children": [ … le décor qui tient les 12 s … ] },
{ "duration": 3.0, "at": "@0b", "children": [ … beat 1 … ] },
{ "duration": 3.0, "at": "@6b", "children": [ … beat 2 … ] },
{ "duration": 3.0, "at": "@12b", "children": [ … beat 3 … ] }
]}]
```

Durée totale : `max(at + duration)`, soit 12 s — pas 21 s. Chaque scène garde
**son propre temps** : une scène qui commence à `@6b` voit son `t` repartir de 0
quand sa fenêtre s'ouvre, donc ses animations d'entrée jouent à son arrivée et non
au début de la vidéo.

## L'ordre de composition, et pourquoi il décide du fond

Les participantes d'une frame sont empilées **dans l'ordre de déclaration** : la
première du tableau est en bas. Et c'est elle seule qui fournit l'arrière-plan —
les scènes au-dessus n'apportent que leurs enfants, sur un fond transparent.

Conséquence à connaître : `background` et `animated-background` déclarés sur une
scène qui n'est pas la plus basse de son recouvrement **ne peignent rien**. Le
décor appartient à la scène qui porte, pas à celles qui passent. C'est aussi ce qui
permet au fond de se transformer en continu sous les beats, au lieu d'être recoupé
à chaque cut.

Les `effects` de scène (grain, vignette, pixelate) suivent la même règle : ceux de
la scène du bas s'appliquent à l'image composée.

## Trois pièges

**Une scène ne peut pas à la fois se superposer et déclarer une `transition`.** Une
transition compose deux tampons de pixels finis ; un recouvrement compose des
scènes vivantes. Les deux ne peuvent pas décrire les mêmes frames. Rustmotion
avertit sur stderr et ignore la transition — retire-la, ou décale `at` pour que les
fenêtres ne se touchent plus.

**`snap: "beat"` ne crée jamais de recouvrement.** Arrondir une coupe sur la grille
peut la tirer avant la fin de la scène précédente ; ce n'est pas une demande de
composition, et le départ est repoussé (avec un avertissement). Un recouvrement se
déclare dans `at`, à la main. Sans cette distinction, `migrate` + `snap`
raccourcirait silencieusement tout fichier qu'il touche.

**Un trou gèle la dernière image.** Si aucune scène n'est vivante à un instant, la
dernière fenêtre fermée tient sa dernière frame — le rendu ne devient jamais noir
par accident.

## Quand préférer une vue `world`

Le recouvrement fait durer un élément **au même endroit du cadre**. La vue `world`
fait autre chose : une caméra traverse un espace où chaque scène occupe une
position. Prends `world` pour un travelling, le recouvrement pour un décor qui
tient pendant que le contenu change. Voir [world-view.md](world-view.md).
Loading
Loading