From 208e8d1cbcc843a81e0b2bb8f2273d585bca362e Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Mon, 20 Jul 2026 10:53:46 +0200 Subject: [PATCH 1/3] fix(fonts): resolve declared custom and Google fonts in text rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom/Google fonts declared in a scenario's `fonts` were downloaded and handed to Skia via `FontMgr::new_from_data`, but that call returns a detached Typeface and never exposes it to `match_family_style` — Skia's default FontMgr only name-resolves installed system fonts. So every `font-family` pointing at a declared font silently rendered in the Helvetica/Arial fallback (e.g. Anton came out as a generic sans-serif). Keep the raw bytes in a global family->bytes registry when loading, and have `typeface_with_fallback` resolve custom families from it first — building and caching the Typeface per render thread (the FontMgr is thread-local, so system-thread registration alone never reached the rayon render workers). One weight per custom family via this path; multi-weight custom families are follow-up. Tests: registry stores first-wins + serves bytes; a registered family resolves to its own typeface over the system fallback (uses the cached Anton TTF when present, skips on a cold cache). --- .../src/engine/renderer/fonts.rs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/crates/rustmotion-core/src/engine/renderer/fonts.rs b/crates/rustmotion-core/src/engine/renderer/fonts.rs index 9d590f0..9388ab3 100644 --- a/crates/rustmotion-core/src/engine/renderer/fonts.rs +++ b/crates/rustmotion-core/src/engine/renderer/fonts.rs @@ -1,3 +1,7 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; + use skia_safe::{FontMgr, FontStyle, Typeface}; use crate::error::{Result, RustmotionError}; @@ -8,12 +12,58 @@ use super::google_fonts::{font_cache_dir, resolve_google_font}; // Thread-local FontMgr instance, created once per thread and reused thread_local! { static THREAD_FONT_MGR: FontMgr = FontMgr::default(); + // Per-thread cache of Typefaces built from the global custom-font bytes, + // so each render thread builds each custom face at most once. + static CUSTOM_TYPEFACES: RefCell> = RefCell::new(HashMap::new()); } pub fn font_mgr() -> FontMgr { THREAD_FONT_MGR.with(|mgr| mgr.clone()) } +/// Global registry of custom/Google-font bytes, keyed by family name. Filled +/// once by [`load_custom_fonts`] on the main thread; read by every render +/// thread through [`custom_typeface`]. A family maps to the first file +/// registered for it (one weight per custom family via this path — sufficient +/// for accent display faces; multi-weight custom families are future work). +fn custom_font_registry() -> &'static Mutex>> { + static REG: OnceLock>>> = OnceLock::new(); + REG.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Store a custom font's bytes under `family` (first registration wins). +pub fn register_custom_font_bytes(family: &str, data: Vec) { + let mut reg = custom_font_registry() + .lock() + .unwrap_or_else(|e| e.into_inner()); + reg.entry(family.to_string()).or_insert(data); +} + +/// The raw bytes registered for `family`, if any (test/introspection helper). +pub fn custom_font_bytes(family: &str) -> Option> { + custom_font_registry() + .lock() + .unwrap_or_else(|e| e.into_inner()) + .get(family) + .cloned() +} + +/// Resolve a registered custom font to a Typeface, building it from the global +/// bytes on first use per thread and caching it thereafter. `None` when no +/// custom font is registered under `family`. +fn custom_typeface(family: &str) -> Option { + CUSTOM_TYPEFACES.with(|cache| { + if let Some(tf) = cache.borrow().get(family) { + return Some(tf.clone()); + } + let data = custom_font_bytes(family)?; + let sk_data = skia_safe::Data::new_copy(&data); + let tf = font_mgr().new_from_data(&sk_data, None)?; + cache.borrow_mut().insert(family.to_string(), tf.clone()); + Some(tf) + }) +} + /// Validate a `FontEntry` and resolve it to a list of TTF file paths. /// /// - Local entry (`path` set, `source` absent): returns `[path]` as-is. @@ -88,7 +138,14 @@ fn register_font_file(font_mgr: &FontMgr, family: &str, path: &std::path::Path) family, path.display() ); + return; } + // Skia's default FontMgr can build a Typeface from `new_from_data` + // but never exposes it to `match_family_style` (name lookup only + // sees installed system fonts). So keep the raw bytes in a global + // registry; `typeface_with_fallback` builds and caches a Typeface + // from them per thread, ahead of the system match. + register_custom_font_bytes(family, data); } Err(e) => { eprintln!( @@ -107,6 +164,12 @@ fn register_font_file(font_mgr: &FontMgr, family: &str, path: &std::path::Path) /// supported platform). Use this instead of `.expect("FontNotFound")` so we /// never panic from a `paint` callback. pub fn typeface_with_fallback(family: &str, style: FontStyle) -> Result { + // Custom/Google fonts declared in the scenario win over system fonts: + // they are not visible to `match_family_style`, so resolve them from the + // registry first. + if let Some(t) = custom_typeface(family) { + return Ok(t); + } let fm = font_mgr(); if let Some(t) = fm.match_family_style(family, style) { return Ok(t); @@ -187,6 +250,40 @@ mod tests { assert_eq!(paths[0].to_str().unwrap(), "fonts/Inter.ttf"); } + #[test] + fn custom_font_registry_stores_first_and_serves_bytes() { + register_custom_font_bytes("RmProbeRegistryFamily", vec![1, 2, 3]); + // First registration wins (a later weight must not clobber it). + register_custom_font_bytes("RmProbeRegistryFamily", vec![9, 9]); + assert_eq!( + custom_font_bytes("RmProbeRegistryFamily"), + Some(vec![1, 2, 3]) + ); + assert!(custom_font_bytes("RmProbeUnregistered").is_none()); + } + + /// The bug this fix targets: a registered custom family must resolve to the + /// custom typeface, not the Helvetica/Arial fallback. Uses the cached Anton + /// TTF when present (Google-font path); skips on a cold cache so CI without + /// network still passes — the render QA is the visual counterpart. + #[test] + fn registered_custom_font_resolves_over_system_fallback() { + let path = format!( + "{}/.cache/rustmotion/fonts/anton-400.ttf", + std::env::var("HOME").unwrap_or_default() + ); + let Ok(bytes) = std::fs::read(&path) else { + return; // cold font cache → skip (render QA covers it) + }; + register_custom_font_bytes("Anton", bytes); + let tf = typeface_with_fallback("Anton", FontStyle::normal()).unwrap(); + assert_eq!( + tf.family_name(), + "Anton", + "must resolve the custom face, not a system fallback" + ); + } + #[test] fn neither_path_nor_source_is_error() { let entry = neither_entry(); From 742c2bcc25b438826a7b313ad7bf1daf1c2ad895 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Mon, 20 Jul 2026 11:02:20 +0200 Subject: [PATCH 2/3] docs: add 1600-style brutalist motion template A 21s, 6-scene reference scenario (examples/1600-style.json) in the style of motion studios like 1600.agency: full-bleed saturated colour blocks that hard-cut between scenes, oversized Anton grotesque type, kinetic entrances (slide/scale with stagger), animated counters, and directional wipe transitions. Plus a skill rule documenting the visual language so the look can be reproduced on any content. Depends on the custom-font fix (declared Anton must actually render). --- .../rustmotion/rules/1600-brutalist-style.md | 41 +++++++ examples/1600-style.json | 103 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 .claude/skills/rustmotion/rules/1600-brutalist-style.md create mode 100644 examples/1600-style.json diff --git a/.claude/skills/rustmotion/rules/1600-brutalist-style.md b/.claude/skills/rustmotion/rules/1600-brutalist-style.md new file mode 100644 index 0000000..4fc8a39 --- /dev/null +++ b/.claude/skills/rustmotion/rules/1600-brutalist-style.md @@ -0,0 +1,41 @@ +# Style "1600" — motion brutalist à blocs de couleur + +Recette pour produire des vidéos dans l'esprit des studios de motion design type [1600.agency](https://www.1600.agency/) : typographie massive, blocs de couleur saturés plein cadre, kinetic typography, transitions franches. Exemple de référence : `examples/1600-style.json`. + +## Langage visuel + +- **Typographie** : une seule grotesque condensée ultra-bold en `UPPERCASE`, partout. Anton (Google Font) est le choix canonique. Déclarer au niveau racine : + ```json + "fonts": [{ "family": "Anton", "source": "google", "weights": [400] }] + ``` + puis `"font-family": "Anton"` sur chaque texte. `line-height` serré (0.92–1.02) pour empiler les lignes, `letter-spacing` 1–2 sur les gros titres, 4–8 sur les petits kickers. +- **Tailles (16:9, 1920×1080)** : hero 240–300px, titres 120–200px, chiffres 190px, kicker/label 32–56px. On cherche le texte qui remplit la largeur. +- **Palette — blocs saturés alternés** : chaque scène est un aplat plein cadre qui bascule brutalement. Un seul aplat par scène, texte en noir `#0A0A0A` ou crème `#F5F0E8` selon le fond. + - Jaune `#FFE500`, cobalt `#1A1AE5`, corail `#FF4A32`, vert acide `#00E676`, noir `#0A0A0A`. + - Accent sur un mot : réutiliser une des couleurs de bloc (ex. jaune sur fond noir). +- **Fond de scène** : un `shape` `rounded_rect` (border-radius 0) plein cadre `1920×1080` en `position: absolute` posé en **premier** enfant ; le contenu passe en flow centré au-dessus. + +## Animation + +- **Kinetic entrances** : `slide_in_up` (lignes empilées, stagger 0.12s), `slide_in_left` (listes de services), `scale_in` avec `overshoot` (mots accent, CTA). Jamais de fondu mou. +- **Transitions entre scènes** : franches et directionnelles — `wipe_up` / `wipe_left` / `wipe_right` / `wipe_down` / `slide`, durée 0.35s. Alterner les directions pour le rythme. +- **Chiffres** : `counter` (`from`/`to`, `prefix`), il monte sur la durée de la scène. Réserver sa hauteur (`"height"` ≈ font-size + 10) et un `gap` ≥ 24 avec son label, sinon le label remonte sur le chiffre (le counter hors `card` n'a pas de correction de baseline). + +## Structure d'une scène type + +```json +{ + "duration": 3.4, + "transition": { "type": "wipe_up", "duration": 0.35 }, + "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 6 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#0A0A0A", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, + { "type": "text", "content": "STUDIO DE", "style": { "font-family": "Anton", "font-size": 150, "color": "#F5F0E8", "line-height": 0.95, "animation": [{ "name": "slide_in_up", "duration": 0.55 }] } }, + { "type": "text", "content": "MOTION DESIGN", "style": { "font-family": "Anton", "font-size": 200, "color": "#FFE500", "line-height": 0.95, "animation": [{ "name": "scale_in", "delay": 0.18, "duration": 0.6, "overshoot": 0.12 }] } } + ] +} +``` + +## Pour dupliquer sur un autre contenu + +Garder le squelette (aplat + typo Anton + entrées kinetic + transitions franches), remplacer les textes/chiffres/palette. Rythme : 3–4s par scène, une idée par scène. Réutiliser une couleur d'accent cohérente d'un bout à l'autre. diff --git a/examples/1600-style.json b/examples/1600-style.json new file mode 100644 index 0000000..8fed73f --- /dev/null +++ b/examples/1600-style.json @@ -0,0 +1,103 @@ +{ + "version": "1.0", + "video": { "width": 1920, "height": 1080, "fps": 30, "background": "#FFE500" }, + "fonts": [{ "family": "Anton", "source": "google", "weights": [400] }], + "scenes": [ + { + "duration": 3.5, + "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 0 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#FFE500", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, + { "type": "text", "content": "ON FAIT DU", "style": { "font-family": "Anton", "font-size": 118, "color": "#0A0A0A", "line-height": 1.0, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.0, "duration": 0.55 }] } }, + { "type": "text", "content": "MOTION", "style": { "font-family": "Anton", "font-size": 300, "color": "#0A0A0A", "line-height": 0.92, "letter-spacing": 2, "animation": [{ "name": "slide_in_up", "delay": 0.12, "duration": 0.6 }] } }, + { "type": "text", "content": "QUI VEND", "style": { "font-family": "Anton", "font-size": 118, "color": "#0A0A0A", "line-height": 1.0, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.24, "duration": 0.6 }] } } + ] + }, + { + "duration": 3.2, + "transition": { "type": "wipe_up", "duration": 0.35 }, + "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 6 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#0A0A0A", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, + { "type": "text", "content": "STUDIO DE", "style": { "font-family": "Anton", "font-size": 150, "color": "#F5F0E8", "line-height": 0.95, "letter-spacing": 2, "animation": [{ "name": "slide_in_up", "delay": 0.0, "duration": 0.55 }] } }, + { "type": "text", "content": "MOTION DESIGN", "style": { "font-family": "Anton", "font-size": 200, "color": "#FFE500", "line-height": 0.95, "letter-spacing": 2, "animation": [{ "name": "scale_in", "delay": 0.18, "duration": 0.6, "overshoot": 0.12 }] } } + ] + }, + { + "duration": 4.0, + "transition": { "type": "wipe_left", "duration": 0.35 }, + "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 56 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#1A1AE5", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, + { "type": "text", "content": "LE STUDIO EN CHIFFRES", "style": { "font-family": "Anton", "font-size": 44, "color": "#8AB0FF", "letter-spacing": 6, "animation": [{ "name": "fade_in", "delay": 0.0, "duration": 0.4 }] } }, + { + "type": "div", + "style": { "flex-direction": "row", "align-items": "flex-start", "justify-content": "center", "gap": 96 }, + "children": [ + { + "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420 }, + "children": [ + { "type": "counter", "from": 0, "to": 30, "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, + { "type": "text", "content": "JOURS DE PROD", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } + ] + }, + { + "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420 }, + "children": [ + { "type": "counter", "from": 0, "to": 120, "prefix": "+", "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, + { "type": "text", "content": "VIDEOS LIVREES", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } + ] + }, + { + "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420 }, + "children": [ + { "type": "counter", "from": 0, "to": 3, "prefix": "x", "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, + { "type": "text", "content": "CONVERSION", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } + ] + } + ] + } + ] + }, + { + "duration": 3.6, + "transition": { "type": "wipe_right", "duration": 0.35 }, + "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 36 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#FF4A32", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, + { "type": "text", "content": "CE QU'ON FAIT", "style": { "font-family": "Anton", "font-size": 40, "color": "#0A0A0A", "letter-spacing": 6, "animation": [{ "name": "fade_in", "delay": 0.0, "duration": 0.35 }] } }, + { + "type": "div", + "style": { "flex-direction": "column", "align-items": "center", "gap": 0 }, + "children": [ + { "type": "text", "content": "BRANDING", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.1, "duration": 0.5 }] } }, + { "type": "text", "content": "PITCH DECK", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.22, "duration": 0.5 }] } }, + { "type": "text", "content": "PRODUIT", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.34, "duration": 0.5 }] } }, + { "type": "text", "content": "SOCIAL", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.46, "duration": 0.5 }] } } + ] + } + ] + }, + { + "duration": 3.4, + "transition": { "type": "wipe_down", "duration": 0.35 }, + "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 4 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#00E676", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, + { "type": "text", "content": "PAS DE BLABLA.", "style": { "font-family": "Anton", "font-size": 170, "color": "#0A0A0A", "line-height": 0.98, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.0, "duration": 0.5 }] } }, + { "type": "text", "content": "QUE DU MOUVEMENT.", "style": { "font-family": "Anton", "font-size": 170, "color": "#0A0A0A", "line-height": 0.98, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.16, "duration": 0.5 }] } } + ] + }, + { + "duration": 3.4, + "transition": { "type": "slide", "duration": 0.35 }, + "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 28 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#0A0A0A", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, + { "type": "text", "content": "ON COMMENCE ?", "style": { "font-family": "Anton", "font-size": 240, "color": "#F5F0E8", "line-height": 0.95, "letter-spacing": 2, "animation": [{ "name": "scale_in", "delay": 0.0, "duration": 0.6, "overshoot": 0.1 }] } }, + { "type": "shape", "shape": "rounded_rect", "fill": "#FFE500", "style": { "width": 520, "height": 10, "border-radius": 5, "animation": [{ "name": "slide_in_left", "delay": 0.35, "duration": 0.5 }] } }, + { "type": "text", "content": "STUDIO@1600.STYLE", "style": { "font-family": "Anton", "font-size": 56, "color": "#FFE500", "letter-spacing": 8, "animation": [{ "name": "fade_in", "delay": 0.5, "duration": 0.5 }] } } + ] + } + ] +} From 452596eb4d5bec720eb25fc8b1066e95b58c6de6 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Mon, 20 Jul 2026 11:14:51 +0200 Subject: [PATCH 3/3] docs: add spatial depth to 1600-style template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the '2D slideshow' feel: every scene now plays on space instead of being a flat panel. - continuous camera per scene (keyframed zoom + focal origin drift, and a subtle roll on two scenes) — no more static frames - three parallax planes via style.depth on scene direct children: a deep tone-on-tone ghost word (~0.4), the content (1.0), floating foreground accents (~1.75) — planes separate as the camera moves - 3D rotational entrances (flip_in_x/y, tilt_in with perspective and a hinge transform-origin) instead of flat slides; they resolve face-on so the type stays legible - camera-roll bleed avoided by keeping a covering zoom during rotation (documented in the style rule) Rule updated with the depth/camera layer. --- .../rustmotion/rules/1600-brutalist-style.md | 14 ++ examples/1600-style.json | 208 +++++++++++++----- 2 files changed, 173 insertions(+), 49 deletions(-) diff --git a/.claude/skills/rustmotion/rules/1600-brutalist-style.md b/.claude/skills/rustmotion/rules/1600-brutalist-style.md index 4fc8a39..0ca28de 100644 --- a/.claude/skills/rustmotion/rules/1600-brutalist-style.md +++ b/.claude/skills/rustmotion/rules/1600-brutalist-style.md @@ -21,6 +21,20 @@ Recette pour produire des vidéos dans l'esprit des studios de motion design typ - **Transitions entre scènes** : franches et directionnelles — `wipe_up` / `wipe_left` / `wipe_right` / `wipe_down` / `slide`, durée 0.35s. Alterner les directions pour le rythme. - **Chiffres** : `counter` (`from`/`to`, `prefix`), il monte sur la durée de la scène. Réserver sa hauteur (`"height"` ≈ font-size + 10) et un `gap` ≥ 24 avec son label, sinon le label remonte sur le chiffre (le counter hors `card` n'a pas de correction de baseline). +## Profondeur & caméra — jouer sur l'espace + +Sans ça, le style est un diaporama 2D. Trois leviers, cumulés sur chaque scène : + +1. **Caméra en mouvement continu** (`scene.camera.keyframes`, propriétés `zoom` / `origin.x` / `origin.y` / `rotation`) : un push-in ou pull-out lent (zoom 1.0↔1.14) + une dérive du point focal (`origin`) donne une vie permanente. Alterner push-in / pull-out d'une scène à l'autre. +2. **Plans de profondeur** (`style.depth` sur les enfants **directs** de la scène, qui deviennent les plans de parallaxe) : structurer chaque scène en 3 plans absolus plein cadre — + - **fond profond** `depth ~0.4` : un mot/chiffre géant ton-sur-ton (`overflow: hidden` sur le plan pour le clipper au cadre) ; + - **contenu** `depth 1.0` : la typo principale ; + - **avant-plan** `depth ~1.75` : petites formes d'accent avec `float_3d` en boucle. + Au mouvement caméra, les plans se séparent → vraie profondeur. +3. **Entrées en rotation 3D** sur la ligne clé : `flip_in_x` / `flip_in_y` / `tilt_in` (+ `perspective` 900–1400 sur l'élément, `transform-origin` pour pivoter sur une charnière), qui se résolvent face caméra → lisible une fois posé. + +**Piège du bleed** : une `rotation` caméra (ou un zoom < 1.0) révèle le `video.background` aux coins. Garder un zoom couvrant pendant toute la rotation (`zoom ≥ ~1.06` pour ±2°), ou fixer `video.background` à la couleur de la scène. Un `origin` qui dérive à zoom 1.0 ne bleede pas (l'origin n'a d'effet qu'avec du zoom). + ## Structure d'une scène type ```json diff --git a/examples/1600-style.json b/examples/1600-style.json index 8fed73f..68a13ee 100644 --- a/examples/1600-style.json +++ b/examples/1600-style.json @@ -4,55 +4,110 @@ "fonts": [{ "family": "Anton", "source": "google", "weights": [400] }], "scenes": [ { - "duration": 3.5, - "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 0 }, + "duration": 3.8, + "camera": { + "keyframes": [ + { "property": "zoom", "values": [{ "time": 0, "value": 1.02 }, { "time": 3.8, "value": 1.12 }], "easing": "ease_out" }, + { "property": "origin.x", "values": [{ "time": 0, "value": 820 }, { "time": 3.8, "value": 1040 }], "easing": "ease_in_out" }, + { "property": "origin.y", "values": [{ "time": 0, "value": 600 }, { "time": 3.8, "value": 500 }], "easing": "ease_in_out" }, + { "property": "rotation", "values": [{ "time": 0, "value": -1.5 }, { "time": 3.8, "value": 0.5 }], "easing": "ease_in_out" } + ] + }, "children": [ { "type": "shape", "shape": "rounded_rect", "fill": "#FFE500", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, - { "type": "text", "content": "ON FAIT DU", "style": { "font-family": "Anton", "font-size": 118, "color": "#0A0A0A", "line-height": 1.0, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.0, "duration": 0.55 }] } }, - { "type": "text", "content": "MOTION", "style": { "font-family": "Anton", "font-size": 300, "color": "#0A0A0A", "line-height": 0.92, "letter-spacing": 2, "animation": [{ "name": "slide_in_up", "delay": 0.12, "duration": 0.6 }] } }, - { "type": "text", "content": "QUI VEND", "style": { "font-family": "Anton", "font-size": 118, "color": "#0A0A0A", "line-height": 1.0, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.24, "duration": 0.6 }] } } + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 0.35, "overflow": "hidden", "flex-direction": "column", "align-items": "center", "justify-content": "center" }, + "children": [ + { "type": "text", "content": "STUDIO", "style": { "font-family": "Anton", "font-size": 440, "color": "#EFD200", "line-height": 0.9, "letter-spacing": 4, "animation": [{ "name": "fade_in", "duration": 1.0 }] } } + ] + }, + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 1.0, "flex-direction": "column", "align-items": "center", "justify-content": "center", "gap": 0 }, + "children": [ + { "type": "text", "content": "ON FAIT DU", "style": { "font-family": "Anton", "font-size": 118, "color": "#0A0A0A", "line-height": 1.0, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.0, "duration": 0.55 }] } }, + { "type": "text", "content": "MOTION", "style": { "font-family": "Anton", "font-size": 300, "color": "#0A0A0A", "line-height": 0.92, "letter-spacing": 2, "perspective": 900, "animation": [{ "name": "flip_in_x", "delay": 0.15, "duration": 0.8 }] } }, + { "type": "text", "content": "QUI VEND", "style": { "font-family": "Anton", "font-size": 118, "color": "#0A0A0A", "line-height": 1.0, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.32, "duration": 0.6 }] } } + ] + }, + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 1.75 }, + "children": [ + { "type": "shape", "shape": "rounded_rect", "fill": "#0A0A0A", "position": "absolute", "x": 250, "y": 240, "style": { "width": 44, "height": 44, "animation": [{ "name": "fade_in", "delay": 0.5, "duration": 0.4 }, { "name": "float_3d", "loop": true }] } }, + { "type": "shape", "shape": "rounded_rect", "fill": "#0A0A0A", "position": "absolute", "x": 1600, "y": 780, "style": { "width": 60, "height": 60, "animation": [{ "name": "fade_in", "delay": 0.7, "duration": 0.4 }, { "name": "float_3d", "loop": true }] } } + ] + } ] }, { - "duration": 3.2, + "duration": 3.4, "transition": { "type": "wipe_up", "duration": 0.35 }, - "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 6 }, + "camera": { + "keyframes": [ + { "property": "zoom", "values": [{ "time": 0, "value": 1.14 }, { "time": 3.4, "value": 1.0 }], "easing": "ease_out" }, + { "property": "origin.x", "values": [{ "time": 0, "value": 1100 }, { "time": 3.4, "value": 900 }], "easing": "ease_in_out" } + ] + }, "children": [ { "type": "shape", "shape": "rounded_rect", "fill": "#0A0A0A", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, - { "type": "text", "content": "STUDIO DE", "style": { "font-family": "Anton", "font-size": 150, "color": "#F5F0E8", "line-height": 0.95, "letter-spacing": 2, "animation": [{ "name": "slide_in_up", "delay": 0.0, "duration": 0.55 }] } }, - { "type": "text", "content": "MOTION DESIGN", "style": { "font-family": "Anton", "font-size": 200, "color": "#FFE500", "line-height": 0.95, "letter-spacing": 2, "animation": [{ "name": "scale_in", "delay": 0.18, "duration": 0.6, "overshoot": 0.12 }] } } + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 0.4, "overflow": "hidden", "flex-direction": "column", "align-items": "center", "justify-content": "center" }, + "children": [ + { "type": "text", "content": "2026", "style": { "font-family": "Anton", "font-size": 640, "color": "#161616", "line-height": 0.9, "letter-spacing": 10, "animation": [{ "name": "fade_in", "duration": 1.0 }] } } + ] + }, + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 1.0, "flex-direction": "column", "align-items": "center", "justify-content": "center", "gap": 6 }, + "children": [ + { "type": "text", "content": "STUDIO DE", "style": { "font-family": "Anton", "font-size": 150, "color": "#F5F0E8", "line-height": 0.95, "letter-spacing": 2, "perspective": 1000, "transform-origin": { "x": "center", "y": "bottom" }, "animation": [{ "name": "flip_in_x", "delay": 0.0, "duration": 0.7 }] } }, + { "type": "text", "content": "MOTION DESIGN", "style": { "font-family": "Anton", "font-size": 200, "color": "#FFE500", "line-height": 0.95, "letter-spacing": 2, "perspective": 1000, "animation": [{ "name": "tilt_in", "delay": 0.2, "duration": 0.7 }, { "name": "float_3d", "loop": true }] } } + ] + } ] }, { - "duration": 4.0, + "duration": 4.2, "transition": { "type": "wipe_left", "duration": 0.35 }, - "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 56 }, + "camera": { + "keyframes": [ + { "property": "zoom", "values": [{ "time": 0, "value": 1.0 }, { "time": 4.2, "value": 1.1 }], "easing": "ease_in_out" }, + { "property": "origin.y", "values": [{ "time": 0, "value": 640 }, { "time": 4.2, "value": 560 }], "easing": "ease_in_out" } + ] + }, "children": [ { "type": "shape", "shape": "rounded_rect", "fill": "#1A1AE5", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, - { "type": "text", "content": "LE STUDIO EN CHIFFRES", "style": { "font-family": "Anton", "font-size": 44, "color": "#8AB0FF", "letter-spacing": 6, "animation": [{ "name": "fade_in", "delay": 0.0, "duration": 0.4 }] } }, { - "type": "div", - "style": { "flex-direction": "row", "align-items": "flex-start", "justify-content": "center", "gap": 96 }, + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 0.4, "overflow": "hidden", "flex-direction": "column", "align-items": "center", "justify-content": "center" }, "children": [ + { "type": "text", "content": "150+", "style": { "font-family": "Anton", "font-size": 620, "color": "#1414C6", "line-height": 0.9, "letter-spacing": 8, "animation": [{ "name": "fade_in", "duration": 1.0 }] } } + ] + }, + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 1.0, "flex-direction": "column", "align-items": "center", "justify-content": "center", "gap": 56 }, + "children": [ + { "type": "text", "content": "LE STUDIO EN CHIFFRES", "style": { "font-family": "Anton", "font-size": 44, "color": "#8AB0FF", "letter-spacing": 6, "animation": [{ "name": "fade_in", "delay": 0.0, "duration": 0.4 }] } }, { - "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420 }, - "children": [ - { "type": "counter", "from": 0, "to": 30, "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, - { "type": "text", "content": "JOURS DE PROD", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } - ] - }, - { - "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420 }, - "children": [ - { "type": "counter", "from": 0, "to": 120, "prefix": "+", "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, - { "type": "text", "content": "VIDEOS LIVREES", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } - ] - }, - { - "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420 }, + "type": "div", + "style": { "flex-direction": "row", "align-items": "flex-start", "justify-content": "center", "gap": 96, "perspective": 1200 }, "children": [ - { "type": "counter", "from": 0, "to": 3, "prefix": "x", "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, - { "type": "text", "content": "CONVERSION", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } + { "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420, "animation": [{ "name": "flip_in_y", "delay": 0.15, "duration": 0.7 }] }, "children": [ + { "type": "counter", "from": 0, "to": 30, "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, + { "type": "text", "content": "JOURS DE PROD", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } + ] }, + { "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420, "animation": [{ "name": "flip_in_y", "delay": 0.32, "duration": 0.7 }] }, "children": [ + { "type": "counter", "from": 0, "to": 120, "prefix": "+", "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, + { "type": "text", "content": "VIDEOS LIVREES", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } + ] }, + { "type": "div", "style": { "flex-direction": "column", "align-items": "center", "gap": 28, "width": 420, "animation": [{ "name": "flip_in_y", "delay": 0.49, "duration": 0.7 }] }, "children": [ + { "type": "counter", "from": 0, "to": 3, "prefix": "x", "style": { "font-family": "Anton", "font-size": 190, "height": 200, "color": "#F5F0E8", "line-height": 1.0, "text-align": "center" } }, + { "type": "text", "content": "CONVERSION", "style": { "font-family": "Anton", "font-size": 32, "color": "#8AB0FF", "letter-spacing": 4, "text-align": "center" } } + ] } ] } ] @@ -60,43 +115,98 @@ ] }, { - "duration": 3.6, + "duration": 3.8, "transition": { "type": "wipe_right", "duration": 0.35 }, - "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 36 }, + "camera": { + "keyframes": [ + { "property": "zoom", "values": [{ "time": 0, "value": 1.08 }, { "time": 3.8, "value": 1.0 }], "easing": "ease_out" }, + { "property": "origin.x", "values": [{ "time": 0, "value": 760 }, { "time": 3.8, "value": 1000 }], "easing": "ease_in_out" } + ] + }, "children": [ { "type": "shape", "shape": "rounded_rect", "fill": "#FF4A32", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, - { "type": "text", "content": "CE QU'ON FAIT", "style": { "font-family": "Anton", "font-size": 40, "color": "#0A0A0A", "letter-spacing": 6, "animation": [{ "name": "fade_in", "delay": 0.0, "duration": 0.35 }] } }, { - "type": "div", - "style": { "flex-direction": "column", "align-items": "center", "gap": 0 }, + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 0.4, "overflow": "hidden", "flex-direction": "column", "align-items": "flex-end", "justify-content": "center" }, "children": [ - { "type": "text", "content": "BRANDING", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.1, "duration": 0.5 }] } }, - { "type": "text", "content": "PITCH DECK", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.22, "duration": 0.5 }] } }, - { "type": "text", "content": "PRODUIT", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.34, "duration": 0.5 }] } }, - { "type": "text", "content": "SOCIAL", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "animation": [{ "name": "slide_in_left", "delay": 0.46, "duration": 0.5 }] } } + { "type": "text", "content": "GO", "style": { "font-family": "Anton", "font-size": 900, "color": "#EE3A22", "line-height": 0.9, "letter-spacing": 4, "animation": [{ "name": "fade_in", "duration": 1.0 }] } } + ] + }, + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 1.0, "flex-direction": "column", "align-items": "center", "justify-content": "center", "gap": 36 }, + "children": [ + { "type": "text", "content": "CE QU'ON FAIT", "style": { "font-family": "Anton", "font-size": 40, "color": "#0A0A0A", "letter-spacing": 6, "animation": [{ "name": "fade_in", "delay": 0.0, "duration": 0.35 }] } }, + { + "type": "div", + "style": { "flex-direction": "column", "align-items": "center", "gap": 0, "perspective": 1400 }, + "children": [ + { "type": "text", "content": "BRANDING", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "transform-origin": { "x": "left", "y": "center" }, "animation": [{ "name": "flip_in_y", "delay": 0.1, "duration": 0.55 }] } }, + { "type": "text", "content": "PITCH DECK", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "transform-origin": { "x": "left", "y": "center" }, "animation": [{ "name": "flip_in_y", "delay": 0.22, "duration": 0.55 }] } }, + { "type": "text", "content": "PRODUIT", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "transform-origin": { "x": "left", "y": "center" }, "animation": [{ "name": "flip_in_y", "delay": 0.34, "duration": 0.55 }] } }, + { "type": "text", "content": "SOCIAL", "style": { "font-family": "Anton", "font-size": 122, "color": "#0A0A0A", "line-height": 1.02, "letter-spacing": 1, "transform-origin": { "x": "left", "y": "center" }, "animation": [{ "name": "flip_in_y", "delay": 0.46, "duration": 0.55 }] } } + ] + } ] } ] }, { - "duration": 3.4, + "duration": 3.6, "transition": { "type": "wipe_down", "duration": 0.35 }, - "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 4 }, + "camera": { + "keyframes": [ + { "property": "zoom", "values": [{ "time": 0, "value": 1.14 }, { "time": 3.6, "value": 1.07 }], "easing": "ease_out" }, + { "property": "rotation", "values": [{ "time": 0, "value": 1.8 }, { "time": 3.6, "value": 0.0 }], "easing": "ease_in_out" } + ] + }, "children": [ { "type": "shape", "shape": "rounded_rect", "fill": "#00E676", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, - { "type": "text", "content": "PAS DE BLABLA.", "style": { "font-family": "Anton", "font-size": 170, "color": "#0A0A0A", "line-height": 0.98, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.0, "duration": 0.5 }] } }, - { "type": "text", "content": "QUE DU MOUVEMENT.", "style": { "font-family": "Anton", "font-size": 170, "color": "#0A0A0A", "line-height": 0.98, "letter-spacing": 1, "animation": [{ "name": "slide_in_up", "delay": 0.16, "duration": 0.5 }] } } + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 0.4, "overflow": "hidden", "flex-direction": "column", "align-items": "flex-start", "justify-content": "flex-start" }, + "children": [ + { "type": "text", "content": "OUI", "style": { "font-family": "Anton", "font-size": 720, "color": "#00C765", "line-height": 0.85, "letter-spacing": 4, "animation": [{ "name": "fade_in", "duration": 1.0 }] } } + ] + }, + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 1.0, "flex-direction": "column", "align-items": "center", "justify-content": "center", "gap": 4 }, + "children": [ + { "type": "text", "content": "PAS DE BLABLA.", "style": { "font-family": "Anton", "font-size": 170, "color": "#0A0A0A", "line-height": 0.98, "letter-spacing": 1, "perspective": 1100, "animation": [{ "name": "flip_in_x", "delay": 0.0, "duration": 0.55 }] } }, + { "type": "text", "content": "QUE DU MOUVEMENT.", "style": { "font-family": "Anton", "font-size": 170, "color": "#0A0A0A", "line-height": 0.98, "letter-spacing": 1, "perspective": 1100, "animation": [{ "name": "flip_in_x", "delay": 0.18, "duration": 0.55 }] } } + ] + } ] }, { - "duration": 3.4, + "duration": 3.8, "transition": { "type": "slide", "duration": 0.35 }, - "layout": { "direction": "column", "align_items": "center", "justify_content": "center", "gap": 28 }, + "camera": { + "keyframes": [ + { "property": "zoom", "values": [{ "time": 0, "value": 1.0 }, { "time": 3.8, "value": 1.14 }], "easing": "ease_in" }, + { "property": "origin.x", "values": [{ "time": 0, "value": 960 }, { "time": 3.8, "value": 900 }], "easing": "ease_in_out" }, + { "property": "origin.y", "values": [{ "time": 0, "value": 480 }, { "time": 3.8, "value": 540 }], "easing": "ease_in_out" } + ] + }, "children": [ { "type": "shape", "shape": "rounded_rect", "fill": "#0A0A0A", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "border-radius": 0 } }, - { "type": "text", "content": "ON COMMENCE ?", "style": { "font-family": "Anton", "font-size": 240, "color": "#F5F0E8", "line-height": 0.95, "letter-spacing": 2, "animation": [{ "name": "scale_in", "delay": 0.0, "duration": 0.6, "overshoot": 0.1 }] } }, - { "type": "shape", "shape": "rounded_rect", "fill": "#FFE500", "style": { "width": 520, "height": 10, "border-radius": 5, "animation": [{ "name": "slide_in_left", "delay": 0.35, "duration": 0.5 }] } }, - { "type": "text", "content": "STUDIO@1600.STYLE", "style": { "font-family": "Anton", "font-size": 56, "color": "#FFE500", "letter-spacing": 8, "animation": [{ "name": "fade_in", "delay": 0.5, "duration": 0.5 }] } } + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 0.4, "overflow": "hidden", "flex-direction": "column", "align-items": "flex-end", "justify-content": "flex-end" }, + "children": [ + { "type": "text", "content": "?", "style": { "font-family": "Anton", "font-size": 1100, "color": "#171717", "line-height": 0.8, "animation": [{ "name": "fade_in", "duration": 1.0 }] } } + ] + }, + { + "type": "div", "position": "absolute", "x": 0, "y": 0, + "style": { "width": 1920, "height": 1080, "depth": 1.0, "flex-direction": "column", "align-items": "center", "justify-content": "center", "gap": 28 }, + "children": [ + { "type": "text", "content": "ON COMMENCE ?", "style": { "font-family": "Anton", "font-size": 240, "color": "#F5F0E8", "line-height": 0.95, "letter-spacing": 2, "perspective": 1000, "animation": [{ "name": "tilt_in", "delay": 0.0, "duration": 0.6 }] } }, + { "type": "shape", "shape": "rounded_rect", "fill": "#FFE500", "style": { "width": 520, "height": 10, "border-radius": 5, "animation": [{ "name": "slide_in_left", "delay": 0.35, "duration": 0.5 }] } }, + { "type": "text", "content": "STUDIO@1600.STYLE", "style": { "font-family": "Anton", "font-size": 56, "color": "#FFE500", "letter-spacing": 8, "animation": [{ "name": "fade_in", "delay": 0.5, "duration": 0.5 }] } } + ] + } ] } ]