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..0ca28de --- /dev/null +++ b/.claude/skills/rustmotion/rules/1600-brutalist-style.md @@ -0,0 +1,55 @@ +# 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). + +## 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 +{ + "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/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(); diff --git a/examples/1600-style.json b/examples/1600-style.json new file mode 100644 index 0000000..68a13ee --- /dev/null +++ b/examples/1600-style.json @@ -0,0 +1,213 @@ +{ + "version": "1.0", + "video": { "width": 1920, "height": 1080, "fps": 30, "background": "#FFE500" }, + "fonts": [{ "family": "Anton", "source": "google", "weights": [400] }], + "scenes": [ + { + "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": "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.4, + "transition": { "type": "wipe_up", "duration": 0.35 }, + "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": "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.2, + "transition": { "type": "wipe_left", "duration": 0.35 }, + "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": "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": "row", "align-items": "flex-start", "justify-content": "center", "gap": 96, "perspective": 1200 }, + "children": [ + { "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" } } + ] } + ] + } + ] + } + ] + }, + { + "duration": 3.8, + "transition": { "type": "wipe_right", "duration": 0.35 }, + "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": "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": "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.6, + "transition": { "type": "wipe_down", "duration": 0.35 }, + "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": "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.8, + "transition": { "type": "slide", "duration": 0.35 }, + "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": "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 }] } } + ] + } + ] + } + ] +}