diff --git a/crates/rustmotion-core/src/engine/transition.rs b/crates/rustmotion-core/src/engine/transition.rs index 1cb9bfc..9d59e95 100644 --- a/crates/rustmotion-core/src/engine/transition.rs +++ b/crates/rustmotion-core/src/engine/transition.rs @@ -1,5 +1,5 @@ use crate::engine::animator::ease; -use crate::schema::{EasingType, TransitionType}; +use crate::schema::{EasingType, PanBackground, TransitionType}; use skia_safe::{surfaces, Color4f, ColorType, ImageInfo, Paint, Path, Rect}; /// Composite two RGBA frames during a transition. @@ -377,20 +377,32 @@ fn dissolve_transition( blend_fade(frame_a, frame_b, progress) } -/// Camera pan transition: static background + sliding foreground children. -/// `bg` is the static background, `fg_a`/`fg_b` are children-only (transparent). -/// fg_a slides out by (-dx*t, -dy*t), fg_b slides in from (dx*(1-t), dy*(1-t)). -/// Composite a camera pan between two scenes. +/// Camera pan transition: composited background + sliding foreground children. +/// `bg_a`/`bg_b` are the outgoing/incoming backgrounds, `fg_a`/`fg_b` are +/// children-only (transparent). fg_a slides out by (-dx*t, -dy*t), fg_b +/// slides in from (dx*(1-t), dy*(1-t)). /// -/// `bg_b` is `None` when the backgrounds stay put: `bg_a` is then drawn once as -/// a fixed backdrop and only the foregrounds slide, which is what keeps a -/// shared ambience continuous across a beat. When `Some`, each background -/// travels locked to its own foreground, so the two beats read as different -/// places rather than one space. +/// `pan_background` controls how the two backgrounds combine: +/// - `Static`: neither travels nor scales — the backdrop holds its position, +/// which is what keeps a shared ambience continuous across a beat when both +/// scenes actually share the same background (the crossfade below is then +/// a no-op, since blending a frame with itself returns that frame). +/// - `Travel`: each background moves locked to its own foreground, so the +/// two beats read as different places rather than one space. +/// +/// Both modes crossfade the two background layers in f32 rather than through +/// Skia's `Paint` alpha, which quantizes to an 8-bit byte: while the byte +/// climbs, the premultiplied blend truncates ~1 LSB per channel across the +/// whole frame, and the instant it reaches 255 Skia takes the opaque fast +/// path and every pixel regains that level in a single frame — a visible +/// step at 40-80x the local per-frame rate. `blend_fade` already does this +/// crossfade correctly (see its doc); we render each background into its own +/// full-frame layer first (needed for `Travel`'s scale + translate), then +/// hand both raw buffers to it. #[allow(clippy::too_many_arguments)] pub fn camera_pan_transition( bg_a: &[u8], - bg_b: Option<&[u8]>, + bg_b: &[u8], fg_a: &[u8], fg_b: &[u8], width: u32, @@ -399,6 +411,7 @@ pub fn camera_pan_transition( dx: f32, dy: f32, easing: &EasingType, + pan_background: PanBackground, ) -> Vec { let t = ease(progress, easing) as f32; @@ -406,10 +419,6 @@ pub fn camera_pan_transition( Some(s) => s, None => return bg_a.to_vec(), }; - let img_bg_a = match frame_to_image(bg_a, width, height) { - Some(i) => i, - None => return bg_a.to_vec(), - }; let img_fg_a = match frame_to_image(fg_a, width, height) { Some(i) => i, None => return bg_a.to_vec(), @@ -419,14 +428,12 @@ pub fn camera_pan_transition( None => return bg_a.to_vec(), }; - let canvas = surface.canvas(); - // Offsets: the outgoing plane exits, the incoming one arrives. They tile // exactly, so together they always cover the frame. let (out_x, out_y) = (-dx * t, -dy * t); let (in_x, in_y) = (dx * (1.0 - t), dy * (1.0 - t)); - match bg_b.and_then(|b| frame_to_image(b, width, height)) { + let blended_bg = match pan_background { // Travelling: each background moves with its own scene, but at a // fraction of the foreground's distance and fading across the pan. // @@ -435,7 +442,16 @@ pub fn camera_pan_transition( // overlap across most of the frame instead of meeting edge to edge. // Opaque images laid side by side join on a hard line no crossfade can // hide; overlapping ones dissolve into each other. - Some(img_bg_b) => { + PanBackground::Travel => { + let img_bg_a = match frame_to_image(bg_a, width, height) { + Some(i) => i, + None => return bg_a.to_vec(), + }; + let img_bg_b = match frame_to_image(bg_b, width, height) { + Some(i) => i, + None => return bg_a.to_vec(), + }; + // Backgrounds drift at a fraction of the foreground's distance — // that is how parallax works, and it keeps them overlapping // instead of meeting edge to edge, where two opaque images join on @@ -464,23 +480,156 @@ pub fn camera_pan_transition( Rect::from_ltrb(-mx + ox, -my + oy, w + mx + ox, h + my + oy) }; - // The outgoing background stays opaque so the frame is always - // covered; the incoming one dissolves over it. Fading both would - // darken wherever only one of them reaches. - canvas.draw_image_rect(&img_bg_a, None, spread(bax, bay), &Paint::default()); - let mut incoming = Paint::default(); - incoming.set_alpha_f(t); - canvas.draw_image_rect(&img_bg_b, None, spread(bbx, bby), &incoming); - } - // Fixed backdrop: drawn once at rest, so a shared ambience stays - // continuous and the join is invisible. - None => { - canvas.draw_image(&img_bg_a, (0.0, 0.0), None); + let layer_a = match render_layer(&img_bg_a, spread(bax, bay), width, height) { + Some(p) => p, + None => return bg_a.to_vec(), + }; + let layer_b = match render_layer(&img_bg_b, spread(bbx, bby), width, height) { + Some(p) => p, + None => return bg_a.to_vec(), + }; + blend_fade(&layer_a, &layer_b, t) } - } + // Static: no spatial movement, but still crossfaded in place. When + // both scenes share the same background this is a no-op — blending a + // frame with itself is that frame, so it stays visually frozen, which + // is what makes the junction invisible. When they don't, holding A + // for the whole pan and jump-cutting to B on the first normal frame + // afterward measured +8.25 mean luminance in a single frame (385x the + // local rate) — a hard cut. Crossfading spreads that change across + // the whole pan instead of concentrating it at the boundary. + PanBackground::Static => blend_fade(bg_a, bg_b, t), + }; + let img_bg = match frame_to_image(&blended_bg, width, height) { + Some(i) => i, + None => return bg_a.to_vec(), + }; + let canvas = surface.canvas(); + canvas.draw_image(&img_bg, (0.0, 0.0), None); canvas.draw_image(&img_fg_a, (out_x, out_y), None); canvas.draw_image(&img_fg_b, (in_x, in_y), None); surface_to_pixels(surface, width, height) } + +/// Draw `img` into `dest` on a fresh full-frame surface and read back the raw +/// pixels. Used to pre-render a background plane (with its `Travel` scale + +/// translate applied) before crossfading it against its counterpart in f32. +fn render_layer(img: &skia_safe::Image, dest: Rect, width: u32, height: u32) -> Option> { + let mut surface = create_skia_surface(width, height)?; + surface + .canvas() + .draw_image_rect(img, None, dest, &Paint::default()); + Some(surface_to_pixels(surface, width, height)) +} + +#[cfg(test)] +mod camera_pan_tests { + use super::*; + + fn solid(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> Vec { + (0..width * height).flat_map(|_| [r, g, b, a]).collect() + } + + // Fully transparent so the foreground planes never contribute — isolates + // the background compositing under test. + fn transparent(width: u32, height: u32) -> Vec { + solid(width, height, 0, 0, 0, 0) + } + + // Issue #124 item 2: `Static` used to hold bg_a for the entire pan and + // hard-cut to bg_b afterward. It must now crossfade in place instead — + // a mid-pan frame should show a genuine blend of both, not either one + // alone. + #[test] + fn static_background_crossfades_instead_of_freezing() { + let (w, h) = (4, 4); + let bg_a = solid(w, h, 10, 10, 10, 255); + let bg_b = solid(w, h, 200, 200, 200, 255); + let fg = transparent(w, h); + + let out = camera_pan_transition( + &bg_a, + &bg_b, + &fg, + &fg, + w, + h, + 0.5, + 0.0, + 0.0, + &EasingType::Linear, + PanBackground::Static, + ); + + // blend_fade(10, 200, 0.5) = (10*0.5 + 200*0.5 + 0.5) as u8 = 105. + for px in out.chunks_exact(4) { + assert_eq!( + px, + [105, 105, 105, 255], + "mid-pan Static frame must be a blend of bg_a and bg_b, not a copy of either" + ); + } + assert_ne!(out, bg_a, "must have moved away from bg_a by the midpoint"); + assert_ne!( + out, bg_b, + "must not have already reached bg_b at the midpoint" + ); + } + + // Issue #124 item 1 + 3: the crossfade must be exact float math with no + // residual once progress reaches 1.0 — no Skia alpha-byte quantization + // left over from an `Option`-based alpha blend. + #[test] + fn static_background_reaches_bg_b_exactly_at_full_progress() { + let (w, h) = (4, 4); + let bg_a = solid(w, h, 10, 10, 10, 255); + let bg_b = solid(w, h, 200, 200, 200, 255); + let fg = transparent(w, h); + + let out = camera_pan_transition( + &bg_a, + &bg_b, + &fg, + &fg, + w, + h, + 1.0, + 0.0, + 0.0, + &EasingType::Linear, + PanBackground::Static, + ); + assert_eq!(out, bg_b, "progress=1.0 must land exactly on bg_b"); + } + + // Travel mode's incoming layer has zero offset at t=1 (in_x = dx*(1-t) = + // 0), so it is drawn 1:1 with no resampling — the crossfade should still + // land exactly on bg_b there too. + #[test] + fn travel_background_reaches_bg_b_exactly_at_full_progress() { + let (w, h) = (4, 4); + let bg_a = solid(w, h, 10, 10, 10, 255); + let bg_b = solid(w, h, 200, 200, 200, 255); + let fg = transparent(w, h); + + let out = camera_pan_transition( + &bg_a, + &bg_b, + &fg, + &fg, + w, + h, + 1.0, + 100.0, + 0.0, + &EasingType::Linear, + PanBackground::Travel, + ); + assert_eq!( + out, bg_b, + "progress=1.0 must land exactly on bg_b under Travel too" + ); + } +} diff --git a/crates/rustmotion/src/encode/video/tasks.rs b/crates/rustmotion/src/encode/video/tasks.rs index 5f86e35..9610db7 100644 --- a/crates/rustmotion/src/encode/video/tasks.rs +++ b/crates/rustmotion/src/encode/video/tasks.rs @@ -1,8 +1,8 @@ use crate::engine::transition::{apply_transition, camera_pan_transition}; use crate::error::Result; use crate::schema::{ - EasingType, PanBackground, ResolvedScenario as Scenario, ResolvedView, Scene, TransitionType, - VideoConfig, ViewType, + EasingType, ResolvedScenario as Scenario, ResolvedView, Scene, TransitionType, VideoConfig, + ViewType, }; /// Description of what to render for a specific frame @@ -132,7 +132,7 @@ pub fn render_frame_task_scaled( let scaled_w = (config.width as f32 * scale_factor) as u32; let scaled_h = (config.height as f32 * scale_factor) as u32; let fps = config.fps; - let progress = *frame_in_transition as f64 / (transition_duration * fps as f64); + let progress = transition_progress(*frame_in_transition, *transition_duration, fps); let frame_a_idx = scene_a_frame_offset + frame_in_transition; if matches!(transition_type, TransitionType::CameraPan) { @@ -148,7 +148,7 @@ pub fn render_frame_task_scaled( .unwrap_or((0.0, 0.0)); let dx = bx - ax; let dy = by - ay; - let bg = render_scene_bg_scaled( + let bg_a = render_scene_bg_scaled( config, &scenes[*scene_a_idx], frame_a_idx, @@ -161,17 +161,15 @@ pub fn render_frame_task_scaled( .as_ref() .map(|t| t.background) .unwrap_or_default(); - // Only rendered when the backgrounds travel — otherwise scene A's - // is a fixed backdrop and rendering B's would be wasted work. - let bg_b = match pan_bg { - PanBackground::Travel => Some(render_scene_bg_scaled( - config, - &scenes[*scene_b_idx], - *frame_in_transition, - scale_factor, - )?), - PanBackground::Static => None, - }; + // Rendered in both modes: `Static` now crossfades in place too + // (see camera_pan_transition's doc), so it needs scene B's + // background just as `Travel` does. + let bg_b = render_scene_bg_scaled( + config, + &scenes[*scene_b_idx], + *frame_in_transition, + scale_factor, + )?; let fg_a = render_scene_fg_scaled( config, &scenes[*scene_a_idx], @@ -188,8 +186,8 @@ pub fn render_frame_task_scaled( )?; // CameraPan composites two scenes; apply scene_b effects to the composited result. let mut composited = camera_pan_transition( - &bg, - bg_b.as_deref(), + &bg_a, + &bg_b, &fg_a, &fg_b, scaled_w, @@ -198,6 +196,7 @@ pub fn render_frame_task_scaled( dx * scale_factor, dy * scale_factor, easing, + pan_bg, ); apply_post_effects( &mut composited, @@ -289,7 +288,7 @@ pub fn render_frame_task_scaled( let scaled_w = (config.width as f32 * scale_factor) as u32; let scaled_h = (config.height as f32 * scale_factor) as u32; let fps = config.fps; - let progress = *frame_in_transition as f64 / (transition_duration * fps as f64); + let progress = transition_progress(*frame_in_transition, *transition_duration, fps); let view_a = &scenario.views[*view_a_idx]; let view_b = &scenario.views[*view_b_idx]; @@ -309,6 +308,24 @@ pub fn render_frame_task_scaled( } } +/// Frame index within a transition → progress in `[0.0, 1.0]`. +/// +/// `build_frame_tasks` emits exactly `(transition_duration * fps).round()` +/// frames for a transition (`frame_in_transition` in `0..transition_frames`). +/// Dividing by the raw, unrounded `transition_duration * fps` instead of by +/// that same emitted frame count can disagree once rounding is involved — +/// e.g. 23 emitted frames for a 22.5-frame duration — which left `progress` +/// maxing out at 22/22.5 ≈ 0.978 on the last emitted frame and discharging +/// the residual as a snap in the very next (non-transition) frame: measured +/// at 22.6px of foreground displacement in one frame with linear easing. +/// Dividing by `transition_frames - 1` instead makes `frame_in_transition == +/// transition_frames - 1` land on exactly `1.0`, so the transition's last +/// frame is the fully-completed state and there is nothing left to snap. +fn transition_progress(frame_in_transition: u32, transition_duration: f64, fps: u32) -> f64 { + let transition_frames = (transition_duration * fps as f64).round() as u32; + frame_in_transition as f64 / transition_frames.saturating_sub(1).max(1) as f64 +} + fn render_last_frame_of_view( config: &VideoConfig, view: &ResolvedView, @@ -734,6 +751,68 @@ pub fn hash_video_config(config: &VideoConfig) -> u64 { hasher.finish() } +#[cfg(test)] +mod transition_progress_tests { + use super::*; + + // The audit's own reproduction case: a 0.75s transition at 30fps rounds + // up to 23 emitted frames (0.75*30 = 22.5), so frame_in_transition runs + // 0..=22. The old code divided by the raw 22.5 instead of by the + // (frame_count - 1) = 22 those indices actually span. + #[test] + fn last_emitted_frame_reaches_exactly_one() { + let p = transition_progress(22, 0.75, 30); + assert_eq!( + p, 1.0, + "last frame of a 23-frame/22.5-raw transition must be exactly 1.0, got {p}" + ); + // The old formula's value, for contrast: 22.0 / 22.5 ≈ 0.9778 — this + // is what used to be discharged as a snap in the next normal frame. + assert!((22.0 / 22.5 - p).abs() > 0.02); + } + + #[test] + fn first_frame_is_zero() { + assert_eq!(transition_progress(0, 0.75, 30), 0.0); + } + + #[test] + fn progress_is_monotonic_and_bounded() { + let frames = (0.75_f64 * 30.0).round() as u32; // 23 + let mut prev = -1.0; + for f in 0..frames { + let p = transition_progress(f, 0.75, 30); + assert!( + (0.0..=1.0).contains(&p), + "progress {p} out of range at frame {f}" + ); + assert!( + p > prev, + "progress must be strictly increasing: {prev} -> {p} at frame {f}" + ); + prev = p; + } + assert_eq!(prev, 1.0); + } + + // Frame counts that are already exact multiples of fps had the same bug, + // just a smaller residual: dividing by N instead of N-1 never reaches 1.0 + // on the last frame either. + #[test] + fn exact_integer_duration_still_reaches_one() { + // 0.5s @ 30fps = 15 frames exactly, indices 0..=14. + let p = transition_progress(14, 0.5, 30); + assert_eq!(p, 1.0); + } + + // A single-frame transition must not divide by zero. + #[test] + fn single_frame_transition_does_not_panic() { + let p = transition_progress(0, 1.0 / 60.0, 30); + assert!(p.is_finite()); + } +} + #[cfg(test)] mod hit_tests { use super::*; diff --git a/examples/dynamic-glass.mp4 b/examples/dynamic-glass.mp4 new file mode 100644 index 0000000..ab9a292 Binary files /dev/null and b/examples/dynamic-glass.mp4 differ diff --git a/examples/ferriskey-presentation.json b/examples/ferriskey-presentation.json new file mode 100644 index 0000000..47100b6 --- /dev/null +++ b/examples/ferriskey-presentation.json @@ -0,0 +1,1988 @@ +{ + "version": "1.0", + "video": { + "width": 1920, + "height": 1080, + "fps": 30, + "background": "#09090b" + }, + "fonts": [ + { + "family": "Inter", + "source": "google", + "weights": [ + 700 + ] + } + ], + "scenes": [ + { + "duration": 4.0, + "camera": { + "keyframes": [ + { + "property": "zoom", + "values": [ + { + "time": 0, + "value": 1.0 + }, + { + "time": 4.0, + "value": 1.08 + } + ], + "easing": "ease_in_out" + } + ] + }, + "children": [ + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 0.3, + "overflow": "hidden" + }, + "children": [ + { + "type": "shape", + "shape": "circle", + "fill": "#f97316", + "position": "absolute", + "x": 680, + "y": 300, + "style": { + "width": 560, + "height": 520, + "opacity": 0.16, + "filter": [ + { + "fn": "blur", + "radius": 150 + } + ] + } + } + ] + }, + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 1.12, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 30, + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "rotate_y", + "keyframes": [ + { + "time": 0, + "value": -7 + }, + { + "time": 4.0, + "value": 6 + } + ], + "easing": "ease_in_out" + }, + { + "property": "perspective", + "keyframes": [ + { + "time": 0, + "value": 1300 + }, + { + "time": 4.0, + "value": 1300 + } + ], + "easing": "linear" + } + ] + } + ] + }, + "children": [ + { + "type": "text", + "content": "IDENTITY & ACCESS MANAGEMENT", + "style": { + "font-family": "Inter", + "font-size": 30, + "color": "#f59e0b", + "letter-spacing": 8, + "width": 900, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_fade_in", + "granularity": "char", + "stagger": 0.012, + "duration": 0.3, + "delay": 0.0 + } + ] + } + }, + { + "type": "div", + "style": { + "flex-direction": "column", + "align-items": "center", + "gap": -18 + }, + "children": [ + { + "type": "div", + "style": { + "flex-direction": "row", + "align-items": "center", + "justify-content": "center", + "gap": 42 + }, + "children": [ + { + "type": "div", + "style": { + "overflow": "hidden", + "height": 190, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center" + }, + "children": [ + { + "type": "text", + "content": "QUI", + "style": { + "font-family": "Inter", + "font-size": 148, + "color": "#fafafa", + "line-height": 1.04, + "letter-spacing": -2, + "white-space": "nowrap", + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "translate_y", + "keyframes": [ + { + "time": 0, + "value": 190 + }, + { + "time": 0.1, + "value": 190 + }, + { + "time": 0.7, + "value": 0 + } + ], + "easing": "ease_out" + } + ] + } + ] + } + } + ] + }, + { + "type": "div", + "style": { + "overflow": "hidden", + "height": 190, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center" + }, + "children": [ + { + "type": "text", + "content": "A", + "style": { + "font-family": "Inter", + "font-size": 148, + "color": "#fafafa", + "line-height": 1.04, + "letter-spacing": -2, + "white-space": "nowrap", + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "translate_y", + "keyframes": [ + { + "time": 0, + "value": 190 + }, + { + "time": 0.28, + "value": 190 + }, + { + "time": 0.88, + "value": 0 + } + ], + "easing": "ease_out" + } + ] + } + ] + } + } + ] + }, + { + "type": "div", + "style": { + "overflow": "hidden", + "height": 190, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center" + }, + "children": [ + { + "type": "text", + "content": "ACCÈS", + "style": { + "font-family": "Inter", + "font-size": 148, + "color": "#fafafa", + "line-height": 1.04, + "letter-spacing": -2, + "white-space": "nowrap", + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "translate_y", + "keyframes": [ + { + "time": 0, + "value": 190 + }, + { + "time": 0.46, + "value": 190 + }, + { + "time": 1.06, + "value": 0 + } + ], + "easing": "ease_out" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "type": "div", + "style": { + "flex-direction": "row", + "align-items": "center", + "justify-content": "center", + "gap": 42 + }, + "children": [ + { + "type": "div", + "style": { + "overflow": "hidden", + "height": 190, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center" + }, + "children": [ + { + "type": "text", + "content": "À", + "style": { + "font-family": "Inter", + "font-size": 148, + "color": "#fafafa", + "line-height": 1.04, + "letter-spacing": -2, + "white-space": "nowrap", + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "translate_y", + "keyframes": [ + { + "time": 0, + "value": 190 + }, + { + "time": 0.68, + "value": 190 + }, + { + "time": 1.28, + "value": 0 + } + ], + "easing": "ease_out" + } + ] + } + ] + } + } + ] + }, + { + "type": "div", + "style": { + "overflow": "hidden", + "height": 190, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center" + }, + "children": [ + { + "type": "text", + "content": "QUOI", + "style": { + "font-family": "Inter", + "font-size": 148, + "color": "#fafafa", + "line-height": 1.04, + "letter-spacing": -2, + "white-space": "nowrap", + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "translate_y", + "keyframes": [ + { + "time": 0, + "value": 190 + }, + { + "time": 0.86, + "value": 190 + }, + { + "time": 1.46, + "value": 0 + } + ], + "easing": "ease_out" + } + ] + } + ] + } + } + ] + }, + { + "type": "div", + "style": { + "overflow": "hidden", + "height": 190, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center" + }, + "children": [ + { + "type": "text", + "content": "?", + "style": { + "font-family": "Inter", + "font-size": 148, + "color": "#fafafa", + "line-height": 1.04, + "letter-spacing": -2, + "white-space": "nowrap", + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "translate_y", + "keyframes": [ + { + "time": 0, + "value": 190 + }, + { + "time": 1.04, + "value": 190 + }, + { + "time": 1.64, + "value": 0 + } + ], + "easing": "ease_out" + } + ] + } + ] + } + } + ] + } + ] + } + ] + }, + { + "type": "text", + "content": "Votre stack a besoin d'une seule couche d'identité.", + "style": { + "font-family": "Inter", + "font-size": 38, + "color": "#a1a1aa", + "width": 1200, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_fade_in", + "granularity": "word", + "stagger": 0.05, + "duration": 0.4, + "delay": 1.7 + } + ] + } + } + ] + } + ] + }, + { + "duration": 4.2, + "transition": { + "type": "dissolve", + "duration": 0.5 + }, + "camera": { + "keyframes": [ + { + "property": "zoom", + "values": [ + { + "time": 0, + "value": 1.18 + }, + { + "time": 4.2, + "value": 1.0 + } + ], + "easing": "ease_out" + }, + { + "property": "rotation", + "values": [ + { + "time": 0, + "value": -3.5 + }, + { + "time": 4.2, + "value": 2.5 + } + ], + "easing": "ease_in_out" + } + ] + }, + "children": [ + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 0.3, + "overflow": "hidden" + }, + "children": [ + { + "type": "shape", + "shape": "circle", + "fill": "#f97316", + "position": "absolute", + "x": 700, + "y": 300, + "style": { + "width": 560, + "height": 520, + "opacity": 0.2, + "filter": [ + { + "fn": "blur", + "radius": 150 + } + ] + } + } + ] + }, + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 1.12, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 24 + }, + "children": [ + { + "type": "icon", + "icon": "lucide:key-round", + "style": { + "width": 96, + "height": 96, + "color": "#f59e0b", + "animation": [ + { + "name": "scale_in", + "delay": 0.0, + "duration": 0.6, + "overshoot": 0.15 + } + ] + } + }, + { + "type": "text", + "content": "FERRISKEY", + "style": { + "font-family": "Inter", + "font-size": 210, + "color": "#fbbf24", + "line-height": 1.0, + "letter-spacing": -4, + "width": 1600, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_scale_in", + "granularity": "char", + "stagger": 0.05, + "duration": 0.5, + "delay": 0.15, + "overshoot": 0.12 + } + ] + } + }, + { + "type": "text", + "content": "A MODERN IAM FOR DISTRIBUTED SYSTEMS", + "style": { + "font-family": "Inter", + "font-size": 40, + "color": "#d4d4d8", + "letter-spacing": 4, + "width": 1300, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_fade_in", + "granularity": "word", + "stagger": 0.05, + "duration": 0.4, + "delay": 0.7 + } + ] + } + } + ] + } + ] + }, + { + "duration": 4.4, + "transition": { + "type": "dissolve", + "duration": 0.5 + }, + "camera": { + "keyframes": [ + { + "property": "zoom", + "values": [ + { + "time": 0, + "value": 1.0 + }, + { + "time": 4.4, + "value": 1.06 + } + ], + "easing": "ease_in_out" + }, + { + "property": "origin.y", + "values": [ + { + "time": 0, + "value": 660 + }, + { + "time": 4.4, + "value": 500 + } + ], + "easing": "ease_in_out" + } + ] + }, + "children": [ + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 0.3, + "overflow": "hidden" + }, + "children": [ + { + "type": "shape", + "shape": "circle", + "fill": "#f59e0b", + "position": "absolute", + "x": 200, + "y": 500, + "style": { + "width": 560, + "height": 520, + "opacity": 0.13, + "filter": [ + { + "fn": "blur", + "radius": 150 + } + ] + } + } + ] + }, + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 1.1, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 48, + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "rotate_x", + "keyframes": [ + { + "time": 0, + "value": -8 + }, + { + "time": 4.4, + "value": 7 + } + ], + "easing": "ease_in_out" + }, + { + "property": "perspective", + "keyframes": [ + { + "time": 0, + "value": 1500 + }, + { + "time": 4.4, + "value": 1500 + } + ], + "easing": "linear" + } + ] + } + ] + }, + "children": [ + { + "type": "div", + "style": { + "flex-direction": "column", + "align-items": "center", + "gap": 12 + }, + "children": [ + { + "type": "text", + "content": "AUTHENTIFICATION", + "style": { + "font-family": "Inter", + "font-size": 28, + "color": "#f59e0b", + "letter-spacing": 8, + "width": 700, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_fade_in", + "granularity": "char", + "stagger": 0.015, + "duration": 0.3, + "delay": 0.0 + } + ] + } + }, + { + "type": "text", + "content": "TOUS LES STANDARDS", + "style": { + "font-family": "Inter", + "font-size": 92, + "color": "#fafafa", + "letter-spacing": -1, + "width": 1300, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_slide_up", + "granularity": "word", + "stagger": 0.08, + "duration": 0.5, + "delay": 0.15 + } + ] + } + } + ] + }, + { + "type": "div", + "style": { + "flex-direction": "row", + "align-items": "center", + "justify-content": "center", + "gap": 28 + }, + "children": [ + { + "type": "card", + "style": { + "width": 380, + "height": 190, + "background": "#ffffff0a", + "border-radius": 20, + "backdrop-filter": [ + { + "fn": "blur", + "radius": 12 + } + ], + "gradient-border": { + "colors": [ + "#f59e0b", + "#f97316" + ], + "width": 1.5, + "angle": 120 + }, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 16, + "animation": [ + { + "name": "scale_in", + "delay": 0.6, + "duration": 0.45, + "overshoot": 0.14 + } + ] + }, + "children": [ + { + "type": "icon", + "icon": "lucide:shield-check", + "style": { + "width": 52, + "height": 52, + "color": "#fcd34d" + } + }, + { + "type": "text", + "content": "OAUTH2 · OIDC", + "style": { + "font-family": "Inter", + "font-size": 34, + "color": "#fafafa", + "letter-spacing": 1, + "width": 340, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + }, + { + "type": "card", + "style": { + "width": 380, + "height": 190, + "background": "#ffffff0a", + "border-radius": 20, + "backdrop-filter": [ + { + "fn": "blur", + "radius": 12 + } + ], + "gradient-border": { + "colors": [ + "#f59e0b", + "#f97316" + ], + "width": 1.5, + "angle": 120 + }, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 16, + "animation": [ + { + "name": "scale_in", + "delay": 0.72, + "duration": 0.45, + "overshoot": 0.14 + } + ] + }, + "children": [ + { + "type": "icon", + "icon": "lucide:fingerprint", + "style": { + "width": 52, + "height": 52, + "color": "#fcd34d" + } + }, + { + "type": "text", + "content": "PASSKEYS", + "style": { + "font-family": "Inter", + "font-size": 34, + "color": "#fafafa", + "letter-spacing": 1, + "width": 340, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + }, + { + "type": "card", + "style": { + "width": 380, + "height": 190, + "background": "#ffffff0a", + "border-radius": 20, + "backdrop-filter": [ + { + "fn": "blur", + "radius": 12 + } + ], + "gradient-border": { + "colors": [ + "#f59e0b", + "#f97316" + ], + "width": 1.5, + "angle": 120 + }, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 16, + "animation": [ + { + "name": "scale_in", + "delay": 0.84, + "duration": 0.45, + "overshoot": 0.14 + } + ] + }, + "children": [ + { + "type": "icon", + "icon": "lucide:key-round", + "style": { + "width": 52, + "height": 52, + "color": "#fcd34d" + } + }, + { + "type": "text", + "content": "WEBAUTHN", + "style": { + "font-family": "Inter", + "font-size": 34, + "color": "#fafafa", + "letter-spacing": 1, + "width": 340, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + }, + { + "type": "card", + "style": { + "width": 380, + "height": 190, + "background": "#ffffff0a", + "border-radius": 20, + "backdrop-filter": [ + { + "fn": "blur", + "radius": 12 + } + ], + "gradient-border": { + "colors": [ + "#f59e0b", + "#f97316" + ], + "width": 1.5, + "angle": 120 + }, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 16, + "animation": [ + { + "name": "scale_in", + "delay": 0.96, + "duration": 0.45, + "overshoot": 0.14 + } + ] + }, + "children": [ + { + "type": "icon", + "icon": "lucide:mail", + "style": { + "width": 52, + "height": 52, + "color": "#fcd34d" + } + }, + { + "type": "text", + "content": "MAGIC LINKS", + "style": { + "font-family": "Inter", + "font-size": 34, + "color": "#fafafa", + "letter-spacing": 1, + "width": 340, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + } + ] + } + ] + } + ] + }, + { + "duration": 4.2, + "transition": { + "type": "dissolve", + "duration": 0.5 + }, + "camera": { + "keyframes": [ + { + "property": "zoom", + "values": [ + { + "time": 0, + "value": 1.08 + }, + { + "time": 4.2, + "value": 1.04 + } + ], + "easing": "ease_in_out" + }, + { + "property": "origin.x", + "values": [ + { + "time": 0, + "value": 1250 + }, + { + "time": 4.2, + "value": 680 + } + ], + "easing": "ease_in_out" + } + ] + }, + "children": [ + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 0.3, + "overflow": "hidden" + }, + "children": [ + { + "type": "shape", + "shape": "circle", + "fill": "#f97316", + "position": "absolute", + "x": 1150, + "y": 200, + "style": { + "width": 560, + "height": 520, + "opacity": 0.14, + "filter": [ + { + "fn": "blur", + "radius": 150 + } + ] + } + } + ] + }, + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 1.1, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 44, + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "rotate_y", + "keyframes": [ + { + "time": 0, + "value": 4 + }, + { + "time": 4.2, + "value": -4 + } + ], + "easing": "ease_in_out" + }, + { + "property": "perspective", + "keyframes": [ + { + "time": 0, + "value": 1600 + }, + { + "time": 4.2, + "value": 1600 + } + ], + "easing": "linear" + } + ] + } + ] + }, + "children": [ + { + "type": "div", + "style": { + "flex-direction": "column", + "align-items": "center", + "gap": 12 + }, + "children": [ + { + "type": "text", + "content": "CLOUD-NATIVE", + "style": { + "font-family": "Inter", + "font-size": 28, + "color": "#f59e0b", + "letter-spacing": 8, + "width": 600, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_fade_in", + "granularity": "char", + "stagger": 0.015, + "duration": 0.3, + "delay": 0.0 + } + ] + } + }, + { + "type": "text", + "content": "PENSÉ POUR L'ÉCHELLE", + "style": { + "font-family": "Inter", + "font-size": 92, + "color": "#fafafa", + "letter-spacing": -1, + "width": 1300, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_slide_up", + "granularity": "word", + "stagger": 0.09, + "duration": 0.5, + "delay": 0.15 + } + ] + } + } + ] + }, + { + "type": "div", + "style": { + "flex-direction": "row", + "align-items": "stretch", + "justify-content": "center", + "gap": 24 + }, + "children": [ + { + "type": "card", + "style": { + "width": 400, + "height": 260, + "background": "#ffffff0a", + "border-radius": 20, + "backdrop-filter": [ + { + "fn": "blur", + "radius": 12 + } + ], + "gradient-border": { + "colors": [ + "#f59e0b", + "#f97316" + ], + "width": 1.5, + "angle": 120 + }, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 18, + "animation": [ + { + "name": "scale_in", + "delay": 0.5, + "duration": 0.45, + "overshoot": 0.14 + } + ] + }, + "children": [ + { + "type": "icon", + "icon": "lucide:boxes", + "style": { + "width": 60, + "height": 60, + "color": "#fcd34d" + } + }, + { + "type": "text", + "content": "MULTI-TENANCY", + "style": { + "font-family": "Inter", + "font-size": 38, + "color": "#fafafa", + "width": 360, + "text-align": "center", + "white-space": "nowrap" + } + }, + { + "type": "text", + "content": "Realms isolés", + "style": { + "font-family": "Inter", + "font-size": 26, + "color": "#a1a1aa", + "width": 360, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + }, + { + "type": "card", + "style": { + "width": 400, + "height": 260, + "background": "#ffffff0a", + "border-radius": 20, + "backdrop-filter": [ + { + "fn": "blur", + "radius": 12 + } + ], + "gradient-border": { + "colors": [ + "#f59e0b", + "#f97316" + ], + "width": 1.5, + "angle": 120 + }, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 18, + "animation": [ + { + "name": "scale_in", + "delay": 0.64, + "duration": 0.45, + "overshoot": 0.14 + } + ] + }, + "children": [ + { + "type": "icon", + "icon": "simple-icons:kubernetes", + "style": { + "width": 60, + "height": 60, + "color": "#fcd34d" + } + }, + { + "type": "text", + "content": "KUBERNETES", + "style": { + "font-family": "Inter", + "font-size": 38, + "color": "#fafafa", + "width": 360, + "text-align": "center", + "white-space": "nowrap" + } + }, + { + "type": "text", + "content": "Operator & Helm", + "style": { + "font-family": "Inter", + "font-size": 26, + "color": "#a1a1aa", + "width": 360, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + }, + { + "type": "card", + "style": { + "width": 400, + "height": 260, + "background": "#ffffff0a", + "border-radius": 20, + "backdrop-filter": [ + { + "fn": "blur", + "radius": 12 + } + ], + "gradient-border": { + "colors": [ + "#f59e0b", + "#f97316" + ], + "width": 1.5, + "angle": 120 + }, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 18, + "animation": [ + { + "name": "scale_in", + "delay": 0.78, + "duration": 0.45, + "overshoot": 0.14 + } + ] + }, + "children": [ + { + "type": "icon", + "icon": "lucide:webhook", + "style": { + "width": 60, + "height": 60, + "color": "#fcd34d" + } + }, + { + "type": "text", + "content": "EVENT-DRIVEN", + "style": { + "font-family": "Inter", + "font-size": 38, + "color": "#fafafa", + "width": 360, + "text-align": "center", + "white-space": "nowrap" + } + }, + { + "type": "text", + "content": "Streaming & Webhooks", + "style": { + "font-family": "Inter", + "font-size": 26, + "color": "#a1a1aa", + "width": 380, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + } + ] + } + ] + } + ] + }, + { + "duration": 4.0, + "transition": { + "type": "dissolve", + "duration": 0.5 + }, + "camera": { + "keyframes": [ + { + "property": "zoom", + "values": [ + { + "time": 0, + "value": 1.0 + }, + { + "time": 4.0, + "value": 1.09 + } + ], + "easing": "ease_in_out" + } + ] + }, + "children": [ + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 0.3, + "overflow": "hidden" + }, + "children": [ + { + "type": "shape", + "shape": "circle", + "fill": "#f97316", + "position": "absolute", + "x": 700, + "y": 300, + "style": { + "width": 560, + "height": 520, + "opacity": 0.14, + "filter": [ + { + "fn": "blur", + "radius": 150 + } + ] + } + } + ] + }, + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 1.12, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 60, + "animation": [ + { + "name": "keyframes", + "keyframes": [ + { + "property": "rotate_y", + "keyframes": [ + { + "time": 0, + "value": 8 + }, + { + "time": 4.0, + "value": -8 + } + ], + "easing": "ease_in_out" + }, + { + "property": "perspective", + "keyframes": [ + { + "time": 0, + "value": 1400 + }, + { + "time": 4.0, + "value": 1400 + } + ], + "easing": "linear" + } + ] + } + ] + }, + "children": [ + { + "type": "text", + "content": "ÉCRIT EN RUST", + "style": { + "font-family": "Inter", + "font-size": 40, + "color": "#f59e0b", + "letter-spacing": 8, + "width": 700, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_slide_up", + "granularity": "word", + "stagger": 0.08, + "duration": 0.4, + "delay": 0.0 + } + ] + } + }, + { + "type": "div", + "style": { + "flex-direction": "row", + "align-items": "flex-start", + "justify-content": "center", + "gap": 70 + }, + "children": [ + { + "type": "div", + "style": { + "flex-direction": "column", + "align-items": "center", + "gap": 16, + "animation": [ + { + "name": "scale_in", + "delay": 0.3, + "duration": 0.5, + "overshoot": 0.16 + } + ] + }, + "children": [ + { + "type": "gradient_text", + "content": "~10MB", + "colors": [ + "#fcd34d", + "#f97316" + ], + "angle": 100, + "style": { + "font-family": "Inter", + "font-size": 122, + "line-height": 1.0, + "letter-spacing": -2, + "width": 400, + "text-align": "center", + "white-space": "nowrap" + } + }, + { + "type": "text", + "content": "BINAIRE UNIQUE", + "style": { + "font-family": "Inter", + "font-size": 28, + "color": "#a1a1aa", + "letter-spacing": 3, + "width": 400, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + }, + { + "type": "div", + "style": { + "flex-direction": "column", + "align-items": "center", + "gap": 16, + "animation": [ + { + "name": "scale_in", + "delay": 0.45, + "duration": 0.5, + "overshoot": 0.16 + } + ] + }, + "children": [ + { + "type": "gradient_text", + "content": "<10ms", + "colors": [ + "#fcd34d", + "#f97316" + ], + "angle": 100, + "style": { + "font-family": "Inter", + "font-size": 122, + "line-height": 1.0, + "letter-spacing": -2, + "width": 400, + "text-align": "center", + "white-space": "nowrap" + } + }, + { + "type": "text", + "content": "DE LATENCE", + "style": { + "font-family": "Inter", + "font-size": 28, + "color": "#a1a1aa", + "letter-spacing": 3, + "width": 400, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + }, + { + "type": "div", + "style": { + "flex-direction": "column", + "align-items": "center", + "gap": 16, + "animation": [ + { + "name": "scale_in", + "delay": 0.6, + "duration": 0.5, + "overshoot": 0.16 + } + ] + }, + "children": [ + { + "type": "gradient_text", + "content": "100%", + "colors": [ + "#fcd34d", + "#f97316" + ], + "angle": 100, + "style": { + "font-family": "Inter", + "font-size": 122, + "line-height": 1.0, + "letter-spacing": -2, + "width": 400, + "text-align": "center", + "white-space": "nowrap" + } + }, + { + "type": "text", + "content": "SELF-HOSTED", + "style": { + "font-family": "Inter", + "font-size": 28, + "color": "#a1a1aa", + "letter-spacing": 3, + "width": 400, + "text-align": "center", + "white-space": "nowrap" + } + } + ] + } + ] + } + ] + } + ] + }, + { + "duration": 3.8, + "transition": { + "type": "dissolve", + "duration": 0.5 + }, + "camera": { + "keyframes": [ + { + "property": "zoom", + "values": [ + { + "time": 0, + "value": 1.14 + }, + { + "time": 3.8, + "value": 1.0 + } + ], + "easing": "ease_out" + }, + { + "property": "rotation", + "values": [ + { + "time": 0, + "value": 3.5 + }, + { + "time": 3.8, + "value": 0.0 + } + ], + "easing": "ease_in_out" + } + ] + }, + "children": [ + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 0.3, + "overflow": "hidden" + }, + "children": [ + { + "type": "shape", + "shape": "circle", + "fill": "#f97316", + "position": "absolute", + "x": 660, + "y": 280, + "style": { + "width": 620, + "height": 520, + "opacity": 0.2, + "filter": [ + { + "fn": "blur", + "radius": 160 + } + ] + } + } + ] + }, + { + "type": "div", + "position": "absolute", + "x": 0, + "y": 0, + "style": { + "width": 1920, + "height": 1080, + "depth": 1.12, + "flex-direction": "column", + "align-items": "center", + "justify-content": "center", + "gap": 22 + }, + "children": [ + { + "type": "icon", + "icon": "lucide:key-round", + "style": { + "width": 72, + "height": 72, + "color": "#f59e0b", + "animation": [ + { + "name": "scale_in", + "delay": 0.0, + "duration": 0.5, + "overshoot": 0.15 + } + ] + } + }, + { + "type": "text", + "content": "FERRISKEY", + "style": { + "font-family": "Inter", + "font-size": 170, + "color": "#fbbf24", + "line-height": 1.0, + "letter-spacing": -3, + "width": 1400, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_scale_in", + "granularity": "char", + "stagger": 0.045, + "duration": 0.45, + "delay": 0.1, + "overshoot": 0.12 + } + ] + } + }, + { + "type": "text", + "content": "L'IAM OPEN-SOURCE POUR VOTRE STACK", + "style": { + "font-family": "Inter", + "font-size": 36, + "color": "#d4d4d8", + "letter-spacing": 2, + "width": 1200, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "char_fade_in", + "granularity": "word", + "stagger": 0.05, + "duration": 0.4, + "delay": 0.6 + } + ] + } + }, + { + "type": "shape", + "shape": "rounded_rect", + "fill": "#f59e0b", + "style": { + "width": 300, + "height": 6, + "border-radius": 3, + "animation": [ + { + "name": "slide_in_left", + "delay": 1.0, + "duration": 0.5 + } + ] + } + }, + { + "type": "text", + "content": "FERRISKEY.RS · APACHE 2.0", + "style": { + "font-family": "Inter", + "font-size": 44, + "color": "#f59e0b", + "letter-spacing": 6, + "width": 1100, + "text-align": "center", + "white-space": "nowrap", + "animation": [ + { + "name": "fade_in", + "delay": 1.2, + "duration": 0.5 + } + ] + } + } + ] + } + ] + } + ], + "annotations": [ + { + "id": "an_18c402275241bcb8", + "note": "Je veux une animation en timing", + "status": "resolved", + "frame": 56, + "view": 0, + "scene": 0, + "target": { + "pointer": "/scenes/0/children/2/children/1/children/0", + "kind": "text" + } + }, + { + "id": "an_18c40228017135d8", + "note": "Je veux une animation en timing", + "status": "resolved", + "frame": 56, + "view": 0, + "scene": 0, + "target": { + "pointer": "/scenes/0/children/2/children/1/children/1", + "kind": "text" + } + } + ] +} diff --git a/examples/ferriskey-presentation.mp4 b/examples/ferriskey-presentation.mp4 new file mode 100644 index 0000000..e0a1997 Binary files /dev/null and b/examples/ferriskey-presentation.mp4 differ diff --git a/examples/rustmotion-promo.mp4 b/examples/rustmotion-promo.mp4 new file mode 100644 index 0000000..0607403 Binary files /dev/null and b/examples/rustmotion-promo.mp4 differ diff --git a/untitled.json b/untitled.json new file mode 100644 index 0000000..93a2cc0 --- /dev/null +++ b/untitled.json @@ -0,0 +1,8 @@ +{ + "video": { "width": 1920, "height": 1080, "background": "#0f172a" }, + "scenes": [ + { "duration": 3.0, "children": [ + { "type": "text", "content": "New scenario", "style": { "font-size": 64, "color": "#ffffff", "text-align": "center" } } + ] } + ] +}