diff --git a/.claude/dev-methodology.local.md b/.claude/dev-methodology.local.md new file mode 100644 index 0000000..504753a --- /dev/null +++ b/.claude/dev-methodology.local.md @@ -0,0 +1,9 @@ +# dev-methodology preferences (user answers — change only when the user does) +commit: claude-allowed +create-pr: claude-allowed +merge-pr: claude-allowed # granted 2026-07-18 ("tu as l'autorisation de squash and merge toi-même") +feature-merge: squash # feature → main +workstream-merge: squash +tracking: github-issues # parent chantier issue + one issue per workstream +model-strategy: split # orchestration/review: session model (Fable); implementation sub-agents: sonnet +attribution: none # never cite Claude as author/co-author in commits, PRs, or issues diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..bfe4707 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check formatting + run: cargo fmt --all --check + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install system dependencies + # webkit/gtk/xdo: required to compile rustmotion-studio (dioxus desktop) + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libwebkit2gtk-4.1-dev libgtk-3-dev libxdo-dev + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install system dependencies + # webkit/gtk/xdo: required to compile rustmotion-studio (dioxus desktop) + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libwebkit2gtk-4.1-dev libgtk-3-dev libxdo-dev + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run tests + run: cargo test --workspace diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..3e042bb --- /dev/null +++ b/clippy.toml @@ -0,0 +1,5 @@ +# Painter/render entry points legitimately thread many context parameters +# (canvas, layout, props, ctx, …); restructuring them into bags would hurt +# call-site clarity more than it helps. +too-many-arguments-threshold = 16 +type-complexity-threshold = 400 diff --git a/crates/rustmotion-cli/src/commands/geometry.rs b/crates/rustmotion-cli/src/commands/geometry.rs index d87915b..3efce42 100644 --- a/crates/rustmotion-cli/src/commands/geometry.rs +++ b/crates/rustmotion-cli/src/commands/geometry.rs @@ -17,6 +17,8 @@ //! This walker runs the new CSS-engine pipeline (taffy + cosmic-text) so the //! geometry it checks matches what the renderer will actually paint. +use rustmotion::components::box_builder::build_scene_from_refs; +use rustmotion::components::intrinsic::TextIntrinsic; use rustmotion::components::{ChildComponent, Component}; use rustmotion::core::css::taffy_bridge::ConversionContext; use rustmotion::core::engine::box_tree::{AvailableSpace, BoxNode, IntrinsicMeasure}; @@ -24,8 +26,6 @@ use rustmotion::core::engine::layout_pass::{run_layout, BoxLayout, LayoutResult} use rustmotion::engine::animator::{ apply_orbits, apply_wiggles, extract_effects, resolve_animations, AnimatedProperties, }; -use rustmotion::components::box_builder::build_scene_from_refs; -use rustmotion::components::intrinsic::TextIntrinsic; use rustmotion::engine::render; use rustmotion::schema::ResolvedScenario; use serde::Serialize; @@ -59,6 +59,7 @@ pub enum Axis { #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] +#[allow(clippy::enum_variant_names)] // "Overflow" postfix is load-bearing: serde output matches CLI docs pub enum ViolationKind { /// Component bbox crosses the viewport edge. ViewportOverflow, @@ -271,7 +272,10 @@ fn check_unwrappable_text( if let Component::Text(t) = component { let nowrap = matches!( t.style.white_space, - Some(rustmotion::core::css::style::WhiteSpace::Nowrap | rustmotion::core::css::style::WhiteSpace::Pre) + Some( + rustmotion::core::css::style::WhiteSpace::Nowrap + | rustmotion::core::css::style::WhiteSpace::Pre + ) ); if !nowrap { return; @@ -316,7 +320,11 @@ fn check_auto_scroll( let font_size = cb.style.font_size_px_or(14.0); let actual_line_height = cb.style.line_height_for(font_size); let line_count = cb.code.lines().count().max(1) as f32; - let chrome_h = if cb.chrome.as_ref().is_some_and(|c| c.enabled) { 36.0 } else { 0.0 }; + let chrome_h = if cb.chrome.as_ref().is_some_and(|c| c.enabled) { + 36.0 + } else { + 0.0 + }; let pad = 32.0; // ~16 top + 16 bottom default let natural_h = chrome_h + pad + line_count * actual_line_height; if natural_h > bbox.h + 0.5 { @@ -587,7 +595,11 @@ fn resolve_props_for_validation( return AnimatedProperties::default(); } } - let base = if let Some(start) = start_at { time - start } else { time }; + let base = if let Some(start) = start_at { + time - start + } else { + time + }; base.max(0.0) } else { time @@ -595,7 +607,13 @@ fn resolve_props_for_validation( let mut props = AnimatedProperties::default(); for (preset, preset_config) in &extracted.presets { - let p = resolve_animations(&[], Some(preset), Some(preset_config), anim_time, scene_duration); + let p = resolve_animations( + &[], + Some(preset), + Some(preset_config), + anim_time, + scene_duration, + ); props.merge(&p); } if !extracted.keyframes.is_empty() { @@ -671,6 +689,37 @@ fn hint_for_animated( } } +/// Render a violation for human consumption (multi-line, color-free). +pub fn format_violation(v: &GeometryViolation) -> String { + let axis_str = match v.axis { + Axis::X => "x", + Axis::Y => "y", + Axis::Both => "x+y", + }; + let kind_str = match v.kind { + ViolationKind::ViewportOverflow => "viewport overflow", + ViolationKind::UnwrappableTextOverflow => "wrap=false but text too wide", + ViolationKind::AutoScrollDisabledOverflow => "auto_scroll=false but content too tall", + ViolationKind::AnimatedTextOverflow => "animation pushes content outside viewport", + }; + format!( + "ERROR: {} ({})\n view: {}, scene: {}\n path: {}\n bbox: [{:.0}, {:.0}] -> [{:.0}, {:.0}] (viewport: {}x{})\n axis: {}\n hint: {}", + v.component, + kind_str, + v.view_index, + v.scene_index, + v.path, + v.bbox.x, + v.bbox.y, + v.bbox.x + v.bbox.w, + v.bbox.y + v.bbox.h, + v.viewport.0, + v.viewport.1, + axis_str, + v.hint, + ) +} + #[cfg(test)] mod tests { use super::*; @@ -698,7 +747,11 @@ mod tests { }"##; let scenario = parse(json); let violations = validate_geometry(&scenario); - assert!(violations.is_empty(), "expected clean, got: {:?}", violations); + assert!( + violations.is_empty(), + "expected clean, got: {:?}", + violations + ); } #[test] @@ -721,12 +774,27 @@ mod tests { }"##; let scenario = parse(json); let violations = validate_geometry(&scenario); - let viewport = violations.iter().find(|v| v.kind == ViolationKind::ViewportOverflow); - assert!(viewport.is_some(), "missing viewport violation in {:?}", violations); + let viewport = violations + .iter() + .find(|v| v.kind == ViolationKind::ViewportOverflow); + assert!( + viewport.is_some(), + "missing viewport violation in {:?}", + violations + ); let v = viewport.unwrap(); - assert_eq!(v.axis, Axis::X, "expected X-axis overflow, got {:?}", v.axis); + assert_eq!( + v.axis, + Axis::X, + "expected X-axis overflow, got {:?}", + v.axis + ); assert_eq!(v.component, "shape"); - assert!(v.path.contains("children[0]"), "path missing index: {}", v.path); + assert!( + v.path.contains("children[0]"), + "path missing index: {}", + v.path + ); } #[test] @@ -751,8 +819,14 @@ mod tests { }"##; let scenario = parse(json); let violations = validate_geometry(&scenario); - let v = violations.iter().find(|v| v.kind == ViolationKind::UnwrappableTextOverflow); - assert!(v.is_some(), "missing UnwrappableTextOverflow in {:?}", violations); + let v = violations + .iter() + .find(|v| v.kind == ViolationKind::UnwrappableTextOverflow); + assert!( + v.is_some(), + "missing UnwrappableTextOverflow in {:?}", + violations + ); let v = v.unwrap(); assert_eq!(v.component, "text"); assert_eq!(v.axis, Axis::X); @@ -801,39 +875,14 @@ mod tests { }"##; let scenario = parse(json); let violations = validate_geometry(&scenario); - let v = violations.iter().find(|v| v.kind == ViolationKind::AutoScrollDisabledOverflow); - assert!(v.is_some(), "missing AutoScrollDisabledOverflow in {:?}", violations); + let v = violations + .iter() + .find(|v| v.kind == ViolationKind::AutoScrollDisabledOverflow); + assert!( + v.is_some(), + "missing AutoScrollDisabledOverflow in {:?}", + violations + ); assert_eq!(v.unwrap().component, "codeblock"); } } - -/// Render a violation for human consumption (multi-line, color-free). -pub fn format_violation(v: &GeometryViolation) -> String { - let axis_str = match v.axis { - Axis::X => "x", - Axis::Y => "y", - Axis::Both => "x+y", - }; - let kind_str = match v.kind { - ViolationKind::ViewportOverflow => "viewport overflow", - ViolationKind::UnwrappableTextOverflow => "wrap=false but text too wide", - ViolationKind::AutoScrollDisabledOverflow => "auto_scroll=false but content too tall", - ViolationKind::AnimatedTextOverflow => "animation pushes content outside viewport", - }; - format!( - "ERROR: {} ({})\n view: {}, scene: {}\n path: {}\n bbox: [{:.0}, {:.0}] -> [{:.0}, {:.0}] (viewport: {}x{})\n axis: {}\n hint: {}", - v.component, - kind_str, - v.view_index, - v.scene_index, - v.path, - v.bbox.x, - v.bbox.y, - v.bbox.x + v.bbox.w, - v.bbox.y + v.bbox.h, - v.viewport.0, - v.viewport.1, - axis_str, - v.hint, - ) -} diff --git a/crates/rustmotion-cli/src/commands/info.rs b/crates/rustmotion-cli/src/commands/info.rs index f381b9c..76920b0 100644 --- a/crates/rustmotion-cli/src/commands/info.rs +++ b/crates/rustmotion-cli/src/commands/info.rs @@ -16,7 +16,10 @@ pub fn cmd_info(input: &PathBuf) -> Result<()> { let total_layers: usize = all_scenes.iter().map(|s| s.children.len()).sum(); println!("File: {}", input.display()); - println!("Resolution: {}x{}", scenario.video.width, scenario.video.height); + println!( + "Resolution: {}x{}", + scenario.video.width, scenario.video.height + ); println!("FPS: {}", fps); println!("Duration: {:.1}s ({} frames)", total_duration, total_frames); println!("Views: {}", scenario.views.len()); @@ -29,7 +32,12 @@ pub fn cmd_info(input: &PathBuf) -> Result<()> { schema::ViewType::Slide => "Slide", schema::ViewType::World => "World", }; - println!(" View {}: {} ({} scenes)", vi + 1, vtype, view.scenes.len()); + println!( + " View {}: {} ({} scenes)", + vi + 1, + vtype, + view.scenes.len() + ); for (si, scene) in view.scenes.iter().enumerate() { let scene_frames = (scene.duration * fps as f64).round() as u32; println!( diff --git a/crates/rustmotion-cli/src/commands/render.rs b/crates/rustmotion-cli/src/commands/render.rs index 34e501c..7d52f38 100644 --- a/crates/rustmotion-cli/src/commands/render.rs +++ b/crates/rustmotion-cli/src/commands/render.rs @@ -1,9 +1,9 @@ +use crate::tui; +use crate::OutputFormat; use rustmotion::encode; use rustmotion::engine; use rustmotion::error::{Result, RustmotionError}; use rustmotion::schema::ResolvedScenario; -use crate::tui; -use crate::OutputFormat; use std::path::{Path, PathBuf}; use crate::commands::validation::{self, ValidationSource}; @@ -31,7 +31,7 @@ fn load_for_watch( pub fn cmd_render( scenario: ResolvedScenario, - output: &PathBuf, + output: &Path, frame: Option, output_format: Option<&OutputFormat>, quiet: bool, @@ -59,7 +59,7 @@ pub fn cmd_render( let png_path = if output.extension().map(|e| e == "mp4").unwrap_or(false) { output.with_extension("png") } else { - output.clone() + output.to_path_buf() }; render_single_frame(&scenario, frame_num, &png_path)?; if !quiet { @@ -67,44 +67,68 @@ pub fn cmd_render( } } else { // Determine output format - let fmt = format.as_deref().unwrap_or_else(|| { - output.extension() - .and_then(|e| e.to_str()) - .unwrap_or("mp4") - }); + let fmt = format + .as_deref() + .unwrap_or_else(|| output.extension().and_then(|e| e.to_str()).unwrap_or("mp4")); let config = &scenario.video; - let output_str = output.to_str().ok_or_else(|| RustmotionError::NonUtf8Path { - path: output.to_string_lossy().into_owned(), - })?; + let output_str = output + .to_str() + .ok_or_else(|| RustmotionError::NonUtf8Path { + path: output.to_string_lossy().into_owned(), + })?; // Helper: create TUI and wrap encode call with progress let make_tui = |codec_label: &str| -> Option { - if quiet { return None; } + if quiet { + return None; + } let total = encode::build_frame_tasks(&scenario).len() as u32; - tui::TuiProgress::new(total, output_str, config.width, config.height, config.fps, codec_label).ok() + tui::TuiProgress::new( + total, + output_str, + config.width, + config.height, + config.fps, + codec_label, + ) + .ok() }; match fmt { "png-seq" => { let mut tui = make_tui("png"); let mut cb = |p: encode::EncodeProgress| { - if let (Some(ref mut t), encode::EncodeProgress::Rendering(c, _)) = (&mut tui, &p) { + if let (Some(ref mut t), encode::EncodeProgress::Rendering(c, _)) = + (&mut tui, &p) + { t.set_progress(*c); } }; - encode::encode_png_sequence(&scenario, output_str, quiet, transparent, Some(&mut cb))?; - if let Some(ref mut t) = tui { t.finish("Done!"); } + encode::encode_png_sequence( + &scenario, + output_str, + quiet, + transparent, + Some(&mut cb), + )?; + if let Some(ref mut t) = tui { + t.finish("Done!"); + } } "gif" => { let mut tui = make_tui("gif"); let mut cb = |p: encode::EncodeProgress| { - if let (Some(ref mut t), encode::EncodeProgress::Rendering(c, _)) = (&mut tui, &p) { + if let (Some(ref mut t), encode::EncodeProgress::Rendering(c, _)) = + (&mut tui, &p) + { t.set_progress(*c); } }; encode::encode_gif(&scenario, output_str, quiet, Some(&mut cb))?; - if let Some(ref mut t) = tui { t.finish("Done!"); } + if let Some(ref mut t) = tui { + t.finish("Done!"); + } } "raw" => { encode::encode_raw_stdout(&scenario, false)?; @@ -131,8 +155,18 @@ pub fn cmd_render( } } }; - encode::encode_with_ffmpeg(&scenario, output_str, quiet, codec_str, crf, transparent, Some(&mut cb))?; - if let Some(ref mut t) = tui { t.finish("Done!"); } + encode::encode_with_ffmpeg( + &scenario, + output_str, + quiet, + codec_str, + crf, + transparent, + Some(&mut cb), + )?; + if let Some(ref mut t) = tui { + t.finish("Done!"); + } } else { let mut tui = make_tui("h264"); let mut cb = |p: encode::EncodeProgress| { @@ -148,7 +182,9 @@ pub fn cmd_render( } }; encode::encode_video(&scenario, output_str, quiet, Some(&mut cb))?; - if let Some(ref mut t) = tui { t.finish("Done!"); } + if let Some(ref mut t) = tui { + t.finish("Done!"); + } } } } @@ -170,7 +206,7 @@ pub fn cmd_render( pub fn cmd_watch( input: &PathBuf, - output: &PathBuf, + output: &Path, frame: Option, output_format: Option<&OutputFormat>, quiet: bool, @@ -182,25 +218,25 @@ pub fn cmd_watch( lenient: bool, strict_anim: bool, ) -> Result<()> { - use notify::{Watcher, RecursiveMode}; + use notify::{RecursiveMode, Watcher}; use std::sync::mpsc; // Determine if we can use incremental rendering (native h264 only) - let fmt = format.as_deref().unwrap_or_else(|| { - output.extension() - .and_then(|e| e.to_str()) - .unwrap_or("mp4") - }); - let use_ffmpeg = codec.as_deref().map_or(false, |c| c != "h264") + let fmt = format + .as_deref() + .unwrap_or_else(|| output.extension().and_then(|e| e.to_str()).unwrap_or("mp4")); + let use_ffmpeg = codec.as_deref().is_some_and(|c| c != "h264") || matches!(fmt, "webm" | "mov") || transparent; - let can_incremental = frame.is_none() - && !matches!(fmt, "png-seq" | "gif" | "raw") - && !use_ffmpeg; + let can_incremental = + frame.is_none() && !matches!(fmt, "png-seq" | "gif" | "raw") && !use_ffmpeg; - let output_str = output.to_str().ok_or_else(|| RustmotionError::NonUtf8Path { - path: output.to_string_lossy().into_owned(), - })?.to_string(); + let output_str = output + .to_str() + .ok_or_else(|| RustmotionError::NonUtf8Path { + path: output.to_string_lossy().into_owned(), + })? + .to_string(); // State for incremental rendering let mut prev_segments: Option> = None; @@ -231,7 +267,8 @@ pub fn cmd_watch( scenario.video.height, scenario.video.fps, codec_label, - ).ok(); + ) + .ok(); } if can_incremental { @@ -243,7 +280,9 @@ pub fn cmd_watch( let mut cb = |progress: encode::EncodeProgress| { if let Some(ref mut tui) = tui_watch { match progress { - encode::EncodeProgress::Rendering(current, total) => tui.set_frame_progress(current, total), + encode::EncodeProgress::Rendering(current, total) => { + tui.set_frame_progress(current, total) + } encode::EncodeProgress::Encoding(current, total) => { if !encoding_started { tui.set_phase(tui::WatchPhase::Encoding); @@ -251,11 +290,19 @@ pub fn cmd_watch( } tui.set_frame_progress(current, total); } - encode::EncodeProgress::Muxing => tui.set_phase(tui::WatchPhase::Muxing), + encode::EncodeProgress::Muxing => { + tui.set_phase(tui::WatchPhase::Muxing) + } } } }; - match encode::encode_video_incremental(&scenario, &output_str, quiet, None, Some(&mut cb)) { + match encode::encode_video_incremental( + &scenario, + &output_str, + quiet, + None, + Some(&mut cb), + ) { Ok(segments) => { prev_segments = Some(segments); prev_config_hash = Some(config_hash); @@ -267,7 +314,17 @@ pub fn cmd_watch( } } else { engine::clear_asset_cache(); - if let Err(e) = cmd_render(scenario, output, frame, output_format, quiet, codec.clone(), crf, format.clone(), transparent) { + if let Err(e) = cmd_render( + scenario, + output, + frame, + output_format, + quiet, + codec.clone(), + crf, + format.clone(), + transparent, + ) { eprintln!("Render error: {}", e); } if let Some(ref mut tui) = tui_watch { @@ -280,13 +337,15 @@ pub fn cmd_watch( let (tx, rx) = mpsc::channel(); - let mut watcher = notify::recommended_watcher(move |res: std::result::Result| { - if let Ok(event) = res { - if event.kind.is_modify() || event.kind.is_create() { - let _ = tx.send(()); + let mut watcher = notify::recommended_watcher( + move |res: std::result::Result| { + if let Ok(event) = res { + if event.kind.is_modify() || event.kind.is_create() { + let _ = tx.send(()); + } } - } - })?; + }, + )?; watcher.watch(input.as_ref(), RecursiveMode::NonRecursive)?; @@ -337,33 +396,47 @@ pub fn cmd_watch( }; // Count changed scenes for the TUI - let view0_scenes = scenario.views.get(0).map(|v| &v.scenes[..]).unwrap_or(&[]); + let view0_scenes = scenario.views.first().map(|v| &v.scenes[..]).unwrap_or(&[]); let num_scenes = view0_scenes.len(); - let scene_hashes: Vec = view0_scenes.iter() - .map(|s| encode::hash_video_config_scene(s)) + let scene_hashes: Vec = view0_scenes + .iter() + .map(encode::hash_video_config_scene) .collect(); - let changed = if let Some(ref prev) = use_prev { + let changed = if let Some(prev) = use_prev { if prev.len() == num_scenes { - (0..num_scenes).filter(|&i| { - let hash_changed = scene_hashes[i] != prev[i].scene_hash; - let next_changed = if i + 1 < num_scenes { - scene_hashes[i + 1] != prev[i + 1].scene_hash - && view0_scenes[i + 1].transition.is_some() - } else { false }; - hash_changed || next_changed - }).count() - } else { num_scenes } - } else { num_scenes }; + (0..num_scenes) + .filter(|&i| { + let hash_changed = scene_hashes[i] != prev[i].scene_hash; + let next_changed = if i + 1 < num_scenes { + scene_hashes[i + 1] != prev[i + 1].scene_hash + && view0_scenes[i + 1].transition.is_some() + } else { + false + }; + hash_changed || next_changed + }) + .count() + } else { + num_scenes + } + } else { + num_scenes + }; if let Some(ref mut tui) = tui_watch { - tui.set_phase(tui::WatchPhase::Rerendering { changed, total: num_scenes }); + tui.set_phase(tui::WatchPhase::Rerendering { + changed, + total: num_scenes, + }); } let mut encoding_started = false; let mut cb = |progress: encode::EncodeProgress| { if let Some(ref mut tui) = tui_watch { match progress { - encode::EncodeProgress::Rendering(current, total) => tui.set_frame_progress(current, total), + encode::EncodeProgress::Rendering(current, total) => { + tui.set_frame_progress(current, total) + } encode::EncodeProgress::Encoding(current, total) => { if !encoding_started { tui.set_phase(tui::WatchPhase::Encoding); @@ -371,11 +444,19 @@ pub fn cmd_watch( } tui.set_frame_progress(current, total); } - encode::EncodeProgress::Muxing => tui.set_phase(tui::WatchPhase::Muxing), + encode::EncodeProgress::Muxing => { + tui.set_phase(tui::WatchPhase::Muxing) + } } } }; - match encode::encode_video_incremental(&scenario, &output_str, quiet, use_prev, Some(&mut cb)) { + match encode::encode_video_incremental( + &scenario, + &output_str, + quiet, + use_prev, + Some(&mut cb), + ) { Ok(segments) => { prev_segments = Some(segments); prev_config_hash = Some(config_hash); @@ -387,7 +468,17 @@ pub fn cmd_watch( } } else { engine::clear_asset_cache(); - if let Err(e) = cmd_render(scenario, output, frame, output_format, quiet, codec.clone(), crf, format.clone(), transparent) { + if let Err(e) = cmd_render( + scenario, + output, + frame, + output_format, + quiet, + codec.clone(), + crf, + format.clone(), + transparent, + ) { eprintln!("Render error: {}", e); } if let Some(ref mut tui) = tui_watch { @@ -418,7 +509,11 @@ pub fn cmd_watch( } } -fn render_single_frame(scenario: &ResolvedScenario, frame_num: u32, output: &PathBuf) -> Result<()> { +fn render_single_frame( + scenario: &ResolvedScenario, + frame_num: u32, + output: &PathBuf, +) -> Result<()> { // Use the same task pipeline as the encoder so `--frame N` always shows // exactly what frame N of the rendered MP4 contains. Summing scene // durations directly would skip transition overlaps and drift the @@ -428,7 +523,10 @@ fn render_single_frame(scenario: &ResolvedScenario, frame_num: u32, output: &Pat let total = tasks.len() as u32; let task = tasks .get(frame_num as usize) - .ok_or(RustmotionError::FrameOutOfRange { frame: frame_num, total })?; + .ok_or(RustmotionError::FrameOutOfRange { + frame: frame_num, + total, + })?; let rgba = encode::render_frame_task_scaled(config, scenario, task, 1.0)?; let img = image::RgbaImage::from_raw(config.width, config.height, rgba) diff --git a/crates/rustmotion-cli/src/commands/still.rs b/crates/rustmotion-cli/src/commands/still.rs index c46ac40..8da5c13 100644 --- a/crates/rustmotion-cli/src/commands/still.rs +++ b/crates/rustmotion-cli/src/commands/still.rs @@ -29,7 +29,10 @@ pub fn cmd_still( let scene_frames = (scene.duration * fps as f64).round() as u32; let rgba = engine::render::render_scene_frame( - config, scene, frame_index.min(scene_frames.saturating_sub(1)), scene_frames, + config, + scene, + frame_index.min(scene_frames.saturating_sub(1)), + scene_frames, )?; // Create parent directories @@ -42,11 +45,9 @@ pub fn cmd_still( let img = image::RgbaImage::from_raw(config.width, config.height, rgba) .ok_or(RustmotionError::PixelImage)?; - let fmt = format.as_deref().unwrap_or_else(|| { - output.extension() - .and_then(|e| e.to_str()) - .unwrap_or("png") - }); + let fmt = format + .as_deref() + .unwrap_or_else(|| output.extension().and_then(|e| e.to_str()).unwrap_or("png")); match fmt { "jpeg" | "jpg" => { @@ -82,5 +83,5 @@ pub fn cmd_still( scene_start = scene_end; } - Err(RustmotionError::TimeOutOfRange { time }.into()) + Err(RustmotionError::TimeOutOfRange { time }) } diff --git a/crates/rustmotion-cli/src/commands/validate.rs b/crates/rustmotion-cli/src/commands/validate.rs index 5953a72..ca3b9ec 100644 --- a/crates/rustmotion-cli/src/commands/validate.rs +++ b/crates/rustmotion-cli/src/commands/validate.rs @@ -108,9 +108,7 @@ fn apply_fixes(root: &mut serde_json::Value, violations: &[GeometryViolation]) - match v.kind { ViolationKind::UnwrappableTextOverflow => { if let Some(obj) = target.as_object_mut() { - let style = obj - .entry("style") - .or_insert_with(|| serde_json::json!({})); + let style = obj.entry("style").or_insert_with(|| serde_json::json!({})); if let Some(style_obj) = style.as_object_mut() { style_obj.insert("wrap".into(), serde_json::Value::Bool(true)); applied += 1; @@ -134,10 +132,7 @@ fn apply_fixes(root: &mut serde_json::Value, violations: &[GeometryViolation]) - /// Walk a path like `views[0].scenes[1].children[2].children[0]` against the /// raw JSON, transparently handling the legacy `scenes` and `composition` shapes. -fn navigate<'a>( - root: &'a mut serde_json::Value, - path: &str, -) -> Option<&'a mut serde_json::Value> { +fn navigate<'a>(root: &'a mut serde_json::Value, path: &str) -> Option<&'a mut serde_json::Value> { let segments = parse_segments(path); let mut cursor: &mut serde_json::Value = root; let mut idx = 0; diff --git a/crates/rustmotion-cli/src/commands/validate_schema.rs b/crates/rustmotion-cli/src/commands/validate_schema.rs index c4bda41..3c0027e 100644 --- a/crates/rustmotion-cli/src/commands/validate_schema.rs +++ b/crates/rustmotion-cli/src/commands/validate_schema.rs @@ -12,10 +12,8 @@ pub fn validate_scenario(scenario: &ResolvedScenario) -> (Vec, Vec 0".to_string()); } - if scenario.video.width % 2 != 0 || scenario.video.height % 2 != 0 { - errors.push( - "video.width and video.height must be even (required for H.264)".to_string(), - ); + if !scenario.video.width.is_multiple_of(2) || !scenario.video.height.is_multiple_of(2) { + errors.push("video.width and video.height must be even (required for H.264)".to_string()); } if scenario.video.fps == 0 { errors.push("video.fps must be > 0".to_string()); @@ -29,10 +27,7 @@ pub fn validate_scenario(scenario: &ResolvedScenario) -> (Vec, Vec 0", - vi, si - )); + errors.push(format!("views[{}].scenes[{}].duration must be > 0", vi, si)); } let children = rustmotion::engine::render::deserialize_children(scene); @@ -48,10 +43,7 @@ pub fn validate_scenario(scenario: &ResolvedScenario) -> (Vec, Vec= e { - errors.push(format!( - "{}: start_at ({}) must be < end_at ({})", - p, s, e - )); + errors.push(format!("{}: start_at ({}) must be < end_at ({})", p, s, e)); } } } // Animation completion budget check: ensure entrance animations finish within the scene. if let Some(anim) = child.component.as_animatable() { - let start_at = child.component.as_timed() + let start_at = child + .component + .as_timed() .and_then(|t| t.timing().0) .unwrap_or(0.0); @@ -155,8 +146,10 @@ fn validate_children( } } Component::Counter(c) => { - let from_len = counter_display_len(c.from, c.decimals, &c.separator, &c.prefix, &c.suffix); - let to_len = counter_display_len(c.to, c.decimals, &c.separator, &c.prefix, &c.suffix); + let from_len = + counter_display_len(c.from, c.decimals, &c.separator, &c.prefix, &c.suffix); + let to_len = + counter_display_len(c.to, c.decimals, &c.separator, &c.prefix, &c.suffix); if from_len != to_len { warnings.push(format!( "{}: counter from={} to={} — display width changes from {} to {} chars. \ @@ -300,7 +293,11 @@ fn counter_display_len( }; let sign = if value < 0.0 { 1 } else { 0 }; - let decimal_chars = if decimals > 0 { 1 + decimals as usize } else { 0 }; + let decimal_chars = if decimals > 0 { + 1 + decimals as usize + } else { + 0 + }; let prefix_len = prefix.as_deref().map(str::len).unwrap_or(0); let suffix_len = suffix.as_deref().map(str::len).unwrap_or(0); diff --git a/crates/rustmotion-cli/src/commands/validation.rs b/crates/rustmotion-cli/src/commands/validation.rs index 3b4dc8a..ffffa86 100644 --- a/crates/rustmotion-cli/src/commands/validation.rs +++ b/crates/rustmotion-cli/src/commands/validation.rs @@ -92,7 +92,11 @@ pub fn load(source: ValidationSource<'_>) -> Result { } else { s }; - (s, Some(path.to_path_buf()), IncludeSource::File(path.to_path_buf())) + ( + s, + Some(path.to_path_buf()), + IncludeSource::File(path.to_path_buf()), + ) } ValidationSource::Inline(json) => (json.to_string(), None, IncludeSource::Inline), }; @@ -190,31 +194,14 @@ pub fn warn_on_silent_defaults(loaded: &LoadedScenario) { } } -#[cfg(test)] -mod misplaced_animation_tests { - use super::*; - use serde_json::json; - - #[test] - fn flags_only_top_level_animation() { - let raw = json!({ - "scenes": [{ "children": [ - { "type": "text", "style": { "color": "#fff" }, "animation": [{ "name": "fade_in" }] }, - { "type": "text", "style": { "color": "#fff", "animation": [{ "name": "fade_in" }] } } - ]}] - }); - let w = warn_misplaced_animation(&raw); - assert_eq!(w.len(), 1, "only the top-level animation should warn: {w:?}"); - assert!(w[0].contains("style.animation")); - } -} - /// Validate `--codec` against the list ffmpeg can drive. Defaults to OK if None. pub fn check_codec(codec: Option<&str>) -> Result<()> { if let Some(c) = codec { let allowed = ["h264", "h265", "vp9", "prores"]; if !allowed.contains(&c) { - return Err(RustmotionError::UnknownCodec { codec: c.to_string() }); + return Err(RustmotionError::UnknownCodec { + codec: c.to_string(), + }); } } Ok(()) @@ -255,3 +242,26 @@ pub fn print_report(report: &ValidationReport, source_label: &str) { } } } + +#[cfg(test)] +mod misplaced_animation_tests { + use super::*; + use serde_json::json; + + #[test] + fn flags_only_top_level_animation() { + let raw = json!({ + "scenes": [{ "children": [ + { "type": "text", "style": { "color": "#fff" }, "animation": [{ "name": "fade_in" }] }, + { "type": "text", "style": { "color": "#fff", "animation": [{ "name": "fade_in" }] } } + ]}] + }); + let w = warn_misplaced_animation(&raw); + assert_eq!( + w.len(), + 1, + "only the top-level animation should warn: {w:?}" + ); + assert!(w[0].contains("style.animation")); + } +} diff --git a/crates/rustmotion-cli/src/lib.rs b/crates/rustmotion-cli/src/lib.rs index 94a6b39..5cd471c 100644 --- a/crates/rustmotion-cli/src/lib.rs +++ b/crates/rustmotion-cli/src/lib.rs @@ -9,7 +9,11 @@ use std::io::Write; use std::path::PathBuf; #[derive(Parser)] -#[command(name = "rustmotion", version, about = "Render motion design videos from JSON scenarios")] +#[command( + name = "rustmotion", + version, + about = "Render motion design videos from JSON scenarios" +)] struct Cli { #[command(subcommand)] command: Commands, @@ -219,16 +223,24 @@ pub fn run() -> Result<()> { } match cli.command { - Commands::Studio { file } => { - match load_input(&file) { - Ok(scenario) => rustmotion_studio::run_preview(scenario, Some(file), true), - Err(e) => rustmotion_studio::run_preview_with_error(format!("{}", e), Some(file), true), - } - } + Commands::Studio { file } => match load_input(&file) { + Ok(scenario) => rustmotion_studio::run_preview(scenario, Some(file), true), + Err(e) => rustmotion_studio::run_preview_with_error(format!("{}", e), Some(file), true), + }, Commands::Render { - file, json, output, frame, output_format, - codec, crf, format, transparent, watch, - no_validate, lenient, strict_anim, + file, + json, + output, + frame, + output_format, + codec, + crf, + format, + transparent, + watch, + no_validate, + lenient, + strict_anim, } => { // Validate codec / CRF up-front so we never spawn an encoder with bad args. commands::validation::check_codec(codec.as_deref())?; @@ -237,9 +249,18 @@ pub fn run() -> Result<()> { if watch { let input_path = file.ok_or(RustmotionError::WatchRequiresFile)?; commands::cmd_watch( - &input_path, &output, frame, output_format.as_ref(), - cli.quiet, codec, crf, format, transparent, - no_validate, lenient, strict_anim, + &input_path, + &output, + frame, + output_format.as_ref(), + cli.quiet, + codec, + crf, + format, + transparent, + no_validate, + lenient, + strict_anim, ) } else { let source = match (file.as_ref(), json.as_deref()) { @@ -271,24 +292,44 @@ pub fn run() -> Result<()> { } commands::cmd_render( - loaded.scenario, &output, frame, output_format.as_ref(), - cli.quiet, codec, crf, format, transparent, + loaded.scenario, + &output, + frame, + output_format.as_ref(), + cli.quiet, + codec, + crf, + format, + transparent, ) } } - Commands::Still { file, output, time, format, quality } => { + Commands::Still { + file, + output, + time, + format, + quality, + } => { let scenario = load_input(&file)?; commands::cmd_still(scenario, &output, time, format, quality) } - Commands::Validate { file, report, fix, strict_anim, lenient } => { - commands::cmd_validate(&file, report.as_deref(), fix, strict_anim, lenient) - } + Commands::Validate { + file, + report, + fix, + strict_anim, + lenient, + } => commands::cmd_validate(&file, report.as_deref(), fix, strict_anim, lenient), Commands::Schema { output } => commands::cmd_schema(output.as_deref()), Commands::Info { file } => commands::cmd_info(&file), Commands::Skills { action } => match action { SkillsAction::Install { global } => skills::install(global), SkillsAction::Uninstall { global } => skills::uninstall(global), - SkillsAction::List => { skills::list(); Ok(()) } + SkillsAction::List => { + skills::list(); + Ok(()) + } SkillsAction::Show { name } => skills::show(&name), }, Commands::Completions { action } => match action { @@ -309,7 +350,7 @@ pub fn run() -> Result<()> { } CompletionsAction::Install => install_completions(), CompletionsAction::Uninstall => uninstall_completions(), - } + }, } } @@ -328,15 +369,23 @@ fn detect_shell() -> Option { fn install_completions() -> Result<()> { let shell = detect_shell().ok_or_else(|| { - RustmotionError::Generic("Could not detect shell. Use 'completions generate ' instead.".into()) + RustmotionError::Generic( + "Could not detect shell. Use 'completions generate ' instead.".into(), + ) })?; let mut buf = Vec::new(); clap_complete::generate(shell, &mut Cli::command(), "rustmotion", &mut buf); - clap_complete::generate(shell, &mut rustmotion_studio::command(), "rustmotion-studio", &mut buf); + clap_complete::generate( + shell, + &mut rustmotion_studio::command(), + "rustmotion-studio", + &mut buf, + ); let completions = String::from_utf8(buf).unwrap(); - let home = dirs::home_dir().ok_or_else(|| RustmotionError::Generic("Could not find home directory".into()))?; + let home = dirs::home_dir() + .ok_or_else(|| RustmotionError::Generic("Could not find home directory".into()))?; match shell { clap_complete::Shell::Zsh => { @@ -350,7 +399,10 @@ fn install_completions() -> Result<()> { let zshrc = home.join(".zshrc"); let zshrc_content = std::fs::read_to_string(&zshrc).unwrap_or_default(); if !zshrc_content.contains(".zfunc") { - let mut f = std::fs::OpenOptions::new().append(true).create(true).open(&zshrc)?; + let mut f = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(&zshrc)?; writeln!(f, "\n# rustmotion completions")?; writeln!(f, "fpath=(~/.zfunc $fpath)")?; writeln!(f, "autoload -Uz compinit && compinit")?; @@ -375,7 +427,10 @@ fn install_completions() -> Result<()> { eprintln!("Installed completions to {}", path.display()); } _ => { - return Err(RustmotionError::Generic(format!("Unsupported shell for install: {:?}. Use 'completions generate' instead.", shell)).into()); + return Err(RustmotionError::Generic(format!( + "Unsupported shell for install: {:?}. Use 'completions generate' instead.", + shell + ))); } } @@ -383,11 +438,11 @@ fn install_completions() -> Result<()> { } fn uninstall_completions() -> Result<()> { - let shell = detect_shell().ok_or_else(|| { - RustmotionError::Generic("Could not detect shell.".into()) - })?; + let shell = + detect_shell().ok_or_else(|| RustmotionError::Generic("Could not detect shell.".into()))?; - let home = dirs::home_dir().ok_or_else(|| RustmotionError::Generic("Could not find home directory".into()))?; + let home = dirs::home_dir() + .ok_or_else(|| RustmotionError::Generic("Could not find home directory".into()))?; match shell { clap_complete::Shell::Zsh => { @@ -401,12 +456,15 @@ fn uninstall_completions() -> Result<()> { let zshrc = home.join(".zshrc"); if zshrc.exists() { let content = std::fs::read_to_string(&zshrc)?; - let filtered: Vec<&str> = content.lines().filter(|line| { - let trimmed = line.trim(); - trimmed != "# rustmotion completions" - && !(trimmed.contains(".zfunc") && trimmed.starts_with("fpath=")) - && !(trimmed.contains("compinit") && trimmed.starts_with("autoload")) - }).collect(); + let filtered: Vec<&str> = content + .lines() + .filter(|line| { + let trimmed = line.trim(); + trimmed != "# rustmotion completions" + && !(trimmed.contains(".zfunc") && trimmed.starts_with("fpath=")) + && !(trimmed.contains("compinit") && trimmed.starts_with("autoload")) + }) + .collect(); std::fs::write(&zshrc, filtered.join("\n") + "\n")?; eprintln!("Cleaned ~/.zshrc"); } @@ -426,7 +484,10 @@ fn uninstall_completions() -> Result<()> { } } _ => { - return Err(RustmotionError::Generic(format!("Unsupported shell: {:?}", shell)).into()); + return Err(RustmotionError::Generic(format!( + "Unsupported shell: {:?}", + shell + ))); } } diff --git a/crates/rustmotion-cli/src/skills.rs b/crates/rustmotion-cli/src/skills.rs index 02be432..2a3147e 100644 --- a/crates/rustmotion-cli/src/skills.rs +++ b/crates/rustmotion-cli/src/skills.rs @@ -14,37 +14,129 @@ const CLAUDE_MD: &str = include_str!("../../../CLAUDE.md"); /// All skill files embedded at compile time. const SKILL_FILES: &[SkillFile] = &[ - SkillFile { path: ".claude/skills/rustmotion/SKILL.md", content: include_str!("../../../.claude/skills/rustmotion/SKILL.md") }, + SkillFile { + path: ".claude/skills/rustmotion/SKILL.md", + content: include_str!("../../../.claude/skills/rustmotion/SKILL.md"), + }, // Rules - SkillFile { path: ".claude/skills/rustmotion/rules/3d-perspective.md", content: include_str!("../../../.claude/skills/rustmotion/rules/3d-perspective.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/card-flex-layout.md", content: include_str!("../../../.claude/skills/rustmotion/rules/card-flex-layout.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/chart-types.md", content: include_str!("../../../.claude/skills/rustmotion/rules/chart-types.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/continuous-presets.md", content: include_str!("../../../.claude/skills/rustmotion/rules/continuous-presets.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/counter-standalone.md", content: include_str!("../../../.claude/skills/rustmotion/rules/counter-standalone.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/data-viz-components.md", content: include_str!("../../../.claude/skills/rustmotion/rules/data-viz-components.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/dot-map-coordinates.md", content: include_str!("../../../.claude/skills/rustmotion/rules/dot-map-coordinates.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/easing-guidelines.md", content: include_str!("../../../.claude/skills/rustmotion/rules/easing-guidelines.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/even-dimensions.md", content: include_str!("../../../.claude/skills/rustmotion/rules/even-dimensions.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/gradient-quality.md", content: include_str!("../../../.claude/skills/rustmotion/rules/gradient-quality.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/grid-card-height.md", content: include_str!("../../../.claude/skills/rustmotion/rules/grid-card-height.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/hex-colors.md", content: include_str!("../../../.claude/skills/rustmotion/rules/hex-colors.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/icon-format.md", content: include_str!("../../../.claude/skills/rustmotion/rules/icon-format.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/layer-order.md", content: include_str!("../../../.claude/skills/rustmotion/rules/layer-order.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/module-structure.md", content: include_str!("../../../.claude/skills/rustmotion/rules/module-structure.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/notification-stacking.md", content: include_str!("../../../.claude/skills/rustmotion/rules/notification-stacking.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/paint-context.md", content: include_str!("../../../.claude/skills/rustmotion/rules/paint-context.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/prefer-presets.md", content: include_str!("../../../.claude/skills/rustmotion/rules/prefer-presets.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/responsive-device-sizing.md", content: include_str!("../../../.claude/skills/rustmotion/rules/responsive-device-sizing.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/stagger-animations.md", content: include_str!("../../../.claude/skills/rustmotion/rules/stagger-animations.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/stat-cards.md", content: include_str!("../../../.claude/skills/rustmotion/rules/stat-cards.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/text-background.md", content: include_str!("../../../.claude/skills/rustmotion/rules/text-background.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/timeline-sequencing.md", content: include_str!("../../../.claude/skills/rustmotion/rules/timeline-sequencing.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/timing-constraints.md", content: include_str!("../../../.claude/skills/rustmotion/rules/timing-constraints.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/ui-controls.md", content: include_str!("../../../.claude/skills/rustmotion/rules/ui-controls.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/validate-json.md", content: include_str!("../../../.claude/skills/rustmotion/rules/validate-json.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/vertical-align.md", content: include_str!("../../../.claude/skills/rustmotion/rules/vertical-align.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/video-wizard.md", content: include_str!("../../../.claude/skills/rustmotion/rules/video-wizard.md") }, - SkillFile { path: ".claude/skills/rustmotion/rules/wiggle-additive.md", content: include_str!("../../../.claude/skills/rustmotion/rules/wiggle-additive.md") }, + SkillFile { + path: ".claude/skills/rustmotion/rules/3d-perspective.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/3d-perspective.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/card-flex-layout.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/card-flex-layout.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/chart-types.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/chart-types.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/continuous-presets.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/continuous-presets.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/counter-standalone.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/counter-standalone.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/data-viz-components.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/data-viz-components.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/dot-map-coordinates.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/dot-map-coordinates.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/easing-guidelines.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/easing-guidelines.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/even-dimensions.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/even-dimensions.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/gradient-quality.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/gradient-quality.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/grid-card-height.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/grid-card-height.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/hex-colors.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/hex-colors.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/icon-format.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/icon-format.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/layer-order.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/layer-order.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/module-structure.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/module-structure.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/notification-stacking.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/notification-stacking.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/paint-context.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/paint-context.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/prefer-presets.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/prefer-presets.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/responsive-device-sizing.md", + content: include_str!( + "../../../.claude/skills/rustmotion/rules/responsive-device-sizing.md" + ), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/stagger-animations.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/stagger-animations.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/stat-cards.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/stat-cards.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/text-background.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/text-background.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/timeline-sequencing.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/timeline-sequencing.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/timing-constraints.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/timing-constraints.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/ui-controls.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/ui-controls.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/validate-json.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/validate-json.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/vertical-align.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/vertical-align.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/video-wizard.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/video-wizard.md"), + }, + SkillFile { + path: ".claude/skills/rustmotion/rules/wiggle-additive.md", + content: include_str!("../../../.claude/skills/rustmotion/rules/wiggle-additive.md"), + }, ]; /// Resolve the target directory for skill installation. @@ -52,7 +144,10 @@ fn resolve_target(global: bool) -> Result { if global { let home = dirs::home_dir().ok_or(RustmotionError::FileRead { path: "~".to_string(), - source: std::io::Error::new(std::io::ErrorKind::NotFound, "Could not determine home directory"), + source: std::io::Error::new( + std::io::ErrorKind::NotFound, + "Could not determine home directory", + ), })?; Ok(home) } else { @@ -103,12 +198,22 @@ pub fn install(global: bool) -> Result<()> { } } - let location = if global { "~/.claude/skills/rustmotion/" } else { ".claude/skills/rustmotion/" }; + let location = if global { + "~/.claude/skills/rustmotion/" + } else { + ".claude/skills/rustmotion/" + }; if written == 0 { - println!("Skills already up to date ({} files) in {}", skipped, location); + println!( + "Skills already up to date ({} files) in {}", + skipped, location + ); } else { - println!("Installed {} file(s) to {} ({} unchanged)", written, location, skipped); + println!( + "Installed {} file(s) to {} ({} unchanged)", + written, location, skipped + ); if !global { println!("Claude Code will now use rustmotion skills in this project."); } @@ -126,7 +231,13 @@ pub fn list() { if sf.path.contains("/rules/") { let name = sf.path.rsplit('/').next().unwrap_or(sf.path); // Extract first line as title - let title = sf.content.lines().next().unwrap_or("").trim_start_matches("# ").trim_start_matches("Rule: "); + let title = sf + .content + .lines() + .next() + .unwrap_or("") + .trim_start_matches("# ") + .trim_start_matches("Rule: "); println!(" {:<35} {}", name, title); } } @@ -141,7 +252,12 @@ pub fn show(name: &str) -> Result<()> { // Try matching by filename (with or without .md) let needle = name.trim_end_matches(".md"); for sf in SKILL_FILES { - let filename = sf.path.rsplit('/').next().unwrap_or("").trim_end_matches(".md"); + let filename = sf + .path + .rsplit('/') + .next() + .unwrap_or("") + .trim_end_matches(".md"); if filename == needle { print!("{}", sf.content); return Ok(()); @@ -154,7 +270,9 @@ pub fn show(name: &str) -> Result<()> { return Ok(()); } - Err(RustmotionError::UnknownSkill { name: name.to_string() }) + Err(RustmotionError::UnknownSkill { + name: name.to_string(), + }) } /// Uninstall skills from the target directory. @@ -163,7 +281,11 @@ pub fn show(name: &str) -> Result<()> { pub fn uninstall(global: bool) -> Result<()> { let root = resolve_target(global)?; let skills_dir = root.join(".claude/skills/rustmotion"); - let location = if global { "~/.claude/skills/rustmotion/" } else { ".claude/skills/rustmotion/" }; + let location = if global { + "~/.claude/skills/rustmotion/" + } else { + ".claude/skills/rustmotion/" + }; if !skills_dir.exists() { println!("Nothing to remove — {} does not exist.", location); @@ -192,6 +314,9 @@ pub fn uninstall(global: bool) -> Result<()> { } } - println!("Removed rustmotion skills from {} ({} items)", location, removed); + println!( + "Removed rustmotion skills from {} ({} items)", + location, removed + ); Ok(()) } diff --git a/crates/rustmotion-cli/src/tui.rs b/crates/rustmotion-cli/src/tui.rs index 97bd720..6f1c0e2 100644 --- a/crates/rustmotion-cli/src/tui.rs +++ b/crates/rustmotion-cli/src/tui.rs @@ -1,8 +1,8 @@ use std::io::{self, Stderr}; use std::time::Instant; -use crossterm::execute; use crossterm::cursor::{Hide, Show}; +use crossterm::execute; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Layout}, @@ -85,10 +85,7 @@ impl TuiProgress { } fn cleanup(&mut self) { - let _ = execute!( - self.terminal.backend_mut(), - Show, - ); + let _ = execute!(self.terminal.backend_mut(), Show,); } fn draw(&mut self) -> rustmotion::error::Result<()> { @@ -293,22 +290,18 @@ impl TuiWatch { ), WatchPhase::Watching => ( if state.render_count > 0 { - format!("Waiting for changes (incremental mode ready, {} render{})", + format!( + "Waiting for changes (incremental mode ready, {} render{})", state.render_count, - if state.render_count > 1 { "s" } else { "" }) + if state.render_count > 1 { "s" } else { "" } + ) } else { "Waiting for changes (incremental mode ready)".to_string() }, Color::Green, ), - WatchPhase::Encoding => ( - "Encoding H.264...".to_string(), - Color::Yellow, - ), - WatchPhase::Muxing => ( - "Muxing MP4...".to_string(), - Color::Yellow, - ), + WatchPhase::Encoding => ("Encoding H.264...".to_string(), Color::Yellow), + WatchPhase::Muxing => ("Muxing MP4...".to_string(), Color::Yellow), WatchPhase::Rerendering { changed, total } => ( format!("Re-rendering {}/{} changed scenes...", changed, total), Color::Cyan, @@ -334,10 +327,7 @@ impl TuiWatch { let input_path = state.input_path.clone(); let output_path = state.output_path.clone(); - let config_line = format!( - "{} @ {}fps · {}", - state.resolution, state.fps, state.codec - ); + let config_line = format!("{} @ {}fps · {}", state.resolution, state.fps, state.codec); // Build all 5 lines (fixed height, always overwrite everything) let line0 = Line::from(vec![ @@ -362,7 +352,10 @@ impl TuiWatch { WatchPhase::Encoding => "Encode ", _ => "Frames ", }; - let progress_label = format!("{}/{} {}%", state.frames_current, state.frames_total, percent); + let progress_label = format!( + "{}/{} {}%", + state.frames_current, state.frames_total, percent + ); let bar_width: usize = 20; let filled = (ratio * bar_width as f64).round() as usize; let unfilled = bar_width - filled; @@ -401,10 +394,7 @@ impl TuiWatch { } fn cleanup(&mut self) { - let _ = execute!( - self.terminal.backend_mut(), - Show, - ); + let _ = execute!(self.terminal.backend_mut(), Show,); } } diff --git a/crates/rustmotion-components/src/arrow.rs b/crates/rustmotion-components/src/arrow.rs index 7572bdc..f014a12 100644 --- a/crates/rustmotion-components/src/arrow.rs +++ b/crates/rustmotion-components/src/arrow.rs @@ -65,10 +65,18 @@ pub struct ControlPoint { pub y: f32, } -fn default_arrow_width() -> f32 { 3.0 } -fn default_arrow_color() -> String { "#FFFFFF".to_string() } -fn default_arrow_size() -> f32 { 12.0 } -fn default_true() -> bool { true } +fn default_arrow_width() -> f32 { + 3.0 +} +fn default_arrow_color() -> String { + "#FFFFFF".to_string() +} +fn default_arrow_size() -> f32 { + 12.0 +} +fn default_true() -> bool { + true +} rustmotion_core::impl_traits!(Arrow { Animatable => animation, @@ -108,10 +116,18 @@ impl Arrow { } /// Draw an arrowhead at the given position along the path. - fn draw_arrowhead(canvas: &Canvas, path: &Path, at_end: bool, size: f32, paint: &skia_safe::Paint) { + fn draw_arrowhead( + canvas: &Canvas, + path: &Path, + at_end: bool, + size: f32, + paint: &skia_safe::Paint, + ) { let mut measure = PathMeasure::new(path, false, None); let total_len = measure.length(); - if total_len < 1.0 { return; } + if total_len < 1.0 { + return; + } let (pos, tangent) = if at_end { let dist = total_len - 0.1; diff --git a/crates/rustmotion-components/src/avatar.rs b/crates/rustmotion-components/src/avatar.rs index 532b409..9f5aa8e 100644 --- a/crates/rustmotion-components/src/avatar.rs +++ b/crates/rustmotion-components/src/avatar.rs @@ -2,7 +2,7 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::error::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, Paint, PaintStyle, Rect, RRect}; +use skia_safe::{Canvas, Paint, PaintStyle, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; @@ -13,20 +13,16 @@ use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum AvatarStatus { Online, Offline, Away, #[serde(rename = "none")] + #[default] NoStatus, } -impl Default for AvatarStatus { - fn default() -> Self { - Self::NoStatus - } -} - impl AvatarStatus { fn default_color(&self) -> &str { match self { @@ -81,11 +77,16 @@ impl Avatar { let img = if let Some(cached) = cache.get(&self.src) { cached.clone() } else { - let data = std::fs::read(&self.src) - .map_err(|e| RustmotionError::ImageLoad { path: self.src.clone(), reason: e.to_string() })?; + let data = std::fs::read(&self.src).map_err(|e| RustmotionError::ImageLoad { + path: self.src.clone(), + reason: e.to_string(), + })?; let skia_data = skia_safe::Data::new_copy(&data); - let decoded = skia_safe::Image::from_encoded(skia_data) - .ok_or_else(|| RustmotionError::ImageDecode { path: self.src.clone() })?; + let decoded = skia_safe::Image::from_encoded(skia_data).ok_or_else(|| { + RustmotionError::ImageDecode { + path: self.src.clone(), + } + })?; cache.insert(self.src.clone(), decoded.clone()); decoded }; diff --git a/crates/rustmotion-components/src/avatar_group.rs b/crates/rustmotion-components/src/avatar_group.rs index edcb1fd..4f5bac1 100644 --- a/crates/rustmotion-components/src/avatar_group.rs +++ b/crates/rustmotion-components/src/avatar_group.rs @@ -2,7 +2,7 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::error::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, Paint, PaintStyle, Rect, RRect}; +use skia_safe::{Canvas, Paint, PaintStyle, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; @@ -120,7 +120,8 @@ impl AvatarGroup { let cx = x + s / 2.0; let cy = s / 2.0; - let clip_rect = Rect::from_xywh(cx - inner_r, cy - inner_r, inner_r * 2.0, inner_r * 2.0); + let clip_rect = + Rect::from_xywh(cx - inner_r, cy - inner_r, inner_r * 2.0, inner_r * 2.0); let clip_rrect = RRect::new_oval(clip_rect); canvas.save(); @@ -183,7 +184,14 @@ impl AvatarGroup { let text_y = cy + (-metrics.ascent) / 2.0; draw_text_with_fallback( - canvas, &text, &font, &emoji_font, 0.0, text_x, text_y, &text_paint, + canvas, + &text, + &font, + &emoji_font, + 0.0, + text_x, + text_y, + &text_paint, ); } diff --git a/crates/rustmotion-components/src/badge.rs b/crates/rustmotion-components/src/badge.rs index 79bf043..da60b8e 100644 --- a/crates/rustmotion-components/src/badge.rs +++ b/crates/rustmotion-components/src/badge.rs @@ -5,38 +5,33 @@ use skia_safe::{Canvas, ColorType, ImageInfo, Paint, PaintStyle, RRect, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{asset_cache, fetch_icon_svg, font_mgr, paint_from_hex, emoji_typeface, draw_text_with_fallback, measure_text_with_fallback}; +use rustmotion_core::engine::renderer::{ + asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, font_mgr, + measure_text_with_fallback, paint_from_hex, +}; use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum BadgeVariant { + #[default] Solid, Outline, } -impl Default for BadgeVariant { - fn default() -> Self { - Self::Solid - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum BadgeSize { Sm, + #[default] Md, Lg, } -impl Default for BadgeSize { - fn default() -> Self { - Self::Md - } -} - impl BadgeSize { fn params(&self) -> (f32, f32, f32, f32) { // (font_size, h_padding, v_padding, icon_size) @@ -87,8 +82,7 @@ rustmotion_core::impl_traits!(Badge { impl Badge { fn resolved_font_size(&self) -> f32 { - self.style - .font_size_px_or(self.badge_size.params().0) + self.style.font_size_px_or(self.badge_size.params().0) } /// Returns (h_padding, v_padding, icon_size) scaled proportionally @@ -112,11 +106,10 @@ impl Badge { .or_else(|| fm.match_family_style("Arial", font_style)) .or_else(|| fm.match_family_style("sans-serif", font_style)) .or_else(|| fm.legacy_make_typeface(None, font_style)) - .expect(&RustmotionError::FontNotFound.to_string()); + .unwrap_or_else(|| panic!("{}", RustmotionError::FontNotFound.to_string())); skia_safe::Font::from_typeface(typeface, self.resolved_font_size()) } - } impl Badge { @@ -232,7 +225,16 @@ impl Badge { }; let text_y = (h - cap_h) / 2.0 + cap_h; - draw_text_with_fallback(canvas, &self.text, &font, &emoji_font, 0.0, x_offset, text_y, &text_paint); + draw_text_with_fallback( + canvas, + &self.text, + &font, + &emoji_font, + 0.0, + x_offset, + text_y, + &text_paint, + ); // Dot indicator (top-right) if self.dot { @@ -273,9 +275,13 @@ impl Badge { .match_family_style("Inter", skia_safe::FontStyle::bold()) .or_else(|| fm.match_family_style("Helvetica", skia_safe::FontStyle::bold())) .or_else(|| fm.match_family_style("Arial", skia_safe::FontStyle::bold())) - .unwrap_or_else(|| fm.legacy_make_typeface(None, skia_safe::FontStyle::bold()).unwrap()); + .unwrap_or_else(|| { + fm.legacy_make_typeface(None, skia_safe::FontStyle::bold()) + .unwrap() + }); let count_font = skia_safe::Font::from_typeface(count_typeface, count_fs); - let count_emoji = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, count_fs)); + let count_emoji = + emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, count_fs)); let count_w = measure_text_with_fallback(&count_text, &count_font, &count_emoji, 0.0); let badge_pad = count_fs * 0.4; @@ -298,7 +304,16 @@ impl Badge { let (_, count_metrics) = count_font.metrics(); let cx = badge_x + (badge_w - count_w) / 2.0; let cy = badge_y + (badge_h + (-count_metrics.ascent)) / 2.0; - draw_text_with_fallback(canvas, &count_text, &count_font, &count_emoji, 0.0, cx, cy, &count_paint); + draw_text_with_fallback( + canvas, + &count_text, + &count_font, + &count_emoji, + 0.0, + cx, + cy, + &count_paint, + ); } } } diff --git a/crates/rustmotion-components/src/box_builder.rs b/crates/rustmotion-components/src/box_builder.rs index a5738f0..be95267 100644 --- a/crates/rustmotion-components/src/box_builder.rs +++ b/crates/rustmotion-components/src/box_builder.rs @@ -51,10 +51,7 @@ pub struct BuiltScene<'a> { /// The implicit scene root is a `display: flex; flex-direction: column; /// width/height: 100%`; children flow vertically unless they specify /// `position: { x, y }`, in which case they become `position: absolute`. -pub fn build_scene<'a>( - children: &'a [ChildComponent], - viewport: (f32, f32), -) -> BuiltScene<'a> { +pub fn build_scene<'a>(children: &'a [ChildComponent], viewport: (f32, f32)) -> BuiltScene<'a> { build_scene_with_root(children, viewport, default_root_css(viewport)) } @@ -87,7 +84,12 @@ pub fn build_scene_with_anim<'a>( viewport: (f32, f32), anim: BuildAnimationCtx, ) -> BuiltScene<'a> { - build_scene_from_refs(children.iter(), viewport, default_root_css(viewport), Some(anim)) + build_scene_from_refs( + children.iter(), + viewport, + default_root_css(viewport), + Some(anim), + ) } /// Same as [`build_scene_with_root`] but accepts an iterator over @@ -224,9 +226,13 @@ fn component_intrinsic( GradientText(t) => Some(Arc::new( crate::intrinsic::GradientTextIntrinsic::from_gradient_text(t), )), - Caption(c) => Some(Arc::new(crate::intrinsic::CaptionIntrinsic::from_caption(c))), + Caption(c) => Some(Arc::new(crate::intrinsic::CaptionIntrinsic::from_caption( + c, + ))), Kbd(k) => Some(Arc::new(crate::intrinsic::KbdIntrinsic::from_kbd(k))), - Counter(c) => Some(Arc::new(crate::intrinsic::CounterIntrinsic::from_counter(c))), + Counter(c) => Some(Arc::new(crate::intrinsic::CounterIntrinsic::from_counter( + c, + ))), Badge(b) => Some(Arc::new(crate::intrinsic::BadgeIntrinsic::from_badge(b))), _ => None, } @@ -252,7 +258,15 @@ fn container_children<'a>( children .iter() .enumerate() - .map(|(j, c)| build_child(c, components, next_id, anim, format!("{parent_path}/children/{j}"))) + .map(|(j, c)| { + build_child( + c, + components, + next_id, + anim, + format!("{parent_path}/children/{j}"), + ) + }) .collect() } @@ -453,7 +467,11 @@ fn apply_intrinsic_overrides(component: &Component, css: &mut CssStyle) { let extra = if c.overflow_count() > 0 { 1.0 } else { 0.0 }; let total = visible + extra; let step = (c.size - c.overlap).max(0.0); - let w = if total <= 0.0 { 0.0 } else { c.size + (total - 1.0) * step }; + let w = if total <= 0.0 { + 0.0 + } else { + c.size + (total - 1.0) * step + }; css.width = Some(CSize::Length(CLP::Px(w))); } if css.height.is_none() { @@ -468,21 +486,19 @@ fn apply_intrinsic_overrides(component: &Component, css: &mut CssStyle) { css.height = Some(CSize::Length(CLP::Px(c.size))); } } - Countdown(c) => { - if css.width.is_none() || css.height.is_none() { - let visible = [c.show_hours, c.show_minutes, c.show_seconds] - .iter() - .filter(|v| **v) - .count() as f32; - let box_w = c.digit_size * 0.75; - let box_h = c.digit_size * 1.2; - let w = (visible * 2.0 * box_w) + ((visible - 1.0).max(0.0) * c.gap); - if css.width.is_none() { - css.width = Some(CSize::Length(CLP::Px(w))); - } - if css.height.is_none() { - css.height = Some(CSize::Length(CLP::Px(box_h))); - } + Countdown(c) if (css.width.is_none() || css.height.is_none()) => { + let visible = [c.show_hours, c.show_minutes, c.show_seconds] + .iter() + .filter(|v| **v) + .count() as f32; + let box_w = c.digit_size * 0.75; + let box_h = c.digit_size * 1.2; + let w = (visible * 2.0 * box_w) + ((visible - 1.0).max(0.0) * c.gap); + if css.width.is_none() { + css.width = Some(CSize::Length(CLP::Px(w))); + } + if css.height.is_none() { + css.height = Some(CSize::Length(CLP::Px(box_h))); } } _ => {} @@ -613,15 +629,16 @@ pub fn component_kind(c: &Component) -> &'static str { } } - #[cfg(test)] mod tests { use super::*; - use rustmotion_core::css::style::{CssStyle, Display, Edges, FlexDirection, Gap, Size as CSize}; + use rustmotion_core::css::style::{ + CssStyle, Display, Edges, FlexDirection, Gap, Size as CSize, + }; + use rustmotion_core::css::taffy_bridge::ConversionContext; use rustmotion_core::css::units::LengthPercentage; use rustmotion_core::css::units::LengthPercentage as CLP; use rustmotion_core::engine::layout_pass::run_layout; - use rustmotion_core::css::taffy_bridge::ConversionContext; fn make_card(children: Vec, style: CssStyle) -> Component { Component::Card(crate::card::Card { @@ -725,8 +742,12 @@ mod tests { assert_eq!(card_layout.width, 300.0); assert_eq!(card_layout.height, 200.0); - let c1 = layout.get(card_box.children[0].id).expect("shape 1 laid out"); - let c2 = layout.get(card_box.children[1].id).expect("shape 2 laid out"); + let c1 = layout + .get(card_box.children[0].id) + .expect("shape 1 laid out"); + let c2 = layout + .get(card_box.children[1].id) + .expect("shape 2 laid out"); // Padding 20 from top, then first shape 50 high, gap 10 → 80. assert_eq!(c1.x, 20.0); assert_eq!(c1.y, 20.0); @@ -903,7 +924,9 @@ mod tests { let scene = vec![arrow]; let built = build_scene(&scene, (800.0, 600.0)); let layout = run_layout(&built.root, (800.0, 600.0), &ConversionContext::default()); - let l = layout.get(built.root.children[0].id).expect("arrow laid out"); + let l = layout + .get(built.root.children[0].id) + .expect("arrow laid out"); // bbox 100×60 + (16 padding + 12 arrow_size) = 128×88. assert_eq!(l.width, 128.0); assert_eq!(l.height, 88.0); @@ -936,7 +959,9 @@ mod tests { let scene = vec![conn]; let built = build_scene(&scene, (800.0, 600.0)); let layout = run_layout(&built.root, (800.0, 600.0), &ConversionContext::default()); - let l = layout.get(built.root.children[0].id).expect("connector laid out"); + let l = layout + .get(built.root.children[0].id) + .expect("connector laid out"); // bbox 100×50 + (16 + 10) = 126×76. assert_eq!(l.width, 126.0); assert_eq!(l.height, 76.0); @@ -978,7 +1003,11 @@ mod tests { let layout = run_layout(&built.root, (800.0, 600.0), &ConversionContext::default()); let id = built.root.children[0].id; let l = layout.get(id).expect("counter laid out"); - assert!(l.width > 0.0, "counter width should be > 0, got {}", l.width); + assert!( + l.width > 0.0, + "counter width should be > 0, got {}", + l.width + ); // line_height defaults to font_size × 1.3 = 83.2. Allow some slack. assert!( l.height >= 60.0, @@ -1020,7 +1049,11 @@ mod tests { let id = built.root.children[0].id; let l = layout.get(id).expect("badge laid out"); // h_pad×2 = 24 alone, plus the text width. - assert!(l.width > 24.0, "badge width should exceed padding alone, got {}", l.width); + assert!( + l.width > 24.0, + "badge width should exceed padding alone, got {}", + l.width + ); assert!( (l.height - 30.2).abs() < 2.0, "badge height should be ~30.2, got {}", diff --git a/crates/rustmotion-components/src/callout.rs b/crates/rustmotion-components/src/callout.rs index a0ee2af..64fe726 100644 --- a/crates/rustmotion-components/src/callout.rs +++ b/crates/rustmotion-components/src/callout.rs @@ -2,7 +2,7 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::error::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, PaintStyle, Path, Rect, RRect}; +use skia_safe::{Canvas, PaintStyle, Path, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; @@ -13,19 +13,15 @@ use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum ArrowDirection { Top, + #[default] Bottom, Left, Right, } -impl Default for ArrowDirection { - fn default() -> Self { - Self::Bottom - } -} - /// Speech bubble with a directional arrow. /// /// CSS-like properties go in `style`: diff --git a/crates/rustmotion-components/src/caption.rs b/crates/rustmotion-components/src/caption.rs index f723ba3..2b5570e 100644 --- a/crates/rustmotion-components/src/caption.rs +++ b/crates/rustmotion-components/src/caption.rs @@ -5,7 +5,9 @@ use skia_safe::{Canvas, Font, FontStyle, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{font_mgr, paint_from_hex, emoji_typeface, draw_text_with_fallback, measure_text_with_fallback}; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, +}; use rustmotion_core::schema::{CaptionStyle, CaptionWord, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter}; @@ -43,7 +45,10 @@ impl Caption { .or_else(|| fm.match_family_style("Helvetica", FontStyle::bold())) .or_else(|| fm.match_family_style("Arial", FontStyle::bold())) .or_else(|| fm.match_family_style("sans-serif", FontStyle::bold())) - .unwrap_or_else(|| fm.legacy_make_typeface(None, FontStyle::bold()).expect("No fallback font")); + .unwrap_or_else(|| { + fm.legacy_make_typeface(None, FontStyle::bold()) + .expect("No fallback font") + }); let font = Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, font_size)); @@ -53,7 +58,8 @@ impl Caption { for word in &self.words { if time >= word.start && time < word.end { let paint = paint_from_hex(&self.active_color); - let text_width = measure_text_with_fallback(&word.text, &font, &emoji_font, 0.0); + let text_width = + measure_text_with_fallback(&word.text, &font, &emoji_font, 0.0); let cx = layout_width / 2.0; @@ -71,7 +77,16 @@ impl Caption { } let x = cx - text_width / 2.0; - draw_text_with_fallback(canvas, &word.text, &font, &emoji_font, 0.0, x, 0.0, &paint); + draw_text_with_fallback( + canvas, + &word.text, + &font, + &emoji_font, + 0.0, + x, + 0.0, + &paint, + ); break; } } @@ -84,7 +99,8 @@ impl Caption { let mut current_x = 0.0f32; for (i, word) in self.words.iter().enumerate() { - let word_width = measure_text_with_fallback(&word.text, &font, &emoji_font, 0.0); + let word_width = + measure_text_with_fallback(&word.text, &font, &emoji_font, 0.0); if current_x + word_width > max_width && !lines.last().unwrap().is_empty() { lines.push(vec![]); current_x = 0.0; @@ -99,9 +115,13 @@ impl Caption { if let Some(bg_color) = self.style.background_color_str() { let padding = font_size * 0.3; let total_height = lines.len() as f32 * line_height; - let max_line_width = lines.iter().map(|line| { - line.iter().map(|(_, w)| w).sum::() + (line.len().saturating_sub(1)) as f32 * space_width - }).fold(0.0f32, f32::max); + let max_line_width = lines + .iter() + .map(|line| { + line.iter().map(|(_, w)| w).sum::() + + (line.len().saturating_sub(1)) as f32 * space_width + }) + .fold(0.0f32, f32::max); let bg_rect = Rect::from_xywh( cx - max_line_width / 2.0 - padding, -font_size - padding / 2.0, @@ -125,7 +145,16 @@ impl Caption { let word_color = if is_active { &self.active_color } else { color }; let paint = paint_from_hex(word_color); - draw_text_with_fallback(canvas, &word.text, &font, &emoji_font, 0.0, x, y, &paint); + draw_text_with_fallback( + canvas, + &word.text, + &font, + &emoji_font, + 0.0, + x, + y, + &paint, + ); x += word_width + space_width; } } @@ -146,4 +175,6 @@ impl Painter for Caption { } } -fn default_active_color() -> String { "#FFFF00".to_string() } +fn default_active_color() -> String { + "#FFFF00".to_string() +} diff --git a/crates/rustmotion-components/src/chart/axes.rs b/crates/rustmotion-components/src/chart/axes.rs index f78af79..f80a4ca 100644 --- a/crates/rustmotion-components/src/chart/axes.rs +++ b/crates/rustmotion-components/src/chart/axes.rs @@ -79,8 +79,7 @@ impl Chart { let label = format_number(val); let mut label_paint = paint_from_hex(&self.label_color); label_paint.set_anti_alias(true); - let label_w = - measure_text_with_fallback(&label, &font, &emoji_font, 0.0); + let label_w = measure_text_with_fallback(&label, &font, &emoji_font, 0.0); let lx = chart_x - label_w - 6.0; let ly = y + ascent / 2.0; draw_text_with_fallback( @@ -110,8 +109,7 @@ impl Chart { } else { chart_x + (i as f32 / (n - 1).max(1) as f32) * chart_w }; - let label_w = - measure_text_with_fallback(label, &font, &emoji_font, 0.0); + let label_w = measure_text_with_fallback(label, &font, &emoji_font, 0.0); let lx = x - label_w / 2.0; let ly = chart_y + chart_h + ascent + 6.0; draw_text_with_fallback( diff --git a/crates/rustmotion-components/src/chart/bar.rs b/crates/rustmotion-components/src/chart/bar.rs index bcb68ec..88a07fb 100644 --- a/crates/rustmotion-components/src/chart/bar.rs +++ b/crates/rustmotion-components/src/chart/bar.rs @@ -1,9 +1,7 @@ use rustmotion_core::error::Result; use skia_safe::{Canvas, PaintStyle, Rect}; -use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, paint_from_hex, -}; +use rustmotion_core::engine::renderer::{draw_text_with_fallback, emoji_typeface, paint_from_hex}; use super::axes::contrast_text_color; use super::Chart; @@ -21,13 +19,11 @@ impl Chart { .fold(0.0_f64, f64::max) .max(0.001); - let x_labels: Vec = self - .data - .iter() - .filter_map(|d| d.label.clone()) - .collect(); + let x_labels: Vec = self.data.iter().filter_map(|d| d.label.clone()).collect(); - self.draw_axes(canvas, ml, mt, chart_w, chart_h, 0.0, max_val, &x_labels, true); + self.draw_axes( + canvas, ml, mt, chart_w, chart_h, 0.0, max_val, &x_labels, true, + ); let n = self.data.len(); let gap = 8.0; @@ -164,7 +160,9 @@ impl Chart { .max(0.001); let x_labels: Vec = self.categories.clone(); - self.draw_axes(canvas, ml, mt, chart_w, chart_h, 0.0, max_val, &x_labels, true); + self.draw_axes( + canvas, ml, mt, chart_w, chart_h, 0.0, max_val, &x_labels, true, + ); let gap = 8.0; let bar_w = (chart_w - gap * (n_cats + 1) as f32) / n_cats as f32; diff --git a/crates/rustmotion-components/src/chart/funnel.rs b/crates/rustmotion-components/src/chart/funnel.rs index f6642a2..5540b4c 100644 --- a/crates/rustmotion-components/src/chart/funnel.rs +++ b/crates/rustmotion-components/src/chart/funnel.rs @@ -9,7 +9,13 @@ use super::axes::contrast_text_color; use super::Chart; impl Chart { - pub(super) fn render_funnel(&self, canvas: &Canvas, w: f32, h: f32, progress: f32) -> Result<()> { + pub(super) fn render_funnel( + &self, + canvas: &Canvas, + w: f32, + h: f32, + progress: f32, + ) -> Result<()> { let horizontal = self .direction .as_deref() @@ -79,7 +85,14 @@ impl Chart { let lx = (w - label_w) / 2.0; let ly = y_top + seg_h / 2.0 + ascent / 2.0; draw_text_with_fallback( - canvas, label, &font, &emoji_font, 0.0, lx, ly, &label_paint, + canvas, + label, + &font, + &emoji_font, + 0.0, + lx, + ly, + &label_paint, ); } } @@ -88,7 +101,13 @@ impl Chart { Ok(()) } - fn render_funnel_horizontal(&self, canvas: &Canvas, w: f32, h: f32, progress: f32) -> Result<()> { + fn render_funnel_horizontal( + &self, + canvas: &Canvas, + w: f32, + h: f32, + progress: f32, + ) -> Result<()> { let n = self.data.len(); let max_val = self .data @@ -144,7 +163,14 @@ impl Chart { let lx = x_left + (seg_w - label_w) / 2.0; let ly = h / 2.0 + ascent / 2.0; draw_text_with_fallback( - canvas, label, &font, &emoji_font, 0.0, lx, ly, &label_paint, + canvas, + label, + &font, + &emoji_font, + 0.0, + lx, + ly, + &label_paint, ); } } diff --git a/crates/rustmotion-components/src/chart/line.rs b/crates/rustmotion-components/src/chart/line.rs index 675b3e2..cc10c03 100644 --- a/crates/rustmotion-components/src/chart/line.rs +++ b/crates/rustmotion-components/src/chart/line.rs @@ -17,11 +17,7 @@ impl Chart { .map(|d| d.value) .fold(0.0_f64, f64::max) .max(0.001); - let min_val = self - .data - .iter() - .map(|d| d.value) - .fold(f64::MAX, f64::min); + let min_val = self.data.iter().map(|d| d.value).fold(f64::MAX, f64::min); let range = (max_val - min_val).max(0.001); let n = self.data.len(); @@ -29,12 +25,10 @@ impl Chart { return Ok(()); } - let x_labels: Vec = self - .data - .iter() - .filter_map(|d| d.label.clone()) - .collect(); - self.draw_axes(canvas, ml, mt, chart_w, chart_h, min_val, max_val, &x_labels, false); + let x_labels: Vec = self.data.iter().filter_map(|d| d.label.clone()).collect(); + self.draw_axes( + canvas, ml, mt, chart_w, chart_h, min_val, max_val, &x_labels, false, + ); let mut path = Path::new(); let mut fill_path = Path::new(); @@ -107,11 +101,7 @@ impl Chart { .map(|d| d.value) .fold(0.0_f64, f64::max) .max(0.001); - let min_val = self - .data - .iter() - .map(|d| d.value) - .fold(f64::MAX, f64::min); + let min_val = self.data.iter().map(|d| d.value).fold(f64::MAX, f64::min); let range = (max_val - min_val).max(0.001); let n = self.data.len(); @@ -119,12 +109,10 @@ impl Chart { return Ok(()); } - let x_labels: Vec = self - .data - .iter() - .filter_map(|d| d.label.clone()) - .collect(); - self.draw_axes(canvas, ml, mt, chart_w, chart_h, min_val, max_val, &x_labels, false); + let x_labels: Vec = self.data.iter().filter_map(|d| d.label.clone()).collect(); + self.draw_axes( + canvas, ml, mt, chart_w, chart_h, min_val, max_val, &x_labels, false, + ); // Compute points let pts: Vec<(f32, f32)> = self diff --git a/crates/rustmotion-components/src/chart/mod.rs b/crates/rustmotion-components/src/chart/mod.rs index 3a35178..f77fa6e 100644 --- a/crates/rustmotion-components/src/chart/mod.rs +++ b/crates/rustmotion-components/src/chart/mod.rs @@ -3,9 +3,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use skia_safe::Canvas; +use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::css::CssStyle; use rustmotion_core::engine::renderer::font_mgr; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; diff --git a/crates/rustmotion-components/src/chart/pie.rs b/crates/rustmotion-components/src/chart/pie.rs index bc4a123..422d3be 100644 --- a/crates/rustmotion-components/src/chart/pie.rs +++ b/crates/rustmotion-components/src/chart/pie.rs @@ -40,7 +40,13 @@ impl Chart { Ok(()) } - pub(super) fn render_donut(&self, canvas: &Canvas, w: f32, h: f32, progress: f32) -> Result<()> { + pub(super) fn render_donut( + &self, + canvas: &Canvas, + w: f32, + h: f32, + progress: f32, + ) -> Result<()> { // Draw pie first self.render_pie(canvas, w, h, progress)?; diff --git a/crates/rustmotion-components/src/chart/radar.rs b/crates/rustmotion-components/src/chart/radar.rs index 86cdeeb..d78d5f6 100644 --- a/crates/rustmotion-components/src/chart/radar.rs +++ b/crates/rustmotion-components/src/chart/radar.rs @@ -8,7 +8,13 @@ use rustmotion_core::engine::renderer::{ use super::Chart; impl Chart { - pub(super) fn render_radar(&self, canvas: &Canvas, w: f32, h: f32, progress: f32) -> Result<()> { + pub(super) fn render_radar( + &self, + canvas: &Canvas, + w: f32, + h: f32, + progress: f32, + ) -> Result<()> { let n_axes = self.axes.len(); if n_axes < 3 || self.radar_data.is_empty() { return Ok(()); @@ -85,16 +91,9 @@ impl Chart { if rd.values.len() != n_axes { continue; } - let max_val = rd - .values - .iter() - .fold(0.0_f64, |a, &b| a.max(b)) - .max(0.001); - - let color_str = rd - .color - .as_deref() - .unwrap_or_else(|| self.get_color(di)); + let max_val = rd.values.iter().fold(0.0_f64, |a, &b| a.max(b)).max(0.001); + + let color_str = rd.color.as_deref().unwrap_or_else(|| self.get_color(di)); let mut data_path = Path::new(); for (i, &val) in rd.values.iter().enumerate() { diff --git a/crates/rustmotion-components/src/chart/radial.rs b/crates/rustmotion-components/src/chart/radial.rs index d09760d..1353c7c 100644 --- a/crates/rustmotion-components/src/chart/radial.rs +++ b/crates/rustmotion-components/src/chart/radial.rs @@ -17,7 +17,7 @@ impl Chart { let cy = h / 2.0; let max_radius = cx.min(cy) - 16.0; let n = self.data.len(); - let track_width = (max_radius / (n as f32 * 1.5)).min(20.0).max(6.0); + let track_width = (max_radius / (n as f32 * 1.5)).clamp(6.0, 20.0); let ring_gap = track_width * 0.5; let max_val = self diff --git a/crates/rustmotion-components/src/chart/scatter.rs b/crates/rustmotion-components/src/chart/scatter.rs index 4cadd9f..2a89d18 100644 --- a/crates/rustmotion-components/src/chart/scatter.rs +++ b/crates/rustmotion-components/src/chart/scatter.rs @@ -6,7 +6,13 @@ use rustmotion_core::engine::renderer::paint_from_hex; use super::Chart; impl Chart { - pub(super) fn render_scatter(&self, canvas: &Canvas, w: f32, h: f32, progress: f32) -> Result<()> { + pub(super) fn render_scatter( + &self, + canvas: &Canvas, + w: f32, + h: f32, + progress: f32, + ) -> Result<()> { if self.points.is_empty() { return Ok(()); } @@ -15,26 +21,10 @@ impl Chart { let chart_w = w - ml - mr; let chart_h = h - mt - mb; - let min_x = self - .points - .iter() - .map(|p| p.x) - .fold(f64::MAX, f64::min); - let max_x = self - .points - .iter() - .map(|p| p.x) - .fold(f64::MIN, f64::max); - let min_y = self - .points - .iter() - .map(|p| p.y) - .fold(f64::MAX, f64::min); - let max_y = self - .points - .iter() - .map(|p| p.y) - .fold(f64::MIN, f64::max); + let min_x = self.points.iter().map(|p| p.x).fold(f64::MAX, f64::min); + let max_x = self.points.iter().map(|p| p.x).fold(f64::MIN, f64::max); + let min_y = self.points.iter().map(|p| p.y).fold(f64::MAX, f64::min); + let max_y = self.points.iter().map(|p| p.y).fold(f64::MIN, f64::max); let range_x = (max_x - min_x).max(0.001); let range_y = (max_y - min_y).max(0.001); @@ -45,10 +35,7 @@ impl Chart { let px = ml + ((pt.x - min_x) / range_x) as f32 * chart_w; let py = mt + chart_h - ((pt.y - min_y) / range_y) as f32 * chart_h; - let color = pt - .color - .as_deref() - .unwrap_or_else(|| self.get_color(i)); + let color = pt.color.as_deref().unwrap_or_else(|| self.get_color(i)); let mut paint = paint_from_hex(color); paint.set_style(PaintStyle::Fill); paint.set_anti_alias(true); diff --git a/crates/rustmotion-components/src/chart/waterfall.rs b/crates/rustmotion-components/src/chart/waterfall.rs index 4946098..8b7c269 100644 --- a/crates/rustmotion-components/src/chart/waterfall.rs +++ b/crates/rustmotion-components/src/chart/waterfall.rs @@ -6,7 +6,13 @@ use rustmotion_core::engine::renderer::paint_from_hex; use super::Chart; impl Chart { - pub(super) fn render_waterfall(&self, canvas: &Canvas, w: f32, h: f32, progress: f32) -> Result<()> { + pub(super) fn render_waterfall( + &self, + canvas: &Canvas, + w: f32, + h: f32, + progress: f32, + ) -> Result<()> { let (mt, mr, mb, ml) = self.chart_margins(); let chart_w = w - ml - mr; let chart_h = h - mt - mb; @@ -29,12 +35,10 @@ impl Chart { let max_val = all_vals.iter().fold(f64::MIN, |a, &b| a.max(b)); let range = (max_val - min_val).max(0.001); - let x_labels: Vec = self - .data - .iter() - .filter_map(|d| d.label.clone()) - .collect(); - self.draw_axes(canvas, ml, mt, chart_w, chart_h, min_val, max_val, &x_labels, true); + let x_labels: Vec = self.data.iter().filter_map(|d| d.label.clone()).collect(); + self.draw_axes( + canvas, ml, mt, chart_w, chart_h, min_val, max_val, &x_labels, true, + ); let n = self.data.len(); let gap = 6.0; @@ -54,7 +58,7 @@ impl Chart { let x = ml + gap + i as f32 * (bar_w + gap); // Green for positive, red for negative - let color = dp.color.as_deref().unwrap_or_else(|| { + let color = dp.color.as_deref().unwrap_or({ if dp.value >= 0.0 { "#22C55E" } else { diff --git a/crates/rustmotion-components/src/codeblock/chrome.rs b/crates/rustmotion-components/src/codeblock/chrome.rs index 1bd259a..3f23b04 100644 --- a/crates/rustmotion-components/src/codeblock/chrome.rs +++ b/crates/rustmotion-components/src/codeblock/chrome.rs @@ -1,7 +1,9 @@ use skia_safe::{Canvas, Font, FontStyle, Rect}; -use rustmotion_core::engine::renderer::{draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex}; use super::Codeblock; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, +}; pub(super) fn draw_chrome( canvas: &Canvas, @@ -43,8 +45,7 @@ pub(super) fn draw_chrome( .or_else(|| fm.match_family_style("Helvetica", FontStyle::normal())) .or_else(|| fm.match_family_style("Arial", FontStyle::normal())) .unwrap_or_else(|| { - fm - .match_family_style("sans-serif", FontStyle::normal()) + fm.match_family_style("sans-serif", FontStyle::normal()) .unwrap() }); let title_font = Font::from_typeface(typeface, 13.0); @@ -54,6 +55,15 @@ pub(super) fn draw_chrome( let title_y = dot_y + 4.0; let mut title_paint = paint_from_hex("#999999"); title_paint.set_anti_alias(true); - draw_text_with_fallback(canvas, title, &title_font, &emoji_font, 0.0, title_x, title_y, &title_paint); + draw_text_with_fallback( + canvas, + title, + &title_font, + &emoji_font, + 0.0, + title_x, + title_y, + &title_paint, + ); } } diff --git a/crates/rustmotion-components/src/codeblock/diff.rs b/crates/rustmotion-components/src/codeblock/diff.rs index b75e4fe..5726c80 100644 --- a/crates/rustmotion-components/src/codeblock/diff.rs +++ b/crates/rustmotion-components/src/codeblock/diff.rs @@ -2,13 +2,13 @@ use similar::{ChangeTag, TextDiff}; use skia_safe::{Canvas, Font, PaintStyle, Rect}; use syntect::highlighting::Theme; -use rustmotion_core::engine::animator::ease; -use rustmotion_core::engine::renderer::paint_from_hex; -use rustmotion_core::schema::EasingType; -use super::Codeblock; use super::dimensions::lerp; use super::highlight::highlight_code; use super::reveal::{draw_line_number_at, draw_single_highlighted_line}; +use super::Codeblock; +use rustmotion_core::engine::animator::ease; +use rustmotion_core::engine::renderer::paint_from_hex; +use rustmotion_core::schema::EasingType; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -56,7 +56,10 @@ pub(super) struct FragmentEdit { // ─── State management ──────────────────────────────────────────────────────── -pub(super) fn determine_active_state(layer: &Codeblock, time: f64) -> (String, Option) { +pub(super) fn determine_active_state( + layer: &Codeblock, + time: f64, +) -> (String, Option) { if layer.states.is_empty() { return (layer.code.clone(), None); } @@ -174,13 +177,13 @@ pub(super) fn render_diff_transition( let highlighted_a = highlight_code(&trans.code_a, &layer.language, theme); let highlighted_b = highlight_code(&trans.code_b, &layer.language, theme); - let cursor_enabled = trans.cursor_config.as_ref().map_or(true, |c| c.enabled); + let cursor_enabled = trans.cursor_config.as_ref().is_none_or(|c| c.enabled); let cursor_color = trans .cursor_config .as_ref() .map_or("#FFFFFF", |c| c.color.as_str()); let cursor_width = trans.cursor_config.as_ref().map_or(2.0, |c| c.width); - let cursor_blink = trans.cursor_config.as_ref().map_or(true, |c| c.blink); + let cursor_blink = trans.cursor_config.as_ref().is_none_or(|c| c.blink); // Build animated line placements with proper interpolated positions. // Track "virtual cursors" for old and new index space so that diff --git a/crates/rustmotion-components/src/codeblock/highlight.rs b/crates/rustmotion-components/src/codeblock/highlight.rs index 6e03445..e6a9a97 100644 --- a/crates/rustmotion-components/src/codeblock/highlight.rs +++ b/crates/rustmotion-components/src/codeblock/highlight.rs @@ -250,7 +250,10 @@ fn theme_set() -> &'static ThemeSet { "dark-plus", include_str!("../../../../themes/vscode/dark-plus.json"), ), - ("dracula", include_str!("../../../../themes/vscode/dracula.json")), + ( + "dracula", + include_str!("../../../../themes/vscode/dracula.json"), + ), ( "dracula-soft", include_str!("../../../../themes/vscode/dracula-soft.json"), @@ -315,12 +318,18 @@ fn theme_set() -> &'static ThemeSet { "gruvbox-light-soft", include_str!("../../../../themes/vscode/gruvbox-light-soft.json"), ), - ("horizon", include_str!("../../../../themes/vscode/horizon.json")), + ( + "horizon", + include_str!("../../../../themes/vscode/horizon.json"), + ), ( "horizon-bright", include_str!("../../../../themes/vscode/horizon-bright.json"), ), - ("houston", include_str!("../../../../themes/vscode/houston.json")), + ( + "houston", + include_str!("../../../../themes/vscode/houston.json"), + ), ( "kanagawa-dragon", include_str!("../../../../themes/vscode/kanagawa-dragon.json"), @@ -369,7 +378,10 @@ fn theme_set() -> &'static ThemeSet { "min-light", include_str!("../../../../themes/vscode/min-light.json"), ), - ("monokai", include_str!("../../../../themes/vscode/monokai.json")), + ( + "monokai", + include_str!("../../../../themes/vscode/monokai.json"), + ), ( "night-owl", include_str!("../../../../themes/vscode/night-owl.json"), @@ -387,7 +399,10 @@ fn theme_set() -> &'static ThemeSet { "one-light", include_str!("../../../../themes/vscode/one-light.json"), ), - ("plastic", include_str!("../../../../themes/vscode/plastic.json")), + ( + "plastic", + include_str!("../../../../themes/vscode/plastic.json"), + ), ( "poimandres", include_str!("../../../../themes/vscode/poimandres.json"), @@ -433,7 +448,10 @@ fn theme_set() -> &'static ThemeSet { "tokyo-night", include_str!("../../../../themes/vscode/tokyo-night.json"), ), - ("vesper", include_str!("../../../../themes/vscode/vesper.json")), + ( + "vesper", + include_str!("../../../../themes/vscode/vesper.json"), + ), ( "vitesse-black", include_str!("../../../../themes/vscode/vitesse-black.json"), @@ -536,7 +554,7 @@ pub(super) fn resolve_monospace_font(family: &str, size: f32, weight: FontWeight .unwrap_or_else(|| { if font_mgr.count_families() > 0 { font_mgr - .match_family_style(&font_mgr.family_name(0), style) + .match_family_style(font_mgr.family_name(0), style) .unwrap() } else { panic!("No fonts available on this system"); diff --git a/crates/rustmotion-components/src/codeblock/mod.rs b/crates/rustmotion-components/src/codeblock/mod.rs index 8393c46..9d35d64 100644 --- a/crates/rustmotion-components/src/codeblock/mod.rs +++ b/crates/rustmotion-components/src/codeblock/mod.rs @@ -12,7 +12,9 @@ use skia_safe::Canvas; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::schema::{CodeblockChrome, CodeblockHighlight, CodeblockReveal, CodeblockState, TimelineStep}; +use rustmotion_core::schema::{ + CodeblockChrome, CodeblockHighlight, CodeblockReveal, CodeblockState, TimelineStep, +}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -51,7 +53,9 @@ pub struct Codeblock { pub stagger: Option, } -fn default_auto_scroll() -> bool { true } +fn default_auto_scroll() -> bool { + true +} rustmotion_core::impl_traits!(Codeblock { Animatable => animation, @@ -71,5 +75,9 @@ impl Painter for Codeblock { } } -fn default_language() -> String { "plain".to_string() } -fn default_theme() -> String { "base16-ocean.dark".to_string() } +fn default_language() -> String { + "plain".to_string() +} +fn default_theme() -> String { + "base16-ocean.dark".to_string() +} diff --git a/crates/rustmotion-components/src/codeblock/render.rs b/crates/rustmotion-components/src/codeblock/render.rs index 10e2645..0a47c88 100644 --- a/crates/rustmotion-components/src/codeblock/render.rs +++ b/crates/rustmotion-components/src/codeblock/render.rs @@ -7,12 +7,12 @@ use rustmotion_core::engine::renderer::paint_from_hex; use rustmotion_core::schema::FontWeight; use rustmotion_core::traits::PaintCtx; -use super::Codeblock; use super::chrome::draw_chrome; use super::diff::{determine_active_state, draw_diff_backgrounds, render_diff_transition}; use super::dimensions::{compute_code_dimensions, lerp}; use super::highlight::{get_theme, highlight_code, resolve_monospace_font}; use super::reveal::{compute_reveal, draw_highlighted_lines, draw_highlights, draw_line_numbers}; +use super::Codeblock; pub(super) fn render_codeblock( canvas: &Canvas, @@ -44,7 +44,7 @@ pub(super) fn render_codeblock( let (current_code, transition) = determine_active_state(layer, time); - let chrome_enabled = layer.chrome.as_ref().map_or(false, |c| c.enabled); + let chrome_enabled = layer.chrome.as_ref().is_some_and(|c| c.enabled); let chrome_height = if chrome_enabled { 36.0 } else { 0.0 }; // Pre-compute the max gutter width across all states so line numbers @@ -68,7 +68,11 @@ pub(super) fn render_codeblock( let natural_height = if let Some(ref trans) = transition { let dims_a = compute_code_dimensions(&trans.code_a, &font, padding, chrome_height, layer); let dims_b = compute_code_dimensions(&trans.code_b, &font, padding, chrome_height, layer); - lerp(dims_a.total_height, dims_b.total_height, trans.progress as f32) + lerp( + dims_a.total_height, + dims_b.total_height, + trans.progress as f32, + ) } else { compute_code_dimensions(¤t_code, &font, padding, chrome_height, layer).total_height }; @@ -115,7 +119,12 @@ pub(super) fn render_codeblock( }; canvas.save(); canvas.clip_rect( - Rect::from_xywh(x, y + chrome_height, total_width, total_height - chrome_height), + Rect::from_xywh( + x, + y + chrome_height, + total_width, + total_height - chrome_height, + ), skia_safe::ClipOp::Intersect, true, ); diff --git a/crates/rustmotion-components/src/codeblock/reveal.rs b/crates/rustmotion-components/src/codeblock/reveal.rs index 8be20ac..8f29fbe 100644 --- a/crates/rustmotion-components/src/codeblock/reveal.rs +++ b/crates/rustmotion-components/src/codeblock/reveal.rs @@ -1,10 +1,12 @@ use skia_safe::{Canvas, Font, Paint, Rect, TextBlob}; +use super::highlight::HighlightedLine; +use super::Codeblock; use rustmotion_core::engine::animator::ease; -use rustmotion_core::engine::renderer::{draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex}; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, +}; use rustmotion_core::schema::{CodeblockHighlight, RevealMode}; -use super::Codeblock; -use super::highlight::HighlightedLine; // ─── Reveal ────────────────────────────────────────────────────────────────── @@ -90,7 +92,14 @@ pub(super) fn draw_line_numbers( } /// Draw a single line number at an arbitrary Y with given opacity -pub(super) fn draw_line_number_at(canvas: &Canvas, font: &Font, x: f32, y: f32, num: usize, opacity: f32) { +pub(super) fn draw_line_number_at( + canvas: &Canvas, + font: &Font, + x: f32, + y: f32, + num: usize, + opacity: f32, +) { let num_str = format!("{}", num); let mut paint = paint_from_hex("#65737E"); paint.set_anti_alias(true); @@ -227,7 +236,16 @@ pub(super) fn draw_single_highlighted_line_partial( ); let emoji_f = emoji_typeface().map(|tf| Font::from_typeface(tf, font.size())); - draw_text_with_fallback(canvas, &text_to_draw, font, &emoji_f, 0.0, cursor_x, y, &paint); + draw_text_with_fallback( + canvas, + &text_to_draw, + font, + &emoji_f, + 0.0, + cursor_x, + y, + &paint, + ); let w = measure_text_with_fallback(&text_to_draw, font, &emoji_f, 0.0); cursor_x += w; chars_drawn += text_to_draw.len(); diff --git a/crates/rustmotion-components/src/comparison.rs b/crates/rustmotion-components/src/comparison.rs index 3916654..ec767ae 100644 --- a/crates/rustmotion-components/src/comparison.rs +++ b/crates/rustmotion-components/src/comparison.rs @@ -1,6 +1,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, PaintStyle, Rect, RRect}; +use skia_safe::{Canvas, PaintStyle, RRect, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; @@ -126,7 +126,7 @@ impl Comparison { let right_rect = Rect::from_xywh(divider_x, 0.0, w - divider_x, h); canvas.draw_rect(right_rect, &right_paint); - let font_size = (h * 0.08).max(16.0).min(36.0); + let font_size = (h * 0.08).clamp(16.0, 36.0); let fm = font_mgr(); let font_style = skia_safe::FontStyle::bold(); let typeface = fm @@ -145,7 +145,14 @@ impl Comparison { let text_x = divider_x / 2.0 - text_w / 2.0; let text_y = h / 2.0 + (-metrics.ascent) / 2.0; draw_text_with_fallback( - canvas, label, &font, &emoji_font, 0.0, text_x, text_y, &label_paint, + canvas, + label, + &font, + &emoji_font, + 0.0, + text_x, + text_y, + &label_paint, ); } @@ -155,7 +162,14 @@ impl Comparison { let text_x = divider_x + (w - divider_x) / 2.0 - text_w / 2.0; let text_y = h / 2.0 + (-metrics.ascent) / 2.0; draw_text_with_fallback( - canvas, label, &font, &emoji_font, 0.0, text_x, text_y, &label_paint, + canvas, + label, + &font, + &emoji_font, + 0.0, + text_x, + text_y, + &label_paint, ); } diff --git a/crates/rustmotion-components/src/connector.rs b/crates/rustmotion-components/src/connector.rs index 2cac458..1c0710f 100644 --- a/crates/rustmotion-components/src/connector.rs +++ b/crates/rustmotion-components/src/connector.rs @@ -69,11 +69,21 @@ pub enum RoutingMode { Elbow, } -fn default_curvature() -> f32 { 0.4 } -fn default_connector_width() -> f32 { 2.0 } -fn default_connector_color() -> String { "#FFFFFF".to_string() } -fn default_arrow_size() -> f32 { 10.0 } -fn default_true() -> bool { true } +fn default_curvature() -> f32 { + 0.4 +} +fn default_connector_width() -> f32 { + 2.0 +} +fn default_connector_color() -> String { + "#FFFFFF".to_string() +} +fn default_arrow_size() -> f32 { + 10.0 +} +fn default_true() -> bool { + true +} rustmotion_core::impl_traits!(Connector { Animatable => animation, @@ -117,10 +127,18 @@ impl Connector { path } - fn draw_arrowhead(canvas: &Canvas, path: &Path, at_end: bool, size: f32, paint: &skia_safe::Paint) { + fn draw_arrowhead( + canvas: &Canvas, + path: &Path, + at_end: bool, + size: f32, + paint: &skia_safe::Paint, + ) { let mut measure = PathMeasure::new(path, false, None); let total_len = measure.length(); - if total_len < 1.0 { return; } + if total_len < 1.0 { + return; + } let (pos, tangent) = if at_end { match measure.pos_tan(total_len - 0.1) { diff --git a/crates/rustmotion-components/src/countdown.rs b/crates/rustmotion-components/src/countdown.rs index b1e8f30..d7c8e6a 100644 --- a/crates/rustmotion-components/src/countdown.rs +++ b/crates/rustmotion-components/src/countdown.rs @@ -1,6 +1,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, PaintStyle, Rect, RRect}; +use skia_safe::{Canvas, PaintStyle, RRect, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; @@ -133,7 +133,14 @@ impl Countdown { let text_y = y + (box_h + (-metrics.ascent)) / 2.0; draw_text_with_fallback( - canvas, &digit_str, font, emoji_font, 0.0, text_x, text_y, &text_paint, + canvas, + &digit_str, + font, + emoji_font, + 0.0, + text_x, + text_y, + &text_paint, ); } @@ -156,9 +163,7 @@ impl Countdown { let sep_x = x + (self.gap - sep_w) / 2.0; let sep_y = y + (box_h + (-metrics.ascent)) / 2.0; - draw_text_with_fallback( - canvas, sep, font, emoji_font, 0.0, sep_x, sep_y, &sep_paint, - ); + draw_text_with_fallback(canvas, sep, font, emoji_font, 0.0, sep_x, sep_y, &sep_paint); self.gap } diff --git a/crates/rustmotion-components/src/counter.rs b/crates/rustmotion-components/src/counter.rs index c4ca3f5..8e805a1 100644 --- a/crates/rustmotion-components/src/counter.rs +++ b/crates/rustmotion-components/src/counter.rs @@ -3,13 +3,20 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, Font, FontStyle, PaintStyle}; -use rustmotion_core::css::style::{FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign}; +use rustmotion_core::css::style::{ + FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign, +}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{font_mgr, format_counter_value, paint_from_hex, emoji_typeface, draw_text_with_fallback, measure_text_with_fallback}; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, format_counter_value, + measure_text_with_fallback, paint_from_hex, +}; use rustmotion_core::error::RustmotionError; -use rustmotion_core::schema::{EasingType, FontStyleType, FontWeight, Stroke, TextAlign, TextShadow, TimelineStep}; +use rustmotion_core::schema::{ + EasingType, FontStyleType, FontWeight, Stroke, TextAlign, TextShadow, TimelineStep, +}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -47,14 +54,22 @@ rustmotion_core::impl_traits!(Counter { }); impl Counter { - fn paint(&self, canvas: &Canvas, layout_width: f32, time: f64, scene_duration: f64) -> Result<()> { + fn paint( + &self, + canvas: &Canvas, + layout_width: f32, + time: f64, + scene_duration: f64, + ) -> Result<()> { use rustmotion_core::engine::animator::ease; let font_size = self.style.font_size_px_or(48.0); let color = self.style.color_str_or("#FFFFFF"); let font_family = self.style.font_family_or("Inter"); let font_weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => FontWeight::Bold, + Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { + FontWeight::Bold + } Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold, Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n), _ => FontWeight::Normal, @@ -81,7 +96,13 @@ impl Counter { let progress = ease(t, &self.easing); let value = self.from + (self.to - self.from) * progress; - let content = format_counter_value(value, self.decimals, &self.separator, &self.prefix, &self.suffix); + let content = format_counter_value( + value, + self.decimals, + &self.separator, + &self.prefix, + &self.suffix, + ); let fm = font_mgr(); let slant = match font_style_type { @@ -110,7 +131,8 @@ impl Counter { let letter_spacing = self.style.letter_spacing_px(); - let advance_width = measure_text_with_fallback(&content, &font, &emoji_font, letter_spacing); + let advance_width = + measure_text_with_fallback(&content, &font, &emoji_font, letter_spacing); // For center/right alignment, anchor positioning on the same `absmax` // width that `measure()` reserved. This keeps the right edge (or @@ -118,8 +140,18 @@ impl Counter { // of letting it shift sub-pixel as the digit count changes. let stable_width = if matches!(align, TextAlign::Center | TextAlign::Right) { let absmax = self.from.abs().max(self.to.abs()); - let signed = if self.from < 0.0 || self.to < 0.0 { -absmax } else { absmax }; - let display = format_counter_value(signed, self.decimals, &self.separator, &self.prefix, &self.suffix); + let signed = if self.from < 0.0 || self.to < 0.0 { + -absmax + } else { + absmax + }; + let display = format_counter_value( + signed, + self.decimals, + &self.separator, + &self.prefix, + &self.suffix, + ); measure_text_with_fallback(&display, &font, &emoji_font, letter_spacing) } else { advance_width @@ -127,8 +159,9 @@ impl Counter { let raw_x = match align { TextAlign::Left => 0.0, - TextAlign::Center => (layout_width - stable_width) / 2.0 - + (stable_width - advance_width) / 2.0, + TextAlign::Center => { + (layout_width - stable_width) / 2.0 + (stable_width - advance_width) / 2.0 + } TextAlign::Right => layout_width - advance_width, }; // Snap to whole pixels to eliminate the sub-pixel jitter that the @@ -153,7 +186,16 @@ impl Counter { sp.set_image_filter(filter); } } - draw_text_with_fallback(canvas, &content, &font, &emoji_font, letter_spacing, x + shadow.offset_x, y + shadow.offset_y, &sp); + draw_text_with_fallback( + canvas, + &content, + &font, + &emoji_font, + letter_spacing, + x + shadow.offset_x, + y + shadow.offset_y, + &sp, + ); } // Draw stroke @@ -161,10 +203,28 @@ impl Counter { let mut sp = paint_from_hex(&stroke.color); sp.set_style(PaintStyle::Stroke); sp.set_stroke_width(stroke.width); - draw_text_with_fallback(canvas, &content, &font, &emoji_font, letter_spacing, x, y, &sp); + draw_text_with_fallback( + canvas, + &content, + &font, + &emoji_font, + letter_spacing, + x, + y, + &sp, + ); } - draw_text_with_fallback(canvas, &content, &font, &emoji_font, letter_spacing, x, y, &paint); + draw_text_with_fallback( + canvas, + &content, + &font, + &emoji_font, + letter_spacing, + x, + y, + &paint, + ); Ok(()) } @@ -178,6 +238,6 @@ impl Painter for Counter { _props: &AnimatedProperties, ctx: &PaintCtx, ) { - let _ = self.paint(canvas, layout.width, ctx.time, ctx.scene_duration as f64); + let _ = self.paint(canvas, layout.width, ctx.time, ctx.scene_duration); } } diff --git a/crates/rustmotion-components/src/cursor.rs b/crates/rustmotion-components/src/cursor.rs index 3dc9df7..28e4ab6 100644 --- a/crates/rustmotion-components/src/cursor.rs +++ b/crates/rustmotion-components/src/cursor.rs @@ -164,7 +164,8 @@ impl Cursor { let t = match self.path_easing.as_str() { "linear" => raw_t, "ease_out" => 1.0 - (1.0 - raw_t).powi(3), - _ => { // ease_in_out + _ => { + // ease_in_out if raw_t < 0.5 { 4.0 * raw_t * raw_t * raw_t } else { @@ -174,8 +175,16 @@ impl Cursor { } as f32; // Catmull-Rom interpolation for smooth curves - let p_prev = if seg_idx > 0 { &waypoints[seg_idx - 1] } else { wp0 }; - let p_next = if seg_idx + 2 < waypoints.len() { &waypoints[seg_idx + 2] } else { wp1 }; + let p_prev = if seg_idx > 0 { + &waypoints[seg_idx - 1] + } else { + wp0 + }; + let p_next = if seg_idx + 2 < waypoints.len() { + &waypoints[seg_idx + 2] + } else { + wp1 + }; let x = catmull_rom(t, p_prev.x, wp0.x, wp1.x, p_next.x); let y = catmull_rom(t, p_prev.y, wp0.y, wp1.y, p_next.y); @@ -223,7 +232,8 @@ impl Painter for Cursor { }; let click_scale = if in_click { - let closest_click = click_times.iter() + let closest_click = click_times + .iter() .filter(|&&t| ctx.time >= t && ctx.time < t + self.click_duration as f64) .copied() .last() diff --git a/crates/rustmotion-components/src/divider.rs b/crates/rustmotion-components/src/divider.rs index b3c8b93..0350cb7 100644 --- a/crates/rustmotion-components/src/divider.rs +++ b/crates/rustmotion-components/src/divider.rs @@ -10,31 +10,23 @@ use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum DividerDirection { + #[default] Horizontal, Vertical, } -impl Default for DividerDirection { - fn default() -> Self { - Self::Horizontal - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum DividerLineStyle { + #[default] Solid, Dashed, Dotted, } -impl Default for DividerLineStyle { - fn default() -> Self { - Self::Solid - } -} - #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Divider { #[serde(default)] diff --git a/crates/rustmotion-components/src/dot_map.rs b/crates/rustmotion-components/src/dot_map.rs index 19ceecb..685836e 100644 --- a/crates/rustmotion-components/src/dot_map.rs +++ b/crates/rustmotion-components/src/dot_map.rs @@ -200,9 +200,8 @@ impl DotMap { } else { 0.0 }; - let dot_progress = - ((time - stagger_delay) / (self.animation_duration * 0.4)).clamp(0.0, 1.0) - as f32; + let dot_progress = ((time - stagger_delay) / (self.animation_duration * 0.4)) + .clamp(0.0, 1.0) as f32; dot_progress * progress } else { 1.0 @@ -256,8 +255,7 @@ impl DotMap { label_paint.set_anti_alias(true); label_paint.set_alpha_f(dot_alpha * 0.9); - let text_w = - measure_text_with_fallback(label, &label_font, &emoji_font, 0.0); + let text_w = measure_text_with_fallback(label, &label_font, &emoji_font, 0.0); let label_x = px - text_w / 2.0; let label_y = py + dot_size / 2.0 + label_font_size + 4.0; diff --git a/crates/rustmotion-components/src/gauge.rs b/crates/rustmotion-components/src/gauge.rs index 3178114..25fc573 100644 --- a/crates/rustmotion-components/src/gauge.rs +++ b/crates/rustmotion-components/src/gauge.rs @@ -156,7 +156,14 @@ impl Gauge { let text_y = cy + (-metrics.ascent) / 2.0; draw_text_with_fallback( - canvas, &text, &font, &emoji_font, 0.0, text_x, text_y, &text_paint, + canvas, + &text, + &font, + &emoji_font, + 0.0, + text_x, + text_y, + &text_paint, ); } @@ -182,7 +189,14 @@ impl Gauge { let label_y = cy + radius * 0.35; draw_text_with_fallback( - canvas, label, &font, &emoji_font, 0.0, label_x, label_y, &label_paint, + canvas, + label, + &font, + &emoji_font, + 0.0, + label_x, + label_y, + &label_paint, ); } } diff --git a/crates/rustmotion-components/src/gif.rs b/crates/rustmotion-components/src/gif.rs index 299489c..126d49e 100644 --- a/crates/rustmotion-components/src/gif.rs +++ b/crates/rustmotion-components/src/gif.rs @@ -11,7 +11,9 @@ use rustmotion_core::engine::renderer::gif_cache; use rustmotion_core::schema::{ImageFit, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; -fn default_loop_true() -> bool { true } +fn default_loop_true() -> bool { + true +} #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Gif { @@ -49,11 +51,15 @@ impl Painter for Gif { let cached = if let Some(cached) = gcache.get(&self.src) { cached.clone() } else { - let Ok(file) = std::fs::File::open(&self.src) else { return }; + let Ok(file) = std::fs::File::open(&self.src) else { + return; + }; let mut decoder = gif::DecodeOptions::new(); decoder.set_color_output(gif::ColorOutput::RGBA); - let Ok(mut decoder) = decoder.read_info(file) else { return }; + let Ok(mut decoder) = decoder.read_info(file) else { + return; + }; let gif_width = decoder.width() as u32; let gif_height = decoder.height() as u32; diff --git a/crates/rustmotion-components/src/gradient_text.rs b/crates/rustmotion-components/src/gradient_text.rs index b876ddc..5f27cf2 100644 --- a/crates/rustmotion-components/src/gradient_text.rs +++ b/crates/rustmotion-components/src/gradient_text.rs @@ -2,13 +2,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, Font, FontStyle, Point, TextBlob}; -use rustmotion_core::css::style::{FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw}; +use rustmotion_core::css::style::{ + FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, +}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - emoji_typeface, font_mgr, paint_from_hex, - parse_hex_color, + emoji_typeface, font_mgr, paint_from_hex, parse_hex_color, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -64,7 +65,9 @@ impl GradientText { _ => skia_safe::font_style::Slant::Upright, }; let weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => skia_safe::font_style::Weight::BOLD, + Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { + skia_safe::font_style::Weight::BOLD + } Some(CssFontWeight::Number(n)) => skia_safe::font_style::Weight::from(*n as i32), _ => skia_safe::font_style::Weight::NORMAL, }; diff --git a/crates/rustmotion-components/src/heatmap.rs b/crates/rustmotion-components/src/heatmap.rs index e27d350..246ae1f 100644 --- a/crates/rustmotion-components/src/heatmap.rs +++ b/crates/rustmotion-components/src/heatmap.rs @@ -78,7 +78,9 @@ rustmotion_core::impl_traits!(Heatmap { }); fn lerp_u8(a: u8, b: u8, t: f32) -> u8 { - (a as f32 + (b as f32 - a as f32) * t).round().clamp(0.0, 255.0) as u8 + (a as f32 + (b as f32 - a as f32) * t) + .round() + .clamp(0.0, 255.0) as u8 } fn interpolate_color(scale: &[String], t: f32) -> (u8, u8, u8) { diff --git a/crates/rustmotion-components/src/icon.rs b/crates/rustmotion-components/src/icon.rs index 39e8ccf..3110eb6 100644 --- a/crates/rustmotion-components/src/icon.rs +++ b/crates/rustmotion-components/src/icon.rs @@ -53,13 +53,19 @@ impl Painter for Icon { let img = if let Some(cached) = cache.get(&cache_key) { cached.clone() } else { - let Ok(svg_data) = fetch_icon_svg(&self.icon, color, render_w, render_h) else { return }; + let Ok(svg_data) = fetch_icon_svg(&self.icon, color, render_w, render_h) else { + return; + }; let opt = usvg::Options::default(); - let Ok(tree) = usvg::Tree::from_data(&svg_data, &opt) else { return }; + let Ok(tree) = usvg::Tree::from_data(&svg_data, &opt) else { + return; + }; let svg_size = tree.size(); - let Some(mut pixmap) = tiny_skia::Pixmap::new(render_w, render_h) else { return }; + let Some(mut pixmap) = tiny_skia::Pixmap::new(render_w, render_h) else { + return; + }; let scale_x = render_w as f32 / svg_size.width(); let scale_y = render_h as f32 / svg_size.height(); @@ -76,7 +82,9 @@ impl Painter for Icon { ); let Some(decoded) = skia_safe::images::raster_from_data(&img_info, img_data, render_w as usize * 4) - else { return }; + else { + return; + }; cache.insert(cache_key, decoded.clone()); decoded }; diff --git a/crates/rustmotion-components/src/image.rs b/crates/rustmotion-components/src/image.rs index f90afb3..e1adb95 100644 --- a/crates/rustmotion-components/src/image.rs +++ b/crates/rustmotion-components/src/image.rs @@ -42,9 +42,13 @@ impl Painter for Image { let img = if let Some(cached) = cache.get(&self.src) { cached.clone() } else { - let Ok(data) = std::fs::read(&self.src) else { return }; + let Ok(data) = std::fs::read(&self.src) else { + return; + }; let skia_data = skia_safe::Data::new_copy(&data); - let Some(decoded) = skia_safe::Image::from_encoded(skia_data) else { return }; + let Some(decoded) = skia_safe::Image::from_encoded(skia_data) else { + return; + }; cache.insert(self.src.clone(), decoded.clone()); decoded }; diff --git a/crates/rustmotion-components/src/intrinsic.rs b/crates/rustmotion-components/src/intrinsic.rs index 4c95e23..082a685 100644 --- a/crates/rustmotion-components/src/intrinsic.rs +++ b/crates/rustmotion-components/src/intrinsic.rs @@ -8,8 +8,7 @@ use skia_safe::{Font, FontStyle as SkFontStyle}; use rustmotion_core::css::style::{ - CssStyle, FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, - LineHeight, + CssStyle, FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, LineHeight, }; use rustmotion_core::engine::box_tree::{AvailableSpace, IntrinsicMeasure}; use rustmotion_core::engine::renderer::{ @@ -62,7 +61,12 @@ impl TextIntrinsic { /// Build with an explicit `wrap` override (used by atomic components like /// counter, kbd, badge that never wrap). - pub fn from_parts_with_wrap(content: &str, style: &CssStyle, max_width: Option, wrap: bool) -> Self { + pub fn from_parts_with_wrap( + content: &str, + style: &CssStyle, + max_width: Option, + wrap: bool, + ) -> Self { let mut t = Self::from_parts(content, style, max_width); t.wrap = wrap; t @@ -123,7 +127,10 @@ impl TextIntrinsic { .or_else(|| fm.match_family_style("Helvetica", sk_style)) .or_else(|| fm.match_family_style("Arial", sk_style)) .or_else(|| fm.match_family_style("sans-serif", sk_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, sk_style).expect("no fallback font")); + .unwrap_or_else(|| { + fm.legacy_make_typeface(None, sk_style) + .expect("no fallback font") + }); Font::from_typeface(typeface, self.font_size) } } @@ -240,10 +247,11 @@ impl CounterIntrinsic { } else { absmax }; - let display = - format_counter_value(signed, c.decimals, &c.separator, &c.prefix, &c.suffix); + let display = format_counter_value(signed, c.decimals, &c.separator, &c.prefix, &c.suffix); // Counter is atomic: it never wraps. - Self(TextIntrinsic::from_parts_with_wrap(&display, &c.style, None, false)) + Self(TextIntrinsic::from_parts_with_wrap( + &display, &c.style, None, false, + )) } } @@ -351,7 +359,10 @@ mod tests { content: "Hello World".into(), max_width: None, timing: Default::default(), - style: CssStyle { font_size: Some(Length::Px(32.0)), ..Default::default() }, + style: CssStyle { + font_size: Some(Length::Px(32.0)), + ..Default::default() + }, timeline: Vec::new(), stagger: None, text_shadow: None, @@ -364,7 +375,11 @@ mod tests { (AvailableSpace::MaxContent, AvailableSpace::MaxContent), ); assert!(w > 0.0, "width should be > 0, got {}", w); - assert!(h > 30.0, "height should be roughly font_size * line_height, got {}", h); + assert!( + h > 30.0, + "height should be roughly font_size * line_height, got {}", + h + ); } #[test] @@ -373,7 +388,10 @@ mod tests { content: "the quick brown fox jumps over the lazy dog".into(), max_width: None, timing: Default::default(), - style: CssStyle { font_size: Some(Length::Px(20.0)), ..Default::default() }, + style: CssStyle { + font_size: Some(Length::Px(20.0)), + ..Default::default() + }, timeline: Vec::new(), stagger: None, text_shadow: None, @@ -403,7 +421,10 @@ mod tests { content: "".into(), max_width: None, timing: Default::default(), - style: CssStyle { font_size: Some(Length::Px(24.0)), ..Default::default() }, + style: CssStyle { + font_size: Some(Length::Px(24.0)), + ..Default::default() + }, timeline: Vec::new(), stagger: None, text_shadow: None, diff --git a/crates/rustmotion-components/src/kbd.rs b/crates/rustmotion-components/src/kbd.rs index cdb2ef3..3ddbc58 100644 --- a/crates/rustmotion-components/src/kbd.rs +++ b/crates/rustmotion-components/src/kbd.rs @@ -68,7 +68,6 @@ impl Kbd { .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); skia_safe::Font::from_typeface(typeface, fs) } - } impl Kbd { @@ -77,7 +76,10 @@ impl Kbd { let h = layout_h; let radius = 6.0; - let bg_color = self.style.background_color_str().unwrap_or(&self.background_color); + let bg_color = self + .style + .background_color_str() + .unwrap_or(&self.background_color); // Shadow (bottom edge to simulate physical key depth) let shadow_h = 3.0; diff --git a/crates/rustmotion-components/src/legacy_dispatch.rs b/crates/rustmotion-components/src/legacy_dispatch.rs index d152904..84133f1 100644 --- a/crates/rustmotion-components/src/legacy_dispatch.rs +++ b/crates/rustmotion-components/src/legacy_dispatch.rs @@ -126,11 +126,11 @@ mod tests { use crate::box_builder::build_scene; use crate::shape::Shape; use crate::PositionMode; + use rustmotion_core::css::style::{CssStyle, Size as CSize}; use rustmotion_core::css::taffy_bridge::ConversionContext; + use rustmotion_core::css::units::LengthPercentage as CLP; use rustmotion_core::engine::box_tree::BoxKind; use rustmotion_core::engine::layout_pass::run_layout; - use rustmotion_core::css::style::{CssStyle, Size as CSize}; - use rustmotion_core::css::units::LengthPercentage as CLP; use rustmotion_core::schema::ShapeType; use std::sync::Arc; @@ -169,8 +169,8 @@ mod tests { // Build a Skia raster surface and run a paint pass against the // dispatcher — this just exercises the dispatcher hook end-to-end. - let mut surface = skia_safe::surfaces::raster_n32_premul((200, 200)) - .expect("raster surface"); + let mut surface = + skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface"); let canvas = surface.canvas(); let dispatcher = LegacyPaintDispatcher::new(&built.components); let frame = PaintFrame { @@ -365,7 +365,10 @@ mod tests { let built = crate::box_builder::build_scene_with_anim( &scene, (200.0, 200.0), - crate::box_builder::BuildAnimationCtx { time, scene_duration: 1.0 }, + crate::box_builder::BuildAnimationCtx { + time, + scene_duration: 1.0, + }, ); let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default()); let mut surface = @@ -382,7 +385,11 @@ mod tests { scene_duration: 1.0, }; rustmotion_core::engine::paint_pass::paint_tree( - canvas, &built.root, &layout, &frame, &dispatcher, + canvas, + &built.root, + &layout, + &frame, + &dispatcher, ); let snap = surface.image_snapshot(); let info = skia_safe::ImageInfo::new( diff --git a/crates/rustmotion-components/src/lib.rs b/crates/rustmotion-components/src/lib.rs index 6e0225b..761c50d 100644 --- a/crates/rustmotion-components/src/lib.rs +++ b/crates/rustmotion-components/src/lib.rs @@ -19,7 +19,6 @@ pub mod counter; pub mod cursor; pub mod divider; pub mod dot_map; -pub mod world_bitmap; pub mod flex; pub mod gauge; pub mod gif; @@ -27,12 +26,12 @@ pub mod gradient_text; pub mod grid; pub mod heatmap; pub mod icon; +pub mod image; pub mod kbd; pub mod line; pub mod list; pub mod lottie; pub mod marquee; -pub mod image; pub mod mockup; pub mod notification; pub mod particle; @@ -58,6 +57,7 @@ pub mod timeline; pub mod tooltip; pub mod treemap; pub mod video; +pub mod world_bitmap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -240,78 +240,180 @@ pub enum Component { impl Component { pub fn as_animatable(&self) -> Option<&dyn Animatable> { match self { - Component::Text(c) => Some(c), Component::Shape(c) => Some(c), Component::Image(c) => Some(c), - Component::Icon(c) => Some(c), Component::Svg(c) => Some(c), Component::Video(c) => Some(c), - Component::Gif(c) => Some(c), Component::Counter(c) => Some(c), Component::Cursor(c) => Some(c), - Component::Caption(c) => Some(c), Component::Codeblock(c) => Some(c), Component::Avatar(c) => Some(c), - Component::AvatarGroup(c) => Some(c), Component::Arrow(c) => Some(c), Component::Connector(c) => Some(c), - Component::Badge(c) => Some(c), Component::Callout(c) => Some(c), Component::Chart(c) => Some(c), - Component::Comparison(c) => Some(c), Component::Countdown(c) => Some(c), - Component::Divider(c) => Some(c), Component::DotMap(c) => Some(c), Component::Gauge(c) => Some(c), - Component::GradientText(c) => Some(c), Component::Heatmap(c) => Some(c), - Component::Kbd(c) => Some(c), Component::Line(c) => Some(c), Component::List(c) => Some(c), - Component::Lottie(c) => Some(c), Component::Marquee(c) => Some(c), Component::Mockup(c) => Some(c), - Component::Notification(c) => Some(c), Component::Particle(c) => Some(c), - Component::PillNav(c) => Some(c), Component::Progress(c) => Some(c), Component::QrCode(c) => Some(c), - Component::Rating(c) => Some(c), Component::Skeleton(c) => Some(c), Component::Slider(c) => Some(c), - Component::Sparkline(c) => Some(c), Component::Stat(c) => Some(c), Component::Stepper(c) => Some(c), - Component::Switch(c) => Some(c), Component::RichText(c) => Some(c), Component::Table(c) => Some(c), - Component::TagCloud(c) => Some(c), Component::Terminal(c) => Some(c), Component::Timeline(c) => Some(c), - Component::Tooltip(c) => Some(c), Component::Treemap(c) => Some(c), - Component::Flex(c) => Some(c), Component::Grid(c) => Some(c), - Component::Card(c) => Some(c), Component::Container(c) => Some(c), + Component::Text(c) => Some(c), + Component::Shape(c) => Some(c), + Component::Image(c) => Some(c), + Component::Icon(c) => Some(c), + Component::Svg(c) => Some(c), + Component::Video(c) => Some(c), + Component::Gif(c) => Some(c), + Component::Counter(c) => Some(c), + Component::Cursor(c) => Some(c), + Component::Caption(c) => Some(c), + Component::Codeblock(c) => Some(c), + Component::Avatar(c) => Some(c), + Component::AvatarGroup(c) => Some(c), + Component::Arrow(c) => Some(c), + Component::Connector(c) => Some(c), + Component::Badge(c) => Some(c), + Component::Callout(c) => Some(c), + Component::Chart(c) => Some(c), + Component::Comparison(c) => Some(c), + Component::Countdown(c) => Some(c), + Component::Divider(c) => Some(c), + Component::DotMap(c) => Some(c), + Component::Gauge(c) => Some(c), + Component::GradientText(c) => Some(c), + Component::Heatmap(c) => Some(c), + Component::Kbd(c) => Some(c), + Component::Line(c) => Some(c), + Component::List(c) => Some(c), + Component::Lottie(c) => Some(c), + Component::Marquee(c) => Some(c), + Component::Mockup(c) => Some(c), + Component::Notification(c) => Some(c), + Component::Particle(c) => Some(c), + Component::PillNav(c) => Some(c), + Component::Progress(c) => Some(c), + Component::QrCode(c) => Some(c), + Component::Rating(c) => Some(c), + Component::Skeleton(c) => Some(c), + Component::Slider(c) => Some(c), + Component::Sparkline(c) => Some(c), + Component::Stat(c) => Some(c), + Component::Stepper(c) => Some(c), + Component::Switch(c) => Some(c), + Component::RichText(c) => Some(c), + Component::Table(c) => Some(c), + Component::TagCloud(c) => Some(c), + Component::Terminal(c) => Some(c), + Component::Timeline(c) => Some(c), + Component::Tooltip(c) => Some(c), + Component::Treemap(c) => Some(c), + Component::Flex(c) => Some(c), + Component::Grid(c) => Some(c), + Component::Card(c) => Some(c), + Component::Container(c) => Some(c), Component::Positioned(_) => None, } } pub fn as_timed(&self) -> Option<&dyn Timed> { match self { - Component::Text(c) => Some(c), Component::Shape(c) => Some(c), Component::Image(c) => Some(c), - Component::Icon(c) => Some(c), Component::Svg(c) => Some(c), Component::Video(c) => Some(c), - Component::Gif(c) => Some(c), Component::Counter(c) => Some(c), Component::Cursor(c) => Some(c), - Component::Codeblock(c) => Some(c), Component::Avatar(c) => Some(c), - Component::AvatarGroup(c) => Some(c), Component::Arrow(c) => Some(c), Component::Connector(c) => Some(c), - Component::Badge(c) => Some(c), Component::Callout(c) => Some(c), Component::Chart(c) => Some(c), - Component::Comparison(c) => Some(c), Component::Countdown(c) => Some(c), - Component::Divider(c) => Some(c), Component::DotMap(c) => Some(c), Component::Gauge(c) => Some(c), - Component::GradientText(c) => Some(c), Component::Heatmap(c) => Some(c), - Component::Kbd(c) => Some(c), Component::Line(c) => Some(c), Component::List(c) => Some(c), - Component::Lottie(c) => Some(c), Component::Marquee(c) => Some(c), Component::Mockup(c) => Some(c), - Component::Notification(c) => Some(c), Component::Particle(c) => Some(c), - Component::PillNav(c) => Some(c), Component::Progress(c) => Some(c), Component::QrCode(c) => Some(c), - Component::Rating(c) => Some(c), Component::Skeleton(c) => Some(c), Component::Slider(c) => Some(c), - Component::Sparkline(c) => Some(c), Component::Stat(c) => Some(c), Component::Stepper(c) => Some(c), - Component::Switch(c) => Some(c), Component::RichText(c) => Some(c), Component::Table(c) => Some(c), - Component::TagCloud(c) => Some(c), Component::Terminal(c) => Some(c), Component::Timeline(c) => Some(c), - Component::Tooltip(c) => Some(c), Component::Treemap(c) => Some(c), - Component::Flex(c) => Some(c), Component::Grid(c) => Some(c), - Component::Card(c) => Some(c), Component::Container(c) => Some(c), + Component::Text(c) => Some(c), + Component::Shape(c) => Some(c), + Component::Image(c) => Some(c), + Component::Icon(c) => Some(c), + Component::Svg(c) => Some(c), + Component::Video(c) => Some(c), + Component::Gif(c) => Some(c), + Component::Counter(c) => Some(c), + Component::Cursor(c) => Some(c), + Component::Codeblock(c) => Some(c), + Component::Avatar(c) => Some(c), + Component::AvatarGroup(c) => Some(c), + Component::Arrow(c) => Some(c), + Component::Connector(c) => Some(c), + Component::Badge(c) => Some(c), + Component::Callout(c) => Some(c), + Component::Chart(c) => Some(c), + Component::Comparison(c) => Some(c), + Component::Countdown(c) => Some(c), + Component::Divider(c) => Some(c), + Component::DotMap(c) => Some(c), + Component::Gauge(c) => Some(c), + Component::GradientText(c) => Some(c), + Component::Heatmap(c) => Some(c), + Component::Kbd(c) => Some(c), + Component::Line(c) => Some(c), + Component::List(c) => Some(c), + Component::Lottie(c) => Some(c), + Component::Marquee(c) => Some(c), + Component::Mockup(c) => Some(c), + Component::Notification(c) => Some(c), + Component::Particle(c) => Some(c), + Component::PillNav(c) => Some(c), + Component::Progress(c) => Some(c), + Component::QrCode(c) => Some(c), + Component::Rating(c) => Some(c), + Component::Skeleton(c) => Some(c), + Component::Slider(c) => Some(c), + Component::Sparkline(c) => Some(c), + Component::Stat(c) => Some(c), + Component::Stepper(c) => Some(c), + Component::Switch(c) => Some(c), + Component::RichText(c) => Some(c), + Component::Table(c) => Some(c), + Component::TagCloud(c) => Some(c), + Component::Terminal(c) => Some(c), + Component::Timeline(c) => Some(c), + Component::Tooltip(c) => Some(c), + Component::Treemap(c) => Some(c), + Component::Flex(c) => Some(c), + Component::Grid(c) => Some(c), + Component::Card(c) => Some(c), + Component::Container(c) => Some(c), Component::Caption(_) | Component::Positioned(_) => None, } } pub fn as_styled(&self) -> &dyn Styled { match self { - Component::Text(c) => c, Component::Shape(c) => c, Component::Image(c) => c, - Component::Icon(c) => c, Component::Svg(c) => c, Component::Video(c) => c, - Component::Gif(c) => c, Component::Counter(c) => c, Component::Cursor(c) => c, - Component::Caption(c) => c, Component::Codeblock(c) => c, Component::Avatar(c) => c, - Component::AvatarGroup(c) => c, Component::Arrow(c) => c, Component::Connector(c) => c, - Component::Badge(c) => c, Component::Callout(c) => c, Component::Chart(c) => c, - Component::Comparison(c) => c, Component::Countdown(c) => c, - Component::Divider(c) => c, Component::DotMap(c) => c, Component::Gauge(c) => c, - Component::GradientText(c) => c, Component::Heatmap(c) => c, - Component::Kbd(c) => c, Component::Line(c) => c, Component::List(c) => c, - Component::Lottie(c) => c, Component::Marquee(c) => c, Component::Mockup(c) => c, - Component::Notification(c) => c, Component::Particle(c) => c, - Component::PillNav(c) => c, Component::Progress(c) => c, Component::QrCode(c) => c, - Component::Rating(c) => c, Component::Skeleton(c) => c, Component::Slider(c) => c, - Component::Sparkline(c) => c, Component::Stat(c) => c, Component::Stepper(c) => c, - Component::Switch(c) => c, Component::RichText(c) => c, Component::Table(c) => c, - Component::TagCloud(c) => c, Component::Terminal(c) => c, Component::Timeline(c) => c, - Component::Tooltip(c) => c, Component::Treemap(c) => c, - Component::Positioned(c) => c, Component::Flex(c) => c, Component::Grid(c) => c, - Component::Card(c) => c, Component::Container(c) => c, + Component::Text(c) => c, + Component::Shape(c) => c, + Component::Image(c) => c, + Component::Icon(c) => c, + Component::Svg(c) => c, + Component::Video(c) => c, + Component::Gif(c) => c, + Component::Counter(c) => c, + Component::Cursor(c) => c, + Component::Caption(c) => c, + Component::Codeblock(c) => c, + Component::Avatar(c) => c, + Component::AvatarGroup(c) => c, + Component::Arrow(c) => c, + Component::Connector(c) => c, + Component::Badge(c) => c, + Component::Callout(c) => c, + Component::Chart(c) => c, + Component::Comparison(c) => c, + Component::Countdown(c) => c, + Component::Divider(c) => c, + Component::DotMap(c) => c, + Component::Gauge(c) => c, + Component::GradientText(c) => c, + Component::Heatmap(c) => c, + Component::Kbd(c) => c, + Component::Line(c) => c, + Component::List(c) => c, + Component::Lottie(c) => c, + Component::Marquee(c) => c, + Component::Mockup(c) => c, + Component::Notification(c) => c, + Component::Particle(c) => c, + Component::PillNav(c) => c, + Component::Progress(c) => c, + Component::QrCode(c) => c, + Component::Rating(c) => c, + Component::Skeleton(c) => c, + Component::Slider(c) => c, + Component::Sparkline(c) => c, + Component::Stat(c) => c, + Component::Stepper(c) => c, + Component::Switch(c) => c, + Component::RichText(c) => c, + Component::Table(c) => c, + Component::TagCloud(c) => c, + Component::Terminal(c) => c, + Component::Timeline(c) => c, + Component::Tooltip(c) => c, + Component::Treemap(c) => c, + Component::Positioned(c) => c, + Component::Flex(c) => c, + Component::Grid(c) => c, + Component::Card(c) => c, + Component::Container(c) => c, } } @@ -384,7 +486,7 @@ pub fn draw_gradient_border( rrect: &skia_safe::RRect, gb: &rustmotion_core::schema::GradientBorder, ) { - use skia_safe::{Paint, PaintStyle, Point, gradient_shader::GradientShaderColors}; + use skia_safe::{gradient_shader::GradientShaderColors, Paint, PaintStyle, Point}; if gb.colors.len() < 2 { return; @@ -405,10 +507,14 @@ pub fn draw_gradient_border( cy + angle_rad.sin() * half_diag, ); - let colors: Vec = gb.colors.iter().map(|c| { - let (r, g, b, a) = rustmotion_core::engine::renderer::parse_hex_color(c); - skia_safe::Color::from_argb(a, r, g, b) - }).collect(); + let colors: Vec = gb + .colors + .iter() + .map(|c| { + let (r, g, b, a) = rustmotion_core::engine::renderer::parse_hex_color(c); + skia_safe::Color::from_argb(a, r, g, b) + }) + .collect(); let shader = skia_safe::shader::Shader::linear_gradient( (start, end), diff --git a/crates/rustmotion-components/src/line.rs b/crates/rustmotion-components/src/line.rs index 1c4d314..78d8d3b 100644 --- a/crates/rustmotion-components/src/line.rs +++ b/crates/rustmotion-components/src/line.rs @@ -77,11 +77,7 @@ impl Line { } } - canvas.draw_line( - (self.x1, self.y1), - (self.x2, self.y2), - &paint, - ); + canvas.draw_line((self.x1, self.y1), (self.x2, self.y2), &paint); } } diff --git a/crates/rustmotion-components/src/list.rs b/crates/rustmotion-components/src/list.rs index 469e542..d487238 100644 --- a/crates/rustmotion-components/src/list.rs +++ b/crates/rustmotion-components/src/list.rs @@ -30,18 +30,14 @@ fn default_width() -> f32 { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum ListVariant { + #[default] Bullet, Numbered, Checklist, } -impl Default for ListVariant { - fn default() -> Self { - Self::Bullet - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ListItem { pub text: String, @@ -132,11 +128,9 @@ impl List { skia_safe::AlphaType::Premul, None, ); - if let Some(decoded) = skia_safe::images::raster_from_data( - &info, - img_data, - icon_w as usize * 4, - ) { + if let Some(decoded) = + skia_safe::images::raster_from_data(&info, img_data, icon_w as usize * 4) + { cache.insert(cache_key, decoded.clone()); decoded } else { diff --git a/crates/rustmotion-components/src/lottie.rs b/crates/rustmotion-components/src/lottie.rs index 259a008..9a4878f 100644 --- a/crates/rustmotion-components/src/lottie.rs +++ b/crates/rustmotion-components/src/lottie.rs @@ -1,13 +1,13 @@ use rustmotion_core::css::CssStyle; -use rustmotion_core::error::{Result, RustmotionError}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, ColorType, ImageInfo, Paint, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::asset_cache; +use rustmotion_core::error::{Result, RustmotionError}; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use skia_safe::{Canvas, ColorType, ImageInfo, Paint, Rect}; /// A Lottie animation component that renders frame-by-frame from a .json Lottie file. /// @@ -61,8 +61,10 @@ impl Lottie { /// Parse Lottie JSON metadata (fr, ip, op, w, h). fn parse_metadata(&self) -> Result<(f64, usize, f64, f32, f32)> { let json_str = if let Some(ref src) = self.src { - std::fs::read_to_string(src) - .map_err(|e| RustmotionError::LottieRead { path: src.clone(), reason: e.to_string() })? + std::fs::read_to_string(src).map_err(|e| RustmotionError::LottieRead { + path: src.clone(), + reason: e.to_string(), + })? } else if let Some(ref data) = self.data { data.clone() } else { @@ -90,11 +92,16 @@ impl Lottie { /// Load a pre-rendered frame from frames_dir. fn load_frame_from_dir(&self, frames_dir: &str, frame: usize) -> Result { let frame_path = format!("{}/{:04}.png", frames_dir, frame); - let data = std::fs::read(&frame_path) - .map_err(|e| RustmotionError::LottieFrameRead { path: frame_path.clone(), reason: e.to_string() })?; - - let img = image::load_from_memory(&data) - .map_err(|e| RustmotionError::LottieFrameDecode { path: frame_path.clone(), reason: e.to_string() })?; + let data = std::fs::read(&frame_path).map_err(|e| RustmotionError::LottieFrameRead { + path: frame_path.clone(), + reason: e.to_string(), + })?; + + let img = + image::load_from_memory(&data).map_err(|e| RustmotionError::LottieFrameDecode { + path: frame_path.clone(), + reason: e.to_string(), + })?; let rgba = img.to_rgba8(); let (w, h) = rgba.dimensions(); @@ -106,8 +113,11 @@ impl Lottie { None, ); - skia_safe::images::raster_from_data(&img_info, img_data, w as usize * 4) - .ok_or(RustmotionError::SkiaImageCreation { target: "lottie frame".to_string() }) + skia_safe::images::raster_from_data(&img_info, img_data, w as usize * 4).ok_or( + RustmotionError::SkiaImageCreation { + target: "lottie frame".to_string(), + }, + ) } } @@ -119,7 +129,8 @@ impl Painter for Lottie { _props: &AnimatedProperties, ctx: &PaintCtx, ) { - let Ok((fr, total_frames, duration, _intrinsic_w, _intrinsic_h)) = self.parse_metadata() else { + let Ok((fr, total_frames, duration, _intrinsic_w, _intrinsic_h)) = self.parse_metadata() + else { return; }; diff --git a/crates/rustmotion-components/src/marquee.rs b/crates/rustmotion-components/src/marquee.rs index a94eb83..abd88dc 100644 --- a/crates/rustmotion-components/src/marquee.rs +++ b/crates/rustmotion-components/src/marquee.rs @@ -27,17 +27,13 @@ fn default_color() -> String { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum MarqueeDirection { + #[default] Left, Right, } -impl Default for MarqueeDirection { - fn default() -> Self { - Self::Left - } -} - #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Marquee { pub content: String, diff --git a/crates/rustmotion-components/src/mockup.rs b/crates/rustmotion-components/src/mockup.rs index 1e784f1..ad29189 100644 --- a/crates/rustmotion-components/src/mockup.rs +++ b/crates/rustmotion-components/src/mockup.rs @@ -2,7 +2,7 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::error::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, Paint, PaintStyle, Path, Rect, RRect}; +use skia_safe::{Canvas, Paint, PaintStyle, Path, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; @@ -13,32 +13,24 @@ use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum MockupDevice { + #[default] Iphone, Android, Laptop, Browser, } -impl Default for MockupDevice { - fn default() -> Self { - Self::Iphone - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum MockupTheme { + #[default] Dark, Light, } -impl Default for MockupTheme { - fn default() -> Self { - Self::Dark - } -} - impl MockupTheme { fn bezel_color(&self) -> &str { match self { @@ -122,20 +114,21 @@ impl Mockup { return Ok(cached.clone()); } - let data = std::fs::read(&self.src) - .map_err(|e| RustmotionError::ImageLoad { path: self.src.clone(), reason: e.to_string() })?; + let data = std::fs::read(&self.src).map_err(|e| RustmotionError::ImageLoad { + path: self.src.clone(), + reason: e.to_string(), + })?; let skia_data = skia_safe::Data::new_copy(&data); - let decoded = skia_safe::Image::from_encoded(skia_data) - .ok_or_else(|| RustmotionError::ImageDecode { path: self.src.clone() })?; + let decoded = skia_safe::Image::from_encoded(skia_data).ok_or_else(|| { + RustmotionError::ImageDecode { + path: self.src.clone(), + } + })?; cache.insert(self.src.clone(), decoded.clone()); Ok(decoded) } - fn draw_content_image( - &self, - canvas: &Canvas, - screen_rect: Rect, - ) -> Result<()> { + fn draw_content_image(&self, canvas: &Canvas, screen_rect: Rect) -> Result<()> { let img = self.load_content_image()?; let img_w = img.width() as f32; let img_h = img.height() as f32; @@ -158,13 +151,7 @@ impl Mockup { Ok(()) } - fn render_phone( - &self, - canvas: &Canvas, - w: f32, - h: f32, - m: &DeviceMetrics, - ) -> Result<()> { + fn render_phone(&self, canvas: &Canvas, w: f32, h: f32, m: &DeviceMetrics) -> Result<()> { // Device body let body_rect = Rect::from_xywh(0.0, 0.0, w, h); let body_rrect = RRect::new_rect_xy(body_rect, m.corner_radius, m.corner_radius); @@ -185,7 +172,8 @@ impl Mockup { let mut screen_bg = paint_from_hex(self.theme.screen_bg()); screen_bg.set_style(PaintStyle::Fill); let screen_radius = m.corner_radius - m.bezel_side; - let screen_rrect = RRect::new_rect_xy(screen_rect, screen_radius.max(0.0), screen_radius.max(0.0)); + let screen_rrect = + RRect::new_rect_xy(screen_rect, screen_radius.max(0.0), screen_radius.max(0.0)); canvas.draw_rrect(screen_rrect, &screen_bg); // Content image @@ -237,21 +225,17 @@ impl Mockup { indicator_paint.set_style(PaintStyle::Fill); indicator_paint.set_anti_alias(true); - let indicator_rect = Rect::from_xywh(indicator_x, indicator_y, indicator_w, indicator_h); - let indicator_rrect = RRect::new_rect_xy(indicator_rect, indicator_h / 2.0, indicator_h / 2.0); + let indicator_rect = + Rect::from_xywh(indicator_x, indicator_y, indicator_w, indicator_h); + let indicator_rrect = + RRect::new_rect_xy(indicator_rect, indicator_h / 2.0, indicator_h / 2.0); canvas.draw_rrect(indicator_rrect, &indicator_paint); } Ok(()) } - fn render_laptop( - &self, - canvas: &Canvas, - w: f32, - h: f32, - m: &DeviceMetrics, - ) -> Result<()> { + fn render_laptop(&self, canvas: &Canvas, w: f32, h: f32, m: &DeviceMetrics) -> Result<()> { let screen_h = h * 0.85; let _base_h = h - screen_h; @@ -301,13 +285,7 @@ impl Mockup { Ok(()) } - fn render_browser( - &self, - canvas: &Canvas, - w: f32, - h: f32, - m: &DeviceMetrics, - ) -> Result<()> { + fn render_browser(&self, canvas: &Canvas, w: f32, h: f32, m: &DeviceMetrics) -> Result<()> { // Window background let body_rect = Rect::from_xywh(0.0, 0.0, w, h); let body_rrect = RRect::new_rect_xy(body_rect, m.corner_radius, m.corner_radius); diff --git a/crates/rustmotion-components/src/notification.rs b/crates/rustmotion-components/src/notification.rs index 7c9f068..0ebdcec 100644 --- a/crates/rustmotion-components/src/notification.rs +++ b/crates/rustmotion-components/src/notification.rs @@ -27,19 +27,15 @@ fn default_stack_gap() -> f32 { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum NotificationVariant { + #[default] Info, Success, Warning, Error, } -impl Default for NotificationVariant { - fn default() -> Self { - Self::Info - } -} - impl NotificationVariant { fn default_color(&self) -> &str { match self { @@ -198,11 +194,9 @@ impl Notification { skia_safe::AlphaType::Premul, None, ); - if let Some(decoded) = skia_safe::images::raster_from_data( - &info, - img_data, - icon_w as usize * 4, - ) { + if let Some(decoded) = + skia_safe::images::raster_from_data(&info, img_data, icon_w as usize * 4) + { cache.insert(cache_key, decoded.clone()); decoded } else { diff --git a/crates/rustmotion-components/src/particle.rs b/crates/rustmotion-components/src/particle.rs index 49a8dee..c845846 100644 --- a/crates/rustmotion-components/src/particle.rs +++ b/crates/rustmotion-components/src/particle.rs @@ -11,7 +11,9 @@ use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum ParticleType { + #[default] Confetti, Snow, Stars, @@ -19,12 +21,6 @@ pub enum ParticleType { Halo, } -impl Default for ParticleType { - fn default() -> Self { - Self::Confetti - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct SizeRange { pub min: f32, @@ -33,7 +29,10 @@ pub struct SizeRange { impl Default for SizeRange { fn default() -> Self { - Self { min: 4.0, max: 12.0 } + Self { + min: 4.0, + max: 12.0, + } } } @@ -150,7 +149,8 @@ impl Painter for Particle { let base_x = rv[0] as f32 * w; let base_y = rv[1] as f32 * h; - let size = self.size_range.min + rv[2] as f32 * (self.size_range.max - self.size_range.min); + let size = + self.size_range.min + rv[2] as f32 * (self.size_range.max - self.size_range.min); let color_idx = (rv[3] * colors.len() as f64) as usize % colors.len(); let phase = rv[4] as f32 * std::f32::consts::TAU; let speed_var = 0.7 + rv[5] as f32 * 0.6; @@ -238,7 +238,9 @@ impl Painter for Particle { paint.set_alpha_f(pulse * 0.6); let blur_sigma = radius * 0.6; - if let Some(mask) = MaskFilter::blur(skia_safe::BlurStyle::Normal, blur_sigma, false) { + if let Some(mask) = + MaskFilter::blur(skia_safe::BlurStyle::Normal, blur_sigma, false) + { paint.set_mask_filter(mask); } diff --git a/crates/rustmotion-components/src/pill_nav.rs b/crates/rustmotion-components/src/pill_nav.rs index c827bfa..85c5de6 100644 --- a/crates/rustmotion-components/src/pill_nav.rs +++ b/crates/rustmotion-components/src/pill_nav.rs @@ -191,10 +191,7 @@ impl PillNav { let to_x = tab_positions[to]; let from_w = tab_widths[from]; let to_w = tab_widths[to]; - ( - from_x + (to_x - from_x) * t, - from_w + (to_w - from_w) * t, - ) + (from_x + (to_x - from_x) * t, from_w + (to_w - from_w) * t) } else { (tab_positions[active_idx], tab_widths[active_idx]) }; diff --git a/crates/rustmotion-components/src/progress.rs b/crates/rustmotion-components/src/progress.rs index a4f9ca2..71bd918 100644 --- a/crates/rustmotion-components/src/progress.rs +++ b/crates/rustmotion-components/src/progress.rs @@ -2,13 +2,13 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::error::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, PaintStyle, Rect, RRect}; +use skia_safe::{Canvas, PaintStyle, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - color4f_from_hex, draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, - paint_from_hex, + color4f_from_hex, draw_text_with_fallback, emoji_typeface, font_mgr, + measure_text_with_fallback, paint_from_hex, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -31,17 +31,13 @@ fn default_track_width() -> f32 { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum ProgressVariant { + #[default] Linear, Circular, } -impl Default for ProgressVariant { - fn default() -> Self { - Self::Linear - } -} - #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Progress { #[serde(default)] @@ -117,8 +113,7 @@ impl Progress { // Fill (progress) if progress > 0.001 { - let mut fill_paint = - skia_safe::Paint::new(color4f_from_hex(&self.fill_color), None); + let mut fill_paint = skia_safe::Paint::new(color4f_from_hex(&self.fill_color), None); fill_paint.set_style(PaintStyle::Fill); fill_paint.set_anti_alias(true); @@ -186,7 +181,14 @@ impl Progress { let text_x = cx - text_w / 2.0; let text_y = cy + (-metrics.ascent) / 2.0; draw_text_with_fallback( - canvas, &text, &font, &emoji_font, 0.0, text_x, text_y, &text_paint, + canvas, + &text, + &font, + &emoji_font, + 0.0, + text_x, + text_y, + &text_paint, ); } diff --git a/crates/rustmotion-components/src/qrcode.rs b/crates/rustmotion-components/src/qrcode.rs index e6067f5..194f99d 100644 --- a/crates/rustmotion-components/src/qrcode.rs +++ b/crates/rustmotion-components/src/qrcode.rs @@ -54,7 +54,9 @@ impl Painter for QrCode { ) { use qrcode::QrCode as QrCodeLib; - let Ok(code) = QrCodeLib::new(self.content.as_bytes()) else { return }; + let Ok(code) = QrCodeLib::new(self.content.as_bytes()) else { + return; + }; let modules = code.to_colors(); let module_count = code.width() as f32; @@ -62,10 +64,7 @@ impl Painter for QrCode { let mut bg_paint = skia_safe::Paint::new(color4f_from_hex(&self.background_color), None); bg_paint.set_style(PaintStyle::Fill); - canvas.draw_rect( - Rect::from_xywh(0.0, 0.0, self.size, self.size), - &bg_paint, - ); + canvas.draw_rect(Rect::from_xywh(0.0, 0.0, self.size, self.size), &bg_paint); let mut fg_paint = skia_safe::Paint::new(color4f_from_hex(&self.foreground_color), None); fg_paint.set_style(PaintStyle::Fill); diff --git a/crates/rustmotion-components/src/rating.rs b/crates/rustmotion-components/src/rating.rs index a3ae3d4..85af5c7 100644 --- a/crates/rustmotion-components/src/rating.rs +++ b/crates/rustmotion-components/src/rating.rs @@ -84,8 +84,7 @@ impl Rating { let mut path = Path::new(); for i in 0..10 { - let angle = - -std::f32::consts::FRAC_PI_2 + i as f32 * std::f32::consts::PI / 5.0; + let angle = -std::f32::consts::FRAC_PI_2 + i as f32 * std::f32::consts::PI / 5.0; let r = if i % 2 == 0 { outer_radius } else { @@ -131,8 +130,7 @@ impl Rating { let fill_fraction = star_fill as f32; let clip_x = cx - outer_radius; let clip_w = self.size * fill_fraction; - let clip_rect = - skia_safe::Rect::from_xywh(clip_x, 0.0, clip_w, self.size); + let clip_rect = skia_safe::Rect::from_xywh(clip_x, 0.0, clip_w, self.size); canvas.save(); canvas.clip_rect(clip_rect, skia_safe::ClipOp::Intersect, true); diff --git a/crates/rustmotion-components/src/rich_text.rs b/crates/rustmotion-components/src/rich_text.rs index 49d187c..917065f 100644 --- a/crates/rustmotion-components/src/rich_text.rs +++ b/crates/rustmotion-components/src/rich_text.rs @@ -2,11 +2,15 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, Font, FontStyle}; -use rustmotion_core::css::style::{FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign}; +use rustmotion_core::css::style::{ + FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign, +}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{font_mgr, paint_from_hex, draw_text_with_fallback, measure_text_with_fallback, emoji_typeface}; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, +}; use rustmotion_core::schema::{FontStyleType, FontWeight, TextAlign, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -75,7 +79,8 @@ fn make_font( .or_else(|| fm.match_family_style("Arial", skia_style)) .or_else(|| fm.match_family_style("sans-serif", skia_style)) .unwrap_or_else(|| { - fm.legacy_make_typeface(None, skia_style).expect("No fallback font") + fm.legacy_make_typeface(None, skia_style) + .expect("No fallback font") }); Font::from_typeface(typeface, size) } @@ -95,7 +100,9 @@ impl RichText { let default_color = self.style.color_str_or("#FFFFFF"); let default_family = self.style.font_family_or("Inter"); let default_weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => FontWeight::Bold, + Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { + FontWeight::Bold + } Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold, Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n), _ => FontWeight::Normal, @@ -115,24 +122,38 @@ impl RichText { let emoji_tf = emoji_typeface(); // Prepare all spans with their fonts and measurements - let mut prepared: Vec = self.spans.iter().map(|span| { - let size = span.font_size.unwrap_or(default_size); - let family = span.font_family.as_deref().unwrap_or(default_family); - let weight = span.font_weight.as_ref().unwrap_or(&default_weight); - let fstyle = span.font_style.as_ref().unwrap_or(&default_font_style); - let color = span.color.as_deref().unwrap_or(default_color).to_string(); - let letter_spacing = span.letter_spacing.unwrap_or(0.0); - let font = make_font(family, weight, fstyle, size); - let emoji_font = emoji_tf.as_ref().map(|tf| Font::from_typeface(tf.clone(), size)); - let width = measure_text_with_fallback(&span.text, &font, &emoji_font, letter_spacing); - - PreparedSpan { text: span.text.clone(), font, color, letter_spacing, width } - }).collect(); + let mut prepared: Vec = self + .spans + .iter() + .map(|span| { + let size = span.font_size.unwrap_or(default_size); + let family = span.font_family.as_deref().unwrap_or(default_family); + let weight = span.font_weight.as_ref().unwrap_or(&default_weight); + let fstyle = span.font_style.as_ref().unwrap_or(&default_font_style); + let color = span.color.as_deref().unwrap_or(default_color).to_string(); + let letter_spacing = span.letter_spacing.unwrap_or(0.0); + let font = make_font(family, weight, fstyle, size); + let emoji_font = emoji_tf + .as_ref() + .map(|tf| Font::from_typeface(tf.clone(), size)); + let width = + measure_text_with_fallback(&span.text, &font, &emoji_font, letter_spacing); + + PreparedSpan { + text: span.text.clone(), + font, + color, + letter_spacing, + width, + } + }) + .collect(); // Typewriter animation: truncate spans based on visible_chars_progress if props.visible_chars_progress >= 0.0 { let total_chars: usize = prepared.iter().map(|ps| ps.text.chars().count()).sum(); - let visible = ((props.visible_chars_progress * total_chars as f32).round() as usize).min(total_chars); + let visible = ((props.visible_chars_progress * total_chars as f32).round() as usize) + .min(total_chars); if visible == 0 { return; } @@ -146,8 +167,15 @@ impl RichText { truncated.push(ps); } else { let truncated_text: String = ps.text.chars().take(remaining).collect(); - let emoji_font = emoji_tf.as_ref().map(|tf| Font::from_typeface(tf.clone(), ps.font.size())); - ps.width = measure_text_with_fallback(&truncated_text, &ps.font, &emoji_font, ps.letter_spacing); + let emoji_font = emoji_tf + .as_ref() + .map(|tf| Font::from_typeface(tf.clone(), ps.font.size())); + ps.width = measure_text_with_fallback( + &truncated_text, + &ps.font, + &emoji_font, + ps.letter_spacing, + ); ps.text = truncated_text; truncated.push(ps); break; @@ -177,14 +205,20 @@ impl RichText { width: f32, } - let mut lines: Vec = vec![Line { spans: vec![], width: 0.0 }]; + let mut lines: Vec = vec![Line { + spans: vec![], + width: 0.0, + }]; for (i, ps) in prepared.iter().enumerate() { let current = lines.last_mut().unwrap(); if current.width + ps.width > wrap_width && !current.spans.is_empty() { // Start new line lines.push(Line { - spans: vec![LineSpan { span_idx: i, x: 0.0 }], + spans: vec![LineSpan { + span_idx: i, + x: 0.0, + }], width: ps.width, }); } else { @@ -201,10 +235,13 @@ impl RichText { }; // Find max ascent for baseline alignment - let max_ascent = prepared.iter().map(|ps| { - let (_, m) = ps.font.metrics(); - -m.ascent - }).fold(0.0f32, f32::max); + let max_ascent = prepared + .iter() + .map(|ps| { + let (_, m) = ps.font.metrics(); + -m.ascent + }) + .fold(0.0f32, f32::max); let baseline_offset = (line_height_val + max_ascent) / 2.0; @@ -220,7 +257,9 @@ impl RichText { for ls in &line.spans { let ps = &prepared[ls.span_idx]; let paint = paint_from_hex(&ps.color); - let emoji_font = emoji_tf.as_ref().map(|tf| Font::from_typeface(tf.clone(), ps.font.size())); + let emoji_font = emoji_tf + .as_ref() + .map(|tf| Font::from_typeface(tf.clone(), ps.font.size())); draw_text_with_fallback( canvas, diff --git a/crates/rustmotion-components/src/shape.rs b/crates/rustmotion-components/src/shape.rs index 4ceb569..a86b994 100644 --- a/crates/rustmotion-components/src/shape.rs +++ b/crates/rustmotion-components/src/shape.rs @@ -6,9 +6,14 @@ use skia_safe::{Canvas, Paint, PaintStyle, Point}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{build_shape_path, color4f_from_hex, draw_shape_path, font_mgr, paint_from_hex, wrap_text_with_fallback, draw_text_with_fallback, measure_text_with_fallback, emoji_typeface}; +use rustmotion_core::engine::renderer::{ + build_shape_path, color4f_from_hex, draw_shape_path, draw_text_with_fallback, emoji_typeface, + font_mgr, measure_text_with_fallback, paint_from_hex, wrap_text_with_fallback, +}; use rustmotion_core::error::RustmotionError; -use rustmotion_core::schema::{Fill, GradientType, ShapeText, ShapeType, Stroke, TextAlign, TimelineStep, FontWeight}; +use rustmotion_core::schema::{ + Fill, FontWeight, GradientType, ShapeText, ShapeType, Stroke, TextAlign, TimelineStep, +}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -52,8 +57,11 @@ impl Painter for Shape { let mut paint = match fill { Fill::Solid(color) => paint_from_hex(color), Fill::Gradient(gradient) => { - let colors: Vec = - gradient.colors.iter().map(|c| color4f_from_hex(c)).collect(); + let colors: Vec = gradient + .colors + .iter() + .map(|c| color4f_from_hex(c)) + .collect(); let stops: Option> = gradient.stops.clone(); let mut paint = Paint::default(); paint.set_anti_alias(true); @@ -111,7 +119,11 @@ impl Painter for Shape { if let Some(stroke) = &self.stroke { let mut paint = paint_from_hex(&stroke.color); paint.set_style(PaintStyle::Stroke); - let stroke_w = if props.stroke_width >= 0.0 { props.stroke_width } else { stroke.width }; + let stroke_w = if props.stroke_width >= 0.0 { + props.stroke_width + } else { + stroke.width + }; paint.set_stroke_width(stroke_w); if props.draw_progress >= 0.0 && props.draw_progress < 1.0 { @@ -157,7 +169,11 @@ fn render_shape_text( let font_style = match text.font_weight { FontWeight::Bold => skia_safe::FontStyle::bold(), FontWeight::Normal => skia_safe::FontStyle::normal(), - FontWeight::Weight(w) => skia_safe::FontStyle::new(skia_safe::font_style::Weight::from(w as i32), skia_safe::font_style::Width::NORMAL, skia_safe::font_style::Slant::Upright), + FontWeight::Weight(w) => skia_safe::FontStyle::new( + skia_safe::font_style::Weight::from(w as i32), + skia_safe::font_style::Width::NORMAL, + skia_safe::font_style::Slant::Upright, + ), }; let typeface = fm @@ -167,7 +183,7 @@ fn render_shape_text( .or_else(|| fm.match_family_style("sans-serif", font_style)) .or_else(|| { if fm.count_families() > 0 { - fm.match_family_style(&fm.family_name(0), font_style) + fm.match_family_style(fm.family_name(0), font_style) } else { None } @@ -215,7 +231,16 @@ fn render_shape_text( TextAlign::Right => area_x + area_w - line_width, }; let y = y_start + i as f32 * line_height; - draw_text_with_fallback(canvas, line, &font, &emoji_font, letter_spacing, x, y, &paint); + draw_text_with_fallback( + canvas, + line, + &font, + &emoji_font, + letter_spacing, + x, + y, + &paint, + ); } Ok(()) diff --git a/crates/rustmotion-components/src/skeleton.rs b/crates/rustmotion-components/src/skeleton.rs index a47d4f6..b9bbe27 100644 --- a/crates/rustmotion-components/src/skeleton.rs +++ b/crates/rustmotion-components/src/skeleton.rs @@ -27,18 +27,14 @@ fn default_speed() -> f32 { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum SkeletonVariant { + #[default] Rectangle, Circle, Text, } -impl Default for SkeletonVariant { - fn default() -> Self { - Self::Rectangle - } -} - #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Skeleton { #[serde(default)] @@ -89,13 +85,7 @@ rustmotion_core::impl_traits!(Skeleton { }); impl Skeleton { - fn draw_shimmer_rect( - &self, - canvas: &Canvas, - rect: Rect, - radius: f32, - time: f64, - ) { + fn draw_shimmer_rect(&self, canvas: &Canvas, rect: Rect, radius: f32, time: f64) { let w = rect.width(); // Base color @@ -121,7 +111,9 @@ impl Skeleton { Point::new(shimmer_x + shimmer_w, 0.0), ), skia_safe::gradient_shader::GradientShaderColors::Colors(&[ - transparent, highlight, transparent, + transparent, + highlight, + transparent, ]), None, skia_safe::TileMode::Clamp, @@ -161,11 +153,7 @@ impl Skeleton { let total_lines = self.lines.max(1); for i in 0..total_lines { let y = i as f32 * (self.line_height + self.line_gap); - let line_w = if i == total_lines - 1 { - w * 0.6 - } else { - w - }; + let line_w = if i == total_lines - 1 { w * 0.6 } else { w }; let rect = Rect::from_xywh(0.0, y, line_w, self.line_height); self.draw_shimmer_rect(canvas, rect, self.border_radius * 0.5, time); } diff --git a/crates/rustmotion-components/src/slider.rs b/crates/rustmotion-components/src/slider.rs index 678d384..4cd7d35 100644 --- a/crates/rustmotion-components/src/slider.rs +++ b/crates/rustmotion-components/src/slider.rs @@ -156,7 +156,14 @@ impl Slider { let text_y = thumb_cy - thumb_r - 4.0 - (-metrics.descent); draw_text_with_fallback( - canvas, &text, &font, &emoji_font, 0.0, text_x, text_y, &text_paint, + canvas, + &text, + &font, + &emoji_font, + 0.0, + text_x, + text_y, + &text_paint, ); } } diff --git a/crates/rustmotion-components/src/stat.rs b/crates/rustmotion-components/src/stat.rs index b50b11a..cb939d5 100644 --- a/crates/rustmotion-components/src/stat.rs +++ b/crates/rustmotion-components/src/stat.rs @@ -30,18 +30,14 @@ fn default_label_color() -> String { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum TrendDirection { Up, Down, + #[default] Neutral, } -impl Default for TrendDirection { - fn default() -> Self { - Self::Neutral - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct StatTrend { pub value: String, @@ -123,7 +119,14 @@ impl Stat { let ly = y_cursor + (-metrics.ascent); draw_text_with_fallback( - canvas, label, &font, &emoji_font, 0.0, pad, ly, &label_paint, + canvas, + label, + &font, + &emoji_font, + 0.0, + pad, + ly, + &label_paint, ); y_cursor += self.label_font_size * 1.5; } @@ -136,8 +139,8 @@ impl Stat { .or_else(|| fm.match_family_style("Helvetica", font_style)) .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); let font = skia_safe::Font::from_typeface(typeface, self.value_font_size); - let emoji_font = emoji_typeface() - .map(|tf| skia_safe::Font::from_typeface(tf, self.value_font_size)); + let emoji_font = + emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.value_font_size)); let (_, metrics) = font.metrics(); let mut val_paint = paint_from_hex(&self.value_color); @@ -157,25 +160,25 @@ impl Stat { // Trend inline after value if let Some(trend) = &self.trend { - let val_w = - measure_text_with_fallback(&self.value, &font, &emoji_font, 0.0); + let val_w = measure_text_with_fallback(&self.value, &font, &emoji_font, 0.0); let trend_fs = self.value_font_size * 0.4; let bold_style = skia_safe::FontStyle::bold(); let trend_typeface = fm .match_family_style("Inter", bold_style) .or_else(|| fm.match_family_style("Helvetica", bold_style)) .or_else(|| fm.match_family_style("Arial", bold_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, bold_style).expect("no font available")); + .unwrap_or_else(|| { + fm.legacy_make_typeface(None, bold_style) + .expect("no font available") + }); let trend_font = skia_safe::Font::from_typeface(trend_typeface, trend_fs); let trend_emoji = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, trend_fs)); - let trend_color = trend.color.as_deref().unwrap_or_else(|| { - match trend.direction { - TrendDirection::Up => "#22C55E", - TrendDirection::Down => "#EF4444", - TrendDirection::Neutral => "#94A3B8", - } + let trend_color = trend.color.as_deref().unwrap_or(match trend.direction { + TrendDirection::Up => "#22C55E", + TrendDirection::Down => "#EF4444", + TrendDirection::Neutral => "#94A3B8", }); let mut trend_paint = paint_from_hex(trend_color); @@ -194,19 +197,28 @@ impl Stat { if let Some(icon_id) = icon_id { let icon_sz = (trend_fs * 1.0).round() as u32; - let cache_key = format!("stat_icon:{}:{}:{}x{}", icon_id, trend_color, icon_sz, icon_sz); + let cache_key = format!( + "stat_icon:{}:{}:{}x{}", + icon_id, trend_color, icon_sz, icon_sz + ); let cache = asset_cache(); let icon_img = if let Some(cached) = cache.get(&cache_key) { Some(cached.clone()) - } else if let Ok(svg_data) = fetch_icon_svg(icon_id, trend_color, icon_sz, icon_sz) { + } else if let Ok(svg_data) = + fetch_icon_svg(icon_id, trend_color, icon_sz, icon_sz) + { let opt = usvg::Options::default(); if let Ok(tree) = usvg::Tree::from_data(&svg_data, &opt) { let svg_size = tree.size(); if let Some(mut pixmap) = tiny_skia::Pixmap::new(icon_sz, icon_sz) { let sx = icon_sz as f32 / svg_size.width(); let sy = icon_sz as f32 / svg_size.height(); - resvg::render(&tree, tiny_skia::Transform::from_scale(sx, sy), &mut pixmap.as_mut()); + resvg::render( + &tree, + tiny_skia::Transform::from_scale(sx, sy), + &mut pixmap.as_mut(), + ); let img_data = skia_safe::Data::new_copy(pixmap.data()); let info = ImageInfo::new( (icon_sz as i32, icon_sz as i32), @@ -214,13 +226,25 @@ impl Stat { skia_safe::AlphaType::Premul, None, ); - if let Some(decoded) = skia_safe::images::raster_from_data(&info, img_data, icon_sz as usize * 4) { + if let Some(decoded) = skia_safe::images::raster_from_data( + &info, + img_data, + icon_sz as usize * 4, + ) { cache.insert(cache_key, decoded.clone()); Some(decoded) - } else { None } - } else { None } - } else { None } - } else { None }; + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + }; if let Some(img) = icon_img { let icon_y = ty - trend_fs * 0.8; @@ -256,10 +280,7 @@ impl Stat { let range = (max_v - min_v).max(0.001); let n = self.sparkline_data.len(); - let spark_color = self - .sparkline_color - .as_deref() - .unwrap_or("#3B82F6"); + let spark_color = self.sparkline_color.as_deref().unwrap_or("#3B82F6"); let mut line_path = Path::new(); let mut fill_path = Path::new(); @@ -285,10 +306,7 @@ impl Stat { let top_color = Color::from_argb(50, r, g, b); let bottom_color = Color::from_argb(0, r, g, b); let shader = skia_safe::shader::Shader::linear_gradient( - ( - Point::new(0.0, spark_y), - Point::new(0.0, spark_y + spark_h), - ), + (Point::new(0.0, spark_y), Point::new(0.0, spark_y + spark_h)), skia_safe::gradient_shader::GradientShaderColors::Colors(&[ top_color, bottom_color, diff --git a/crates/rustmotion-components/src/svg.rs b/crates/rustmotion-components/src/svg.rs index bfecc69..9bdaac9 100644 --- a/crates/rustmotion-components/src/svg.rs +++ b/crates/rustmotion-components/src/svg.rs @@ -39,17 +39,35 @@ impl Painter for Svg { _props: &AnimatedProperties, _ctx: &PaintCtx, ) { - let target_w_opt: Option = if layout.width > 0.0 { Some(layout.width as u32) } else { None }; - let target_h_opt: Option = if layout.height > 0.0 { Some(layout.height as u32) } else { None }; + let target_w_opt: Option = if layout.width > 0.0 { + Some(layout.width as u32) + } else { + None + }; + let target_h_opt: Option = if layout.height > 0.0 { + Some(layout.height as u32) + } else { + None + }; let cache_key = if let Some(ref src) = self.src { - format!("svg:{}:{}x{}", src, target_w_opt.unwrap_or(0), target_h_opt.unwrap_or(0)) + format!( + "svg:{}:{}x{}", + src, + target_w_opt.unwrap_or(0), + target_h_opt.unwrap_or(0) + ) } else if let Some(ref data) = self.data { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); data.hash(&mut hasher); - format!("svg-inline:{}:{}x{}", hasher.finish(), target_w_opt.unwrap_or(0), target_h_opt.unwrap_or(0)) + format!( + "svg-inline:{}:{}x{}", + hasher.finish(), + target_w_opt.unwrap_or(0), + target_h_opt.unwrap_or(0) + ) } else { return; }; @@ -68,13 +86,17 @@ impl Painter for Svg { }; let opt = usvg::Options::default(); - let Ok(tree) = usvg::Tree::from_data(&svg_data, &opt) else { return }; + let Ok(tree) = usvg::Tree::from_data(&svg_data, &opt) else { + return; + }; let svg_size = tree.size(); let target_w = target_w_opt.unwrap_or(svg_size.width() as u32); let target_h = target_h_opt.unwrap_or(svg_size.height() as u32); - let Some(mut pixmap) = tiny_skia::Pixmap::new(target_w, target_h) else { return }; + let Some(mut pixmap) = tiny_skia::Pixmap::new(target_w, target_h) else { + return; + }; let scale_x = target_w as f32 / svg_size.width(); let scale_y = target_h as f32 / svg_size.height(); @@ -91,7 +113,9 @@ impl Painter for Svg { ); let Some(decoded) = skia_safe::images::raster_from_data(&img_info, img_data, target_w as usize * 4) - else { return }; + else { + return; + }; cache.insert(cache_key, decoded.clone()); decoded }; diff --git a/crates/rustmotion-components/src/switch.rs b/crates/rustmotion-components/src/switch.rs index 23f7e88..31ab1e4 100644 --- a/crates/rustmotion-components/src/switch.rs +++ b/crates/rustmotion-components/src/switch.rs @@ -149,7 +149,14 @@ impl Switch { let text_y = h / 2.0 + (-metrics.ascent) / 2.0; draw_text_with_fallback( - canvas, label, &font, &emoji_font, 0.0, text_x, text_y, &text_paint, + canvas, + label, + &font, + &emoji_font, + 0.0, + text_x, + text_y, + &text_paint, ); } } diff --git a/crates/rustmotion-components/src/table.rs b/crates/rustmotion-components/src/table.rs index 7808c5b..0ae74f2 100644 --- a/crates/rustmotion-components/src/table.rs +++ b/crates/rustmotion-components/src/table.rs @@ -5,7 +5,9 @@ use skia_safe::{Canvas, PaintStyle, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{font_mgr, paint_from_hex, emoji_typeface, draw_text_with_fallback, measure_text_with_fallback}; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, +}; use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -13,18 +15,14 @@ use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; /// Text alignment for table columns. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum ColumnAlign { + #[default] Left, Center, Right, } -impl Default for ColumnAlign { - fn default() -> Self { - Self::Left - } -} - fn default_cell_padding() -> f32 { 12.0 } @@ -90,18 +88,14 @@ impl Table { skia_safe::FontStyle::normal() }; - let family = self - .style - .font_family - .as_deref() - .unwrap_or("Inter"); + let family = self.style.font_family.as_deref().unwrap_or("Inter"); let typeface = fm .match_family_style(family, font_style) .or_else(|| fm.match_family_style("Helvetica", font_style)) .or_else(|| fm.match_family_style("Arial", font_style)) .or_else(|| fm.legacy_make_typeface(None, font_style)) - .expect(&RustmotionError::FontNotFound.to_string()); + .unwrap_or_else(|| panic!("{}", RustmotionError::FontNotFound.to_string())); skia_safe::Font::from_typeface(typeface, self.font_size()) } @@ -110,7 +104,7 @@ impl Table { fn resolve_column_widths(&self, total_w: f32) -> Vec { let col_count = self.headers.len().max(1); if let Some(widths) = &self.column_widths { - let mut result: Vec = widths.iter().copied().collect(); + let mut result: Vec = widths.to_vec(); // Pad with equal-share for missing columns while result.len() < col_count { let remaining = total_w - result.iter().sum::(); @@ -188,7 +182,16 @@ impl Table { let x = self.align_text_x(i, col_x, cw, text_w); let y = (row_h - self.font_size()) / 2.0 + header_ascent; - draw_text_with_fallback(canvas, header, &header_font, &emoji_font, 0.0, x, y, &header_text_paint); + draw_text_with_fallback( + canvas, + header, + &header_font, + &emoji_font, + 0.0, + x, + y, + &header_text_paint, + ); col_x += cw; } @@ -221,7 +224,16 @@ impl Table { let x = self.align_text_x(col_idx, cx, cw, text_w); let y = y_base + (row_h - self.font_size()) / 2.0 + body_ascent; - draw_text_with_fallback(canvas, cell, &body_font, &emoji_font, 0.0, x, y, &text_paint); + draw_text_with_fallback( + canvas, + cell, + &body_font, + &emoji_font, + 0.0, + x, + y, + &text_paint, + ); cx += cw; } } diff --git a/crates/rustmotion-components/src/tag_cloud.rs b/crates/rustmotion-components/src/tag_cloud.rs index 002e04d..ac01765 100644 --- a/crates/rustmotion-components/src/tag_cloud.rs +++ b/crates/rustmotion-components/src/tag_cloud.rs @@ -107,16 +107,8 @@ impl TagCloud { let palette = self.palette(); // Collect weights - let min_weight = self - .tags - .iter() - .map(|t| t.weight) - .fold(f64::MAX, f64::min); - let max_weight = self - .tags - .iter() - .map(|t| t.weight) - .fold(f64::MIN, f64::max); + let min_weight = self.tags.iter().map(|t| t.weight).fold(f64::MAX, f64::min); + let max_weight = self.tags.iter().map(|t| t.weight).fold(f64::MIN, f64::max); // Sort tags by weight descending (indices for stagger) let mut sorted_indices: Vec = (0..self.tags.len()).collect(); @@ -211,13 +203,14 @@ impl TagCloud { // Compute total content height for vertical centering let total_height = if let Some(last) = placed.last() { - last.y + placed.iter().fold(0.0_f32, |max_h, p| { - if (p.y - last.y).abs() < 0.01 { - max_h.max(p.line_height) - } else { - max_h - } - }) + last.y + + placed.iter().fold(0.0_f32, |max_h, p| { + if (p.y - last.y).abs() < 0.01 { + max_h.max(p.line_height) + } else { + max_h + } + }) } else { 0.0 }; @@ -230,9 +223,8 @@ impl TagCloud { // Staggered opacity animation let tag_alpha = if self.animated { let stagger_delay = (draw_order as f64 / tag_count as f64) * 0.6; - let tag_progress = - ((time - stagger_delay) / (self.animation_duration * 0.4)).clamp(0.0, 1.0) - as f32; + let tag_progress = ((time - stagger_delay) / (self.animation_duration * 0.4)) + .clamp(0.0, 1.0) as f32; tag_progress * progress } else { 1.0 diff --git a/crates/rustmotion-components/src/terminal.rs b/crates/rustmotion-components/src/terminal.rs index f622820..9f74c2c 100644 --- a/crates/rustmotion-components/src/terminal.rs +++ b/crates/rustmotion-components/src/terminal.rs @@ -1,29 +1,27 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use skia_safe::{Canvas, PaintStyle, Rect, RRect}; +use skia_safe::{Canvas, PaintStyle, RRect, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::{ease, AnimatedProperties}; use rustmotion_core::engine::layout_pass::BoxLayout; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, paint_from_hex, +}; use rustmotion_core::error::RustmotionError; -use rustmotion_core::engine::renderer::{font_mgr, paint_from_hex, emoji_typeface, draw_text_with_fallback}; use rustmotion_core::schema::{CodeblockReveal, RevealMode, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum TerminalLineType { Prompt, Command, + #[default] Output, } -impl Default for TerminalLineType { - fn default() -> Self { - Self::Output - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct TerminalLine { pub text: String, @@ -35,17 +33,13 @@ pub struct TerminalLine { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum TerminalTheme { + #[default] Dark, Light, } -impl Default for TerminalTheme { - fn default() -> Self { - Self::Dark - } -} - impl TerminalTheme { fn bg(&self) -> &str { match self { @@ -149,7 +143,7 @@ impl Terminal { .or_else(|| fm.match_family_style("monospace", font_style)) .or_else(|| fm.match_family_style("Courier", font_style)) .or_else(|| fm.legacy_make_typeface(None, font_style)) - .expect(&RustmotionError::FontNotFound.to_string()); + .unwrap_or_else(|| panic!("{}", RustmotionError::FontNotFound.to_string())); let size = self.style.font_size_px_or(FONT_SIZE); @@ -277,10 +271,16 @@ impl Terminal { if let Some(title) = &self.title { let font = self.make_font(); let font_size = self.style.font_size_px_or(FONT_SIZE); - let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); + let emoji_font = + emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); let mut title_paint = paint_from_hex(self.theme.title_color()); title_paint.set_anti_alias(true); - let title_w = rustmotion_core::engine::renderer::measure_text_with_fallback(title, &font, &emoji_font, 0.0); + let title_w = rustmotion_core::engine::renderer::measure_text_with_fallback( + title, + &font, + &emoji_font, + 0.0, + ); let x = (w - title_w) / 2.0; let (_, metrics) = font.metrics(); let y = CHROME_HEIGHT / 2.0 + (-metrics.ascent) / 2.0; @@ -328,7 +328,11 @@ impl Terminal { } let is_last_visible = i == visible_lines - 1; - let opacity = if is_last_visible { last_line_opacity } else { 1.0 }; + let opacity = if is_last_visible { + last_line_opacity + } else { + 1.0 + }; let prefix = Self::line_prefix(&line.line_type); let (prefix_color, text_color) = match line.line_type { @@ -368,8 +372,22 @@ impl Terminal { let mut prefix_paint = paint_from_hex(prefix_color); prefix_paint.set_anti_alias(true); prefix_paint.set_alpha_f(opacity); - let prefix_w = rustmotion_core::engine::renderer::measure_text_with_fallback(&draw_prefix, &font, &emoji_font, 0.0); - draw_text_with_fallback(canvas, &draw_prefix, &font, &emoji_font, 0.0, x, y, &prefix_paint); + let prefix_w = rustmotion_core::engine::renderer::measure_text_with_fallback( + &draw_prefix, + &font, + &emoji_font, + 0.0, + ); + draw_text_with_fallback( + canvas, + &draw_prefix, + &font, + &emoji_font, + 0.0, + x, + y, + &prefix_paint, + ); x += prefix_w + 2.0; } @@ -378,8 +396,22 @@ impl Terminal { let mut text_paint = paint_from_hex(color); text_paint.set_anti_alias(true); text_paint.set_alpha_f(opacity); - let text_w = rustmotion_core::engine::renderer::measure_text_with_fallback(&draw_text, &font, &emoji_font, 0.0); - draw_text_with_fallback(canvas, &draw_text, &font, &emoji_font, 0.0, x, y, &text_paint); + let text_w = rustmotion_core::engine::renderer::measure_text_with_fallback( + &draw_text, + &font, + &emoji_font, + 0.0, + ); + draw_text_with_fallback( + canvas, + &draw_text, + &font, + &emoji_font, + 0.0, + x, + y, + &text_paint, + ); x += text_w; } diff --git a/crates/rustmotion-components/src/text.rs b/crates/rustmotion-components/src/text.rs index c0d8851..c8dd15f 100644 --- a/crates/rustmotion-components/src/text.rs +++ b/crates/rustmotion-components/src/text.rs @@ -6,12 +6,20 @@ use skia_safe::{Canvas, Font, FontStyle, Paint, PaintStyle, Rect}; use rustmotion_core::engine::animator::ease; use rustmotion_core::error::RustmotionError; -use rustmotion_core::css::style::{FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign}; +use rustmotion_core::css::style::{ + FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign, +}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{font_mgr, paint_from_hex, wrap_text_with_fallback, draw_text_with_fallback, measure_text_with_fallback, emoji_typeface}; -use rustmotion_core::schema::{CharAnimPreset, CharAnimation, FontStyleType, FontWeight, Stroke, TextAlign, TextAnimGranularity, TextBackground, TextShadow, TimelineStep}; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + wrap_text_with_fallback, +}; +use rustmotion_core::schema::{ + CharAnimPreset, CharAnimation, FontStyleType, FontWeight, Stroke, TextAlign, + TextAnimGranularity, TextBackground, TextShadow, TimelineStep, +}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -90,10 +98,20 @@ fn apply_text_anim_preset( draw_text_with_fallback(canvas, text, font, emoji_font, 0.0, cursor_x, line_y, &p); } CharAnimPreset::Wave => { - let wave_offset = (time as f32 * 4.0 + unit_idx as f32 * 0.5).sin() * 8.0 * (1.0 - t * 0.5); + let wave_offset = + (time as f32 * 4.0 + unit_idx as f32 * 0.5).sin() * 8.0 * (1.0 - t * 0.5); let mut p = paint.clone(); p.set_alpha_f(t.min(1.0) * paint.alpha_f()); - draw_text_with_fallback(canvas, text, font, emoji_font, 0.0, cursor_x, line_y + wave_offset, &p); + draw_text_with_fallback( + canvas, + text, + font, + emoji_font, + 0.0, + cursor_x, + line_y + wave_offset, + &p, + ); } CharAnimPreset::Bounce => { let peak = 1.0 + overshoot.max(0.3); // bounce always overshoots, min 0.3 @@ -121,7 +139,16 @@ fn apply_text_anim_preset( let offset_y = (1.0 - t) * font_size * 0.8; let mut p = paint.clone(); p.set_alpha_f(t * paint.alpha_f()); - draw_text_with_fallback(canvas, text, font, emoji_font, 0.0, cursor_x, line_y + offset_y, &p); + draw_text_with_fallback( + canvas, + text, + font, + emoji_font, + 0.0, + cursor_x, + line_y + offset_y, + &p, + ); } } } @@ -176,9 +203,12 @@ fn render_char_animation( } } if !spaces.is_empty() { - let space_w = measure_text_with_fallback(&spaces, font, emoji_font, letter_spacing); + let space_w = + measure_text_with_fallback(&spaces, font, emoji_font, letter_spacing); // Draw spaces without animation - draw_text_with_fallback(canvas, &spaces, font, emoji_font, 0.0, cursor_x, line_y, paint); + draw_text_with_fallback( + canvas, &spaces, font, emoji_font, 0.0, cursor_x, line_y, paint, + ); cursor_x += space_w; } @@ -195,10 +225,12 @@ fn render_char_animation( continue; } - let word_width = measure_text_with_fallback(&word, font, emoji_font, letter_spacing); + let word_width = + measure_text_with_fallback(&word, font, emoji_font, letter_spacing); // Calculate animation progress for this word - let unit_start = char_anim.delay as f64 + global_unit_idx as f64 * char_anim.stagger as f64; + let unit_start = + char_anim.delay as f64 + global_unit_idx as f64 * char_anim.stagger as f64; let unit_end = unit_start + char_anim.duration as f64; let raw_t = if time <= unit_start { 0.0 @@ -211,9 +243,19 @@ fn render_char_animation( canvas.save(); apply_text_anim_preset( - canvas, &word, font, emoji_font, paint, - cursor_x, line_y, word_width, - &char_anim.preset, t, time, global_unit_idx, font.size(), + canvas, + &word, + font, + emoji_font, + paint, + cursor_x, + line_y, + word_width, + &char_anim.preset, + t, + time, + global_unit_idx, + font.size(), overshoot, ); canvas.restore(); @@ -229,7 +271,8 @@ fn render_char_animation( let (ch_width, _) = font.measure_str(&ch_str, None); let ch_width = ch_width + letter_spacing; - let unit_start = char_anim.delay as f64 + global_unit_idx as f64 * char_anim.stagger as f64; + let unit_start = + char_anim.delay as f64 + global_unit_idx as f64 * char_anim.stagger as f64; let unit_end = unit_start + char_anim.duration as f64; let raw_t = if time <= unit_start { 0.0 @@ -242,9 +285,19 @@ fn render_char_animation( canvas.save(); apply_text_anim_preset( - canvas, &ch_str, font, emoji_font, paint, - cursor_x, line_y, ch_width, - &char_anim.preset, t, time, global_unit_idx, font.size(), + canvas, + &ch_str, + font, + emoji_font, + paint, + cursor_x, + line_y, + ch_width, + &char_anim.preset, + t, + time, + global_unit_idx, + font.size(), overshoot, ); canvas.restore(); @@ -268,7 +321,9 @@ impl Text { let color = self.style.color_str_or("#FFFFFF"); let font_family = self.style.font_family_or("Inter"); let font_weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => FontWeight::Bold, + Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { + FontWeight::Bold + } Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold, Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n), _ => FontWeight::Normal, @@ -306,7 +361,7 @@ impl Text { .or_else(|| fm.match_family_style("sans-serif", skia_font_style)) .or_else(|| { if fm.count_families() > 0 { - fm.match_family_style(&fm.family_name(0), skia_font_style) + fm.match_family_style(fm.family_name(0), skia_font_style) } else { None } @@ -393,9 +448,20 @@ impl Text { delay: resolved.delay, }; render_char_animation( - canvas, &content, &font, &emoji_font, &paint, - letter_spacing, align, align_width, line_height_val, baseline_offset, - &lines, &char_anim, time, resolved.overshoot, + canvas, + &content, + &font, + &emoji_font, + &paint, + letter_spacing, + align, + align_width, + line_height_val, + baseline_offset, + &lines, + &char_anim, + time, + resolved.overshoot, ); return Ok(()); } @@ -405,7 +471,8 @@ impl Text { continue; } - let advance_width = measure_text_with_fallback(line, &font, &emoji_font, letter_spacing); + let advance_width = + measure_text_with_fallback(line, &font, &emoji_font, letter_spacing); let x = match align { TextAlign::Left => 0.0, @@ -425,7 +492,8 @@ impl Text { -font_rect.top + font_rect.bottom + bg.padding, ); if bg.corner_radius > 0.0 { - let rrect = skia_safe::RRect::new_rect_xy(bg_rect, bg.corner_radius, bg.corner_radius); + let rrect = + skia_safe::RRect::new_rect_xy(bg_rect, bg.corner_radius, bg.corner_radius); canvas.draw_rrect(rrect, &bg_paint); } else { canvas.draw_rect(bg_rect, &bg_paint); @@ -434,7 +502,16 @@ impl Text { // Draw shadow if let Some((ref sp, ox, oy)) = shadow_paint { - draw_text_with_fallback(canvas, line, &font, &emoji_font, letter_spacing, x + ox, y + oy, sp); + draw_text_with_fallback( + canvas, + line, + &font, + &emoji_font, + letter_spacing, + x + ox, + y + oy, + sp, + ); } // Draw stroke (outline) @@ -443,7 +520,16 @@ impl Text { } // Draw fill - draw_text_with_fallback(canvas, line, &font, &emoji_font, letter_spacing, x, y, &paint); + draw_text_with_fallback( + canvas, + line, + &font, + &emoji_font, + letter_spacing, + x, + y, + &paint, + ); } Ok(()) diff --git a/crates/rustmotion-components/src/timeline.rs b/crates/rustmotion-components/src/timeline.rs index 5642218..99353d6 100644 --- a/crates/rustmotion-components/src/timeline.rs +++ b/crates/rustmotion-components/src/timeline.rs @@ -5,7 +5,9 @@ use skia_safe::{Canvas, Font, FontStyle, PaintStyle, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{font_mgr, paint_from_hex, emoji_typeface, draw_text_with_fallback, measure_text_with_fallback}; +use rustmotion_core::engine::renderer::{ + draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, +}; use rustmotion_core::schema::TimelineStep as AnimTimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -77,16 +79,36 @@ pub enum TimelineDirection { Vertical, } -fn default_timeline_width() -> f32 { 800.0 } -fn default_node_radius() -> f32 { 24.0 } -fn default_bar_color() -> String { "#333333".to_string() } -fn default_bar_fill_color() -> String { "#58A6FF".to_string() } -fn default_bar_height() -> f32 { 4.0 } -fn default_fill_progress() -> f32 { 1.0 } -fn default_label_font_size() -> f32 { 16.0 } -fn default_label_color() -> String { "#FFFFFF".to_string() } -fn default_sublabel_color() -> String { "#8B949E".to_string() } -fn default_node_color() -> String { "#58A6FF".to_string() } +fn default_timeline_width() -> f32 { + 800.0 +} +fn default_node_radius() -> f32 { + 24.0 +} +fn default_bar_color() -> String { + "#333333".to_string() +} +fn default_bar_fill_color() -> String { + "#58A6FF".to_string() +} +fn default_bar_height() -> f32 { + 4.0 +} +fn default_fill_progress() -> f32 { + 1.0 +} +fn default_label_font_size() -> f32 { + 16.0 +} +fn default_label_color() -> String { + "#FFFFFF".to_string() +} +fn default_sublabel_color() -> String { + "#8B949E".to_string() +} +fn default_node_color() -> String { + "#58A6FF".to_string() +} rustmotion_core::impl_traits!(Timeline { Animatable => animation, @@ -97,14 +119,20 @@ rustmotion_core::impl_traits!(Timeline { impl Timeline { fn paint(&self, canvas: &Canvas, props: &AnimatedProperties) { let n = self.steps.len(); - if n == 0 { return; } + if n == 0 { + return; + } let fm = font_mgr(); - let typeface = fm.match_family_style("Inter", FontStyle::normal()) + let typeface = fm + .match_family_style("Inter", FontStyle::normal()) .or_else(|| fm.match_family_style("Helvetica", FontStyle::normal())) .or_else(|| fm.match_family_style("Arial", FontStyle::normal())) .or_else(|| fm.match_family_style("sans-serif", FontStyle::normal())) - .unwrap_or_else(|| fm.legacy_make_typeface(None, FontStyle::normal()).expect("No fallback font")); + .unwrap_or_else(|| { + fm.legacy_make_typeface(None, FontStyle::normal()) + .expect("No fallback font") + }); let font = Font::from_typeface(&typeface, self.font_size); let icon_font = Font::from_typeface(&typeface, self.node_radius * 0.8); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, self.node_radius * 0.8)); @@ -113,14 +141,38 @@ impl Timeline { let ascent = -metrics.ascent; let r = self.node_radius; - let fill_progress = if props.draw_progress >= 0.0 { props.draw_progress } else { self.fill_progress }; + let fill_progress = if props.draw_progress >= 0.0 { + props.draw_progress + } else { + self.fill_progress + }; match self.direction { TimelineDirection::Horizontal => { - self.render_horizontal(canvas, n, r, fill_progress, &font, &icon_font, &emoji_font, &sublabel_font, ascent); + self.render_horizontal( + canvas, + n, + r, + fill_progress, + &font, + &icon_font, + &emoji_font, + &sublabel_font, + ascent, + ); } TimelineDirection::Vertical => { - self.render_vertical(canvas, n, r, fill_progress, &font, &icon_font, &emoji_font, &sublabel_font, ascent); + self.render_vertical( + canvas, + n, + r, + fill_progress, + &font, + &icon_font, + &emoji_font, + &sublabel_font, + ascent, + ); } } } @@ -144,40 +196,62 @@ impl Timeline { let spacing = if n > 1 { total_w / (n - 1) as f32 } else { 0.0 }; // Draw background bar - let bar_rect = Rect::from_xywh( - 0.0, bar_y - self.bar_height / 2.0, - total_w, self.bar_height, - ); + let bar_rect = + Rect::from_xywh(0.0, bar_y - self.bar_height / 2.0, total_w, self.bar_height); let mut bar_paint = paint_from_hex(&self.bar_color); bar_paint.set_style(PaintStyle::Fill); - canvas.draw_round_rect(bar_rect, self.bar_height / 2.0, self.bar_height / 2.0, &bar_paint); + canvas.draw_round_rect( + bar_rect, + self.bar_height / 2.0, + self.bar_height / 2.0, + &bar_paint, + ); // Draw filled bar if fill_progress > 0.001 { let fill_w = total_w * fill_progress.clamp(0.0, 1.0); - let fill_rect = Rect::from_xywh( - 0.0, bar_y - self.bar_height / 2.0, - fill_w, self.bar_height, - ); + let fill_rect = + Rect::from_xywh(0.0, bar_y - self.bar_height / 2.0, fill_w, self.bar_height); let mut fill_paint = paint_from_hex(&self.bar_fill_color); fill_paint.set_style(PaintStyle::Fill); canvas.save(); - canvas.clip_rect(Rect::from_xywh(0.0, bar_y - self.bar_height / 2.0, total_w, self.bar_height), skia_safe::ClipOp::Intersect, false); - canvas.draw_round_rect(fill_rect, self.bar_height / 2.0, self.bar_height / 2.0, &fill_paint); + canvas.clip_rect( + Rect::from_xywh(0.0, bar_y - self.bar_height / 2.0, total_w, self.bar_height), + skia_safe::ClipOp::Intersect, + false, + ); + canvas.draw_round_rect( + fill_rect, + self.bar_height / 2.0, + self.bar_height / 2.0, + &fill_paint, + ); canvas.restore(); } // Draw nodes and labels for (i, step) in self.steps.iter().enumerate() { - let cx = if n > 1 { i as f32 * spacing } else { total_w / 2.0 }; + let cx = if n > 1 { + i as f32 * spacing + } else { + total_w / 2.0 + }; let cy = bar_y; // Determine if this step is "active" (filled bar has reached it) - let step_progress = if n > 1 { i as f32 / (n - 1) as f32 } else { 0.0 }; + let step_progress = if n > 1 { + i as f32 / (n - 1) as f32 + } else { + 0.0 + }; let is_active = fill_progress >= step_progress; // Node circle - let node_color = if is_active { &step.color } else { &self.bar_color }; + let node_color = if is_active { + &step.color + } else { + &self.bar_color + }; let mut node_paint = paint_from_hex(node_color); node_paint.set_style(PaintStyle::Fill); node_paint.set_anti_alias(true); @@ -193,7 +267,16 @@ impl Timeline { let iy = cy + (icon_ascent - icon_descent) / 2.0; let mut icon_paint = paint_from_hex("#FFFFFF"); icon_paint.set_anti_alias(true); - draw_text_with_fallback(canvas, icon, icon_font, emoji_font, 0.0, ix, iy, &icon_paint); + draw_text_with_fallback( + canvas, + icon, + icon_font, + emoji_font, + 0.0, + ix, + iy, + &icon_paint, + ); } // Label below @@ -211,7 +294,16 @@ impl Timeline { let sy = ly + self.font_size * 1.2; let mut sub_paint = paint_from_hex(&self.sublabel_color); sub_paint.set_anti_alias(true); - draw_text_with_fallback(canvas, sublabel, sublabel_font, &None, 0.0, sx, sy, &sub_paint); + draw_text_with_fallback( + canvas, + sublabel, + sublabel_font, + &None, + 0.0, + sx, + sy, + &sub_paint, + ); } } } @@ -233,34 +325,48 @@ impl Timeline { let total_h = if n > 1 { (n - 1) as f32 * spacing } else { 0.0 }; // Background bar - let bar_rect = Rect::from_xywh( - bar_x - self.bar_height / 2.0, 0.0, - self.bar_height, total_h, - ); + let bar_rect = + Rect::from_xywh(bar_x - self.bar_height / 2.0, 0.0, self.bar_height, total_h); let mut bar_paint = paint_from_hex(&self.bar_color); bar_paint.set_style(PaintStyle::Fill); - canvas.draw_round_rect(bar_rect, self.bar_height / 2.0, self.bar_height / 2.0, &bar_paint); + canvas.draw_round_rect( + bar_rect, + self.bar_height / 2.0, + self.bar_height / 2.0, + &bar_paint, + ); // Filled bar if fill_progress > 0.001 { let fill_h = total_h * fill_progress.clamp(0.0, 1.0); - let fill_rect = Rect::from_xywh( - bar_x - self.bar_height / 2.0, 0.0, - self.bar_height, fill_h, - ); + let fill_rect = + Rect::from_xywh(bar_x - self.bar_height / 2.0, 0.0, self.bar_height, fill_h); let mut fill_paint = paint_from_hex(&self.bar_fill_color); fill_paint.set_style(PaintStyle::Fill); - canvas.draw_round_rect(fill_rect, self.bar_height / 2.0, self.bar_height / 2.0, &fill_paint); + canvas.draw_round_rect( + fill_rect, + self.bar_height / 2.0, + self.bar_height / 2.0, + &fill_paint, + ); } for (i, step) in self.steps.iter().enumerate() { let cx = bar_x; let cy = if n > 1 { i as f32 * spacing } else { 0.0 }; - let step_progress = if n > 1 { i as f32 / (n - 1) as f32 } else { 0.0 }; + let step_progress = if n > 1 { + i as f32 / (n - 1) as f32 + } else { + 0.0 + }; let is_active = fill_progress >= step_progress; - let node_color = if is_active { &step.color } else { &self.bar_color }; + let node_color = if is_active { + &step.color + } else { + &self.bar_color + }; let mut node_paint = paint_from_hex(node_color); node_paint.set_style(PaintStyle::Fill); node_paint.set_anti_alias(true); @@ -275,7 +381,16 @@ impl Timeline { let iy = cy + (icon_ascent - icon_descent) / 2.0; let mut icon_paint = paint_from_hex("#FFFFFF"); icon_paint.set_anti_alias(true); - draw_text_with_fallback(canvas, icon, icon_font, emoji_font, 0.0, ix, iy, &icon_paint); + draw_text_with_fallback( + canvas, + icon, + icon_font, + emoji_font, + 0.0, + ix, + iy, + &icon_paint, + ); } // Label to the right diff --git a/crates/rustmotion-components/src/tooltip.rs b/crates/rustmotion-components/src/tooltip.rs index 96a5583..7949fa1 100644 --- a/crates/rustmotion-components/src/tooltip.rs +++ b/crates/rustmotion-components/src/tooltip.rs @@ -30,20 +30,16 @@ fn default_arrow_size() -> f32 { /// Arrow direction — where the arrow points (toward the target element). #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum TooltipArrow { Top, + #[default] Bottom, Left, Right, None, } -impl Default for TooltipArrow { - fn default() -> Self { - Self::Bottom - } -} - #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Tooltip { pub text: String, @@ -88,14 +84,16 @@ impl Tooltip { .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); skia_safe::Font::from_typeface(typeface, fs) } - } impl Tooltip { fn paint(&self, canvas: &Canvas, layout_w: f32, layout_h: f32) { let w = layout_w; let h = layout_h; - let bg_color = self.style.background_color_str().unwrap_or(&self.background_color); + let bg_color = self + .style + .background_color_str() + .unwrap_or(&self.background_color); let radius = self.style.border_radius_px_or(8.0); let arrow_sz = self.arrow_size; diff --git a/crates/rustmotion-components/src/treemap.rs b/crates/rustmotion-components/src/treemap.rs index 9182a6f..af457f8 100644 --- a/crates/rustmotion-components/src/treemap.rs +++ b/crates/rustmotion-components/src/treemap.rs @@ -166,12 +166,8 @@ impl Treemap { let cy = inset_rect.top + inset_rect.height() / 2.0; let scaled_w = inset_rect.width() * progress; let scaled_h = inset_rect.height() * progress; - let scaled_rect = Rect::from_xywh( - cx - scaled_w / 2.0, - cy - scaled_h / 2.0, - scaled_w, - scaled_h, - ); + let scaled_rect = + Rect::from_xywh(cx - scaled_w / 2.0, cy - scaled_h / 2.0, scaled_w, scaled_h); // Color let color_str = item @@ -214,7 +210,8 @@ impl Treemap { let (_, metrics) = font.metrics(); let text_x = scaled_rect.left + (scaled_rect.width() - text_w) / 2.0; - let text_y = scaled_rect.top + scaled_rect.height() / 2.0 + let text_y = scaled_rect.top + + scaled_rect.height() / 2.0 + (-metrics.ascent - metrics.descent) / 2.0; draw_text_with_fallback( diff --git a/crates/rustmotion-components/src/video.rs b/crates/rustmotion-components/src/video.rs index 6f61e61..2220f98 100644 --- a/crates/rustmotion-components/src/video.rs +++ b/crates/rustmotion-components/src/video.rs @@ -5,11 +5,15 @@ use skia_safe::{Canvas, ColorType, ImageInfo, Paint, Rect}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{extract_video_frame, find_closest_frame, video_frame_cache}; +use rustmotion_core::engine::renderer::{ + extract_video_frame, find_closest_frame, video_frame_cache, +}; use rustmotion_core::schema::{ImageFit, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; -fn default_volume() -> f32 { 1.0 } +fn default_volume() -> f32 { + 1.0 +} #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Video { diff --git a/crates/rustmotion-components/src/world_bitmap.rs b/crates/rustmotion-components/src/world_bitmap.rs index 02dea3e..3a5b5c1 100644 --- a/crates/rustmotion-components/src/world_bitmap.rs +++ b/crates/rustmotion-components/src/world_bitmap.rs @@ -4,185 +4,635 @@ pub fn land_bitmap() -> &'static [u8] { static BITMAP: [u8; 16200] = [ // Row 0: 90°N to 88°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 1: 88°N to 86°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 2: 86°N to 84°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 3: 84°N to 82°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 4: 82°N to 80°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 5: 80°N to 78°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 6: 78°N to 76°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 7: 76°N to 74°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 8: 74°N to 72°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 9: 72°N to 70°N - 0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // Row 10: 70°N to 68°N - 0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // Row 11: 68°N to 66°N - 0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, // Row 12: 66°N to 64°N - 0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, + 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // Row 13: 64°N to 62°N - 0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, // Row 14: 62°N to 60°N - 0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, // Row 15: 60°N to 58°N - 0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, // Row 16: 58°N to 56°N - 0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 17: 56°N to 54°N - 0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 18: 54°N to 52°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 19: 52°N to 50°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 20: 50°N to 48°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 21: 48°N to 46°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 22: 46°N to 44°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 23: 44°N to 42°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 24: 42°N to 40°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,1,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 25: 40°N to 38°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,1,0,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 26: 38°N to 36°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,1,1,1,1,1,0,0,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 27: 36°N to 34°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 28: 34°N to 32°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 29: 32°N to 30°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 30: 30°N to 28°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1,1,1,0,0,1,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 31: 28°N to 26°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,0,0,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, + 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 32: 26°N to 24°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 33: 24°N to 22°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 34: 22°N to 20°N - 0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 35: 20°N to 18°N - 0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 36: 18°N to 16°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 37: 16°N to 14°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 38: 14°N to 12°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 39: 12°N to 10°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 40: 10°N to 8°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 41: 8°N to 6°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 42: 6°N to 4°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 43: 4°N to 2°N - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 44: 2°N to 0° - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 45: 0° to 2°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 46: 2°S to 4°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 47: 4°S to 6°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 48: 6°S to 8°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 49: 8°S to 10°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 50: 10°S to 12°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 51: 12°S to 14°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 52: 14°S to 16°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 53: 16°S to 18°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 54: 18°S to 20°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 55: 20°S to 22°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 56: 22°S to 24°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 57: 24°S to 26°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 58: 26°S to 28°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 59: 28°S to 30°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 60: 30°S to 32°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 61: 32°S to 34°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 62: 34°S to 36°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, // Row 63: 36°S to 38°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, // Row 64: 38°S to 40°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, // Row 65: 40°S to 42°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, // Row 66: 42°S to 44°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, // Row 67: 44°S to 46°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, // Row 68: 46°S to 48°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 69: 48°S to 50°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 70: 50°S to 52°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 71: 52°S to 54°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 72: 54°S to 56°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 73: 56°S to 58°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 74: 58°S to 60°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 75: 60°S to 62°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 76: 62°S to 64°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 77: 64°S to 66°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 78: 66°S to 68°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 79: 68°S to 70°S - 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, // Row 80: 70°S to 72°S - 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, // Row 81: 72°S to 74°S - 1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0, + 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // Row 82: 74°S to 76°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 83: 76°S to 78°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 84: 78°S to 80°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 85: 80°S to 82°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 86: 82°S to 84°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 87: 84°S to 86°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 88: 86°S to 88°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Row 89: 88°S to 90°S - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; &BITMAP } diff --git a/crates/rustmotion-core/src/css/animation.rs b/crates/rustmotion-core/src/css/animation.rs index 4347060..4601ce4 100644 --- a/crates/rustmotion-core/src/css/animation.rs +++ b/crates/rustmotion-core/src/css/animation.rs @@ -34,13 +34,19 @@ pub fn apply_animated_props(css: &mut CssStyle, props: &AnimatedProperties) { tx.push(TransformFn::Scale { x: sx, y: sy }); } if props.rotation.abs() > 1e-3 { - tx.push(TransformFn::Rotate { deg: props.rotation }); + tx.push(TransformFn::Rotate { + deg: props.rotation, + }); } if props.rotate_x.abs() > 1e-3 { - tx.push(TransformFn::RotateX { deg: props.rotate_x }); + tx.push(TransformFn::RotateX { + deg: props.rotate_x, + }); } if props.rotate_y.abs() > 1e-3 { - tx.push(TransformFn::RotateY { deg: props.rotate_y }); + tx.push(TransformFn::RotateY { + deg: props.rotate_y, + }); } if !tx.is_empty() { // Append rather than replace so a CSS-defined transform composes with @@ -101,9 +107,11 @@ mod tests { #[test] fn translate_props_become_transform_translate() { let mut css = CssStyle::default(); - let mut props = AnimatedProperties::default(); - props.translate_x = 10.0; - props.translate_y = -5.0; + let props = AnimatedProperties { + translate_x: 10.0, + translate_y: -5.0, + ..AnimatedProperties::default() + }; apply_animated_props(&mut css, &props); let tx = css.transform.expect("transform list created"); @@ -123,8 +131,10 @@ mod tests { opacity: Some(0.5), ..CssStyle::default() }; - let mut props = AnimatedProperties::default(); - props.opacity = 0.5; + let props = AnimatedProperties { + opacity: 0.5, + ..AnimatedProperties::default() + }; apply_animated_props(&mut css, &props); assert!((css.opacity.unwrap() - 0.25).abs() < 1e-6); } @@ -143,10 +153,12 @@ mod tests { #[test] fn blur_and_glow_compose_into_filter_list() { let mut css = CssStyle::default(); - let mut props = AnimatedProperties::default(); - props.blur = 4.0; - props.glow_radius = 8.0; - props.glow_intensity = 1.0; + let props = AnimatedProperties { + blur: 4.0, + glow_radius: 8.0, + glow_intensity: 1.0, + ..AnimatedProperties::default() + }; apply_animated_props(&mut css, &props); let filters = css.filter.expect("filter list created"); diff --git a/crates/rustmotion-core/src/css/mod.rs b/crates/rustmotion-core/src/css/mod.rs index e58954c..9f28aae 100644 --- a/crates/rustmotion-core/src/css/mod.rs +++ b/crates/rustmotion-core/src/css/mod.rs @@ -10,10 +10,10 @@ //! Layout is computed by [`taffy`]; paint is done by Skia in `engine::paint_pass`. pub mod animation; -pub mod style; -pub mod units; pub mod cascade; +pub mod style; pub mod taffy_bridge; +pub mod units; pub use animation::apply_animated_props; pub use style::CssStyle; diff --git a/crates/rustmotion-core/src/css/style.rs b/crates/rustmotion-core/src/css/style.rs index 1c85333..42f294c 100644 --- a/crates/rustmotion-core/src/css/style.rs +++ b/crates/rustmotion-core/src/css/style.rs @@ -8,7 +8,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::units::{Length, LengthPercentage}; -use crate::schema::{AnimationEffect, deserialize_animation_effects}; +use crate::schema::{deserialize_animation_effects, AnimationEffect}; /// Top-level CSS style block. All fields are optional; `None` means "not set" /// and lets the cascade fill in inherited / initial values. @@ -203,9 +203,12 @@ fn edges_px(e: Option<&Edges>) -> (f32, f32, f32, f32) { let p = v.px(); (p, p, p, p) } - Some(Edges::Sides { top, right, bottom, left }) => { - (top.px(), right.px(), bottom.px(), left.px()) - } + Some(Edges::Sides { + top, + right, + bottom, + left, + }) => (top.px(), right.px(), bottom.px(), left.px()), None => (0.0, 0.0, 0.0, 0.0), } } @@ -296,12 +299,22 @@ pub enum Edges { } impl Edges { - pub fn resolve(&self) -> (LengthPercentage, LengthPercentage, LengthPercentage, LengthPercentage) { + pub fn resolve( + &self, + ) -> ( + LengthPercentage, + LengthPercentage, + LengthPercentage, + LengthPercentage, + ) { match self { Edges::Uniform(v) => (v.clone(), v.clone(), v.clone(), v.clone()), - Edges::Sides { top, right, bottom, left } => { - (top.clone(), right.clone(), bottom.clone(), left.clone()) - } + Edges::Sides { + top, + right, + bottom, + left, + } => (top.clone(), right.clone(), bottom.clone(), left.clone()), } } } @@ -359,38 +372,71 @@ pub enum BorderRadius { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum FlexDirection { Row, RowReverse, Column, ColumnReverse } +pub enum FlexDirection { + Row, + RowReverse, + Column, + ColumnReverse, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum FlexWrap { Nowrap, Wrap, WrapReverse } +pub enum FlexWrap { + Nowrap, + Wrap, + WrapReverse, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum JustifyContent { - FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly, - Start, End, + FlexStart, + FlexEnd, + Center, + SpaceBetween, + SpaceAround, + SpaceEvenly, + Start, + End, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum AlignItems { - Stretch, FlexStart, FlexEnd, Center, Baseline, - Start, End, + Stretch, + FlexStart, + FlexEnd, + Center, + Baseline, + Start, + End, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum AlignSelf { - Auto, Stretch, FlexStart, FlexEnd, Center, Baseline, - Start, End, + Auto, + Stretch, + FlexStart, + FlexEnd, + Center, + Baseline, + Start, + End, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum AlignContent { - Stretch, FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly, - Start, End, + Stretch, + FlexStart, + FlexEnd, + Center, + SpaceBetween, + SpaceAround, + SpaceEvenly, + Start, + End, } /// `gap: 8px` (uniform) or `gap: 8px 16px` (row-gap, column-gap). @@ -413,40 +459,63 @@ pub enum GridTrack { Fr(f32), Keyword(GridTrackKeyword), /// `minmax(min, max)` - Minmax { min: Box, max: Box }, + Minmax { + min: Box, + max: Box, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum GridTrackKeyword { Auto, MinContent, MaxContent } +pub enum GridTrackKeyword { + Auto, + MinContent, + MaxContent, +} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(default, deny_unknown_fields, rename_all = "kebab-case")] +#[derive(Default)] pub struct GridLine { pub start: Option, pub end: Option, pub span: Option, } -impl Default for GridLine { - fn default() -> Self { Self { start: None, end: None, span: None } } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(untagged)] -pub enum GridLineEnd { Index(i32) } +pub enum GridLineEnd { + Index(i32), +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum GridAutoFlow { Row, Column, RowDense, ColumnDense } +pub enum GridAutoFlow { + Row, + Column, + RowDense, + ColumnDense, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum JustifyItems { Stretch, Start, End, Center, Legacy } +pub enum JustifyItems { + Stretch, + Start, + End, + Center, + Legacy, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum JustifySelf { Auto, Stretch, Start, End, Center } +pub enum JustifySelf { + Auto, + Stretch, + Start, + End, + Center, +} // ---- Typography ---- @@ -460,12 +529,19 @@ pub enum FontWeight { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum FontWeightKw { - Normal, Bold, Bolder, Lighter, + Normal, + Bold, + Bolder, + Lighter, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum FontStyle { Normal, Italic, Oblique } +pub enum FontStyle { + Normal, + Italic, + Oblique, +} /// `line-height: 1.5` (number) or `line-height: 24px` (length). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -478,25 +554,46 @@ pub enum LineHeight { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum LineHeightKw { Normal } +pub enum LineHeightKw { + Normal, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum TextAlign { Left, Right, Center, Justify, Start, End } +pub enum TextAlign { + Left, + Right, + Center, + Justify, + Start, + End, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum WhiteSpace { - Normal, Nowrap, Pre, PreLine, PreWrap, BreakSpaces, + Normal, + Nowrap, + Pre, + PreLine, + PreWrap, + BreakSpaces, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum OverflowWrap { Normal, BreakWord, Anywhere } +pub enum OverflowWrap { + Normal, + BreakWord, + Anywhere, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum TextOverflow { Clip, Ellipsis } +pub enum TextOverflow { + Clip, + Ellipsis, +} #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(default, deny_unknown_fields, rename_all = "kebab-case")] @@ -509,11 +606,22 @@ pub struct TextDecoration { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum TextDecorationLine { None, Underline, Overline, LineThrough } +pub enum TextDecorationLine { + None, + Underline, + Overline, + LineThrough, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum TextDecorationStyle { Solid, Double, Dotted, Dashed, Wavy } +pub enum TextDecorationStyle { + Solid, + Double, + Dotted, + Dashed, + Wavy, +} // ---- Color ---- @@ -522,10 +630,18 @@ pub enum TextDecorationStyle { Solid, Double, Dotted, Dashed, Wavy } #[serde(untagged)] pub enum Color { String(String), - Rgba { r: u8, g: u8, b: u8, #[serde(default = "one_f32")] a: f32 }, + Rgba { + r: u8, + g: u8, + b: u8, + #[serde(default = "one_f32")] + a: f32, + }, } -fn one_f32() -> f32 { 1.0 } +fn one_f32() -> f32 { + 1.0 +} // ---- Background ---- @@ -540,7 +656,9 @@ pub enum Background { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind", rename_all = "kebab-case")] pub enum BackgroundLayer { - Color { color: Color }, + Color { + color: Color, + }, LinearGradient { #[serde(default)] angle: Option, @@ -580,13 +698,19 @@ pub struct GradientStop { impl Default for GradientStop { fn default() -> Self { - Self { color: Color::String("#000000".into()), offset: None } + Self { + color: Color::String("#000000".into()), + offset: None, + } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum RadialShape { Circle, Ellipse } +pub enum RadialShape { + Circle, + Ellipse, +} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] @@ -594,12 +718,22 @@ pub enum BackgroundSize { Cover, Contain, Auto, - Length { width: LengthPercentage, height: LengthPercentage }, + Length { + width: LengthPercentage, + height: LengthPercentage, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] -pub enum BackgroundRepeat { Repeat, NoRepeat, RepeatX, RepeatY, Round, Space } +pub enum BackgroundRepeat { + Repeat, + NoRepeat, + RepeatX, + RepeatY, + Round, + Space, +} // ---- Shadows ---- @@ -628,27 +762,82 @@ pub struct TextShadow { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "fn", rename_all = "kebab-case")] pub enum TransformFn { - Translate { x: LengthPercentage, #[serde(default)] y: LengthPercentage }, - TranslateX { x: LengthPercentage }, - TranslateY { y: LengthPercentage }, - TranslateZ { z: Length }, - Translate3d { x: LengthPercentage, y: LengthPercentage, z: Length }, - Scale { x: f32, #[serde(default = "one_f32")] y: f32 }, - ScaleX { x: f32 }, - ScaleY { y: f32 }, - ScaleZ { z: f32 }, - Scale3d { x: f32, y: f32, z: f32 }, - Rotate { deg: f32 }, - RotateX { deg: f32 }, - RotateY { deg: f32 }, - RotateZ { deg: f32 }, - Rotate3d { x: f32, y: f32, z: f32, deg: f32 }, - Skew { x: f32, #[serde(default)] y: f32 }, - SkewX { x: f32 }, - SkewY { y: f32 }, - Perspective { length: Length }, - Matrix { values: [f32; 6] }, - Matrix3d { values: [f32; 16] }, + Translate { + x: LengthPercentage, + #[serde(default)] + y: LengthPercentage, + }, + TranslateX { + x: LengthPercentage, + }, + TranslateY { + y: LengthPercentage, + }, + TranslateZ { + z: Length, + }, + Translate3d { + x: LengthPercentage, + y: LengthPercentage, + z: Length, + }, + Scale { + x: f32, + #[serde(default = "one_f32")] + y: f32, + }, + ScaleX { + x: f32, + }, + ScaleY { + y: f32, + }, + ScaleZ { + z: f32, + }, + Scale3d { + x: f32, + y: f32, + z: f32, + }, + Rotate { + deg: f32, + }, + RotateX { + deg: f32, + }, + RotateY { + deg: f32, + }, + RotateZ { + deg: f32, + }, + Rotate3d { + x: f32, + y: f32, + z: f32, + deg: f32, + }, + Skew { + x: f32, + #[serde(default)] + y: f32, + }, + SkewX { + x: f32, + }, + SkewY { + y: f32, + }, + Perspective { + length: Length, + }, + Matrix { + values: [f32; 6], + }, + Matrix3d { + values: [f32; 16], + }, } #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] @@ -664,14 +853,30 @@ pub struct TransformOrigin { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "fn", rename_all = "kebab-case")] pub enum FilterFn { - Blur { radius: Length }, - Brightness { value: f32 }, - Contrast { value: f32 }, - Saturate { value: f32 }, - HueRotate { deg: f32 }, - Grayscale { value: f32 }, - Invert { value: f32 }, - Sepia { value: f32 }, + Blur { + radius: Length, + }, + Brightness { + value: f32, + }, + Contrast { + value: f32, + }, + Saturate { + value: f32, + }, + HueRotate { + deg: f32, + }, + Grayscale { + value: f32, + }, + Invert { + value: f32, + }, + Sepia { + value: f32, + }, DropShadow { offset_x: Length, offset_y: Length, @@ -680,7 +885,9 @@ pub enum FilterFn { #[serde(default)] color: Option, }, - Opacity { value: f32 }, + Opacity { + value: f32, + }, } // ---- Blend ---- @@ -688,8 +895,22 @@ pub enum FilterFn { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum BlendMode { - Normal, Multiply, Screen, Overlay, Darken, Lighten, ColorDodge, ColorBurn, - HardLight, SoftLight, Difference, Exclusion, Hue, Saturation, Color, Luminosity, + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, PlusLighter, } @@ -699,11 +920,31 @@ pub enum BlendMode { #[serde(tag = "kind", rename_all = "kebab-case")] pub enum ClipPath { None, - Inset { top: LengthPercentage, right: LengthPercentage, bottom: LengthPercentage, left: LengthPercentage, #[serde(default)] radius: Option }, - Circle { radius: LengthPercentage, #[serde(default)] origin: Option }, - Ellipse { rx: LengthPercentage, ry: LengthPercentage, #[serde(default)] origin: Option }, - Polygon { points: Vec<(LengthPercentage, LengthPercentage)> }, - Path { d: String }, + Inset { + top: LengthPercentage, + right: LengthPercentage, + bottom: LengthPercentage, + left: LengthPercentage, + #[serde(default)] + radius: Option, + }, + Circle { + radius: LengthPercentage, + #[serde(default)] + origin: Option, + }, + Ellipse { + rx: LengthPercentage, + ry: LengthPercentage, + #[serde(default)] + origin: Option, + }, + Polygon { + points: Vec<(LengthPercentage, LengthPercentage)>, + }, + Path { + d: String, + }, } // ---- Tests ---- @@ -746,7 +987,9 @@ mod tests { #[test] fn deserialize_color_variants() { let s1: CssStyle = serde_json::from_str(r##"{ "color": "#ff0000" }"##).unwrap(); - let s2: CssStyle = serde_json::from_str(r##"{ "color": { "r": 255, "g": 0, "b": 0, "a": 1.0 } }"##).unwrap(); + let s2: CssStyle = + serde_json::from_str(r##"{ "color": { "r": 255, "g": 0, "b": 0, "a": 1.0 } }"##) + .unwrap(); assert!(matches!(s1.color, Some(Color::String(_)))); assert!(matches!(s2.color, Some(Color::Rgba { r: 255, .. }))); } diff --git a/crates/rustmotion-core/src/css/taffy_bridge.rs b/crates/rustmotion-core/src/css/taffy_bridge.rs index a46566d..4dcdadc 100644 --- a/crates/rustmotion-core/src/css/taffy_bridge.rs +++ b/crates/rustmotion-core/src/css/taffy_bridge.rs @@ -9,22 +9,16 @@ use taffy::prelude as tf; use super::style::{ - AlignContent, AlignItems, AlignSelf, CssStyle, Display, Edges, FlexDirection, FlexWrap, - Gap, JustifyContent, Overflow, Position, Size, + AlignContent, AlignItems, AlignSelf, CssStyle, Display, Edges, FlexDirection, FlexWrap, Gap, + JustifyContent, Overflow, Position, Size, }; use super::units::{LengthContext, LengthPercentage, ParsedLength}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct ConversionContext { pub length: LengthContext, } -impl Default for ConversionContext { - fn default() -> Self { - Self { length: LengthContext::default() } - } -} - /// Convert a [`CssStyle`] into a [`taffy::Style`]. Properties not relevant to /// layout are ignored. Unsupported / unset properties fall back to taffy /// defaults (which match CSS initial values). @@ -126,7 +120,10 @@ pub fn to_taffy_style(css: &CssStyle, ctx: &ConversionContext) -> tf::Style { Gap::Uniform(v) => (lp_to_lp(v, ctx), lp_to_lp(v, ctx)), Gap::RowColumn { row, column } => (lp_to_lp(row, ctx), lp_to_lp(column, ctx)), }; - style.gap = tf::Size { width: col, height: row }; + style.gap = tf::Size { + width: col, + height: row, + }; } // Overflow @@ -203,7 +200,9 @@ fn lp_to_lp_auto( v: Option<&LengthPercentage>, ctx: &ConversionContext, ) -> tf::LengthPercentageAuto { - let Some(v) = v else { return tf::LengthPercentageAuto::auto() }; + let Some(v) = v else { + return tf::LengthPercentageAuto::auto(); + }; match v.parse() { ParsedLength::Auto => tf::LengthPercentageAuto::auto(), ParsedLength::Px(p) => tf::LengthPercentageAuto::length(p), @@ -222,7 +221,9 @@ fn lp_to_lp_auto( /// Convert `Size` → taffy `Dimension`. fn size_to_dim(s: Option<&Size>, ctx: &ConversionContext) -> tf::Dimension { - let Some(s) = s else { return tf::Dimension::auto() }; + let Some(s) = s else { + return tf::Dimension::auto(); + }; match s { Size::Auto(_) => tf::Dimension::auto(), Size::Length(lp) => match lp.parse() { @@ -243,10 +244,7 @@ fn size_to_dim(s: Option<&Size>, ctx: &ConversionContext) -> tf::Dimension { } } -fn edges_to_rect_lp( - e: Option<&Edges>, - ctx: &ConversionContext, -) -> tf::Rect { +fn edges_to_rect_lp(e: Option<&Edges>, ctx: &ConversionContext) -> tf::Rect { let Some(e) = e else { return tf::Rect { top: tf::LengthPercentage::length(0.0), diff --git a/crates/rustmotion-core/src/css/units.rs b/crates/rustmotion-core/src/css/units.rs index ea66658..701c7fe 100644 --- a/crates/rustmotion-core/src/css/units.rs +++ b/crates/rustmotion-core/src/css/units.rs @@ -21,7 +21,9 @@ pub enum Length { } impl Default for Length { - fn default() -> Self { Length::Px(0.0) } + fn default() -> Self { + Length::Px(0.0) + } } /// A CSS length OR percentage (e.g. `width`, `padding`, `top`). @@ -33,7 +35,9 @@ pub enum LengthPercentage { } impl Default for LengthPercentage { - fn default() -> Self { LengthPercentage::Px(0.0) } + fn default() -> Self { + LengthPercentage::Px(0.0) + } } /// Internal parsed representation. Once parsed we know which unit it is. @@ -210,20 +214,29 @@ mod tests { #[test] fn resolve_percent_against_parent() { - let ctx = LengthContext { parent_size: 200.0, ..Default::default() }; + let ctx = LengthContext { + parent_size: 200.0, + ..Default::default() + }; let p = ParsedLength::Percent(50.0); assert_eq!(p.resolve(&ctx), Some(100.0)); } #[test] fn resolve_em_uses_font_size() { - let ctx = LengthContext { font_size: 20.0, ..Default::default() }; + let ctx = LengthContext { + font_size: 20.0, + ..Default::default() + }; assert_eq!(ParsedLength::Em(1.5).resolve(&ctx), Some(30.0)); } #[test] fn resolve_vw_uses_viewport() { - let ctx = LengthContext { viewport_width: 1000.0, ..Default::default() }; + let ctx = LengthContext { + viewport_width: 1000.0, + ..Default::default() + }; assert_eq!(ParsedLength::Vw(50.0).resolve(&ctx), Some(500.0)); } diff --git a/crates/rustmotion-core/src/engine/animator.rs b/crates/rustmotion-core/src/engine/animator.rs index 5f35035..ee00b04 100644 --- a/crates/rustmotion-core/src/engine/animator.rs +++ b/crates/rustmotion-core/src/engine/animator.rs @@ -9,13 +9,21 @@ use crate::schema::{ /// silently propagates into transforms or opacity. #[inline] pub fn safe_div(num: f64, denom: f64, fallback: f64) -> f64 { - if denom.abs() < 1e-9 { fallback } else { num / denom } + if denom.abs() < 1e-9 { + fallback + } else { + num / denom + } } /// Same as `safe_div` but for f32. Useful in render-side hot paths. #[inline] pub fn safe_div_f32(num: f32, denom: f32, fallback: f32) -> f32 { - if denom.abs() < 1e-6 { fallback } else { num / denom } + if denom.abs() < 1e-6 { + fallback + } else { + num / denom + } } // ─── Effect extraction ────────────────────────────────────────────────────── @@ -62,9 +70,12 @@ pub fn extract_effects(effects: &[AnimationEffect]) -> ExtractedEffects<'_> { result.presets.push((preset, timing.to_preset_config())); } else { match effect { - AnimationEffect::CharScaleIn(t) | AnimationEffect::CharFadeIn(t) - | AnimationEffect::CharWave(t) | AnimationEffect::CharBounce(t) - | AnimationEffect::CharRotateIn(t) | AnimationEffect::CharSlideUp(t) => { + AnimationEffect::CharScaleIn(t) + | AnimationEffect::CharFadeIn(t) + | AnimationEffect::CharWave(t) + | AnimationEffect::CharBounce(t) + | AnimationEffect::CharRotateIn(t) + | AnimationEffect::CharSlideUp(t) => { let preset = match effect { AnimationEffect::CharScaleIn(_) => CharAnimPreset::ScaleIn, AnimationEffect::CharFadeIn(_) => CharAnimPreset::FadeIn, @@ -75,9 +86,12 @@ pub fn extract_effects(effects: &[AnimationEffect]) -> ExtractedEffects<'_> { _ => unreachable!(), }; result.char_animation = Some(ResolvedCharAnimation { - preset, granularity: t.granularity.clone(), - stagger: t.stagger as f32, duration: t.duration as f32, - easing: t.easing.clone(), delay: t.delay as f32, + preset, + granularity: t.granularity.clone(), + stagger: t.stagger as f32, + duration: t.duration as f32, + easing: t.easing.clone(), + delay: t.delay as f32, overshoot: t.overshoot.unwrap_or(0.08) as f32, }); } @@ -97,17 +111,24 @@ pub fn extract_effects(effects: &[AnimationEffect]) -> ExtractedEffects<'_> { } AnimationEffect::TiltIn(config) => { let delay = config.delay; - let end = delay + config.duration; - let rx = config.rotate_x.unwrap_or(15.0); - let ry = config.rotate_y.unwrap_or(-15.0); + let end = delay + config.duration; + let rx = config.rotate_x.unwrap_or(15.0); + let ry = config.rotate_y.unwrap_or(-15.0); let persp = config.perspective.unwrap_or(1000.0); - let sc = config.scale_from.unwrap_or(0.9); + let sc = config.scale_from.unwrap_or(0.9); result.owned_keyframes.extend([ - kf_anim("opacity", delay, 0.0, delay + config.duration * 0.3, 1.0, EasingType::EaseOut), - kf_anim("rotate_x", delay, rx, end, 0.0, EasingType::EaseOutCubic), - kf_anim("rotate_y", delay, ry, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "opacity", + delay, + 0.0, + delay + config.duration * 0.3, + 1.0, + EasingType::EaseOut, + ), + kf_anim("rotate_x", delay, rx, end, 0.0, EasingType::EaseOutCubic), + kf_anim("rotate_y", delay, ry, end, 0.0, EasingType::EaseOutCubic), kf_anim("perspective", delay, persp, end, persp, EasingType::Linear), - kf_anim("scale", delay, sc, end, 1.0, EasingType::EaseOutCubic), + kf_anim("scale", delay, sc, end, 1.0, EasingType::EaseOutCubic), ]); } AnimationEffect::MotionBlur(config) => { @@ -150,13 +171,22 @@ pub fn ease(t: f64, easing: &EasingType) -> f64 { } } EasingType::EaseInOutQuad => { - if t < 0.5 { 2.0 * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 } + if t < 0.5 { + 2.0 * t * t + } else { + 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 + } } EasingType::EaseInOutExpo => { - if t == 0.0 { 0.0 } - else if t == 1.0 { 1.0 } - else if t < 0.5 { (2.0f64).powf(20.0 * t - 10.0) / 2.0 } - else { (2.0 - (2.0f64).powf(-20.0 * t + 10.0)) / 2.0 } + if t == 0.0 { + 0.0 + } else if t == 1.0 { + 1.0 + } else if t < 0.5 { + (2.0f64).powf(20.0 * t - 10.0) / 2.0 + } else { + (2.0 - (2.0f64).powf(-20.0 * t + 10.0)) / 2.0 + } } EasingType::EaseInBack => { let c1 = 1.70158; @@ -169,16 +199,16 @@ pub fn ease(t: f64, easing: &EasingType) -> f64 { 1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2) } EasingType::EaseOutElastic => { - if t == 0.0 { 0.0 } - else if t == 1.0 { 1.0 } - else { + if t == 0.0 { + 0.0 + } else if t == 1.0 { + 1.0 + } else { let c4 = (2.0 * std::f64::consts::PI) / 3.0; (2.0f64).powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0 } } - EasingType::Bounce => { - bounce_ease_out(t) - } + EasingType::Bounce => bounce_ease_out(t), EasingType::Spring => t, // Spring handled separately EasingType::CubicBezier { x1, y1, x2, y2 } => cubic_bezier_ease(t, *x1, *y1, *x2, *y2), } @@ -271,7 +301,8 @@ pub fn spring_value(t: f64, config: &SpringConfig) -> f64 { // Underdamped let omega_d = omega * (1.0 - zeta * zeta).sqrt(); let decay = (-zeta * omega * t).exp(); - 1.0 - decay * ((zeta * omega * t / omega_d).sin() * (zeta * omega / omega_d) + (omega_d * t).cos()) + 1.0 - decay + * ((zeta * omega * t / omega_d).sin() * (zeta * omega / omega_d) + (omega_d * t).cos()) } else if (zeta - 1.0).abs() < 1e-6 { // Critically damped let decay = (-omega * t).exp(); @@ -397,23 +428,55 @@ impl AnimatedProperties { self.color = other.color.clone(); } // Sentinel-based fields (-1.0 = not set) - if other.border_radius >= 0.0 { self.border_radius = other.border_radius; } - if other.font_size >= 0.0 { self.font_size = other.font_size; } - if other.width >= 0.0 { self.width = other.width; } - if other.height >= 0.0 { self.height = other.height; } - if other.gap >= 0.0 { self.gap = other.gap; } - if other.padding >= 0.0 { self.padding = other.padding; } - if other.stroke_width >= 0.0 { self.stroke_width = other.stroke_width; } - if other.shadow_blur >= 0.0 { self.shadow_blur = other.shadow_blur; } - if other.glow_radius >= 0.0 { self.glow_radius = other.glow_radius; } - if other.glow_intensity >= 0.0 { self.glow_intensity = other.glow_intensity; } + if other.border_radius >= 0.0 { + self.border_radius = other.border_radius; + } + if other.font_size >= 0.0 { + self.font_size = other.font_size; + } + if other.width >= 0.0 { + self.width = other.width; + } + if other.height >= 0.0 { + self.height = other.height; + } + if other.gap >= 0.0 { + self.gap = other.gap; + } + if other.padding >= 0.0 { + self.padding = other.padding; + } + if other.stroke_width >= 0.0 { + self.stroke_width = other.stroke_width; + } + if other.shadow_blur >= 0.0 { + self.shadow_blur = other.shadow_blur; + } + if other.glow_radius >= 0.0 { + self.glow_radius = other.glow_radius; + } + if other.glow_intensity >= 0.0 { + self.glow_intensity = other.glow_intensity; + } // 3D perspective transforms (additive like rotation) - if other.rotate_x.abs() > 0.01 { self.rotate_x += other.rotate_x; } - if other.rotate_y.abs() > 0.01 { self.rotate_y += other.rotate_y; } - if other.perspective >= 0.0 { self.perspective = other.perspective; } - if other.draw_progress >= 0.0 { self.draw_progress = other.draw_progress; } - if other.motion_progress >= 0.0 { self.motion_progress = other.motion_progress; } - if other.char_animation.is_some() { self.char_animation = other.char_animation.clone(); } + if other.rotate_x.abs() > 0.01 { + self.rotate_x += other.rotate_x; + } + if other.rotate_y.abs() > 0.01 { + self.rotate_y += other.rotate_y; + } + if other.perspective >= 0.0 { + self.perspective = other.perspective; + } + if other.draw_progress >= 0.0 { + self.draw_progress = other.draw_progress; + } + if other.motion_progress >= 0.0 { + self.motion_progress = other.motion_progress; + } + if other.char_animation.is_some() { + self.char_animation = other.char_animation.clone(); + } } } @@ -477,9 +540,7 @@ pub fn resolve_animations( let should_loop = config.repeat; // First, expand preset into animations - let preset_animations = preset.map(|p| { - expand_preset(p, &config, scene_duration) - }); + let preset_animations = preset.map(|p| expand_preset(p, &config, scene_duration)); // Merge preset animations with explicit animations (explicit wins on conflict) let all_animations: Vec<&Animation> = preset_animations @@ -660,10 +721,10 @@ fn apply_property(props: &mut AnimatedProperties, property: &str, value: f64) { fn simplex_noise_1d(x: f64, seed: u64) -> f64 { use std::f64::consts::TAU; let s = seed as f64; - let v = (x * TAU + s * 0.1234).sin() * 0.6 + + (x * TAU + s * 0.1234).sin() * 0.6 + (x * TAU * 1.7 + s * 0.5678).sin() * 0.3 - + (x * TAU * 2.9 + s * 0.9012).sin() * 0.1; - v // roughly -1..1 + + (x * TAU * 2.9 + s * 0.9012).sin() * 0.1 // roughly -1..1 } /// Parameterized noise function with configurable octaves @@ -680,7 +741,11 @@ fn simplex_noise_1d_ext(x: f64, seed: u64, octaves: u32) -> f64 { total_amplitude += amplitude; amplitude *= 0.5; } - if total_amplitude > 0.0 { value / total_amplitude } else { 0.0 } + if total_amplitude > 0.0 { + value / total_amplitude + } else { + 0.0 + } } /// Apply wiggle offsets additively to animated properties @@ -720,7 +785,11 @@ pub fn apply_wiggles(props: &mut AnimatedProperties, wiggles: &[WiggleConfig], t } let offset = amp * noise_val; - apply_property(props, &wiggle.property, get_property_value(props, &wiggle.property) + offset); + apply_property( + props, + &wiggle.property, + get_property_value(props, &wiggle.property) + offset, + ); } } @@ -802,52 +871,152 @@ fn get_property_value(props: &AnimatedProperties, property: &str) -> f64 { // ─── Preset expansion ─────────────────────────────────────────────────────── -fn expand_preset(preset: &AnimationPreset, config: &PresetConfig, _scene_duration: f64) -> Vec { +fn expand_preset( + preset: &AnimationPreset, + config: &PresetConfig, + _scene_duration: f64, +) -> Vec { let delay = config.delay; let dur = config.duration; let end = delay + dur; match preset { // ── Entrées ────────────────────────────────────────────────────── - AnimationPreset::FadeIn => vec![ - kf_anim("opacity", delay, 0.0, end, 1.0, EasingType::EaseOut), - ], + AnimationPreset::FadeIn => vec![kf_anim( + "opacity", + delay, + 0.0, + end, + 1.0, + EasingType::EaseOut, + )], AnimationPreset::FadeInUp => vec![ kf_anim("opacity", delay, 0.0, end, 1.0, EasingType::EaseOut), - kf_anim("position.y", delay, 60.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "position.y", + delay, + 60.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::FadeInDown => vec![ kf_anim("opacity", delay, 0.0, end, 1.0, EasingType::EaseOut), - kf_anim("position.y", delay, -60.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "position.y", + delay, + -60.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::FadeInLeft => vec![ kf_anim("opacity", delay, 0.0, end, 1.0, EasingType::EaseOut), - kf_anim("position.x", delay, -60.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "position.x", + delay, + -60.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::FadeInRight => vec![ kf_anim("opacity", delay, 0.0, end, 1.0, EasingType::EaseOut), - kf_anim("position.x", delay, 60.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "position.x", + delay, + 60.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::SlideInLeft => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), - kf_anim("position.x", delay, -200.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), + kf_anim( + "position.x", + delay, + -200.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::SlideInRight => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), - kf_anim("position.x", delay, 200.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), + kf_anim( + "position.x", + delay, + 200.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::SlideInUp => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), - kf_anim("position.y", delay, 200.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), + kf_anim( + "position.y", + delay, + 200.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::SlideInDown => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), - kf_anim("position.y", delay, -200.0, end, 0.0, EasingType::EaseOutCubic), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), + kf_anim( + "position.y", + delay, + -200.0, + end, + 0.0, + EasingType::EaseOutCubic, + ), ], AnimationPreset::ScaleIn => { let overshoot = config.overshoot.unwrap_or(0.08); vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), Animation { property: "scale".to_string(), keyframes: vec![ @@ -859,9 +1028,16 @@ fn expand_preset(preset: &AnimationPreset, config: &PresetConfig, _scene_duratio spring: None, }, ] - }, + } AnimationPreset::BounceIn => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.2, 1.0, EasingType::EaseOut), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.2, + 1.0, + EasingType::EaseOut, + ), kf_anim_spring("scale", delay, 0.3, end, 1.0), ], AnimationPreset::BlurIn => vec![ @@ -873,42 +1049,112 @@ fn expand_preset(preset: &AnimationPreset, config: &PresetConfig, _scene_duratio kf_anim("rotation", delay, -90.0, end, 0.0, EasingType::EaseOutCubic), kf_anim("scale", delay, 0.5, end, 1.0, EasingType::EaseOutCubic), ], - AnimationPreset::ElasticIn => vec![ - kf_anim_spring_underdamped("scale", delay, 0.0, end, 1.0), - ], + AnimationPreset::ElasticIn => { + vec![kf_anim_spring_underdamped("scale", delay, 0.0, end, 1.0)] + } // ── Sorties ────────────────────────────────────────────────────── - AnimationPreset::FadeOut => vec![ - kf_anim("opacity", delay, 1.0, end, 0.0, EasingType::EaseIn), - ], + AnimationPreset::FadeOut => { + vec![kf_anim("opacity", delay, 1.0, end, 0.0, EasingType::EaseIn)] + } AnimationPreset::FadeOutUp => vec![ kf_anim("opacity", delay, 1.0, end, 0.0, EasingType::EaseIn), - kf_anim("position.y", delay, 0.0, end, -60.0, EasingType::EaseInCubic), + kf_anim( + "position.y", + delay, + 0.0, + end, + -60.0, + EasingType::EaseInCubic, + ), ], AnimationPreset::FadeOutDown => vec![ kf_anim("opacity", delay, 1.0, end, 0.0, EasingType::EaseIn), kf_anim("position.y", delay, 0.0, end, 60.0, EasingType::EaseInCubic), ], AnimationPreset::SlideOutLeft => vec![ - kf_anim("opacity", delay + dur * 0.7, 1.0, end, 0.0, EasingType::EaseIn), - kf_anim("position.x", delay, 0.0, end, -200.0, EasingType::EaseInCubic), + kf_anim( + "opacity", + delay + dur * 0.7, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), + kf_anim( + "position.x", + delay, + 0.0, + end, + -200.0, + EasingType::EaseInCubic, + ), ], AnimationPreset::SlideOutRight => vec![ - kf_anim("opacity", delay + dur * 0.7, 1.0, end, 0.0, EasingType::EaseIn), - kf_anim("position.x", delay, 0.0, end, 200.0, EasingType::EaseInCubic), + kf_anim( + "opacity", + delay + dur * 0.7, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), + kf_anim( + "position.x", + delay, + 0.0, + end, + 200.0, + EasingType::EaseInCubic, + ), ], AnimationPreset::SlideOutUp => vec![ - kf_anim("opacity", delay + dur * 0.7, 1.0, end, 0.0, EasingType::EaseIn), - kf_anim("position.y", delay, 0.0, end, -200.0, EasingType::EaseInCubic), + kf_anim( + "opacity", + delay + dur * 0.7, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), + kf_anim( + "position.y", + delay, + 0.0, + end, + -200.0, + EasingType::EaseInCubic, + ), ], AnimationPreset::SlideOutDown => vec![ - kf_anim("opacity", delay + dur * 0.7, 1.0, end, 0.0, EasingType::EaseIn), - kf_anim("position.y", delay, 0.0, end, 200.0, EasingType::EaseInCubic), + kf_anim( + "opacity", + delay + dur * 0.7, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), + kf_anim( + "position.y", + delay, + 0.0, + end, + 200.0, + EasingType::EaseInCubic, + ), ], AnimationPreset::ScaleOut => { let overshoot = config.overshoot.unwrap_or(0.08); vec![ - kf_anim("opacity", delay + dur * 0.7, 1.0, end, 0.0, EasingType::EaseIn), + kf_anim( + "opacity", + delay + dur * 0.7, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), Animation { property: "scale".to_string(), keyframes: vec![ @@ -920,9 +1166,16 @@ fn expand_preset(preset: &AnimationPreset, config: &PresetConfig, _scene_duratio spring: None, }, ] - }, + } AnimationPreset::BounceOut => vec![ - kf_anim("opacity", delay + dur * 0.8, 1.0, end, 0.0, EasingType::EaseIn), + kf_anim( + "opacity", + delay + dur * 0.8, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), kf_anim_spring("scale", delay, 1.0, end, 0.3), ], AnimationPreset::BlurOut => vec![ @@ -936,45 +1189,99 @@ fn expand_preset(preset: &AnimationPreset, config: &PresetConfig, _scene_duratio ], // ── Effets continus ────────────────────────────────────────────── - AnimationPreset::Pulse => vec![ - kf_anim_loop("scale", 0.95, 1.05), - ], - AnimationPreset::Float => vec![ - kf_anim_3kf("position.y", 0.0, -10.0, 0.0, EasingType::EaseInOut), - ], - AnimationPreset::Shake => vec![ - kf_anim_4kf("position.x", 0.0, 10.0, -10.0, 0.0, EasingType::EaseInOut), - ], - AnimationPreset::Spin => vec![ - kf_anim("rotation", 0.0, 0.0, 1.0, 360.0, EasingType::Linear), - ], + AnimationPreset::Pulse => vec![kf_anim_loop("scale", 0.95, 1.05)], + AnimationPreset::Float => vec![kf_anim_3kf( + "position.y", + 0.0, + -10.0, + 0.0, + EasingType::EaseInOut, + )], + AnimationPreset::Shake => vec![kf_anim_4kf( + "position.x", + 0.0, + 10.0, + -10.0, + 0.0, + EasingType::EaseInOut, + )], + AnimationPreset::Spin => vec![kf_anim( + "rotation", + 0.0, + 0.0, + 1.0, + 360.0, + EasingType::Linear, + )], // ── 3D ─────────────────────────────────────────────────────────── AnimationPreset::FlipInX => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), kf_anim("rotate_x", delay, 90.0, end, 0.0, EasingType::EaseOutCubic), kf_anim("perspective", delay, 800.0, end, 800.0, EasingType::Linear), ], AnimationPreset::FlipInY => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), kf_anim("rotate_y", delay, 90.0, end, 0.0, EasingType::EaseOutCubic), kf_anim("perspective", delay, 800.0, end, 800.0, EasingType::Linear), ], AnimationPreset::FlipOutX => vec![ - kf_anim("opacity", delay + dur * 0.7, 1.0, end, 0.0, EasingType::EaseIn), + kf_anim( + "opacity", + delay + dur * 0.7, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), kf_anim("rotate_x", delay, 0.0, end, -90.0, EasingType::EaseInCubic), kf_anim("perspective", delay, 800.0, end, 800.0, EasingType::Linear), ], AnimationPreset::FlipOutY => vec![ - kf_anim("opacity", delay + dur * 0.7, 1.0, end, 0.0, EasingType::EaseIn), + kf_anim( + "opacity", + delay + dur * 0.7, + 1.0, + end, + 0.0, + EasingType::EaseIn, + ), kf_anim("rotate_y", delay, 0.0, end, -90.0, EasingType::EaseInCubic), kf_anim("perspective", delay, 800.0, end, 800.0, EasingType::Linear), ], AnimationPreset::TiltIn => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), kf_anim("rotate_x", delay, 15.0, end, 0.0, EasingType::EaseOutCubic), kf_anim("rotate_y", delay, -15.0, end, 0.0, EasingType::EaseOutCubic), - kf_anim("perspective", delay, 1000.0, end, 1000.0, EasingType::Linear), + kf_anim( + "perspective", + delay, + 1000.0, + end, + 1000.0, + EasingType::Linear, + ), kf_anim("scale", delay, 0.9, end, 1.0, EasingType::EaseOutCubic), ], @@ -987,29 +1294,64 @@ fn expand_preset(preset: &AnimationPreset, config: &PresetConfig, _scene_duratio ], // ── Spéciaux ──────────────────────────────────────────────────── - AnimationPreset::DrawIn => vec![ - kf_anim("draw_progress", delay, 0.0, end, 1.0, EasingType::EaseInOut), - ], + AnimationPreset::DrawIn => vec![kf_anim( + "draw_progress", + delay, + 0.0, + end, + 1.0, + EasingType::EaseInOut, + )], AnimationPreset::StrokeReveal => vec![ kf_anim("draw_progress", delay, 0.0, end, 1.0, EasingType::EaseOut), - kf_anim("opacity", delay, 0.0, delay + dur * 0.2, 1.0, EasingType::EaseOut), - ], - AnimationPreset::Typewriter => vec![ - kf_anim("visible_chars_progress", delay, 0.0, end, 1.0, EasingType::Linear), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.2, + 1.0, + EasingType::EaseOut, + ), ], + AnimationPreset::Typewriter => vec![kf_anim( + "visible_chars_progress", + delay, + 0.0, + end, + 1.0, + EasingType::Linear, + )], AnimationPreset::WipeLeft => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), kf_anim("position.x", delay, -200.0, end, 0.0, EasingType::EaseInOut), ], AnimationPreset::WipeRight => vec![ - kf_anim("opacity", delay, 0.0, delay + dur * 0.3, 1.0, EasingType::EaseOut), + kf_anim( + "opacity", + delay, + 0.0, + delay + dur * 0.3, + 1.0, + EasingType::EaseOut, + ), kf_anim("position.x", delay, 200.0, end, 0.0, EasingType::EaseInOut), ], } } fn kf(time: f64, value: f64) -> Keyframe { - Keyframe { time, value: KeyframeValue::Number(value), easing: None } + Keyframe { + time, + value: KeyframeValue::Number(value), + easing: None, + } } fn kf_anim(property: &str, t0: f64, v0: f64, t1: f64, v1: f64, easing: EasingType) -> Animation { @@ -1056,7 +1398,14 @@ fn kf_anim_3kf(property: &str, v0: f64, v1: f64, v2: f64, easing: EasingType) -> } } -fn kf_anim_4kf(property: &str, v0: f64, v1: f64, v2: f64, v3: f64, easing: EasingType) -> Animation { +fn kf_anim_4kf( + property: &str, + v0: f64, + v1: f64, + v2: f64, + v3: f64, + easing: EasingType, +) -> Animation { Animation { property: property.to_string(), keyframes: vec![kf(0.0, v0), kf(0.25, v1), kf(0.5, v2), kf(1.0, v3)], diff --git a/crates/rustmotion-core/src/engine/box_tree.rs b/crates/rustmotion-core/src/engine/box_tree.rs index c05fda0..ee8f434 100644 --- a/crates/rustmotion-core/src/engine/box_tree.rs +++ b/crates/rustmotion-core/src/engine/box_tree.rs @@ -28,11 +28,25 @@ pub struct BoxNode { impl BoxNode { pub fn container(css: CssStyle, children: Vec) -> Self { - Self { id: 0, kind: BoxKind::Container, css, children, intrinsic: None, source_path: None } + Self { + id: 0, + kind: BoxKind::Container, + css, + children, + intrinsic: None, + source_path: None, + } } pub fn leaf(css: CssStyle, intrinsic: Arc) -> Self { - Self { id: 0, kind: BoxKind::Container, css, children: Vec::new(), intrinsic: Some(intrinsic), source_path: None } + Self { + id: 0, + kind: BoxKind::Container, + css, + children: Vec::new(), + intrinsic: Some(intrinsic), + source_path: None, + } } /// Walk the tree and assign sequential `id` values to each node. diff --git a/crates/rustmotion-core/src/engine/layout_pass.rs b/crates/rustmotion-core/src/engine/layout_pass.rs index 1c5f4f8..026a16a 100644 --- a/crates/rustmotion-core/src/engine/layout_pass.rs +++ b/crates/rustmotion-core/src/engine/layout_pass.rs @@ -76,11 +76,7 @@ struct NodeData { } /// Run taffy on a [`BoxNode`] tree and return the resolved layouts. -pub fn run_layout( - root: &BoxNode, - viewport: (f32, f32), - ctx: &ConversionContext, -) -> LayoutResult { +pub fn run_layout(root: &BoxNode, viewport: (f32, f32), ctx: &ConversionContext) -> LayoutResult { let mut tree: TaffyTree = TaffyTree::new(); // Disable taffy's pixel rounding: it floors widths/heights to integers, // but our painters use sub-pixel Skia metrics. A width of 710.376 rounded @@ -109,7 +105,10 @@ pub fn run_layout( (known.width, known.height), (available.width.into(), available.height.into()), ); - tf::Size { width: w, height: h } + tf::Size { + width: w, + height: h, + } }, ); @@ -133,9 +132,11 @@ fn build( }; let tf_id = if node.intrinsic.is_some() { // Leaf with intrinsic measurement. - tree.new_leaf_with_context(style, data).expect("taffy new_leaf") + tree.new_leaf_with_context(style, data) + .expect("taffy new_leaf") } else if node.children.is_empty() { - tree.new_leaf_with_context(style, data).expect("taffy new_leaf") + tree.new_leaf_with_context(style, data) + .expect("taffy new_leaf") } else { let mut child_ids = Vec::with_capacity(node.children.len()); for c in &node.children { @@ -160,8 +161,12 @@ fn collect( parent_y: f32, out: &mut HashMap, ) { - let Some(&tf_id) = map.get(&node.id) else { return }; - let Ok(layout) = tree.layout(tf_id) else { return }; + let Some(&tf_id) = map.get(&node.id) else { + return; + }; + let Ok(layout) = tree.layout(tf_id) else { + return; + }; let abs_x = parent_x + layout.location.x; let abs_y = parent_y + layout.location.y; diff --git a/crates/rustmotion-core/src/engine/mod.rs b/crates/rustmotion-core/src/engine/mod.rs index 6157a0a..87c390a 100644 --- a/crates/rustmotion-core/src/engine/mod.rs +++ b/crates/rustmotion-core/src/engine/mod.rs @@ -1,10 +1,10 @@ pub mod animator; -pub mod heropatterns; -pub mod renderer; -pub mod transition; pub mod box_tree; +pub mod heropatterns; pub mod layout_pass; pub mod paint_pass; +pub mod renderer; pub mod text; +pub mod transition; pub use renderer::*; diff --git a/crates/rustmotion-core/src/engine/paint_pass.rs b/crates/rustmotion-core/src/engine/paint_pass.rs index 234248a..d2064f4 100644 --- a/crates/rustmotion-core/src/engine/paint_pass.rs +++ b/crates/rustmotion-core/src/engine/paint_pass.rs @@ -18,13 +18,13 @@ use std::cell::RefCell; use skia_safe::{ - canvas::SaveLayerRec, Canvas, ClipOp, Color as SColor, Color4f, M44, Paint, PaintStyle, - Path, Point, RRect, Rect, V3, + canvas::SaveLayerRec, Canvas, ClipOp, Color as SColor, Color4f, Paint, PaintStyle, Path, Point, + RRect, Rect, M44, V3, }; use crate::css::style::{ - Background, BackgroundLayer, BorderEdges, BorderRadius, BorderStyle, BoxShadow, - Color, CssStyle, Edges, Overflow, TransformFn, + Background, BackgroundLayer, BorderEdges, BorderRadius, BorderStyle, BoxShadow, Color, + CssStyle, Edges, Overflow, TransformFn, }; use crate::css::units::{LengthContext, LengthPercentage, ParsedLength}; use crate::engine::box_tree::{BoxKind, BoxNode, NodeId}; @@ -155,7 +155,9 @@ struct PaintContext<'a> { } fn paint_node(canvas: &Canvas, node: &BoxNode, ctx: &PaintContext) { - let Some(box_layout) = ctx.layout.get(node.id) else { return }; + let Some(box_layout) = ctx.layout.get(node.id) else { + return; + }; if box_layout.width <= 0.0 || box_layout.height <= 0.0 { return; } @@ -177,7 +179,11 @@ fn paint_node(canvas: &Canvas, node: &BoxNode, ctx: &PaintContext) { box_layout.y + box_layout.height / 2.0, ); let transform_list = node.css.transform.as_deref().unwrap_or(&[]); - let perspective_d = node.css.perspective.as_ref().map(|l| l.resolve(&length_ctx).max(1.0)); + let perspective_d = node + .css + .perspective + .as_ref() + .map(|l| l.resolve(&length_ctx).max(1.0)); apply_transform(canvas, transform_list, perspective_d, pivot, &length_ctx); } @@ -217,7 +223,10 @@ fn paint_node(canvas: &Canvas, node: &BoxNode, ctx: &PaintContext) { // 4. clip overflow:hidden / clip let overflow = node.css.overflow.unwrap_or(Overflow::Visible); - if matches!(overflow, Overflow::Hidden | Overflow::Clip | Overflow::Scroll | Overflow::Auto) { + if matches!( + overflow, + Overflow::Hidden | Overflow::Clip | Overflow::Scroll | Overflow::Auto + ) { let radius = node .css .border_radius @@ -225,7 +234,7 @@ fn paint_node(canvas: &Canvas, node: &BoxNode, ctx: &PaintContext) { .map(|r| resolve_border_radius(r, box_layout, &length_ctx)) .unwrap_or([0.0; 4]); let rrect = padding_rrect(box_layout, radius); - canvas.clip_rrect(&rrect, ClipOp::Intersect, true); + canvas.clip_rrect(rrect, ClipOp::Intersect, true); } // 5. outset box-shadow @@ -250,13 +259,8 @@ fn paint_node(canvas: &Canvas, node: &BoxNode, ctx: &PaintContext) { // 8. component-specific content if let BoxKind::Component(payload) = &node.kind { - ctx.dispatcher.dispatch( - canvas, - payload.as_ref(), - &node.css, - box_layout, - ctx.frame, - ); + ctx.dispatcher + .dispatch(canvas, payload.as_ref(), &node.css, box_layout, ctx.frame); } // 9. children (z-index ordered, then source order) @@ -373,18 +377,28 @@ fn apply_transform( /// Maps (x, y, z, 1) → w' = 1 - z/d; perspective divide yields depth scaling. fn css_perspective_m44(d: f32) -> M44 { M44::row_major(&[ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, -1.0 / d, 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + -1.0 / d, + 1.0, ]) } fn transform_to_m44(tr: &TransformFn, ctx: &LengthContext) -> M44 { match tr { - TransformFn::Translate { x, y } => { - M44::translate(x.resolve(ctx), y.resolve(ctx), 0.0) - } + TransformFn::Translate { x, y } => M44::translate(x.resolve(ctx), y.resolve(ctx), 0.0), TransformFn::TranslateX { x } => M44::translate(x.resolve(ctx), 0.0, 0.0), TransformFn::TranslateY { y } => M44::translate(0.0, y.resolve(ctx), 0.0), TransformFn::TranslateZ { z } => M44::translate(0.0, 0.0, z.resolve(ctx)), @@ -405,29 +419,62 @@ fn transform_to_m44(tr: &TransformFn, ctx: &LengthContext) -> M44 { M44::rotate(V3::new(*x, *y, *z), deg.to_radians()) } TransformFn::Skew { x, y } => M44::row_major(&[ - 1.0, x.to_radians().tan(), 0.0, 0.0, - y.to_radians().tan(), 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, + 1.0, + x.to_radians().tan(), + 0.0, + 0.0, + y.to_radians().tan(), + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, ]), TransformFn::SkewX { x } => M44::row_major(&[ - 1.0, x.to_radians().tan(), 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, + 1.0, + x.to_radians().tan(), + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, ]), TransformFn::SkewY { y } => M44::row_major(&[ - 1.0, 0.0, 0.0, 0.0, - y.to_radians().tan(), 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + y.to_radians().tan(), + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, ]), TransformFn::Perspective { length } => css_perspective_m44(length.resolve(ctx).max(1.0)), TransformFn::Matrix { values: v } => M44::row_major(&[ - v[0], v[2], 0.0, v[4], - v[1], v[3], 0.0, v[5], - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, + v[0], v[2], 0.0, v[4], v[1], v[3], 0.0, v[5], 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ]), TransformFn::Matrix3d { values: v } => M44::col_major(v), } @@ -454,7 +501,7 @@ fn paint_background( let mut paint = Paint::default(); paint.set_anti_alias(true); paint.set_color(parse_color(c)); - canvas.draw_rrect(&rrect, &paint); + canvas.draw_rrect(rrect, &paint); } Background::Single(layer) => paint_bg_layer(canvas, &rrect, layer), Background::Layers(layers) => { @@ -584,7 +631,11 @@ fn paint_border( // Compute per-side widths (already resolved into BoxLayout.border by taffy). let widths = layout.border; - let max_w = widths.top.max(widths.right).max(widths.bottom).max(widths.left); + let max_w = widths + .top + .max(widths.right) + .max(widths.bottom) + .max(widths.left); if max_w <= 0.0 { return; } @@ -605,7 +656,7 @@ fn paint_border( paint.set_color(color); // Use DRRect = outer minus inner for an accurate stroked border with radius. - canvas.draw_drrect(&outer, &inner, &paint); + canvas.draw_drrect(outer, inner, &paint); } fn border_rrect(layout: &BoxLayout, radius: [f32; 4]) -> RRect { @@ -676,8 +727,16 @@ fn paint_box_shadow( let dx = shadow.offset_x.resolve(ctx); let dy = shadow.offset_y.resolve(ctx); let blur = shadow.blur.as_ref().map(|b| b.resolve(ctx)).unwrap_or(0.0); - let spread = shadow.spread.as_ref().map(|b| b.resolve(ctx)).unwrap_or(0.0); - let color = shadow.color.as_ref().map(parse_color).unwrap_or(SColor::BLACK); + let spread = shadow + .spread + .as_ref() + .map(|b| b.resolve(ctx)) + .unwrap_or(0.0); + let color = shadow + .color + .as_ref() + .map(parse_color) + .unwrap_or(SColor::BLACK); let radius = css .border_radius @@ -689,11 +748,9 @@ fn paint_box_shadow( paint.set_anti_alias(true); paint.set_color(color); if blur > 0.0 { - if let Some(filter) = skia_safe::MaskFilter::blur( - skia_safe::BlurStyle::Normal, - blur / 2.0, - None, - ) { + if let Some(filter) = + skia_safe::MaskFilter::blur(skia_safe::BlurStyle::Normal, blur / 2.0, None) + { paint.set_mask_filter(filter); } } @@ -706,14 +763,14 @@ fn paint_box_shadow( layout.height + spread * 2.0, ); let rrect = rrect_from_corners(rect, radius); - canvas.draw_rrect(&rrect, &paint); + canvas.draw_rrect(rrect, &paint); } else { // Inset: invert — paint the area outside the inner rect within the box. // Approximation: draw a stroked rrect inside the padding box. let (px, py, pw, ph) = layout.padding_box(); let outer = rrect_from_corners(Rect::from_xywh(px, py, pw, ph), radius); canvas.save(); - canvas.clip_rrect(&outer, ClipOp::Intersect, true); + canvas.clip_rrect(outer, ClipOp::Intersect, true); let inner_rect = Rect::from_xywh( px + dx + spread, py + dy + spread, @@ -725,18 +782,16 @@ fn paint_box_shadow( clear.set_color(color); clear.set_anti_alias(true); if blur > 0.0 { - if let Some(filter) = skia_safe::MaskFilter::blur( - skia_safe::BlurStyle::Normal, - blur / 2.0, - None, - ) { + if let Some(filter) = + skia_safe::MaskFilter::blur(skia_safe::BlurStyle::Normal, blur / 2.0, None) + { clear.set_mask_filter(filter); } } // Cheap approximation — TODO: proper inset shadow with subtraction path. let mut path = Path::new(); - path.add_rrect(&outer, None); - path.add_rrect(&inner, None); + path.add_rrect(outer, None); + path.add_rrect(inner, None); path.set_fill_type(skia_safe::PathFillType::EvenOdd); canvas.draw_path(&path, &clear); canvas.restore(); @@ -997,6 +1052,9 @@ mod tests { #[test] fn parse_named_colors() { assert_eq!(parse_color_string("red").unwrap(), SColor::RED); - assert_eq!(parse_color_string("transparent").unwrap(), SColor::TRANSPARENT); + assert_eq!( + parse_color_string("transparent").unwrap(), + SColor::TRANSPARENT + ); } } diff --git a/crates/rustmotion-core/src/engine/renderer/assets.rs b/crates/rustmotion-core/src/engine/renderer/assets.rs index c0f82cf..18ab631 100644 --- a/crates/rustmotion-core/src/engine/renderer/assets.rs +++ b/crates/rustmotion-core/src/engine/renderer/assets.rs @@ -4,6 +4,14 @@ use dashmap::DashMap; use crate::error::{Result, RustmotionError}; +type GifFrame = (Vec, u32, u32); +type GifData = Arc<(Vec, Vec, f64)>; +type GifCacheMap = Arc>; + +type VideoFrame = (f64, Vec, u32, u32); +type VideoFrameList = Arc>; +type VideoFrameCacheMap = Arc>; + /// Global asset cache for decoded images (keyed by file path) static ASSET_CACHE: OnceLock>> = OnceLock::new(); @@ -20,11 +28,9 @@ pub fn clear_asset_cache() { /// GIF frame data cache: stores decoded frames with pre-computed cumulative timestamps /// (frames_rgba, cumulative_times, total_duration) keyed by file path -static GIF_CACHE: OnceLock, u32, u32)>, Vec, f64)>>>> = - OnceLock::new(); +static GIF_CACHE: OnceLock = OnceLock::new(); -pub fn gif_cache() -> &'static Arc, u32, u32)>, Vec, f64)>>> -{ +pub fn gif_cache() -> &'static GifCacheMap { GIF_CACHE.get_or_init(|| Arc::new(DashMap::new())) } @@ -62,10 +68,9 @@ pub fn fetch_icon_svg(icon: &str, color: &str, width: u32, height: u32) -> Resul // ─── Video frame extraction ───────────────────────────────────────────────── /// Cache for pre-extracted video frames: key = "src:width:height", value = sorted list of (time, RGBA data, width, height) -static VIDEO_FRAME_CACHE: OnceLock, u32, u32)>>>>> = - OnceLock::new(); +static VIDEO_FRAME_CACHE: OnceLock = OnceLock::new(); -pub fn video_frame_cache() -> &'static Arc, u32, u32)>>>> { +pub fn video_frame_cache() -> &'static VideoFrameCacheMap { VIDEO_FRAME_CACHE.get_or_init(|| Arc::new(DashMap::new())) } @@ -120,8 +125,7 @@ pub fn extract_video_frame(src: &str, time: f64, width: u32, height: u32) -> Res if !output.status.success() { return Err(RustmotionError::FfmpegFrameExtract { src: src.to_string(), - } - .into()); + }); } Ok(output.stdout) diff --git a/crates/rustmotion-core/src/engine/renderer/shapes.rs b/crates/rustmotion-core/src/engine/renderer/shapes.rs index f68a740..895f984 100644 --- a/crates/rustmotion-core/src/engine/renderer/shapes.rs +++ b/crates/rustmotion-core/src/engine/renderer/shapes.rs @@ -3,7 +3,14 @@ use skia_safe::{Canvas, Paint, Rect}; use crate::schema::ShapeType; /// Build a `Path` for the given shape type without drawing it. -pub fn build_shape_path(shape_type: &ShapeType, x: f32, y: f32, w: f32, h: f32, corner_radius: Option) -> Option { +pub fn build_shape_path( + shape_type: &ShapeType, + x: f32, + y: f32, + w: f32, + h: f32, + corner_radius: Option, +) -> Option { match shape_type { ShapeType::Rect => { let mut path = skia_safe::Path::new(); @@ -44,11 +51,16 @@ pub fn build_shape_path(shape_type: &ShapeType, x: f32, y: f32, w: f32, h: f32, let n = *points as usize; let mut path = skia_safe::Path::new(); for i in 0..(n * 2) { - let angle = (i as f32) * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2; + let angle = + (i as f32) * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2; let r = if i % 2 == 0 { outer_r } else { inner_r }; let px = cx + r * angle.cos(); let py = cy + r * angle.sin(); - if i == 0 { path.move_to((px, py)); } else { path.line_to((px, py)); } + if i == 0 { + path.move_to((px, py)); + } else { + path.line_to((px, py)); + } } path.close(); Some(path) @@ -60,24 +72,38 @@ pub fn build_shape_path(shape_type: &ShapeType, x: f32, y: f32, w: f32, h: f32, let n = *sides as usize; let mut path = skia_safe::Path::new(); for i in 0..n { - let angle = (i as f32) * 2.0 * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2; + let angle = (i as f32) * 2.0 * std::f32::consts::PI / n as f32 + - std::f32::consts::FRAC_PI_2; let px = cx + r * angle.cos(); let py = cy + r * angle.sin(); - if i == 0 { path.move_to((px, py)); } else { path.line_to((px, py)); } + if i == 0 { + path.move_to((px, py)); + } else { + path.line_to((px, py)); + } } path.close(); Some(path) } - ShapeType::Path { data } => { - skia_safe::Path::from_svg(data) - } + ShapeType::Path { data } => skia_safe::Path::from_svg(data), } } -pub fn draw_shape_path(canvas: &Canvas, shape_type: &ShapeType, x: f32, y: f32, w: f32, h: f32, corner_radius: Option, paint: &Paint) { +pub fn draw_shape_path( + canvas: &Canvas, + shape_type: &ShapeType, + x: f32, + y: f32, + w: f32, + h: f32, + corner_radius: Option, + paint: &Paint, +) { let rect = Rect::from_xywh(x, y, w, h); match shape_type { - ShapeType::Rect => { canvas.draw_rect(rect, paint); } + ShapeType::Rect => { + canvas.draw_rect(rect, paint); + } ShapeType::RoundedRect => { let r = corner_radius.unwrap_or(8.0); let rrect = skia_safe::RRect::new_rect_xy(rect, r, r); @@ -87,7 +113,9 @@ pub fn draw_shape_path(canvas: &Canvas, shape_type: &ShapeType, x: f32, y: f32, let radius = w.min(h) / 2.0; canvas.draw_circle((x + w / 2.0, y + h / 2.0), radius, paint); } - ShapeType::Ellipse => { canvas.draw_oval(rect, paint); } + ShapeType::Ellipse => { + canvas.draw_oval(rect, paint); + } ShapeType::Triangle => { let mut path = skia_safe::Path::new(); path.move_to((x + w / 2.0, y)); @@ -104,7 +132,8 @@ pub fn draw_shape_path(canvas: &Canvas, shape_type: &ShapeType, x: f32, y: f32, let n = *points as usize; let mut path = skia_safe::Path::new(); for i in 0..(n * 2) { - let angle = (i as f32) * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2; + let angle = + (i as f32) * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2; let r = if i % 2 == 0 { outer_r } else { inner_r }; let px = cx + r * angle.cos(); let py = cy + r * angle.sin(); @@ -124,7 +153,8 @@ pub fn draw_shape_path(canvas: &Canvas, shape_type: &ShapeType, x: f32, y: f32, let n = *sides as usize; let mut path = skia_safe::Path::new(); for i in 0..n { - let angle = (i as f32) * 2.0 * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2; + let angle = (i as f32) * 2.0 * std::f32::consts::PI / n as f32 + - std::f32::consts::FRAC_PI_2; let px = cx + r * angle.cos(); let py = cy + r * angle.sin(); if i == 0 { diff --git a/crates/rustmotion-core/src/engine/renderer/text.rs b/crates/rustmotion-core/src/engine/renderer/text.rs index 0a47fa3..dd34df2 100644 --- a/crates/rustmotion-core/src/engine/renderer/text.rs +++ b/crates/rustmotion-core/src/engine/renderer/text.rs @@ -18,8 +18,8 @@ pub fn format_counter_value( let integer_part = parts[0]; // Handle negative sign - let (sign, digits) = if integer_part.starts_with('-') { - ("-", &integer_part[1..]) + let (sign, digits) = if let Some(stripped) = integer_part.strip_prefix('-') { + ("-", stripped) } else { ("", integer_part) }; diff --git a/crates/rustmotion-core/src/engine/renderer/yuv.rs b/crates/rustmotion-core/src/engine/renderer/yuv.rs index 50383ab..9aabbb4 100644 --- a/crates/rustmotion-core/src/engine/renderer/yuv.rs +++ b/crates/rustmotion-core/src/engine/renderer/yuv.rs @@ -20,12 +20,12 @@ pub fn rgba_to_yuv420(rgba: &[u8], width: u32, height: u32) -> Vec { .enumerate() .for_each(|(row, y_row)| { let row_offset = row * w * 4; - for col in 0..w { + for (col, out) in y_row.iter_mut().enumerate().take(w) { let idx = row_offset + col * 4; let r = rgba[idx] as i32; let g = rgba[idx + 1] as i32; let b = rgba[idx + 2] as i32; - y_row[col] = (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16).clamp(0, 255) as u8; + *out = (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16).clamp(0, 255) as u8; } }); diff --git a/crates/rustmotion-core/src/engine/text/cosmic.rs b/crates/rustmotion-core/src/engine/text/cosmic.rs index 62c9a01..7e36894 100644 --- a/crates/rustmotion-core/src/engine/text/cosmic.rs +++ b/crates/rustmotion-core/src/engine/text/cosmic.rs @@ -13,8 +13,8 @@ use std::sync::{Mutex, OnceLock}; use cosmic_text::{ - Attrs, Buffer, Color as CColor, Family, FontSystem, Metrics, Shaping, - SwashCache, SwashContent, Weight, Wrap, + Attrs, Buffer, Color as CColor, Family, FontSystem, Metrics, Shaping, SwashCache, SwashContent, + Weight, Wrap, }; use skia_safe::{images, Canvas, Color, ColorType, Data, ImageInfo, Paint, Point}; @@ -91,7 +91,10 @@ fn attrs_for<'a>(style: &'a TextStyle) -> Attrs<'a> { /// Measure a text string given font + line constraints. pub fn measure_text(text: &str, style: &TextStyle) -> TextMetrics { if text.is_empty() { - return TextMetrics { width: 0.0, height: metrics_for(style).line_height }; + return TextMetrics { + width: 0.0, + height: metrics_for(style).line_height, + }; } let mut fs = font_system().lock().unwrap(); let metrics = metrics_for(style); @@ -156,9 +159,12 @@ pub fn paint_text( physical.x as f32 + img.placement.left as f32, physical.y as f32 - img.placement.top as f32, img.content == SwashContent::Color, - glyph.color_opt.map(|c| Color::from_argb(c.a(), c.r(), c.g(), c.b())).unwrap_or_else(|| { - Color::from_argb(ccolor.a(), ccolor.r(), ccolor.g(), ccolor.b()) - }), + glyph + .color_opt + .map(|c| Color::from_argb(c.a(), c.r(), c.g(), c.b())) + .unwrap_or_else(|| { + Color::from_argb(ccolor.a(), ccolor.r(), ccolor.g(), ccolor.b()) + }), ); } } @@ -189,7 +195,7 @@ fn blit_glyph( let tb = tint.b() as u32; let ta = tint.a() as u32; for &a in data.iter() { - let alpha = (ta * a as u32 / 255) as u32; + let alpha = ta * a as u32 / 255; rgba.push((tr * alpha / 255) as u8); rgba.push((tg * alpha / 255) as u8); rgba.push((tb * alpha / 255) as u8); @@ -224,7 +230,13 @@ mod tests { #[test] fn measure_hello_world_has_positive_width() { - let m = measure_text("Hello, world!", &TextStyle { font_size: 24.0, ..Default::default() }); + let m = measure_text( + "Hello, world!", + &TextStyle { + font_size: 24.0, + ..Default::default() + }, + ); assert!(m.width > 0.0); assert!(m.height > 0.0); } diff --git a/crates/rustmotion-core/src/engine/transition.rs b/crates/rustmotion-core/src/engine/transition.rs index 4874a55..541bcf3 100644 --- a/crates/rustmotion-core/src/engine/transition.rs +++ b/crates/rustmotion-core/src/engine/transition.rs @@ -1,8 +1,6 @@ use crate::engine::animator::ease; use crate::schema::{EasingType, TransitionType}; -use skia_safe::{ - surfaces, Color4f, ColorType, ImageInfo, Paint, Path, Rect, -}; +use skia_safe::{surfaces, Color4f, ColorType, ImageInfo, Paint, Path, Rect}; /// Composite two RGBA frames during a transition. /// `progress` goes from 0.0 (fully frame_a) to 1.0 (fully frame_b). @@ -18,12 +16,20 @@ pub fn apply_transition( match transition_type { TransitionType::Fade => blend_fade(frame_a, frame_b, progress), - TransitionType::WipeLeft => wipe(frame_a, frame_b, width, height, progress, Direction::Left), - TransitionType::WipeRight => wipe(frame_a, frame_b, width, height, progress, Direction::Right), + TransitionType::WipeLeft => { + wipe(frame_a, frame_b, width, height, progress, Direction::Left) + } + TransitionType::WipeRight => { + wipe(frame_a, frame_b, width, height, progress, Direction::Right) + } TransitionType::WipeUp => wipe(frame_a, frame_b, width, height, progress, Direction::Up), - TransitionType::WipeDown => wipe(frame_a, frame_b, width, height, progress, Direction::Down), + TransitionType::WipeDown => { + wipe(frame_a, frame_b, width, height, progress, Direction::Down) + } TransitionType::ZoomIn => zoom_transition(frame_a, frame_b, width, height, progress, true), - TransitionType::ZoomOut => zoom_transition(frame_a, frame_b, width, height, progress, false), + TransitionType::ZoomOut => { + zoom_transition(frame_a, frame_b, width, height, progress, false) + } TransitionType::Flip => flip_transition(frame_a, frame_b, width, height, progress), TransitionType::ClockWipe => clock_wipe(frame_a, frame_b, width, height, progress), TransitionType::Iris => iris_transition(frame_a, frame_b, width, height, progress), @@ -138,7 +144,14 @@ fn surface_to_pixels(mut surface: skia_safe::Surface, width: u32, height: u32) - pixels } -fn zoom_transition(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, progress: f32, zoom_in: bool) -> Vec { +fn zoom_transition( + frame_a: &[u8], + frame_b: &[u8], + width: u32, + height: u32, + progress: f32, + zoom_in: bool, +) -> Vec { let mut surface = match create_skia_surface(width, height) { Some(s) => s, None => return blend_fade(frame_a, frame_b, progress), @@ -185,7 +198,13 @@ fn zoom_transition(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, prog surface_to_pixels(surface, width, height) } -fn flip_transition(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, progress: f32) -> Vec { +fn flip_transition( + frame_a: &[u8], + frame_b: &[u8], + width: u32, + height: u32, + progress: f32, +) -> Vec { let mut surface = match create_skia_surface(width, height) { Some(s) => s, None => return blend_fade(frame_a, frame_b, progress), @@ -273,7 +292,13 @@ fn clock_wipe(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, progress: surface_to_pixels(surface, width, height) } -fn iris_transition(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, progress: f32) -> Vec { +fn iris_transition( + frame_a: &[u8], + frame_b: &[u8], + width: u32, + height: u32, + progress: f32, +) -> Vec { let mut surface = match create_skia_surface(width, height) { Some(s) => s, None => return blend_fade(frame_a, frame_b, progress), @@ -310,7 +335,13 @@ fn iris_transition(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, prog surface_to_pixels(surface, width, height) } -fn slide_transition(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, progress: f32) -> Vec { +fn slide_transition( + frame_a: &[u8], + frame_b: &[u8], + width: u32, + height: u32, + progress: f32, +) -> Vec { let mut surface = match create_skia_surface(width, height) { Some(s) => s, None => return blend_fade(frame_a, frame_b, progress), @@ -335,7 +366,13 @@ fn slide_transition(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, pro surface_to_pixels(surface, width, height) } -fn dissolve_transition(frame_a: &[u8], frame_b: &[u8], _width: u32, _height: u32, progress: f32) -> Vec { +fn dissolve_transition( + frame_a: &[u8], + frame_b: &[u8], + _width: u32, + _height: u32, + progress: f32, +) -> Vec { // Dissolve is a smooth cross-dissolve (same as fade in standard video editing) blend_fade(frame_a, frame_b, progress) } diff --git a/crates/rustmotion-core/src/error.rs b/crates/rustmotion-core/src/error.rs index 51a4a47..d0f2f8a 100644 --- a/crates/rustmotion-core/src/error.rs +++ b/crates/rustmotion-core/src/error.rs @@ -89,7 +89,9 @@ pub enum RustmotionError { #[error("Include: file not found '{path}'")] IncludeFileNotFound { path: String }, - #[error("Scenario cannot have both top-level 'scenes' and 'composition' — use one or the other")] + #[error( + "Scenario cannot have both top-level 'scenes' and 'composition' — use one or the other" + )] CompositionAndScenesConflict, #[error("Unknown background template '{name}' referenced via $ref")] @@ -227,7 +229,9 @@ pub enum RustmotionError { LottieRender { reason: String }, // --- Skills --- - #[error("Unknown skill or rule: '{name}'. Run `rustmotion skills list` to see available rules.")] + #[error( + "Unknown skill or rule: '{name}'. Run `rustmotion skills list` to see available rules." + )] UnknownSkill { name: String }, // --- IO --- diff --git a/crates/rustmotion-core/src/lib.rs b/crates/rustmotion-core/src/lib.rs index 47eee00..aa7290d 100644 --- a/crates/rustmotion-core/src/lib.rs +++ b/crates/rustmotion-core/src/lib.rs @@ -1,8 +1,8 @@ pub mod error; #[macro_use] pub mod macros; -pub mod variables; +pub mod css; +pub mod engine; pub mod schema; pub mod traits; -pub mod engine; -pub mod css; +pub mod variables; diff --git a/crates/rustmotion-core/src/schema/background.rs b/crates/rustmotion-core/src/schema/background.rs index 5e8252c..75458bc 100644 --- a/crates/rustmotion-core/src/schema/background.rs +++ b/crates/rustmotion-core/src/schema/background.rs @@ -139,13 +139,19 @@ impl Serialize for AnimatedBackground { match &self.preset { BackgroundPreset::GradientShift(cfg) => map.serialize_entry("gradient_shift", cfg)?, BackgroundPreset::GridDots(cfg) => map.serialize_entry("grid_dots", cfg)?, - BackgroundPreset::ConcentricCircles(cfg) => map.serialize_entry("concentric_circles", cfg)?, + BackgroundPreset::ConcentricCircles(cfg) => { + map.serialize_entry("concentric_circles", cfg)? + } BackgroundPreset::Halo(cfg) => map.serialize_entry("halo", cfg)?, BackgroundPreset::Heropattern(cfg) => map.serialize_entry("heropattern", cfg)?, } map.serialize_entry("speed", &self.speed)?; - if self.x != 0.0 { map.serialize_entry("x", &self.x)?; } - if self.y != 0.0 { map.serialize_entry("y", &self.y)?; } + if self.x != 0.0 { + map.serialize_entry("x", &self.x)?; + } + if self.y != 0.0 { + map.serialize_entry("y", &self.y)?; + } if let Some(ref dir) = self.direction { map.serialize_entry("direction", dir)?; } @@ -165,13 +171,11 @@ impl<'de> Deserialize<'de> for AnimatedBackground { .get("direction") .and_then(|v| serde_json::from_value(v.clone()).ok()); - let preset_str = map - .get("preset") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let preset_str = map.get("preset").and_then(|v| v.as_str()).unwrap_or(""); // Detect new vs legacy format: new format has a sub-object keyed by preset name - let is_new_format = !preset_str.is_empty() && map.get(preset_str).map_or(false, |v| v.is_object()); + let is_new_format = + !preset_str.is_empty() && map.get(preset_str).is_some_and(|v| v.is_object()); let (preset, speed) = if is_new_format { // New format: config in sub-object @@ -179,24 +183,29 @@ impl<'de> Deserialize<'de> for AnimatedBackground { let speed = map.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32; let preset = match preset_str { "grid_dots" => { - let cfg: GridDotsConfig = serde_json::from_value(sub).map_err(serde::de::Error::custom)?; + let cfg: GridDotsConfig = + serde_json::from_value(sub).map_err(serde::de::Error::custom)?; BackgroundPreset::GridDots(cfg) } "concentric_circles" => { - let cfg: ConcentricCirclesConfig = serde_json::from_value(sub).map_err(serde::de::Error::custom)?; + let cfg: ConcentricCirclesConfig = + serde_json::from_value(sub).map_err(serde::de::Error::custom)?; BackgroundPreset::ConcentricCircles(cfg) } "halo" => { - let cfg: HaloConfig = serde_json::from_value(sub).map_err(serde::de::Error::custom)?; + let cfg: HaloConfig = + serde_json::from_value(sub).map_err(serde::de::Error::custom)?; BackgroundPreset::Halo(cfg) } "heropattern" => { - let cfg: HeropatternConfig = serde_json::from_value(sub).map_err(serde::de::Error::custom)?; + let cfg: HeropatternConfig = + serde_json::from_value(sub).map_err(serde::de::Error::custom)?; BackgroundPreset::Heropattern(cfg) } _ => { // gradient_shift or unknown → gradient_shift - let cfg: GradientShiftConfig = serde_json::from_value(sub).map_err(serde::de::Error::custom)?; + let cfg: GradientShiftConfig = + serde_json::from_value(sub).map_err(serde::de::Error::custom)?; BackgroundPreset::GradientShift(cfg) } }; @@ -206,50 +215,75 @@ impl<'de> Deserialize<'de> for AnimatedBackground { let legacy_speed = map.get("speed").and_then(|v| v.as_f64()).unwrap_or(30.0) as f32; let preset = match preset_str { "grid_dots" => { - let color = map.get("colors") + let color = map + .get("colors") .and_then(|v| v.as_array()) .and_then(|a| a.first()) .and_then(|v| v.as_str()) .unwrap_or("#FFFFFF15") .to_string(); - let element_size = map.get("element_size").and_then(|v| v.as_f64()).unwrap_or(4.0) as f32; - let spacing = map.get("spacing").and_then(|v| v.as_f64()).unwrap_or(60.0) as f32; - BackgroundPreset::GridDots(GridDotsConfig { color, element_size, spacing }) + let element_size = map + .get("element_size") + .and_then(|v| v.as_f64()) + .unwrap_or(4.0) as f32; + let spacing = + map.get("spacing").and_then(|v| v.as_f64()).unwrap_or(60.0) as f32; + BackgroundPreset::GridDots(GridDotsConfig { + color, + element_size, + spacing, + }) } "concentric_circles" => { - let color = map.get("colors") + let color = map + .get("colors") .and_then(|v| v.as_array()) .and_then(|a| a.first()) .and_then(|v| v.as_str()) .unwrap_or("#FFFFFF20") .to_string(); - let element_size = map.get("element_size").and_then(|v| v.as_f64()).unwrap_or(4.0) as f32; - let spacing = map.get("spacing").and_then(|v| v.as_f64()).unwrap_or(60.0) as f32; + let element_size = map + .get("element_size") + .and_then(|v| v.as_f64()) + .unwrap_or(4.0) as f32; + let spacing = + map.get("spacing").and_then(|v| v.as_f64()).unwrap_or(60.0) as f32; let count = map.get("count").and_then(|v| v.as_u64()).map(|n| n as u32); - BackgroundPreset::ConcentricCircles(ConcentricCirclesConfig { color, element_size, spacing, count }) + BackgroundPreset::ConcentricCircles(ConcentricCirclesConfig { + color, + element_size, + spacing, + count, + }) } "halo" => { - let zones: Vec = map.get("zones") + let zones: Vec = map + .get("zones") .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_default(); BackgroundPreset::Halo(HaloConfig { zones }) } _ => { // Default: gradient_shift - let colors: Vec = map.get("colors") + let colors: Vec = map + .get("colors") .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_default(); - let gradient_type: GradientType = map.get("gradient_type") + let gradient_type: GradientType = map + .get("gradient_type") .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_else(default_bg_type); - BackgroundPreset::GradientShift(GradientShiftConfig { colors, gradient_type }) + BackgroundPreset::GradientShift(GradientShiftConfig { + colors, + gradient_type, + }) } }; (preset, legacy_speed) }; // Infer legacy direction if not specified - let direction = direction.or_else(|| { + let direction = direction.or({ if speed > 0.0 && !is_new_format { match &preset { BackgroundPreset::GradientShift(_) => Some(ScrollDirection::Cw), @@ -261,7 +295,13 @@ impl<'de> Deserialize<'de> for AnimatedBackground { } }); - Ok(AnimatedBackground { preset, x, y, speed, direction }) + Ok(AnimatedBackground { + preset, + x, + y, + speed, + direction, + }) } } @@ -278,12 +318,30 @@ impl JsonSchema for AnimatedBackground { props.insert("x".to_string(), gen.subschema_for::()); props.insert("y".to_string(), gen.subschema_for::()); props.insert("speed".to_string(), gen.subschema_for::()); - props.insert("direction".to_string(), gen.subschema_for::>()); - props.insert("gradient_shift".to_string(), gen.subschema_for::>()); - props.insert("grid_dots".to_string(), gen.subschema_for::>()); - props.insert("concentric_circles".to_string(), gen.subschema_for::>()); - props.insert("halo".to_string(), gen.subschema_for::>()); - props.insert("heropattern".to_string(), gen.subschema_for::>()); + props.insert( + "direction".to_string(), + gen.subschema_for::>(), + ); + props.insert( + "gradient_shift".to_string(), + gen.subschema_for::>(), + ); + props.insert( + "grid_dots".to_string(), + gen.subschema_for::>(), + ); + props.insert( + "concentric_circles".to_string(), + gen.subschema_for::>(), + ); + props.insert( + "halo".to_string(), + gen.subschema_for::>(), + ); + props.insert( + "heropattern".to_string(), + gen.subschema_for::>(), + ); SchemaObject { instance_type: Some(InstanceType::Object.into()), @@ -343,7 +401,9 @@ pub enum BackgroundValue { impl Serialize for BackgroundValue { fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { + where + S: serde::Serializer, + { match self { BackgroundValue::Color(s) => serializer.serialize_str(s), BackgroundValue::Single(entry) => entry.serialize(serializer), @@ -387,7 +447,9 @@ fn default_bg_type() -> GradientType { } /// Deserialize `animated-background` as either a single AnimatedBackground or a Vec. -pub(crate) fn deserialize_animated_backgrounds<'de, D>(deserializer: D) -> Result, D::Error> +pub(crate) fn deserialize_animated_backgrounds<'de, D>( + deserializer: D, +) -> Result, D::Error> where D: serde::Deserializer<'de>, { @@ -413,8 +475,7 @@ where where M: de::MapAccess<'de>, { - let bg = - AnimatedBackground::deserialize(de::value::MapAccessDeserializer::new(map))?; + let bg = AnimatedBackground::deserialize(de::value::MapAccessDeserializer::new(map))?; Ok(vec![bg]) } } @@ -423,7 +484,9 @@ where } /// Deserialize `background` as a color string, a single BackgroundEntry object, or an array. -pub(crate) fn deserialize_background_value<'de, D>(deserializer: D) -> Result, D::Error> +pub(crate) fn deserialize_background_value<'de, D>( + deserializer: D, +) -> Result, D::Error> where D: serde::Deserializer<'de>, { @@ -439,22 +502,30 @@ where } fn visit_none(self) -> Result - where E: de::Error { + where + E: de::Error, + { Ok(None) } fn visit_unit(self) -> Result - where E: de::Error { + where + E: de::Error, + { Ok(None) } fn visit_str(self, v: &str) -> Result - where E: de::Error { + where + E: de::Error, + { Ok(Some(BackgroundValue::Color(v.to_string()))) } fn visit_string(self, v: String) -> Result - where E: de::Error { + where + E: de::Error, + { Ok(Some(BackgroundValue::Color(v))) } @@ -470,7 +541,8 @@ where where A: de::SeqAccess<'de>, { - let entries = Vec::::deserialize(de::value::SeqAccessDeserializer::new(seq))?; + let entries = + Vec::::deserialize(de::value::SeqAccessDeserializer::new(seq))?; Ok(Some(BackgroundValue::Multiple(entries))) } } diff --git a/crates/rustmotion-core/src/schema/scenario.rs b/crates/rustmotion-core/src/schema/scenario.rs index ad6c447..139394f 100644 --- a/crates/rustmotion-core/src/schema/scenario.rs +++ b/crates/rustmotion-core/src/schema/scenario.rs @@ -61,17 +61,13 @@ pub struct Scenario { /// Lifecycle of a studio annotation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum AnnotationStatus { + #[default] Open, Resolved, } -impl Default for AnnotationStatus { - fn default() -> Self { - AnnotationStatus::Open - } -} - /// What an annotation points at: a JSON Pointer into the source scenario, /// plus optional context captured at click time. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -136,8 +132,11 @@ pub struct View { #[schemars(skip)] pub background: Option, /// (world) Legacy shared animated backgrounds. - #[serde(default, rename = "animated-background", - deserialize_with = "deserialize_animated_backgrounds")] + #[serde( + default, + rename = "animated-background", + deserialize_with = "deserialize_animated_backgrounds" + )] pub animated_background: Vec, /// (world) Easing for camera pan between scenes. #[serde(default = "default_transition_easing")] @@ -184,6 +183,7 @@ pub struct ResolvedView { /// An entry in the `scenes` array: either a concrete scene or an include directive. #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(untagged)] +#[allow(clippy::large_enum_variant)] // untagged serde enum; boxing Scene would break all match arms pub enum SceneEntry { /// A regular scene defined inline. Scene(Scene), @@ -280,7 +280,11 @@ pub struct Scene { #[serde(default)] pub layout: Option, /// Legacy animated background (kept for backward compat) - #[serde(default, rename = "animated-background", deserialize_with = "deserialize_animated_backgrounds")] + #[serde( + default, + rename = "animated-background", + deserialize_with = "deserialize_animated_backgrounds" + )] pub animated_background: Vec, /// Virtual camera with animatable x, y, zoom, rotation. #[serde(default)] @@ -396,19 +400,15 @@ pub(crate) fn default_transition_easing() -> EasingType { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum VideoCodec { + #[default] H264, H265, Vp9, Prores, } -impl Default for VideoCodec { - fn default() -> Self { - Self::H264 - } -} - // --- Default functions --- fn default_version() -> String { diff --git a/crates/rustmotion-core/src/schema/style.rs b/crates/rustmotion-core/src/schema/style.rs index 6f41e0e..62a92a8 100644 --- a/crates/rustmotion-core/src/schema/style.rs +++ b/crates/rustmotion-core/src/schema/style.rs @@ -10,23 +10,21 @@ use super::video::{ #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum CardDirection { + #[default] Column, Row, ColumnReverse, RowReverse, } -impl Default for CardDirection { - fn default() -> Self { - Self::Column - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum CardAlign { #[serde(alias = "flex-start", alias = "flex_start")] + #[default] Start, Center, #[serde(alias = "flex-end", alias = "flex_end")] @@ -34,16 +32,12 @@ pub enum CardAlign { Stretch, } -impl Default for CardAlign { - fn default() -> Self { - Self::Start - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum CardJustify { #[serde(alias = "flex-start", alias = "flex_start")] + #[default] Start, Center, #[serde(alias = "flex-end", alias = "flex_end")] @@ -56,12 +50,6 @@ pub enum CardJustify { SpaceEvenly, } -impl Default for CardJustify { - fn default() -> Self { - Self::Start - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(untagged)] pub enum Spacing { @@ -90,17 +78,13 @@ impl Spacing { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum CardDisplay { + #[default] Flex, Grid, } -impl Default for CardDisplay { - fn default() -> Self { - Self::Flex - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GridTrack { @@ -129,19 +113,14 @@ pub struct TimelineStep { } /// Font weight — named ("normal"/"bold") or numeric (100-900) -#[derive(Debug, Clone, JsonSchema)] +#[derive(Debug, Clone, JsonSchema, Default)] pub enum FontWeight { + #[default] Normal, Bold, Weight(u16), } -impl Default for FontWeight { - fn default() -> Self { - Self::Normal - } -} - #[allow(dead_code)] impl FontWeight { pub fn to_skia_weight(&self) -> i32 { @@ -194,63 +173,47 @@ impl<'de> Deserialize<'de> for FontWeight { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum FontStyleType { + #[default] Normal, Italic, Oblique, } -impl Default for FontStyleType { - fn default() -> Self { - Self::Normal - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum TextAlign { + #[default] Left, Center, Right, } -impl Default for TextAlign { - fn default() -> Self { - Self::Left - } -} - #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum VerticalAlign { Top, + #[default] Middle, Bottom, } -impl Default for VerticalAlign { - fn default() -> Self { - Self::Middle - } -} - /// CSS-like overflow: controls whether children that exceed this container's /// bounds are clipped (`hidden`) or allowed to bleed out (`visible`, default). /// Validation only fails when content escapes the *viewport*; escaping a /// `visible` parent is legitimate (e.g. a badge sticking out of a card). #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum Overflow { + #[default] Visible, Hidden, } -impl Default for Overflow { - fn default() -> Self { - Self::Visible - } -} - /// Size dimension: fixed px, "auto", or "50%" (percent of parent) #[derive(Debug, Clone, JsonSchema)] pub enum SizeDimension { @@ -307,7 +270,9 @@ impl<'de> Deserialize<'de> for SizeDimension { } /// Deserialize `animation` as either a single AnimationEffect or a Vec. -pub fn deserialize_animation_effects<'de, D>(deserializer: D) -> Result, D::Error> +pub fn deserialize_animation_effects<'de, D>( + deserializer: D, +) -> Result, D::Error> where D: serde::Deserializer<'de>, { @@ -333,8 +298,7 @@ where where M: de::MapAccess<'de>, { - let effect = - AnimationEffect::deserialize(de::value::MapAccessDeserializer::new(map))?; + let effect = AnimationEffect::deserialize(de::value::MapAccessDeserializer::new(map))?; Ok(vec![effect]) } } diff --git a/crates/rustmotion-core/src/schema/video.rs b/crates/rustmotion-core/src/schema/video.rs index 4b7c96e..c6b0319 100644 --- a/crates/rustmotion-core/src/schema/video.rs +++ b/crates/rustmotion-core/src/schema/video.rs @@ -131,8 +131,7 @@ impl AnimationEffect { } /// Timing configuration for preset animations. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct AnimationTiming { /// Delay before animation starts (seconds). #[serde(default)] @@ -187,8 +186,7 @@ impl Default for AnimationTiming { } /// Timing configuration for char animation effect variants (used inside style.animation). -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct CharAnimationTiming { /// Delay before animation starts (seconds). #[serde(default)] @@ -219,8 +217,7 @@ fn default_char_duration_f64() -> f64 { } /// Per-character or per-word text animation configuration (legacy root-level prop). -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct CharAnimation { /// Animation preset: "scale_in", "fade_in", "wave", "bounce", "rotate_in", "slide_up". #[serde(default = "default_char_preset")] @@ -298,8 +295,7 @@ impl AnimationTiming { } /// Custom keyframe animations configuration. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct KeyframesConfig { pub keyframes: Vec, #[serde(default)] @@ -311,8 +307,7 @@ pub struct KeyframesConfig { } /// Motion blur configuration. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct MotionBlurConfig { #[serde(default)] pub intensity: f32, @@ -322,8 +317,7 @@ pub struct MotionBlurConfig { /// Configuration for a 3D orbit/floating animation effect. /// Creates circular or elliptical motion with pseudo-depth (scale + opacity modulation). -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct OrbitConfig { /// Horizontal radius of the orbit in pixels. #[serde(default = "default_orbit_radius")] @@ -351,14 +345,19 @@ pub struct OrbitConfig { pub phase: f64, } -fn default_orbit_radius() -> f64 { 30.0 } -fn default_orbit_speed() -> f64 { 0.5 } -fn default_orbit_depth() -> f64 { 0.15 } +fn default_orbit_radius() -> f64 { + 30.0 +} +fn default_orbit_speed() -> f64 { + 0.5 +} +fn default_orbit_depth() -> f64 { + 0.15 +} // --- Wiggle Config --- -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct WiggleConfig { pub property: String, pub amplitude: f64, @@ -456,18 +455,14 @@ pub struct Stroke { #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum ImageFit { Cover, + #[default] Contain, Fill, } -impl Default for ImageFit { - fn default() -> Self { - Self::Contain - } -} - // --- Shape Text --- #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -502,18 +497,14 @@ pub struct CaptionWord { #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum CaptionStyle { + #[default] Highlight, Karaoke, WordByWord, } -impl Default for CaptionStyle { - fn default() -> Self { - Self::Highlight - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct CardBorder { pub color: String, @@ -623,8 +614,7 @@ fn default_drop_shadow_color() -> String { } /// Glow effect (colored luminous halo around the element) -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct GlowConfig { /// Glow color (hex string, e.g. "#5C39EE") #[serde(default = "default_glow_color")] @@ -678,7 +668,6 @@ pub struct TextGradient { pub angle: Option, } - // --- Default functions --- fn default_font_size() -> f32 { diff --git a/crates/rustmotion-core/src/traits/bordered.rs b/crates/rustmotion-core/src/traits/bordered.rs index 6354219..025387f 100644 --- a/crates/rustmotion-core/src/traits/bordered.rs +++ b/crates/rustmotion-core/src/traits/bordered.rs @@ -25,7 +25,10 @@ pub trait BorderedMut: Bordered { /// Builder API for border. pub trait BorderedExt: BorderedMut + Sized { fn border_color(mut self, color: impl Into, width: f32) -> Self { - self.set_border(Some(Border { color: color.into(), width })); + self.set_border(Some(Border { + color: color.into(), + width, + })); self } diff --git a/crates/rustmotion-core/src/traits/mod.rs b/crates/rustmotion-core/src/traits/mod.rs index 940020b..8ae391c 100644 --- a/crates/rustmotion-core/src/traits/mod.rs +++ b/crates/rustmotion-core/src/traits/mod.rs @@ -9,6 +9,8 @@ pub mod clipped; #[allow(dead_code)] pub mod container; #[allow(dead_code)] +pub mod painter; +#[allow(dead_code)] pub mod rounded; #[allow(dead_code)] pub mod shadowed; @@ -16,16 +18,14 @@ pub mod shadowed; pub mod styled; #[allow(dead_code)] pub mod timed; -#[allow(dead_code)] -pub mod painter; pub use animatable::Animatable; pub use backgrounded::{Backgrounded, BackgroundedMut}; pub use bordered::{Border, Bordered, BorderedMut}; pub use clipped::Clipped; pub use container::{Align, Direction, GridTrack, Justify}; +pub use painter::{AvailableSize, MeasureCtx, PaintCtx, Painter}; pub use rounded::{Rounded, RoundedMut}; pub use shadowed::{Shadow, Shadowed, ShadowedMut}; pub use styled::{Styled, StyledMut}; pub use timed::{Timed, TimingConfig}; -pub use painter::{AvailableSize, MeasureCtx, PaintCtx, Painter}; diff --git a/crates/rustmotion-core/src/traits/painter.rs b/crates/rustmotion-core/src/traits/painter.rs index 687cdca..f1d5f8d 100644 --- a/crates/rustmotion-core/src/traits/painter.rs +++ b/crates/rustmotion-core/src/traits/painter.rs @@ -62,11 +62,7 @@ pub trait Painter { /// Optional intrinsic measurement for leaves like `text`, `image`, /// `codeblock`. Return `None` to let taffy compute the size from the /// CSS style alone. `available` mirrors taffy's `AvailableSpace`. - fn intrinsic_size( - &self, - _available: AvailableSize, - _ctx: &MeasureCtx, - ) -> Option<(f32, f32)> { + fn intrinsic_size(&self, _available: AvailableSize, _ctx: &MeasureCtx) -> Option<(f32, f32)> { None } } diff --git a/crates/rustmotion-core/src/traits/rounded.rs b/crates/rustmotion-core/src/traits/rounded.rs index 918c10c..ee66672 100644 --- a/crates/rustmotion-core/src/traits/rounded.rs +++ b/crates/rustmotion-core/src/traits/rounded.rs @@ -15,13 +15,27 @@ pub trait RoundedExt: RoundedMut + Sized { self } - fn rounded_none(self) -> Self { self.rounded(0.0) } - fn rounded_sm(self) -> Self { self.rounded(4.0) } - fn rounded_md(self) -> Self { self.rounded(8.0) } - fn rounded_lg(self) -> Self { self.rounded(12.0) } - fn rounded_xl(self) -> Self { self.rounded(16.0) } - fn rounded_2xl(self) -> Self { self.rounded(24.0) } - fn rounded_full(self) -> Self { self.rounded(9999.0) } + fn rounded_none(self) -> Self { + self.rounded(0.0) + } + fn rounded_sm(self) -> Self { + self.rounded(4.0) + } + fn rounded_md(self) -> Self { + self.rounded(8.0) + } + fn rounded_lg(self) -> Self { + self.rounded(12.0) + } + fn rounded_xl(self) -> Self { + self.rounded(16.0) + } + fn rounded_2xl(self) -> Self { + self.rounded(24.0) + } + fn rounded_full(self) -> Self { + self.rounded(9999.0) + } } impl RoundedExt for T {} diff --git a/crates/rustmotion-core/src/traits/shadowed.rs b/crates/rustmotion-core/src/traits/shadowed.rs index d4f8487..81fb64c 100644 --- a/crates/rustmotion-core/src/traits/shadowed.rs +++ b/crates/rustmotion-core/src/traits/shadowed.rs @@ -27,7 +27,9 @@ pub trait ShadowedExt: ShadowedMut + Sized { fn shadow_sm(mut self) -> Self { self.set_shadow(Some(Shadow { color: "rgba(0,0,0,0.1)".into(), - offset_x: 0.0, offset_y: 1.0, blur: 2.0, + offset_x: 0.0, + offset_y: 1.0, + blur: 2.0, })); self } @@ -35,7 +37,9 @@ pub trait ShadowedExt: ShadowedMut + Sized { fn shadow_md(mut self) -> Self { self.set_shadow(Some(Shadow { color: "rgba(0,0,0,0.15)".into(), - offset_x: 0.0, offset_y: 4.0, blur: 6.0, + offset_x: 0.0, + offset_y: 4.0, + blur: 6.0, })); self } @@ -43,7 +47,9 @@ pub trait ShadowedExt: ShadowedMut + Sized { fn shadow_lg(mut self) -> Self { self.set_shadow(Some(Shadow { color: "rgba(0,0,0,0.2)".into(), - offset_x: 0.0, offset_y: 8.0, blur: 16.0, + offset_x: 0.0, + offset_y: 8.0, + blur: 16.0, })); self } @@ -51,7 +57,9 @@ pub trait ShadowedExt: ShadowedMut + Sized { fn shadow_xl(mut self) -> Self { self.set_shadow(Some(Shadow { color: "rgba(0,0,0,0.25)".into(), - offset_x: 0.0, offset_y: 16.0, blur: 32.0, + offset_x: 0.0, + offset_y: 16.0, + blur: 32.0, })); self } @@ -61,10 +69,18 @@ pub trait ShadowedExt: ShadowedMut + Sized { self } - fn shadow_custom(mut self, color: impl Into, offset_x: f32, offset_y: f32, blur: f32) -> Self { + fn shadow_custom( + mut self, + color: impl Into, + offset_x: f32, + offset_y: f32, + blur: f32, + ) -> Self { self.set_shadow(Some(Shadow { color: color.into(), - offset_x, offset_y, blur, + offset_x, + offset_y, + blur, })); self } diff --git a/crates/rustmotion-core/src/variables.rs b/crates/rustmotion-core/src/variables.rs index 6d88c59..c8e7c86 100644 --- a/crates/rustmotion-core/src/variables.rs +++ b/crates/rustmotion-core/src/variables.rs @@ -27,8 +27,7 @@ fn merge_variables( return Err(RustmotionError::UndefinedVariable { name: name.clone(), path: path.to_string(), - } - .into()); + }); } merged.insert(name.clone(), value.clone()); } @@ -114,11 +113,7 @@ fn parse_single_var_ref(s: &str) -> Option<&str> { /// Perform string interpolation: replace $name occurrences within a larger string. /// Handles $$ escape sequences. -fn interpolate_string( - s: &str, - vars: &HashMap, - path: &str, -) -> Result { +fn interpolate_string(s: &str, vars: &HashMap, path: &str) -> Result { let mut result = String::with_capacity(s.len()); let mut chars = s.chars().peekable(); @@ -151,8 +146,7 @@ fn interpolate_string( return Err(RustmotionError::VariableInterpolationTypeError { name, path: path.to_string(), - } - .into()); + }); } } } else { @@ -243,8 +237,7 @@ pub fn apply_variables( return Err(RustmotionError::VariableMissingDefault { name: name.clone(), path: path.to_string(), - } - .into()); + }); } } @@ -261,15 +254,12 @@ pub fn apply_variables( return Err(RustmotionError::UnresolvedVariable { name, path: path.to_string(), - } - .into()); + }); } Ok(()) } - None => { - Ok(()) - } + None => Ok(()), } } @@ -427,19 +417,13 @@ mod tests { vars.insert("name".to_string(), json!("resolved")); substitute(&mut val, &vars, "test").unwrap(); // "config" block should be untouched - assert_eq!( - val["config"]["name"]["default"], - json!("$not_a_ref") - ); + assert_eq!(val["config"]["name"]["default"], json!("$not_a_ref")); assert_eq!(val["text"], json!("resolved")); } #[test] fn test_recursive_array_substitution() { - let mut val = json!([ - "$a", - ["$b", "$c"] - ]); + let mut val = json!(["$a", ["$b", "$c"]]); let mut vars = HashMap::new(); vars.insert("a".to_string(), json!(1)); vars.insert("b".to_string(), json!(2)); diff --git a/crates/rustmotion-html/src/lib.rs b/crates/rustmotion-html/src/lib.rs index 373f179..dc8272b 100644 --- a/crates/rustmotion-html/src/lib.rs +++ b/crates/rustmotion-html/src/lib.rs @@ -144,7 +144,10 @@ fn set_text(handle: &Handle, text: &str) -> Option<()> { /// Numeric segments of a JSON pointer, e.g. `/scenes/0/children/2` → `[0, 2]`. /// The first is the scene index; the rest walk content-node children. fn parse_indices(pointer: &str) -> Vec { - pointer.split('/').filter_map(|s| s.parse::().ok()).collect() + pointer + .split('/') + .filter_map(|s| s.parse::().ok()) + .collect() } /// Walk from `` to the element a pointer addresses, mirroring the @@ -252,11 +255,15 @@ mod lib_tests { #[test] fn set_inline_style_updates_property() { let html = r##"

Hi

"##; - let out = crate::set_inline_style(html, "/scenes/0/children/0", "font-size", "120").unwrap(); + let out = + crate::set_inline_style(html, "/scenes/0/children/0", "font-size", "120").unwrap(); assert!(out.contains("font-size:120"), "got: {out}"); assert!(out.contains("color:#fff"), "kept other props: {out}"); let v = crate::html_to_scenario_value(&out).unwrap(); - assert_eq!(v["scenes"][0]["children"][0]["style"]["font-size"], json!(120)); + assert_eq!( + v["scenes"][0]["children"][0]["style"]["font-size"], + json!(120) + ); } #[test] @@ -279,13 +286,17 @@ mod lib_tests { let v = crate::html_to_scenario_value(&out).unwrap(); assert_eq!(v["scenes"][0]["children"][0]["content"], json!("Bonjour")); // The style attribute is preserved. - assert_eq!(v["scenes"][0]["children"][0]["style"]["font-size"], json!(96)); + assert_eq!( + v["scenes"][0]["children"][0]["style"]["font-size"], + json!(96) + ); } #[test] fn set_inline_style_inserts_when_absent() { let html = r##"

Hi

"##; - let out = crate::set_inline_style(html, "/scenes/0/children/0", "color", "#ff0000").unwrap(); + let out = + crate::set_inline_style(html, "/scenes/0/children/0", "color", "#ff0000").unwrap(); assert!(out.contains("color:#ff0000"), "got: {out}"); } diff --git a/crates/rustmotion-studio/src/app/mod.rs b/crates/rustmotion-studio/src/app/mod.rs index a99875c..5237a2f 100644 --- a/crates/rustmotion-studio/src/app/mod.rs +++ b/crates/rustmotion-studio/src/app/mod.rs @@ -78,7 +78,8 @@ pub fn run_preview_root( initial_error, input_path.clone(), ))); - let library: SharedLibrary = Arc::new(Mutex::new(LibraryState::new(workspace, start_in_editor))); + let library: SharedLibrary = + Arc::new(Mutex::new(LibraryState::new(workspace, start_in_editor))); if watch { let tx = spawn_watcher(shared.clone()); diff --git a/crates/rustmotion-studio/src/components/button/component.rs b/crates/rustmotion-studio/src/components/button/component.rs index 961ada2..9579fb6 100644 --- a/crates/rustmotion-studio/src/components/button/component.rs +++ b/crates/rustmotion-studio/src/components/button/component.rs @@ -1,3 +1,5 @@ +// UI kit scaffold — variants are consumed incrementally as the studio grows. +#![allow(dead_code)] use dioxus::prelude::*; use dioxus_primitives::dioxus_attributes::attributes; use dioxus_primitives::merge_attributes; diff --git a/crates/rustmotion-studio/src/components/button/mod.rs b/crates/rustmotion-studio/src/components/button/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/button/mod.rs +++ b/crates/rustmotion-studio/src/components/button/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/color_picker/component.rs b/crates/rustmotion-studio/src/components/color_picker/component.rs index 9bf0750..446e58e 100644 --- a/crates/rustmotion-studio/src/components/color_picker/component.rs +++ b/crates/rustmotion-studio/src/components/color_picker/component.rs @@ -1,11 +1,9 @@ use dioxus::prelude::*; -use dioxus_primitives::color_picker::{ - self, Color, ColorAreaProps, ColorPickerContext, -}; -use dioxus_primitives::popover; -use dioxus_primitives::use_controlled; +use dioxus_primitives::color_picker::{self, Color, ColorAreaProps, ColorPickerContext}; use dioxus_primitives::label::Label; +use dioxus_primitives::popover; use dioxus_primitives::slider::*; +use dioxus_primitives::use_controlled; use palette::{encoding, FromColor, Hsv, IntoColor, RgbHue, Srgb}; use crate::components::input::Input; diff --git a/crates/rustmotion-studio/src/components/color_picker/mod.rs b/crates/rustmotion-studio/src/components/color_picker/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/color_picker/mod.rs +++ b/crates/rustmotion-studio/src/components/color_picker/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/input/mod.rs b/crates/rustmotion-studio/src/components/input/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/input/mod.rs +++ b/crates/rustmotion-studio/src/components/input/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/mod.rs b/crates/rustmotion-studio/src/components/mod.rs index c211df9..90e2d68 100644 --- a/crates/rustmotion-studio/src/components/mod.rs +++ b/crates/rustmotion-studio/src/components/mod.rs @@ -1,14 +1,14 @@ // AUTOGENERATED Components module -pub mod skeleton; -pub mod sidebar; -pub mod sheet; pub mod button; -pub mod separator; -pub mod tooltip; -pub mod select; -pub mod switch; -pub mod popover; pub mod color_picker; -pub mod slider; pub mod input; +pub mod popover; +pub mod select; +pub mod separator; +pub mod sheet; +pub mod sidebar; +pub mod skeleton; +pub mod slider; +pub mod switch; pub mod textarea; +pub mod tooltip; diff --git a/crates/rustmotion-studio/src/components/popover/mod.rs b/crates/rustmotion-studio/src/components/popover/mod.rs index 9a8ae55..f189b1f 100644 --- a/crates/rustmotion-studio/src/components/popover/mod.rs +++ b/crates/rustmotion-studio/src/components/popover/mod.rs @@ -1,2 +1 @@ mod component; -pub use component::*; \ No newline at end of file diff --git a/crates/rustmotion-studio/src/components/select/component.rs b/crates/rustmotion-studio/src/components/select/component.rs index 36f4b70..b6413a9 100644 --- a/crates/rustmotion-studio/src/components/select/component.rs +++ b/crates/rustmotion-studio/src/components/select/component.rs @@ -5,8 +5,6 @@ use dioxus_primitives::select::{ }; use dioxus_primitives::{dioxus_attributes::attributes, merge_attributes}; -pub use dioxus_primitives::select::SelectGroup; - #[component] pub fn Select(props: SelectProps) -> Element { let base = attributes!(div { class: "dx-select" }); @@ -79,7 +77,9 @@ pub fn SelectMulti(props: SelectMultiProps) - #[component] pub fn SelectGroupLabel(props: SelectGroupLabelProps) -> Element { - let base = attributes!(div { class: "dx-select-group-label" }); + let base = attributes!(div { + class: "dx-select-group-label" + }); let merged = merge_attributes(vec![base, props.attributes]); rsx! { @@ -93,7 +93,9 @@ pub fn SelectGroupLabel(props: SelectGroupLabelProps) -> Element { #[component] pub fn SelectOption(props: SelectOptionProps) -> Element { - let base = attributes!(div { class: "dx-select-option" }); + let base = attributes!(div { + class: "dx-select-option" + }); let merged = merge_attributes(vec![base, props.attributes]); rsx! { diff --git a/crates/rustmotion-studio/src/components/select/mod.rs b/crates/rustmotion-studio/src/components/select/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/select/mod.rs +++ b/crates/rustmotion-studio/src/components/select/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/separator/mod.rs b/crates/rustmotion-studio/src/components/separator/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/separator/mod.rs +++ b/crates/rustmotion-studio/src/components/separator/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/sheet/component.rs b/crates/rustmotion-studio/src/components/sheet/component.rs index d7b383f..8c35e84 100644 --- a/crates/rustmotion-studio/src/components/sheet/component.rs +++ b/crates/rustmotion-studio/src/components/sheet/component.rs @@ -1,9 +1,11 @@ +// UI kit scaffold — variants are consumed incrementally as the studio grows. +#![allow(dead_code)] use dioxus::prelude::*; use dioxus_icons::lucide::X; -use dioxus_primitives::dioxus_attributes::attributes; use dioxus_primitives::dialog::{ self, DialogCtx, DialogDescriptionProps, DialogRootProps, DialogTitleProps, }; +use dioxus_primitives::dioxus_attributes::attributes; use dioxus_primitives::merge_attributes; #[css_module("/src/components/sheet/style.css")] @@ -57,7 +59,9 @@ pub fn Sheet(props: DialogRootProps) -> Element { } #[component] -pub fn SheetContentClose(#[props(extends = GlobalAttributes)] attributes: Vec) -> Element { +pub fn SheetContentClose( + #[props(extends = GlobalAttributes)] attributes: Vec, +) -> Element { let base = attributes!(button { class: Styles::dx_sheet_close, }); diff --git a/crates/rustmotion-studio/src/components/sidebar/component.rs b/crates/rustmotion-studio/src/components/sidebar/component.rs index 365c115..55aa3c1 100644 --- a/crates/rustmotion-studio/src/components/sidebar/component.rs +++ b/crates/rustmotion-studio/src/components/sidebar/component.rs @@ -1,3 +1,5 @@ +// UI kit scaffold — variants are consumed incrementally as the studio grows. +#![allow(dead_code)] use crate::components::button::{Button, ButtonVariant}; use crate::components::separator::Separator; use crate::components::sheet::{ diff --git a/crates/rustmotion-studio/src/components/sidebar/mod.rs b/crates/rustmotion-studio/src/components/sidebar/mod.rs index 2590c01..f189b1f 100644 --- a/crates/rustmotion-studio/src/components/sidebar/mod.rs +++ b/crates/rustmotion-studio/src/components/sidebar/mod.rs @@ -1,2 +1 @@ mod component; -pub use component::*; diff --git a/crates/rustmotion-studio/src/components/skeleton/mod.rs b/crates/rustmotion-studio/src/components/skeleton/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/skeleton/mod.rs +++ b/crates/rustmotion-studio/src/components/skeleton/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/slider/mod.rs b/crates/rustmotion-studio/src/components/slider/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/slider/mod.rs +++ b/crates/rustmotion-studio/src/components/slider/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/switch/mod.rs b/crates/rustmotion-studio/src/components/switch/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/switch/mod.rs +++ b/crates/rustmotion-studio/src/components/switch/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/components/textarea/component.rs b/crates/rustmotion-studio/src/components/textarea/component.rs index 56021b0..9df4719 100644 --- a/crates/rustmotion-studio/src/components/textarea/component.rs +++ b/crates/rustmotion-studio/src/components/textarea/component.rs @@ -1,3 +1,5 @@ +// UI kit scaffold — variants are consumed incrementally as the studio grows. +#![allow(dead_code)] use dioxus::prelude::*; #[css_module("/src/components/textarea/style.css")] diff --git a/crates/rustmotion-studio/src/components/tooltip/mod.rs b/crates/rustmotion-studio/src/components/tooltip/mod.rs index 9a8ae55..2590c01 100644 --- a/crates/rustmotion-studio/src/components/tooltip/mod.rs +++ b/crates/rustmotion-studio/src/components/tooltip/mod.rs @@ -1,2 +1,2 @@ mod component; -pub use component::*; \ No newline at end of file +pub use component::*; diff --git a/crates/rustmotion-studio/src/editor/frames.rs b/crates/rustmotion-studio/src/editor/frames.rs index a53848a..089a803 100644 --- a/crates/rustmotion-studio/src/editor/frames.rs +++ b/crates/rustmotion-studio/src/editor/frames.rs @@ -23,7 +23,10 @@ pub fn render_frame( let rgb = image::DynamicImage::ImageRgba8(rgba_img).to_rgb8(); let mut jpeg = Vec::new(); image::DynamicImage::ImageRgb8(rgb) - .write_to(&mut std::io::Cursor::new(&mut jpeg), image::ImageFormat::Jpeg) + .write_to( + &mut std::io::Cursor::new(&mut jpeg), + image::ImageFormat::Jpeg, + ) .expect("encode jpeg"); jpeg } @@ -83,7 +86,12 @@ pub fn scene_prefix( return String::new(); } let idx = (frame as usize).min(tasks.len() - 1); - if let FrameTask::Normal { view_idx, scene_idx, .. } = &tasks[idx] { + if let FrameTask::Normal { + view_idx, + scene_idx, + .. + } = &tasks[idx] + { if raw.get("composition").is_some() { format!("/composition/{view_idx}/scenes/{scene_idx}") } else { @@ -98,8 +106,7 @@ pub fn scene_prefix( mod tests { use super::*; - const SCENARIO: &str = - r##"{ "video": { "width": 1280, "height": 720, "background": "#101418" }, "scenes": [ { "duration": 1.0 } ] }"##; + const SCENARIO: &str = r##"{ "video": { "width": 1280, "height": 720, "background": "#101418" }, "scenes": [ { "duration": 1.0 } ] }"##; #[test] fn renders_frame_to_nonempty_jpeg() { @@ -119,9 +126,14 @@ mod tests { let tasks = rustmotion::encode::build_frame_tasks(&scenario); let hits = frame_hits(&scenario, &tasks, 0, "/scenes/0"); let text = hits.iter().find(|h| h.kind == "text").expect("text hit"); - assert!(hits.iter().all(|h| h.x >= 0.0 && h.x <= 100.0 && h.w <= 100.0)); + assert!(hits + .iter() + .all(|h| h.x >= 0.0 && h.x <= 100.0 && h.w <= 100.0)); assert!( - text.pointer.as_deref().unwrap().starts_with("/scenes/0/children/"), + text.pointer + .as_deref() + .unwrap() + .starts_with("/scenes/0/children/"), "pointer = {:?}", text.pointer ); @@ -129,7 +141,8 @@ mod tests { #[test] fn scene_prefix_handles_top_level_scenes() { - let json = r##"{ "video": { "width": 1, "height": 1 }, "scenes": [ { "duration": 1.0 } ] }"##; + let json = + r##"{ "video": { "width": 1, "height": 1 }, "scenes": [ { "duration": 1.0 } ] }"##; let scenario = rustmotion::loader::load_scenario_from_source(None, Some(json)).unwrap(); let raw: serde_json::Value = serde_json::from_str(json).unwrap(); let tasks = rustmotion::encode::build_frame_tasks(&scenario); diff --git a/crates/rustmotion-studio/src/editor/inspector.rs b/crates/rustmotion-studio/src/editor/inspector.rs index 0b6db87..7bc2f2a 100644 --- a/crates/rustmotion-studio/src/editor/inspector.rs +++ b/crates/rustmotion-studio/src/editor/inspector.rs @@ -78,80 +78,306 @@ const WEIGHTS: &[(&str, &str)] = &[ // ── Field lists ──────────────────────────────────────────────────────────── const F_TYPO: &[Field] = &[ - Field { name: "font-size", label: "Size", ctrl: Ctrl::UnitSlider { min: 8.0, max: 200.0, step: 1.0, unit: "PX" } }, - Field { name: "font-weight", label: "Weight", ctrl: Ctrl::Weight }, - Field { name: "style", label: "Style", ctrl: Ctrl::StyleToggles }, - Field { name: "line-height", label: "Line height", ctrl: Ctrl::UnitSlider { min: 0.8, max: 3.0, step: 0.1, unit: "" } }, - Field { name: "letter-spacing", label: "Tracking", ctrl: Ctrl::UnitSlider { min: -5.0, max: 20.0, step: 0.5, unit: "PX" } }, - Field { name: "text-align", label: "Align", ctrl: Ctrl::Align }, + Field { + name: "font-size", + label: "Size", + ctrl: Ctrl::UnitSlider { + min: 8.0, + max: 200.0, + step: 1.0, + unit: "PX", + }, + }, + Field { + name: "font-weight", + label: "Weight", + ctrl: Ctrl::Weight, + }, + Field { + name: "style", + label: "Style", + ctrl: Ctrl::StyleToggles, + }, + Field { + name: "line-height", + label: "Line height", + ctrl: Ctrl::UnitSlider { + min: 0.8, + max: 3.0, + step: 0.1, + unit: "", + }, + }, + Field { + name: "letter-spacing", + label: "Tracking", + ctrl: Ctrl::UnitSlider { + min: -5.0, + max: 20.0, + step: 0.5, + unit: "PX", + }, + }, + Field { + name: "text-align", + label: "Align", + ctrl: Ctrl::Align, + }, ]; const F_COLOR: &[Field] = &[ - Field { name: "color", label: "Text", ctrl: Ctrl::Color { clearable: false } }, - Field { name: "background", label: "Background", ctrl: Ctrl::Color { clearable: true } }, + Field { + name: "color", + label: "Text", + ctrl: Ctrl::Color { clearable: false }, + }, + Field { + name: "background", + label: "Background", + ctrl: Ctrl::Color { clearable: true }, + }, ]; const F_LAYOUT: &[Field] = &[ - Field { name: "display", label: "Display", ctrl: Ctrl::Select(&["block", "flex", "grid", "inline-block", "none", "contents"]) }, - Field { name: "position", label: "Position", ctrl: Ctrl::Select(&["static", "relative", "absolute"]) }, - Field { name: "top", label: "Top", ctrl: Ctrl::Text }, - Field { name: "right", label: "Right", ctrl: Ctrl::Text }, - Field { name: "bottom", label: "Bottom", ctrl: Ctrl::Text }, - Field { name: "left", label: "Left", ctrl: Ctrl::Text }, - Field { name: "z-index", label: "Z-index", ctrl: Ctrl::Number }, - Field { name: "overflow", label: "Overflow", ctrl: Ctrl::Select(&["visible", "hidden", "auto", "scroll", "clip"]) }, - Field { name: "visibility", label: "Visible", ctrl: Ctrl::Switch("hidden", "visible") }, + Field { + name: "display", + label: "Display", + ctrl: Ctrl::Select(&["block", "flex", "grid", "inline-block", "none", "contents"]), + }, + Field { + name: "position", + label: "Position", + ctrl: Ctrl::Select(&["static", "relative", "absolute"]), + }, + Field { + name: "top", + label: "Top", + ctrl: Ctrl::Text, + }, + Field { + name: "right", + label: "Right", + ctrl: Ctrl::Text, + }, + Field { + name: "bottom", + label: "Bottom", + ctrl: Ctrl::Text, + }, + Field { + name: "left", + label: "Left", + ctrl: Ctrl::Text, + }, + Field { + name: "z-index", + label: "Z-index", + ctrl: Ctrl::Number, + }, + Field { + name: "overflow", + label: "Overflow", + ctrl: Ctrl::Select(&["visible", "hidden", "auto", "scroll", "clip"]), + }, + Field { + name: "visibility", + label: "Visible", + ctrl: Ctrl::Switch("hidden", "visible"), + }, ]; const F_POSITION: &[Field] = &[ - Field { name: "position", label: "Position", ctrl: Ctrl::Select(&["static", "relative", "absolute"]) }, - Field { name: "top", label: "Top", ctrl: Ctrl::Text }, - Field { name: "left", label: "Left", ctrl: Ctrl::Text }, - Field { name: "z-index", label: "Z-index", ctrl: Ctrl::Number }, + Field { + name: "position", + label: "Position", + ctrl: Ctrl::Select(&["static", "relative", "absolute"]), + }, + Field { + name: "top", + label: "Top", + ctrl: Ctrl::Text, + }, + Field { + name: "left", + label: "Left", + ctrl: Ctrl::Text, + }, + Field { + name: "z-index", + label: "Z-index", + ctrl: Ctrl::Number, + }, ]; const F_FLEX: &[Field] = &[ - Field { name: "flex-direction", label: "Direction", ctrl: Ctrl::Select(&["row", "row-reverse", "column", "column-reverse"]) }, - Field { name: "flex-wrap", label: "Wrap", ctrl: Ctrl::Select(&["nowrap", "wrap", "wrap-reverse"]) }, - Field { name: "justify-content", label: "Justify", ctrl: Ctrl::Select(&["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly", "start", "end"]) }, - Field { name: "align-items", label: "Align", ctrl: Ctrl::Select(&["stretch", "flex-start", "flex-end", "center", "baseline", "start", "end"]) }, - Field { name: "align-content", label: "Align content", ctrl: Ctrl::Select(&["stretch", "flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly", "start", "end"]) }, - Field { name: "gap", label: "Gap", ctrl: Ctrl::Text }, + Field { + name: "flex-direction", + label: "Direction", + ctrl: Ctrl::Select(&["row", "row-reverse", "column", "column-reverse"]), + }, + Field { + name: "flex-wrap", + label: "Wrap", + ctrl: Ctrl::Select(&["nowrap", "wrap", "wrap-reverse"]), + }, + Field { + name: "justify-content", + label: "Justify", + ctrl: Ctrl::Select(&[ + "flex-start", + "flex-end", + "center", + "space-between", + "space-around", + "space-evenly", + "start", + "end", + ]), + }, + Field { + name: "align-items", + label: "Align", + ctrl: Ctrl::Select(&[ + "stretch", + "flex-start", + "flex-end", + "center", + "baseline", + "start", + "end", + ]), + }, + Field { + name: "align-content", + label: "Align content", + ctrl: Ctrl::Select(&[ + "stretch", + "flex-start", + "flex-end", + "center", + "space-between", + "space-around", + "space-evenly", + "start", + "end", + ]), + }, + Field { + name: "gap", + label: "Gap", + ctrl: Ctrl::Text, + }, ]; const F_SPACING: &[Field] = &[ - Field { name: "padding", label: "Padding", ctrl: Ctrl::Text }, - Field { name: "margin", label: "Margin", ctrl: Ctrl::Text }, + Field { + name: "padding", + label: "Padding", + ctrl: Ctrl::Text, + }, + Field { + name: "margin", + label: "Margin", + ctrl: Ctrl::Text, + }, ]; -const F_MARGIN: &[Field] = &[Field { name: "margin", label: "Margin", ctrl: Ctrl::Text }]; +const F_MARGIN: &[Field] = &[Field { + name: "margin", + label: "Margin", + ctrl: Ctrl::Text, +}]; const F_SIZING: &[Field] = &[ - Field { name: "width", label: "Width", ctrl: Ctrl::Text }, - Field { name: "height", label: "Height", ctrl: Ctrl::Text }, - Field { name: "min-width", label: "Min W", ctrl: Ctrl::Text }, - Field { name: "min-height", label: "Min H", ctrl: Ctrl::Text }, - Field { name: "max-width", label: "Max W", ctrl: Ctrl::Text }, - Field { name: "max-height", label: "Max H", ctrl: Ctrl::Text }, + Field { + name: "width", + label: "Width", + ctrl: Ctrl::Text, + }, + Field { + name: "height", + label: "Height", + ctrl: Ctrl::Text, + }, + Field { + name: "min-width", + label: "Min W", + ctrl: Ctrl::Text, + }, + Field { + name: "min-height", + label: "Min H", + ctrl: Ctrl::Text, + }, + Field { + name: "max-width", + label: "Max W", + ctrl: Ctrl::Text, + }, + Field { + name: "max-height", + label: "Max H", + ctrl: Ctrl::Text, + }, ]; const F_SIZING_WH: &[Field] = &[ - Field { name: "width", label: "Width", ctrl: Ctrl::Text }, - Field { name: "height", label: "Height", ctrl: Ctrl::Text }, + Field { + name: "width", + label: "Width", + ctrl: Ctrl::Text, + }, + Field { + name: "height", + label: "Height", + ctrl: Ctrl::Text, + }, ]; const F_TEXT_SIZING: &[Field] = &[ - Field { name: "width", label: "Width", ctrl: Ctrl::Text }, - Field { name: "max-width", label: "Max W", ctrl: Ctrl::Text }, + Field { + name: "width", + label: "Width", + ctrl: Ctrl::Text, + }, + Field { + name: "max-width", + label: "Max W", + ctrl: Ctrl::Text, + }, ]; const F_APPEARANCE: &[Field] = &[ - Field { name: "background", label: "Background", ctrl: Ctrl::Color { clearable: true } }, - Field { name: "border-radius", label: "Radius", ctrl: Ctrl::Text }, - Field { name: "opacity", label: "Opacity", ctrl: Ctrl::Slider { min: 0.0, max: 1.0, step: 0.01 } }, + Field { + name: "background", + label: "Background", + ctrl: Ctrl::Color { clearable: true }, + }, + Field { + name: "border-radius", + label: "Radius", + ctrl: Ctrl::Text, + }, + Field { + name: "opacity", + label: "Opacity", + ctrl: Ctrl::Slider { + min: 0.0, + max: 1.0, + step: 0.01, + }, + }, ]; -const F_OPACITY: &[Field] = &[Field { name: "opacity", label: "Opacity", ctrl: Ctrl::Slider { min: 0.0, max: 1.0, step: 0.01 } }]; +const F_OPACITY: &[Field] = &[Field { + name: "opacity", + label: "Opacity", + ctrl: Ctrl::Slider { + min: 0.0, + max: 1.0, + step: 0.01, + }, +}]; // ── Per-family sections (strict: only valid props per element type) ────────── @@ -176,33 +402,87 @@ fn family(kind: &str) -> Family { } const TEXT_SECTIONS: &[Section] = &[ - Section { title: "Typography", fields: F_TYPO }, - Section { title: "Color", fields: F_COLOR }, - Section { title: "Sizing", fields: F_TEXT_SIZING }, - Section { title: "Spacing", fields: F_SPACING }, - Section { title: "Appearance", fields: F_OPACITY }, + Section { + title: "Typography", + fields: F_TYPO, + }, + Section { + title: "Color", + fields: F_COLOR, + }, + Section { + title: "Sizing", + fields: F_TEXT_SIZING, + }, + Section { + title: "Spacing", + fields: F_SPACING, + }, + Section { + title: "Appearance", + fields: F_OPACITY, + }, ]; const CONTAINER_SECTIONS: &[Section] = &[ - Section { title: "Layout", fields: F_LAYOUT }, - Section { title: "Flex", fields: F_FLEX }, - Section { title: "Spacing", fields: F_SPACING }, - Section { title: "Sizing", fields: F_SIZING }, - Section { title: "Appearance", fields: F_APPEARANCE }, + Section { + title: "Layout", + fields: F_LAYOUT, + }, + Section { + title: "Flex", + fields: F_FLEX, + }, + Section { + title: "Spacing", + fields: F_SPACING, + }, + Section { + title: "Sizing", + fields: F_SIZING, + }, + Section { + title: "Appearance", + fields: F_APPEARANCE, + }, ]; const SHAPE_SECTIONS: &[Section] = &[ - Section { title: "Layout", fields: F_POSITION }, - Section { title: "Sizing", fields: F_SIZING }, - Section { title: "Spacing", fields: F_MARGIN }, - Section { title: "Appearance", fields: F_APPEARANCE }, + Section { + title: "Layout", + fields: F_POSITION, + }, + Section { + title: "Sizing", + fields: F_SIZING, + }, + Section { + title: "Spacing", + fields: F_MARGIN, + }, + Section { + title: "Appearance", + fields: F_APPEARANCE, + }, ]; const FALLBACK_SECTIONS: &[Section] = &[ - Section { title: "Layout", fields: F_POSITION }, - Section { title: "Sizing", fields: F_SIZING_WH }, - Section { title: "Spacing", fields: F_SPACING }, - Section { title: "Appearance", fields: F_APPEARANCE }, + Section { + title: "Layout", + fields: F_POSITION, + }, + Section { + title: "Sizing", + fields: F_SIZING_WH, + }, + Section { + title: "Spacing", + fields: F_SPACING, + }, + Section { + title: "Appearance", + fields: F_APPEARANCE, + }, ]; fn sections(f: Family) -> &'static [Section] { @@ -226,7 +506,11 @@ const TEXTAREA_STYLE: &str = "width:100%; box-sizing:border-box; min-height:58px /// The button chrome (bg/hover/active) is themed via `INSPECTOR_CSS` classes — /// inline styles can't override the WKWebView native button `appearance`. fn seg_icon(active: bool) -> &'static str { - if active { "var(--rm-on-accent)" } else { "var(--rm-text-muted)" } + if active { + "var(--rm-on-accent)" + } else { + "var(--rm-text-muted)" + } } // ── Value helpers ──────────────────────────────────────────────────────────── @@ -368,7 +652,11 @@ fn FieldRow(field: Field, pointer: String, style: serde_json::Value) -> Element let control = match field.ctrl { Ctrl::Text | Ctrl::Number => { - let kind = if matches!(field.ctrl, Ctrl::Number) { "number" } else { "text" }; + let kind = if matches!(field.ctrl, Ctrl::Number) { + "number" + } else { + "text" + }; rsx! { input { r#type: "{kind}", @@ -382,7 +670,12 @@ fn FieldRow(field: Field, pointer: String, style: serde_json::Value) -> Element } } } - Ctrl::UnitSlider { min, max, step, unit } => { + Ctrl::UnitSlider { + min, + max, + step, + unit, + } => { // Shared live state so the slider and the number field track each other // (the panel itself doesn't re-render on the file reload). `txt` holds the // field's raw text so typing decimals isn't reformatted mid-keystroke. @@ -500,7 +793,13 @@ fn FieldRow(field: Field, pointer: String, style: serde_json::Value) -> Element } } Ctrl::Align => { - let vr = |v: &str| if value == v { ButtonVariant::Primary } else { ButtonVariant::Ghost }; + let vr = |v: &str| { + if value == v { + ButtonVariant::Primary + } else { + ButtonVariant::Ghost + } + }; rsx! { div { class: "rm-seg", Button { @@ -528,7 +827,8 @@ fn FieldRow(field: Field, pointer: String, style: serde_json::Value) -> Element } Ctrl::StyleToggles => { let weight = prop_str(&style, "font-weight"); - let is_bold = weight == "bold" || weight.parse::().map(|w| w >= 600).unwrap_or(false); + let is_bold = + weight == "bold" || weight.parse::().map(|w| w >= 600).unwrap_or(false); let fstyle = prop_str(&style, "font-style"); let is_italic = fstyle == "italic" || fstyle == "oblique"; rsx! { @@ -663,7 +963,9 @@ fn write_prop(shared: &Shared, pointer: &str, prop: &str, value: &str) { }; if rustmotion::loader::is_html_path(&path) { if let Ok(html) = std::fs::read_to_string(&path) { - if let Some(updated) = rustmotion::loader::set_html_inline_style(&html, pointer, prop, value) { + if let Some(updated) = + rustmotion::loader::set_html_inline_style(&html, pointer, prop, value) + { let _ = std::fs::write(&path, updated); } } diff --git a/crates/rustmotion-studio/src/editor/view.rs b/crates/rustmotion-studio/src/editor/view.rs index 704eea0..0780661 100644 --- a/crates/rustmotion-studio/src/editor/view.rs +++ b/crates/rustmotion-studio/src/editor/view.rs @@ -1,4 +1,6 @@ -use dioxus::desktop::{use_asset_handler, wry::http::Response, AssetRequest, RequestAsyncResponder}; +use dioxus::desktop::{ + use_asset_handler, wry::http::Response, AssetRequest, RequestAsyncResponder, +}; use dioxus::prelude::*; use crate::scenario::{list_annotations, read_field, read_style_object, Shared, View}; @@ -76,7 +78,12 @@ pub fn StudioApp(view: Signal) -> Element { .and_then(|s| s.to_str()) .unwrap_or("Untitled") .to_string(); - (m.total_frames, m.error.clone(), title, list_annotations(&m.raw)) + ( + m.total_frames, + m.error.clone(), + title, + list_annotations(&m.raw), + ) }; let comment_count = annotations.len(); diff --git a/crates/rustmotion-studio/src/lib.rs b/crates/rustmotion-studio/src/lib.rs index 522b923..85d56e6 100644 --- a/crates/rustmotion-studio/src/lib.rs +++ b/crates/rustmotion-studio/src/lib.rs @@ -45,6 +45,13 @@ pub fn run() -> Result<()> { true, ), }, - None => app::run_preview_root(scenario::empty_scenario(), None, None, workspace, false, true), + None => app::run_preview_root( + scenario::empty_scenario(), + None, + None, + workspace, + false, + true, + ), } } diff --git a/crates/rustmotion-studio/src/library/data.rs b/crates/rustmotion-studio/src/library/data.rs index bdc0cee..11205f4 100644 --- a/crates/rustmotion-studio/src/library/data.rs +++ b/crates/rustmotion-studio/src/library/data.rs @@ -132,7 +132,9 @@ pub fn render_thumbnail(path: &Path) -> Option> { if tasks.is_empty() { return None; } - Some(crate::editor::frames::render_frame(&scenario, &tasks, 0, 0.25)) + Some(crate::editor::frames::render_frame( + &scenario, &tasks, 0, 0.25, + )) } /// Cheap check: is this JSON a Rustmotion scenario? Avoids the heavier @@ -279,12 +281,8 @@ mod tests { #[test] fn is_scenario_json_accepts_and_rejects() { - assert!(is_scenario_json( - &json!({ "video": {}, "scenes": [] }) - )); - assert!(is_scenario_json( - &json!({ "video": {}, "composition": [] }) - )); + assert!(is_scenario_json(&json!({ "video": {}, "scenes": [] }))); + assert!(is_scenario_json(&json!({ "video": {}, "composition": [] }))); assert!(!is_scenario_json(&json!({ "foo": 1 }))); assert!(!is_scenario_json(&json!({ "video": {} }))); } diff --git a/crates/rustmotion-studio/src/scenario/edit.rs b/crates/rustmotion-studio/src/scenario/edit.rs index 2405165..c32cf3c 100644 --- a/crates/rustmotion-studio/src/scenario/edit.rs +++ b/crates/rustmotion-studio/src/scenario/edit.rs @@ -2,6 +2,7 @@ use serde_json::Value; /// Read a style property at the element addressed by `pointer`, e.g. /// (`"/scenes/0/children/0"`, `"color"`). Returns the value as a string. +#[allow(dead_code)] // symmetric API counterpart to set_style; consumed as the inspector grows pub fn read_style(raw: &Value, pointer: &str, prop: &str) -> Option { let el = raw.pointer(pointer)?; let v = el.get("style")?.get(prop)?; @@ -83,8 +84,16 @@ pub fn list_annotations(raw: &Value) -> Vec<(String, String, u64, String)> { .map(|a| { a.iter() .map(|x| { - let id = x.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(); - let note = x.get("note").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let id = x + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let note = x + .get("note") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); let frame = x.get("frame").and_then(|v| v.as_u64()).unwrap_or(0); let kind = x .get("target") diff --git a/crates/rustmotion/src/encode/audio.rs b/crates/rustmotion/src/encode/audio.rs index c1d9e72..dceaa5a 100644 --- a/crates/rustmotion/src/encode/audio.rs +++ b/crates/rustmotion/src/encode/audio.rs @@ -15,33 +15,55 @@ const TARGET_CHANNELS: u32 = 2; /// Decode an audio file into PCM i16 samples (stereo, 44100Hz, interleaved) fn decode_audio_file(path: &str) -> Result<(Vec, u32, u32)> { - let file = File::open(path) - .map_err(|e| RustmotionError::AudioOpen { path: path.to_string(), reason: e.to_string() })?; + let file = File::open(path).map_err(|e| RustmotionError::AudioOpen { + path: path.to_string(), + reason: e.to_string(), + })?; let mss = MediaSourceStream::new(Box::new(file), Default::default()); let mut hint = Hint::new(); - if let Some(ext) = std::path::Path::new(path).extension().and_then(|e| e.to_str()) { + if let Some(ext) = std::path::Path::new(path) + .extension() + .and_then(|e| e.to_str()) + { hint.with_extension(ext); } let probed = symphonia::default::get_probe() - .format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default()) - .map_err(|e| RustmotionError::AudioProbe { path: path.to_string(), reason: e.to_string() })?; + .format( + &hint, + mss, + &FormatOptions::default(), + &MetadataOptions::default(), + ) + .map_err(|e| RustmotionError::AudioProbe { + path: path.to_string(), + reason: e.to_string(), + })?; let mut format = probed.format; let track = format .default_track() - .ok_or_else(|| RustmotionError::AudioNoTrack { path: path.to_string() })?; + .ok_or_else(|| RustmotionError::AudioNoTrack { + path: path.to_string(), + })?; let track_id = track.id; let sample_rate = track.codec_params.sample_rate.unwrap_or(44100); - let channels = track.codec_params.channels.map(|c| c.count() as u32).unwrap_or(2); + let channels = track + .codec_params + .channels + .map(|c| c.count() as u32) + .unwrap_or(2); let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default()) - .map_err(|e| RustmotionError::AudioDecoder { path: path.to_string(), reason: e.to_string() })?; + .map_err(|e| RustmotionError::AudioDecoder { + path: path.to_string(), + reason: e.to_string(), + })?; let mut all_samples: Vec = Vec::new(); @@ -103,7 +125,8 @@ pub fn mix_audio_tracks(tracks: &[AudioTrack], total_duration: f64) -> Result Result= mix_buffer.len() { break; @@ -130,7 +153,7 @@ pub fn mix_audio_tracks(tracks: &[AudioTrack], total_duration: f64) -> Result 0.0 && (frame as f64) < fade_in_samples { @@ -214,7 +237,9 @@ fn resample(samples: &[f32], src_rate: u32, dst_rate: u32) -> Vec { return samples.to_vec(); } - use rubato::{Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction}; + use rubato::{ + Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction, + }; let channels = 2usize; let src_frames = samples.len() / channels; @@ -239,7 +264,9 @@ fn resample(samples: &[f32], src_rate: u32, dst_rate: u32) -> Vec { }; // Deinterleave samples into per-channel vectors - let mut channel_data: Vec> = vec![Vec::with_capacity(src_frames); channels]; + let mut channel_data: Vec> = (0..channels) + .map(|_| Vec::with_capacity(src_frames)) + .collect(); for (i, &s) in samples.iter().enumerate() { channel_data[i % channels].push(s as f64); } @@ -277,15 +304,12 @@ fn resample(samples: &[f32], src_rate: u32, dst_rate: u32) -> Vec { }) .collect(); - match resampler.process(&chunk, None) { - Ok(out) => { - let expected_out = (remaining as f64 * ratio).ceil() as usize; - for (ch, data) in out.iter().enumerate() { - let take = expected_out.min(data.len()); - output_channels[ch].extend_from_slice(&data[..take]); - } + if let Ok(out) = resampler.process(&chunk, None) { + let expected_out = (remaining as f64 * ratio).ceil() as usize; + for (ch, data) in out.iter().enumerate() { + let take = expected_out.min(data.len()); + output_channels[ch].extend_from_slice(&data[..take]); } - Err(_) => {} } } diff --git a/crates/rustmotion/src/encode/video/ffmpeg.rs b/crates/rustmotion/src/encode/video/ffmpeg.rs index e341b62..993e677 100644 --- a/crates/rustmotion/src/encode/video/ffmpeg.rs +++ b/crates/rustmotion/src/encode/video/ffmpeg.rs @@ -32,7 +32,7 @@ pub fn encode_with_ffmpeg( let total_frames = tasks.len() as u32; if total_frames == 0 { - return Err(RustmotionError::NoFrames.into()); + return Err(RustmotionError::NoFrames); } // Process audio @@ -57,17 +57,30 @@ pub fn encode_with_ffmpeg( let mut cmd = std::process::Command::new("ffmpeg"); cmd.args([ "-y", - "-loglevel", "error", - "-f", "rawvideo", - "-pixel_format", "rgba", - "-video_size", &format!("{}x{}", width, height), - "-framerate", &fps.to_string(), - "-i", "pipe:0", + "-loglevel", + "error", + "-f", + "rawvideo", + "-pixel_format", + "rgba", + "-video_size", + &format!("{}x{}", width, height), + "-framerate", + &fps.to_string(), + "-i", + "pipe:0", ]); match codec { "h265" | "hevc" => { - cmd.args(["-c:v", "libx265", "-crf", &crf_val.to_string(), "-preset", "medium"]); + cmd.args([ + "-c:v", + "libx265", + "-crf", + &crf_val.to_string(), + "-preset", + "medium", + ]); if transparent { cmd.args(["-pix_fmt", "yuva420p"]); } else { @@ -75,7 +88,14 @@ pub fn encode_with_ffmpeg( } } "vp9" => { - cmd.args(["-c:v", "libvpx-vp9", "-crf", &crf_val.to_string(), "-b:v", "0"]); + cmd.args([ + "-c:v", + "libvpx-vp9", + "-crf", + &crf_val.to_string(), + "-b:v", + "0", + ]); if transparent { cmd.args(["-pix_fmt", "yuva420p"]); } else { @@ -91,20 +111,42 @@ pub fn encode_with_ffmpeg( } } _ => { - cmd.args(["-c:v", "libx264", "-crf", &crf_val.to_string(), "-preset", "medium", "-profile:v", "high10", "-pix_fmt", "yuv420p10le"]); + cmd.args([ + "-c:v", + "libx264", + "-crf", + &crf_val.to_string(), + "-preset", + "medium", + "-profile:v", + "high10", + "-pix_fmt", + "yuv420p10le", + ]); } } if let Some(ref pcm) = pcm_data { let audio_path = audio_tmp_dir.as_ref().unwrap().join("audio.raw"); std::fs::write(&audio_path, pcm)?; - let audio_path_str = audio_path.to_str().ok_or_else(|| RustmotionError::NonUtf8Path { - path: audio_path.to_string_lossy().into_owned(), - })?; + let audio_path_str = audio_path + .to_str() + .ok_or_else(|| RustmotionError::NonUtf8Path { + path: audio_path.to_string_lossy().into_owned(), + })?; cmd.args([ - "-f", "s16le", "-ar", "44100", "-ac", "2", "-i", + "-f", + "s16le", + "-ar", + "44100", + "-ac", + "2", + "-i", audio_path_str, - "-c:a", "aac", "-b:a", "128k", + "-c:a", + "aac", + "-b:a", + "128k", ]); } @@ -116,12 +158,11 @@ pub fn encode_with_ffmpeg( // only on failure. cmd.stderr(std::process::Stdio::piped()); - let mut child = cmd - .spawn() - .map_err(|e| RustmotionError::FfmpegSpawn { reason: e.to_string() })?; + let mut child = cmd.spawn().map_err(|e| RustmotionError::FfmpegSpawn { + reason: e.to_string(), + })?; - let mut stdin = child.stdin.take() - .ok_or(RustmotionError::FfmpegPipe)?; + let mut stdin = child.stdin.take().ok_or(RustmotionError::FfmpegPipe)?; let stderr_handle = child.stderr.take(); // Render frames in parallel batches, pipe RGBA sequentially @@ -152,7 +193,9 @@ pub fn encode_with_ffmpeg( match result { Ok(rgba) => { if let Err(e) = stdin.write_all(&rgba) { - pipe_error = Some(RustmotionError::FfmpegWrite { reason: e.to_string() }); + pipe_error = Some(RustmotionError::FfmpegWrite { + reason: e.to_string(), + }); break; } } @@ -170,8 +213,9 @@ pub fn encode_with_ffmpeg( cb(EncodeProgress::Muxing); } - let status = child.wait() - .map_err(|e| RustmotionError::FfmpegWait { reason: e.to_string() })?; + let status = child.wait().map_err(|e| RustmotionError::FfmpegWait { + reason: e.to_string(), + })?; // Drain stderr (best effort) let stderr_text = stderr_handle.map(|mut h| { @@ -199,10 +243,13 @@ pub fn encode_with_ffmpeg( if !status.success() { // Extract the last few lines of stderr — ffmpeg's actual error message // typically appears in the final 5-10 lines. - let stderr_summary = stderr_text.as_ref().map(|s| { - let lines: Vec<&str> = s.lines().rev().take(8).collect(); - lines.into_iter().rev().collect::>().join("\n") - }).filter(|s| !s.trim().is_empty()); + let stderr_summary = stderr_text + .as_ref() + .map(|s| { + let lines: Vec<&str> = s.lines().rev().take(8).collect(); + lines.into_iter().rev().collect::>().join("\n") + }) + .filter(|s| !s.trim().is_empty()); if !quiet { if let Some(ref text) = stderr_text { @@ -211,7 +258,9 @@ pub fn encode_with_ffmpeg( } } } - return Err(RustmotionError::FfmpegFailed { stderr: stderr_summary }.into()); + return Err(RustmotionError::FfmpegFailed { + stderr: stderr_summary, + }); } Ok(()) diff --git a/crates/rustmotion/src/encode/video/formats.rs b/crates/rustmotion/src/encode/video/formats.rs index 119f875..cce5364 100644 --- a/crates/rustmotion/src/encode/video/formats.rs +++ b/crates/rustmotion/src/encode/video/formats.rs @@ -30,7 +30,7 @@ pub fn encode_png_sequence( let total_frames = tasks.len() as u32; if total_frames == 0 { - return Err(RustmotionError::NoFrames.into()); + return Err(RustmotionError::NoFrames); } std::fs::create_dir_all(output_dir)?; @@ -49,7 +49,10 @@ pub fn encode_png_sequence( .collect(); if let Some(ref mut cb) = on_progress { - cb(EncodeProgress::Rendering(counter.load(Ordering::Relaxed), total_frames)); + cb(EncodeProgress::Rendering( + counter.load(Ordering::Relaxed), + total_frames, + )); } for result in results { @@ -84,18 +87,24 @@ pub fn encode_gif( let total_frames = tasks.len() as u32; if total_frames == 0 { - return Err(RustmotionError::NoFrames.into()); + return Err(RustmotionError::NoFrames); } let gif_w = width.min(65535) as u16; let gif_h = height.min(65535) as u16; let file = File::create(output_path)?; - let mut encoder = gif::Encoder::new(BufWriter::new(file), gif_w, gif_h, &[]) - .map_err(|e| RustmotionError::GifEncoder { reason: e.to_string() })?; + let mut encoder = gif::Encoder::new(BufWriter::new(file), gif_w, gif_h, &[]).map_err(|e| { + RustmotionError::GifEncoder { + reason: e.to_string(), + } + })?; - encoder.set_repeat(gif::Repeat::Infinite) - .map_err(|e| RustmotionError::GifRepeat { reason: e.to_string() })?; + encoder + .set_repeat(gif::Repeat::Infinite) + .map_err(|e| RustmotionError::GifRepeat { + reason: e.to_string(), + })?; let delay = (100.0 / fps as f64).round() as u16; @@ -113,15 +122,21 @@ pub fn encode_gif( .collect(); if let Some(ref mut cb) = on_progress { - cb(EncodeProgress::Rendering(counter.load(Ordering::Relaxed), total_frames)); + cb(EncodeProgress::Rendering( + counter.load(Ordering::Relaxed), + total_frames, + )); } for result in results { let rgba = result?; let mut frame = gif::Frame::from_rgba_speed(gif_w, gif_h, &mut rgba.clone(), 10); frame.delay = delay; - encoder.write_frame(&frame) - .map_err(|e| RustmotionError::GifFrame { reason: e.to_string() })?; + encoder + .write_frame(&frame) + .map_err(|e| RustmotionError::GifFrame { + reason: e.to_string(), + })?; } } @@ -142,7 +157,10 @@ pub fn encode_raw_stdout(scenario: &Scenario, quiet: bool) -> Result<()> { for local_frame in 0..scene_frames { let rgba = crate::engine::render::render_scene_frame( - config, scene, local_frame, scene_frames, + config, + scene, + local_frame, + scene_frames, )?; stdout.write_all(&rgba)?; diff --git a/crates/rustmotion/src/encode/video/h264.rs b/crates/rustmotion/src/encode/video/h264.rs index 120f0cc..cd84df2 100644 --- a/crates/rustmotion/src/encode/video/h264.rs +++ b/crates/rustmotion/src/encode/video/h264.rs @@ -4,23 +4,25 @@ use openh264::OpenH264API; use rayon::prelude::*; use std::sync::atomic::{AtomicU32, Ordering}; -use crate::engine::{rgba_to_yuv420, preextract_video_frames, prefetch_icons}; +use crate::engine::{preextract_video_frames, prefetch_icons, rgba_to_yuv420}; use crate::error::{Result, RustmotionError}; use crate::schema::ResolvedScenario as Scenario; use super::mux::mux_h264_to_mp4; -use super::tasks::{build_frame_tasks, build_scene_frame_tasks, hash_scene, render_frame_task, SceneSegment}; +use super::tasks::{ + build_frame_tasks, build_scene_frame_tasks, hash_scene, render_frame_task, SceneSegment, +}; use super::EncodeProgress; /// Create an OpenH264 encoder with standard settings for the given video dimensions. fn create_encoder(width: u32, height: u32, fps: u32) -> Result { let api = OpenH264API::from_source(); - let pixels = (width * height) as u32; + let pixels = width * height; let target_bitrate = (pixels as f64 * fps as f64 * 0.1) as u32; let config = EncoderConfig::new() .set_bitrate_bps(target_bitrate.max(3_000_000)) .max_frame_rate(fps as f32); - Ok(Encoder::with_api_config(api, config).map_err(|e| RustmotionError::from(e.to_string()))?) + Encoder::with_api_config(api, config).map_err(|e| RustmotionError::from(e.to_string())) } pub fn encode_video( @@ -43,7 +45,7 @@ pub fn encode_video( let total_frames = tasks.len() as u32; if total_frames == 0 { - return Err(RustmotionError::NoFrames.into()); + return Err(RustmotionError::NoFrames); } let batch_size = (rayon::current_num_threads() * 2).max(4); @@ -72,7 +74,9 @@ pub fn encode_video( let yuv = yuv_result?; encoder.force_intra_frame(); let yuv_buf = YUVBuffer::from_vec(yuv, width as usize, height as usize); - let bitstream = encoder.encode(&yuv_buf).map_err(|e| RustmotionError::from(e.to_string()))?; + let bitstream = encoder + .encode(&yuv_buf) + .map_err(|e| RustmotionError::from(e.to_string()))?; bitstream.write_vec(&mut h264_data); } } @@ -82,7 +86,15 @@ pub fn encode_video( } let total_duration = total_frames as f64 / fps as f64; - mux_h264_to_mp4(&h264_data, output_path, width, height, fps, scenario, total_duration)?; + mux_h264_to_mp4( + &h264_data, + output_path, + width, + height, + fps, + scenario, + total_duration, + )?; Ok(()) } @@ -107,7 +119,9 @@ pub fn encode_video_incremental( } if scenario.views.len() > 1 { return Err(RustmotionError::IncrementalUnsupported { - reason: "incremental encoding requires a single slide view (composition has multiple views)".to_string(), + reason: + "incremental encoding requires a single slide view (composition has multiple views)" + .to_string(), }); } if !matches!(scenario.views[0].view_type, crate::schema::ViewType::Slide) { @@ -157,7 +171,7 @@ pub fn encode_video_incremental( let total_frames: u32 = scene_tasks.iter().map(|t| t.len() as u32).sum(); if total_frames == 0 { - return Err(RustmotionError::NoFrames.into()); + return Err(RustmotionError::NoFrames); } // Flatten tasks that need rendering @@ -192,7 +206,10 @@ pub fn encode_video_incremental( .collect(); if let Some(ref mut cb) = on_progress { - cb(EncodeProgress::Rendering(counter.load(Ordering::Relaxed), frames_to_render)); + cb(EncodeProgress::Rendering( + counter.load(Ordering::Relaxed), + frames_to_render, + )); } all_yuv.extend(batch_results); @@ -205,7 +222,8 @@ pub fn encode_video_incremental( let mut encoder = create_encoder(width, height, fps)?; let mut yuv_iter = all_yuv.into_iter(); - let mut rendered_segments: std::collections::HashMap> = std::collections::HashMap::new(); + let mut rendered_segments: std::collections::HashMap> = + std::collections::HashMap::new(); let mut encoded_count: u32 = 0; for &(scene_idx, frame_count) in &scene_frame_counts { @@ -215,7 +233,9 @@ pub fn encode_video_incremental( let yuv = yuv_iter.next().unwrap()?; encoder.force_intra_frame(); let yuv_buf = YUVBuffer::from_vec(yuv, width as usize, height as usize); - let bitstream = encoder.encode(&yuv_buf).map_err(|e| RustmotionError::from(e.to_string()))?; + let bitstream = encoder + .encode(&yuv_buf) + .map_err(|e| RustmotionError::from(e.to_string()))?; bitstream.write_vec(&mut segment_h264); encoded_count += 1; if let Some(ref mut cb) = on_progress { @@ -254,7 +274,15 @@ pub fn encode_video_incremental( } let total_duration = total_frames as f64 / fps as f64; - mux_h264_to_mp4(&h264_data, output_path, width, height, fps, scenario, total_duration)?; + mux_h264_to_mp4( + &h264_data, + output_path, + width, + height, + fps, + scenario, + total_duration, + )?; if !quiet && on_progress.is_none() { eprintln!("Done!"); diff --git a/crates/rustmotion/src/encode/video/mod.rs b/crates/rustmotion/src/encode/video/mod.rs index 379b367..c3fbc01 100644 --- a/crates/rustmotion/src/encode/video/mod.rs +++ b/crates/rustmotion/src/encode/video/mod.rs @@ -1,13 +1,13 @@ -mod tasks; -mod h264; mod ffmpeg; mod formats; +mod h264; mod mux; +mod tasks; -pub use tasks::*; -pub use h264::*; pub use ffmpeg::*; pub use formats::*; +pub use h264::*; +pub use tasks::*; /// Progress events emitted during encoding pub enum EncodeProgress { diff --git a/crates/rustmotion/src/encode/video/tasks.rs b/crates/rustmotion/src/encode/video/tasks.rs index 890e7c2..83676f1 100644 --- a/crates/rustmotion/src/encode/video/tasks.rs +++ b/crates/rustmotion/src/encode/video/tasks.rs @@ -1,6 +1,9 @@ use crate::engine::transition::{apply_transition, camera_pan_transition}; use crate::error::Result; -use crate::schema::{EasingType, ResolvedView, Scene, ResolvedScenario as Scenario, TransitionType, VideoConfig, ViewType}; +use crate::schema::{ + EasingType, ResolvedScenario as Scenario, ResolvedView, Scene, TransitionType, VideoConfig, + ViewType, +}; /// Description of what to render for a specific frame #[derive(Clone)] @@ -39,7 +42,11 @@ pub enum FrameTask { }, } -pub fn render_frame_task(config: &VideoConfig, scenario: &Scenario, task: &FrameTask) -> Result> { +pub fn render_frame_task( + config: &VideoConfig, + scenario: &Scenario, + task: &FrameTask, +) -> Result> { render_frame_task_scaled(config, scenario, task, 1.0) } @@ -51,7 +58,12 @@ pub fn render_frame_task_hits( ) -> Vec { use crate::engine::render::render_scene_hits; match task { - FrameTask::Normal { view_idx, scene_idx, frame_in_scene, .. } => { + FrameTask::Normal { + view_idx, + scene_idx, + frame_in_scene, + .. + } => { let scene = &scenario.views[*view_idx].scenes[*scene_idx]; render_scene_hits(&scenario.video, scene, *frame_in_scene) } @@ -59,36 +71,16 @@ pub fn render_frame_task_hits( } } -#[cfg(test)] -mod hit_tests { - use super::*; - - const SCENARIO: &str = r##"{ - "video": { "width": 800, "height": 600, "background": "#101418" }, - "scenes": [ { "duration": 1.0, "children": [ - { "type": "text", "content": "Hello", "style": { "font-size": 48 } } - ] } ] - }"##; - - #[test] - fn normal_frame_returns_text_hit() { - let scenario = crate::loader::load_scenario_from_source(None, Some(SCENARIO)).unwrap(); - let tasks = crate::encode::build_frame_tasks(&scenario); - let hits = render_frame_task_hits(&scenario, &tasks[0]); - assert!( - hits.iter().any(|h| h.kind == "text"), - "expected a text hit, got {hits:?}" - ); - } -} - pub fn render_frame_task_scaled( config: &VideoConfig, scenario: &Scenario, task: &FrameTask, scale_factor: f32, ) -> Result> { - use crate::engine::render::{render_scene_frame, render_scene_frame_scaled, render_scene_frame_scaled_with_prev_bg, render_scene_bg_scaled, render_scene_fg_scaled}; + use crate::engine::render::{ + render_scene_bg_scaled, render_scene_fg_scaled, render_scene_frame, + render_scene_frame_scaled, render_scene_frame_scaled_with_prev_bg, + }; match task { FrameTask::Normal { @@ -105,7 +97,14 @@ pub fn render_frame_task_scaled( } else { None }; - render_scene_frame_scaled_with_prev_bg(config, scene, *frame_in_scene, *scene_total_frames, scale_factor, prev_bg) + render_scene_frame_scaled_with_prev_bg( + config, + scene, + *frame_in_scene, + *scene_total_frames, + scale_factor, + prev_bg, + ) } FrameTask::SlideTransition { view_idx, @@ -127,29 +126,80 @@ pub fn render_frame_task_scaled( let frame_a_idx = scene_a_frame_offset + frame_in_transition; if matches!(transition_type, TransitionType::CameraPan) { - let (ax, ay) = scenes[*scene_a_idx].world_position.as_ref().map(|p| (p.x, p.y)).unwrap_or((0.0, 0.0)); - let (bx, by) = scenes[*scene_b_idx].world_position.as_ref().map(|p| (p.x, p.y)).unwrap_or((0.0, 0.0)); + let (ax, ay) = scenes[*scene_a_idx] + .world_position + .as_ref() + .map(|p| (p.x, p.y)) + .unwrap_or((0.0, 0.0)); + let (bx, by) = scenes[*scene_b_idx] + .world_position + .as_ref() + .map(|p| (p.x, p.y)) + .unwrap_or((0.0, 0.0)); let dx = bx - ax; let dy = by - ay; - let bg = render_scene_bg_scaled(config, &scenes[*scene_a_idx], frame_a_idx, scale_factor)?; - let fg_a = render_scene_fg_scaled(config, &scenes[*scene_a_idx], frame_a_idx, *scene_a_total_frames, scale_factor)?; - let fg_b = render_scene_fg_scaled(config, &scenes[*scene_b_idx], *frame_in_transition, *scene_b_total_frames, scale_factor)?; + let bg = render_scene_bg_scaled( + config, + &scenes[*scene_a_idx], + frame_a_idx, + scale_factor, + )?; + let fg_a = render_scene_fg_scaled( + config, + &scenes[*scene_a_idx], + frame_a_idx, + *scene_a_total_frames, + scale_factor, + )?; + let fg_b = render_scene_fg_scaled( + config, + &scenes[*scene_b_idx], + *frame_in_transition, + *scene_b_total_frames, + scale_factor, + )?; return Ok(camera_pan_transition( - &bg, &fg_a, &fg_b, - scaled_w, scaled_h, + &bg, + &fg_a, + &fg_b, + scaled_w, + scaled_h, progress, - dx * scale_factor, dy * scale_factor, + dx * scale_factor, + dy * scale_factor, easing, )); } let (frame_a, frame_b) = if scale_factor == 1.0 { - let a = render_scene_frame(config, &scenes[*scene_a_idx], frame_a_idx, *scene_a_total_frames)?; - let b = render_scene_frame(config, &scenes[*scene_b_idx], *frame_in_transition, *scene_b_total_frames)?; + let a = render_scene_frame( + config, + &scenes[*scene_a_idx], + frame_a_idx, + *scene_a_total_frames, + )?; + let b = render_scene_frame( + config, + &scenes[*scene_b_idx], + *frame_in_transition, + *scene_b_total_frames, + )?; (a, b) } else { - let a = render_scene_frame_scaled(config, &scenes[*scene_a_idx], frame_a_idx, *scene_a_total_frames, scale_factor)?; - let b = render_scene_frame_scaled(config, &scenes[*scene_b_idx], *frame_in_transition, *scene_b_total_frames, scale_factor)?; + let a = render_scene_frame_scaled( + config, + &scenes[*scene_a_idx], + frame_a_idx, + *scene_a_total_frames, + scale_factor, + )?; + let b = render_scene_frame_scaled( + config, + &scenes[*scene_b_idx], + *frame_in_transition, + *scene_b_total_frames, + scale_factor, + )?; (a, b) }; @@ -171,7 +221,11 @@ pub fn render_frame_task_scaled( let view = &scenario.views[*view_idx]; let timeline = WorldTimeline::build(view, config.fps, config.width, config.height); crate::engine::render::render_world_frame_scaled( - config, view, &timeline, *frame_in_view, scale_factor, + config, + view, + &timeline, + *frame_in_view, + scale_factor, ) } FrameTask::ViewTransition { @@ -216,16 +270,32 @@ fn render_last_frame_of_view( ViewType::Slide => { if let Some(last_scene) = view.scenes.last() { let scene_frames = (last_scene.duration * fps as f64).round() as u32; - render_scene_frame_scaled(config, last_scene, scene_frames.saturating_sub(1), scene_frames, scale_factor) + render_scene_frame_scaled( + config, + last_scene, + scene_frames.saturating_sub(1), + scene_frames, + scale_factor, + ) } else { - Ok(vec![0u8; (config.width as f32 * scale_factor) as usize * (config.height as f32 * scale_factor) as usize * 4]) + Ok(vec![ + 0u8; + (config.width as f32 * scale_factor) as usize + * (config.height as f32 * scale_factor) as usize + * 4 + ]) } } ViewType::World => { - let timeline = crate::engine::world::WorldTimeline::build(view, fps, config.width, config.height); + let timeline = + crate::engine::world::WorldTimeline::build(view, fps, config.width, config.height); let total_frames = timeline.total_frames(fps); crate::engine::render::render_world_frame_scaled( - config, view, &timeline, total_frames.saturating_sub(1), scale_factor, + config, + view, + &timeline, + total_frames.saturating_sub(1), + scale_factor, ) } } @@ -244,13 +314,23 @@ fn render_first_frame_of_view( let scene_frames = (first_scene.duration * fps as f64).round() as u32; render_scene_frame_scaled(config, first_scene, 0, scene_frames, scale_factor) } else { - Ok(vec![0u8; (config.width as f32 * scale_factor) as usize * (config.height as f32 * scale_factor) as usize * 4]) + Ok(vec![ + 0u8; + (config.width as f32 * scale_factor) as usize + * (config.height as f32 * scale_factor) as usize + * 4 + ]) } } ViewType::World => { - let timeline = crate::engine::world::WorldTimeline::build(view, fps, config.width, config.height); + let timeline = + crate::engine::world::WorldTimeline::build(view, fps, config.width, config.height); crate::engine::render::render_world_frame_scaled( - config, view, &timeline, 0, scale_factor, + config, + view, + &timeline, + 0, + scale_factor, ) } } @@ -279,14 +359,26 @@ pub fn build_frame_tasks(scenario: &Scenario) -> Vec { match view.view_type { ViewType::Slide => build_slide_view_tasks(&mut tasks, view_idx, view, fps), - ViewType::World => build_world_view_tasks(&mut tasks, view_idx, view, fps, scenario.video.width, scenario.video.height), + ViewType::World => build_world_view_tasks( + &mut tasks, + view_idx, + view, + fps, + scenario.video.width, + scenario.video.height, + ), } } tasks } -fn build_slide_view_tasks(tasks: &mut Vec, view_idx: usize, view: &ResolvedView, fps: u32) { +fn build_slide_view_tasks( + tasks: &mut Vec, + view_idx: usize, + view: &ResolvedView, + fps: u32, +) { let scenes = &view.scenes; for (i, scene) in scenes.iter().enumerate() { @@ -340,7 +432,14 @@ fn build_slide_view_tasks(tasks: &mut Vec, view_idx: usize, view: &Re } } -fn build_world_view_tasks(tasks: &mut Vec, view_idx: usize, view: &ResolvedView, fps: u32, video_width: u32, video_height: u32) { +fn build_world_view_tasks( + tasks: &mut Vec, + view_idx: usize, + view: &ResolvedView, + fps: u32, + video_width: u32, + video_height: u32, +) { let timeline = crate::engine::world::WorldTimeline::build(view, fps, video_width, video_height); let total_frames = timeline.total_frames(fps); for f in 0..total_frames { @@ -362,7 +461,9 @@ pub(super) fn build_scene_frame_tasks(scenario: &Scenario, scene_idx: usize) -> let mut tasks = Vec::new(); let scene_frames = (scene.duration * fps as f64).round() as u32; - let next_transition = scenes.get(scene_idx + 1).and_then(|s| s.transition.as_ref()); + let next_transition = scenes + .get(scene_idx + 1) + .and_then(|s| s.transition.as_ref()); let outgoing_transition_frames = next_transition .map(|t| (t.duration * fps as f64).round() as u32) .unwrap_or(0); @@ -435,3 +536,26 @@ pub fn hash_video_config(config: &VideoConfig) -> u64 { json.hash(&mut hasher); hasher.finish() } + +#[cfg(test)] +mod hit_tests { + use super::*; + + const SCENARIO: &str = r##"{ + "video": { "width": 800, "height": 600, "background": "#101418" }, + "scenes": [ { "duration": 1.0, "children": [ + { "type": "text", "content": "Hello", "style": { "font-size": 48 } } + ] } ] + }"##; + + #[test] + fn normal_frame_returns_text_hit() { + let scenario = crate::loader::load_scenario_from_source(None, Some(SCENARIO)).unwrap(); + let tasks = crate::encode::build_frame_tasks(&scenario); + let hits = render_frame_task_hits(&scenario, &tasks[0]); + assert!( + hits.iter().any(|h| h.kind == "text"), + "expected a text hit, got {hits:?}" + ); + } +} diff --git a/crates/rustmotion/src/engine/preload.rs b/crates/rustmotion/src/engine/preload.rs index c1e0fa9..474a047 100644 --- a/crates/rustmotion/src/engine/preload.rs +++ b/crates/rustmotion/src/engine/preload.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use rustmotion_core::engine::renderer::{asset_cache, fetch_icon_svg, video_frame_cache}; -use rustmotion_core::traits::{Styled, Timed}; use crate::components::{ChildComponent, Component}; use crate::schema::Scene; +use rustmotion_core::engine::renderer::{asset_cache, fetch_icon_svg, video_frame_cache}; +use rustmotion_core::traits::{Styled, Timed}; /// Pre-fetch and cache all icon components before rendering. /// Call this before the render loop to avoid HTTP requests during parallel rendering. @@ -29,7 +29,12 @@ pub fn prefetch_icons(scenes: &[Scene]) { Some(CSize::Length(LengthPercentage::Px(v))) => (*v as u32).max(1), _ => 24, }; - seen.insert((icon.icon.clone(), icon.style_config().color_str_or("#FFFFFF").to_string(), w, h)); + seen.insert(( + icon.icon.clone(), + icon.style_config().color_str_or("#FFFFFF").to_string(), + w, + h, + )); } Component::Card(c) => { for child in &c.children { @@ -61,7 +66,9 @@ pub fn prefetch_icons(scenes: &[Scene]) { } for scene in scenes { - let children: Vec = scene.children.iter() + let children: Vec = scene + .children + .iter() .filter_map(|v| serde_json::from_value(v.clone()).ok()) .collect(); for child in &children { @@ -118,10 +125,7 @@ pub fn prefetch_icons(scenes: &[Scene]) { /// Pre-extract all needed frames from video sources in a single ffmpeg pass. /// Called before the render loop to populate the video frame cache. -pub fn preextract_video_frames( - scenes: &[Scene], - fps: u32, -) { +pub fn preextract_video_frames(scenes: &[Scene], fps: u32) { fn collect_videos(child: &ChildComponent, scene_frames: u32, fps: u32) { if let Component::Video(video) = &child.component { use rustmotion_core::css::style::Size as CSize; @@ -146,8 +150,12 @@ pub fn preextract_video_frames( } let (start_at, end_at) = video.timing(); - let start_frame = start_at.map(|s| (s * fps as f64).round() as u32).unwrap_or(0); - let end_frame = end_at.map(|e| (e * fps as f64).round() as u32).unwrap_or(scene_frames); + let start_frame = start_at + .map(|s| (s * fps as f64).round() as u32) + .unwrap_or(0); + let end_frame = end_at + .map(|e| (e * fps as f64).round() as u32) + .unwrap_or(scene_frames); let mut times = Vec::new(); for f in start_frame..end_frame { @@ -166,13 +174,20 @@ pub fn preextract_video_frames( let output = std::process::Command::new("ffmpeg") .args([ - "-ss", &format!("{:.3}", min_time), - "-t", &format!("{:.3}", duration), - "-i", &video.src, - "-vf", &format!("fps={},scale={}:{}", fps, width, height), - "-f", "rawvideo", - "-pix_fmt", "rgba", - "-y", "pipe:1", + "-ss", + &format!("{:.3}", min_time), + "-t", + &format!("{:.3}", duration), + "-i", + &video.src, + "-vf", + &format!("fps={},scale={}:{}", fps, width, height), + "-f", + "rawvideo", + "-pix_fmt", + "rgba", + "-y", + "pipe:1", ]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) @@ -215,7 +230,9 @@ pub fn preextract_video_frames( for scene in scenes { let scene_frames = (scene.duration * fps as f64).round() as u32; - let children: Vec = scene.children.iter() + let children: Vec = scene + .children + .iter() .filter_map(|v| serde_json::from_value(v.clone()).ok()) .collect(); for child in &children { diff --git a/crates/rustmotion/src/engine/render/background.rs b/crates/rustmotion/src/engine/render/background.rs index b59bd8d..2258198 100644 --- a/crates/rustmotion/src/engine/render/background.rs +++ b/crates/rustmotion/src/engine/render/background.rs @@ -1,7 +1,10 @@ use skia_safe::{Canvas, ColorType, ImageInfo, Paint}; +use crate::schema::{ + AnimatedBackground, BackgroundPreset, ConcentricCirclesConfig, GradientShiftConfig, + GradientType, GridDotsConfig, HaloConfig, HaloZone, HeropatternConfig, ScrollDirection, +}; use rustmotion_core::engine::renderer::{color4f_from_hex, paint_from_hex}; -use crate::schema::{AnimatedBackground, BackgroundPreset, ConcentricCirclesConfig, GradientShiftConfig, GradientType, GridDotsConfig, HaloConfig, HaloZone, HeropatternConfig, ScrollDirection}; /// Draw an animated background (gradient, concentric circles, grid dots, halo, or heropattern). pub(super) fn draw_animated_background( @@ -18,16 +21,21 @@ pub(super) fn draw_animated_background( canvas.translate((bg.x + scroll_x, bg.y + scroll_y)); match &bg.preset { - BackgroundPreset::GradientShift(cfg) => - draw_bg_gradient_shift(canvas, cfg, bg.speed, bg.direction.as_ref(), time, width, height), - BackgroundPreset::GridDots(cfg) => - draw_bg_grid_dots(canvas, cfg, time, width, height), - BackgroundPreset::ConcentricCircles(cfg) => - draw_bg_concentric_circles(canvas, cfg, bg.speed, time, width, height), - BackgroundPreset::Halo(cfg) => - draw_bg_halo(canvas, cfg, bg.speed, time, width, height), - BackgroundPreset::Heropattern(cfg) => - draw_bg_heropattern(canvas, cfg, time, width, height), + BackgroundPreset::GradientShift(cfg) => draw_bg_gradient_shift( + canvas, + cfg, + bg.speed, + bg.direction.as_ref(), + time, + width, + height, + ), + BackgroundPreset::GridDots(cfg) => draw_bg_grid_dots(canvas, cfg, time, width, height), + BackgroundPreset::ConcentricCircles(cfg) => { + draw_bg_concentric_circles(canvas, cfg, bg.speed, time, width, height) + } + BackgroundPreset::Halo(cfg) => draw_bg_halo(canvas, cfg, bg.speed, time, width, height), + BackgroundPreset::Heropattern(cfg) => draw_bg_heropattern(canvas, cfg, time, width, height), } canvas.restore(); @@ -69,7 +77,13 @@ pub(super) fn draw_world_bg_with_parallax( let offset_y = -(cam_y % spacing); canvas.save(); canvas.translate((offset_x, offset_y)); - draw_animated_background(canvas, bg, time, width + spacing * 2.0, height + spacing * 2.0); + draw_animated_background( + canvas, + bg, + time, + width + spacing * 2.0, + height + spacing * 2.0, + ); canvas.restore(); } } @@ -90,7 +104,8 @@ fn draw_bg_gradient_shift( return; } - let base_colors: Vec = cfg.colors.iter().map(|c| color4f_from_hex(c)).collect(); + let base_colors: Vec = + cfg.colors.iter().map(|c| color4f_from_hex(c)).collect(); // Direction determines rotation sense; default is cw let sign = match direction { @@ -184,20 +199,14 @@ pub(super) fn subdivide_gradient_stops( } /// Soft colored glow zones (halo preset). -fn draw_bg_halo( - canvas: &Canvas, - cfg: &HaloConfig, - speed: f32, - time: f32, - width: f32, - height: f32, -) { +fn draw_bg_halo(canvas: &Canvas, cfg: &HaloConfig, speed: f32, time: f32, width: f32, height: f32) { for (i, zone) in cfg.zones.iter().enumerate() { let cx = zone.x * width; let cy = zone.y * height; let base_radius = zone.radius * width.max(height); // Each particle gets a unique phase and slightly different frequency - let phase = (zone.x * 17.3 + zone.y * 31.7 + i as f32 * 0.73).fract() * std::f32::consts::TAU; + let phase = + (zone.x * 17.3 + zone.y * 31.7 + i as f32 * 0.73).fract() * std::f32::consts::TAU; let freq = speed * (0.7 + (zone.x * 13.1 + zone.y * 7.9).fract() * 0.6); let breath = 1.0 + 0.15 * (time * freq + phase).sin(); let radius = base_radius * breath; @@ -256,13 +265,7 @@ fn draw_bg_concentric_circles( } /// Animated dot grid pattern. -fn draw_bg_grid_dots( - canvas: &Canvas, - cfg: &GridDotsConfig, - time: f32, - width: f32, - height: f32, -) { +fn draw_bg_grid_dots(canvas: &Canvas, cfg: &GridDotsConfig, time: f32, width: f32, height: f32) { let mut paint = paint_from_hex(&cfg.color); paint.set_anti_alias(true); @@ -306,7 +309,10 @@ fn draw_bg_heropattern( // Build the SVG source with color/opacity substituted let svg_content = format!( r#"{}"#, - def.width, def.height, def.width, def.height, + def.width, + def.height, + def.width, + def.height, def.svg_paths .replace("{{color}}", &cfg.color) .replace("{{opacity}}", &cfg.opacity.to_string()), @@ -361,17 +367,30 @@ fn draw_bg_heropattern( let margin = tile_w.max(tile_h); canvas.draw_rect( - skia_safe::Rect::from_xywh(-margin, -margin, width + margin * 2.0, height + margin * 2.0), + skia_safe::Rect::from_xywh( + -margin, + -margin, + width + margin * 2.0, + height + margin * 2.0, + ), &paint, ); } /// Interpolate two AnimatedBackground structs. `t` goes from 0.0 (fully `a`) to 1.0 (fully `b`). #[allow(dead_code)] -pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackground, t: f32) -> AnimatedBackground { +pub(super) fn interpolate_animated_bg( + a: &AnimatedBackground, + b: &AnimatedBackground, + t: f32, +) -> AnimatedBackground { let lerp = |x: f32, y: f32| x * (1.0 - t) + y * t; - fn lerp_colors(a_colors: &[String], b_colors: &[String], lerp: impl Fn(f32, f32) -> f32) -> Vec { + fn lerp_colors( + a_colors: &[String], + b_colors: &[String], + lerp: impl Fn(f32, f32) -> f32, + ) -> Vec { let max = a_colors.len().max(b_colors.len()); let mut out = Vec::with_capacity(max); for i in 0..max { @@ -379,11 +398,13 @@ pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackgr let cb = b_colors.get(i).map(|c| color4f_from_hex(c)); match (ca, cb) { (Some(ca), Some(cb)) => { - out.push(format!("#{:02X}{:02X}{:02X}{:02X}", + out.push(format!( + "#{:02X}{:02X}{:02X}{:02X}", (lerp(ca.r, cb.r) * 255.0) as u8, (lerp(ca.g, cb.g) * 255.0) as u8, (lerp(ca.b, cb.b) * 255.0) as u8, - (lerp(ca.a, cb.a) * 255.0) as u8)); + (lerp(ca.a, cb.a) * 255.0) as u8 + )); } (None, Some(_)) => out.push(b_colors[i].clone()), (Some(_), None) => out.push(a_colors[i].clone()), @@ -393,7 +414,11 @@ pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackgr out } - fn lerp_zones(a_zones: &[HaloZone], b_zones: &[HaloZone], lerp: impl Fn(f32, f32) -> f32) -> Vec { + fn lerp_zones( + a_zones: &[HaloZone], + b_zones: &[HaloZone], + lerp: impl Fn(f32, f32) -> f32, + ) -> Vec { let max = a_zones.len().max(b_zones.len()); let mut out = Vec::with_capacity(max); for i in 0..max { @@ -402,10 +427,12 @@ pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackgr let ca = color4f_from_hex(&za.color); let cb = color4f_from_hex(&zb.color); out.push(HaloZone { - color: format!("#{:02X}{:02X}{:02X}", + color: format!( + "#{:02X}{:02X}{:02X}", (lerp(ca.r, cb.r) * 255.0) as u8, (lerp(ca.g, cb.g) * 255.0) as u8, - (lerp(ca.b, cb.b) * 255.0) as u8), + (lerp(ca.b, cb.b) * 255.0) as u8 + ), x: lerp(za.x, zb.x), y: lerp(za.y, zb.y), radius: lerp(za.radius, zb.radius), @@ -423,7 +450,7 @@ pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackgr let preset = match (&a.preset, &b.preset) { (BackgroundPreset::GradientShift(ac), BackgroundPreset::GradientShift(bc)) => { BackgroundPreset::GradientShift(GradientShiftConfig { - colors: lerp_colors(&ac.colors, &bc.colors, &lerp), + colors: lerp_colors(&ac.colors, &bc.colors, lerp), gradient_type: bc.gradient_type.clone(), }) } @@ -431,11 +458,13 @@ pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackgr let ca = color4f_from_hex(&ac.color); let cb = color4f_from_hex(&bc.color); BackgroundPreset::GridDots(GridDotsConfig { - color: format!("#{:02X}{:02X}{:02X}{:02X}", + color: format!( + "#{:02X}{:02X}{:02X}{:02X}", (lerp(ca.r, cb.r) * 255.0) as u8, (lerp(ca.g, cb.g) * 255.0) as u8, (lerp(ca.b, cb.b) * 255.0) as u8, - (lerp(ca.a, cb.a) * 255.0) as u8), + (lerp(ca.a, cb.a) * 255.0) as u8 + ), element_size: lerp(ac.element_size, bc.element_size), spacing: lerp(ac.spacing, bc.spacing), }) @@ -444,11 +473,13 @@ pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackgr let ca = color4f_from_hex(&ac.color); let cb = color4f_from_hex(&bc.color); BackgroundPreset::ConcentricCircles(ConcentricCirclesConfig { - color: format!("#{:02X}{:02X}{:02X}{:02X}", + color: format!( + "#{:02X}{:02X}{:02X}{:02X}", (lerp(ca.r, cb.r) * 255.0) as u8, (lerp(ca.g, cb.g) * 255.0) as u8, (lerp(ca.b, cb.b) * 255.0) as u8, - (lerp(ca.a, cb.a) * 255.0) as u8), + (lerp(ca.a, cb.a) * 255.0) as u8 + ), element_size: lerp(ac.element_size, bc.element_size), spacing: lerp(ac.spacing, bc.spacing), count: bc.count, @@ -456,12 +487,16 @@ pub(super) fn interpolate_animated_bg(a: &AnimatedBackground, b: &AnimatedBackgr } (BackgroundPreset::Halo(ac), BackgroundPreset::Halo(bc)) => { BackgroundPreset::Halo(HaloConfig { - zones: lerp_zones(&ac.zones, &bc.zones, &lerp), + zones: lerp_zones(&ac.zones, &bc.zones, lerp), }) } _ => { // Different preset types: snap to b at midpoint - if t >= 0.5 { b.preset.clone() } else { a.preset.clone() } + if t >= 0.5 { + b.preset.clone() + } else { + a.preset.clone() + } } }; diff --git a/crates/rustmotion/src/engine/render/canvas_guard.rs b/crates/rustmotion/src/engine/render/canvas_guard.rs index fbcd75a..631764b 100644 --- a/crates/rustmotion/src/engine/render/canvas_guard.rs +++ b/crates/rustmotion/src/engine/render/canvas_guard.rs @@ -19,7 +19,9 @@ impl CanvasGuard { #[inline] pub fn new(canvas: &Canvas) -> Self { canvas.save(); - Self { canvas: canvas as *const Canvas } + Self { + canvas: canvas as *const Canvas, + } } } @@ -30,6 +32,8 @@ impl Drop for CanvasGuard { // `new`, and Skia canvases are not Send/Sync — the guard cannot // outlive the borrow because we hold no lifetime, but in practice // every call site keeps the canvas alive for the entire scope. - unsafe { (*self.canvas).restore(); } + unsafe { + (*self.canvas).restore(); + } } } diff --git a/crates/rustmotion/src/engine/render/mod.rs b/crates/rustmotion/src/engine/render/mod.rs index 34c262c..16cb7ac 100644 --- a/crates/rustmotion/src/engine/render/mod.rs +++ b/crates/rustmotion/src/engine/render/mod.rs @@ -6,11 +6,8 @@ pub(crate) use canvas_guard::CanvasGuard; #[allow(unused_imports)] pub use scene::{ - render_frame_v2, render_frame_v2_scaled, - render_scene_frame, render_scene_frame_scaled, - render_scene_frame_scaled_with_prev_bg, - render_scene_hits, - render_world_frame_scaled, - render_scene_bg_scaled, render_scene_fg_scaled, - prepare_scene, deserialize_children, root_style, + deserialize_children, prepare_scene, render_frame_v2, render_frame_v2_scaled, + render_scene_bg_scaled, render_scene_fg_scaled, render_scene_frame, render_scene_frame_scaled, + render_scene_frame_scaled_with_prev_bg, render_scene_hits, render_world_frame_scaled, + root_style, }; diff --git a/crates/rustmotion/src/engine/render/scene.rs b/crates/rustmotion/src/engine/render/scene.rs index 40ce118..8a7c81f 100644 --- a/crates/rustmotion/src/engine/render/scene.rs +++ b/crates/rustmotion/src/engine/render/scene.rs @@ -4,15 +4,15 @@ use skia_safe::{surfaces, Canvas, ClipOp, ColorType, ImageInfo, Paint, Rect}; use super::background::draw_animated_background; use super::background::draw_world_bg_with_parallax; use crate::components::ChildComponent; -use rustmotion_core::engine::animator::safe_div; -use rustmotion_core::engine::renderer::color4f_from_hex; use crate::error::RustmotionError; use crate::schema::{Camera, Scene, SceneLayout, VideoConfig}; use rustmotion_core::css::style::{ - AlignItems as CssAlignItems, CssStyle, Edges, FlexDirection as CssFlexDirection, - Gap, JustifyContent as CssJustifyContent, + AlignItems as CssAlignItems, CssStyle, Edges, FlexDirection as CssFlexDirection, Gap, + JustifyContent as CssJustifyContent, }; use rustmotion_core::css::units::LengthPercentage; +use rustmotion_core::engine::animator::safe_div; +use rustmotion_core::engine::renderer::color4f_from_hex; /// Internal render-time context — bundles per-scene timing/dimension info that /// the scene renderer threads down into its helpers. This is intentionally @@ -40,7 +40,15 @@ pub fn render_frame_v2( _total_frames: u32, root_children: &[ChildComponent], ) -> Result> { - render_frame_v2_scaled(config, scene, frame_index, _total_frames, root_children, 1.0, None) + render_frame_v2_scaled( + config, + scene, + frame_index, + _total_frames, + root_children, + 1.0, + None, + ) } /// Render a frame with an optional scale factor for higher-resolution output. @@ -76,8 +84,8 @@ pub fn render_frame_v2_scaled( None, ); - let mut surface = surfaces::raster(&info, None, None) - .ok_or(RustmotionError::SurfaceCreation)?; + let mut surface = + surfaces::raster(&info, None, None).ok_or(RustmotionError::SurfaceCreation)?; let canvas = surface.canvas(); @@ -87,7 +95,11 @@ pub fn render_frame_v2_scaled( } // Fill background - let bg = scene.resolved_background.color.as_deref().unwrap_or(&config.background); + let bg = scene + .resolved_background + .color + .as_deref() + .unwrap_or(&config.background); canvas.clear(color4f_from_hex(bg)); // Animated background gradient(s) — with optional transition crossfade @@ -109,11 +121,16 @@ pub fn render_frame_v2_scaled( if progress < 1.0 { // Draw previous background with fading alpha let bg_info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); if let Some(mut prev_surface) = surfaces::raster(&bg_info, None, None) { let prev_canvas = prev_surface.canvas(); - if scale_factor != 1.0 { prev_canvas.scale((scale_factor, scale_factor)); } + if scale_factor != 1.0 { + prev_canvas.scale((scale_factor, scale_factor)); + } prev_canvas.clear(skia_safe::Color4f::new(0.0, 0.0, 0.0, 0.0)); for anim_bg in &prev.animated { draw_animated_background(prev_canvas, anim_bg, continuous_time, w, h); @@ -126,7 +143,9 @@ pub fn render_frame_v2_scaled( // factor returns to its prior value after restore — adding // another canvas.scale() afterwards would compound it. canvas.save(); - if scale_factor != 1.0 { canvas.reset_matrix(); } + if scale_factor != 1.0 { + canvas.reset_matrix(); + } canvas.draw_image(&snapshot, (0.0, 0.0), Some(&paint)); canvas.restore(); } @@ -134,11 +153,16 @@ pub fn render_frame_v2_scaled( if progress > 0.0 { // Draw current background with growing alpha let bg_info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); if let Some(mut cur_surface) = surfaces::raster(&bg_info, None, None) { let cur_canvas = cur_surface.canvas(); - if scale_factor != 1.0 { cur_canvas.scale((scale_factor, scale_factor)); } + if scale_factor != 1.0 { + cur_canvas.scale((scale_factor, scale_factor)); + } cur_canvas.clear(skia_safe::Color4f::new(0.0, 0.0, 0.0, 0.0)); for anim_bg in &cur_bg.animated { draw_animated_background(cur_canvas, anim_bg, continuous_time, w, h); @@ -147,7 +171,9 @@ pub fn render_frame_v2_scaled( let mut paint = Paint::default(); paint.set_alpha_f(progress); canvas.save(); - if scale_factor != 1.0 { canvas.reset_matrix(); } + if scale_factor != 1.0 { + canvas.reset_matrix(); + } canvas.draw_image(&snapshot, (0.0, 0.0), Some(&paint)); canvas.restore(); } @@ -155,12 +181,24 @@ pub fn render_frame_v2_scaled( } else { // Transition ended — keep using continuous time for the rest of the scene for anim_bg in &cur_bg.animated { - draw_animated_background(canvas, anim_bg, continuous_time, config.width as f32, config.height as f32); + draw_animated_background( + canvas, + anim_bg, + continuous_time, + config.width as f32, + config.height as f32, + ); } } } else { for anim_bg in &cur_bg.animated { - draw_animated_background(canvas, anim_bg, time as f32, config.width as f32, config.height as f32); + draw_animated_background( + canvas, + anim_bg, + time as f32, + config.width as f32, + config.height as f32, + ); } } @@ -178,7 +216,13 @@ pub fn render_frame_v2_scaled( // Apply virtual camera transform let camera_guard = if let Some(ref camera) = scene.camera { let g = super::CanvasGuard::new(canvas); - apply_camera_transform(canvas, camera, time as f32, config.width as f32, config.height as f32); + apply_camera_transform( + canvas, + camera, + time as f32, + config.width as f32, + config.height as f32, + ); Some(g) } else { None @@ -319,7 +363,11 @@ fn render_with_new_pipeline_iter<'a, I>( scene_duration: ctx.scene_duration, }); let built = build_scene_from_refs(root_children, (viewport_w, viewport_h), root_css, anim); - let layout = run_layout(&built.root, (viewport_w, viewport_h), &ConversionContext::default()); + let layout = run_layout( + &built.root, + (viewport_w, viewport_h), + &ConversionContext::default(), + ); let dispatcher = LegacyPaintDispatcher::new(&built.components); let frame = PaintFrame { time: ctx.time, @@ -348,8 +396,16 @@ fn paint_decorative_fullscreen( if let Some(timed) = child.component.as_timed() { let (start_at, end_at) = timed.timing(); - if let Some(s) = start_at { if ctx.time < s { return; } } - if let Some(e) = end_at { if ctx.time > e { return; } } + if let Some(s) = start_at { + if ctx.time < s { + return; + } + } + if let Some(e) = end_at { + if ctx.time > e { + return; + } + } } let props = match child.component.as_animatable() { @@ -363,9 +419,13 @@ fn paint_decorative_fullscreen( } None => AnimatedProperties::default(), }; - if props.opacity <= 0.0 { return; } + if props.opacity <= 0.0 { + return; + } - let Some(painter) = child.component.as_painter() else { return; }; + let Some(painter) = child.component.as_painter() else { + return; + }; let local = BoxLayout { x: 0.0, @@ -409,10 +469,7 @@ pub fn deserialize_children(scene: &Scene) -> Vec { } /// Deserialize a scene's children — ready for render_frame_v2. -pub fn prepare_scene( - scene: &Scene, - _config: &VideoConfig, -) -> Vec { +pub fn prepare_scene(scene: &Scene, _config: &VideoConfig) -> Vec { deserialize_children(scene) } @@ -436,7 +493,15 @@ pub fn render_scene_frame_scaled( scale_factor: f32, ) -> Result> { let children = prepare_scene(scene, config); - render_frame_v2_scaled(config, scene, frame_in_scene, scene_total_frames, &children, scale_factor, None) + render_frame_v2_scaled( + config, + scene, + frame_in_scene, + scene_total_frames, + &children, + scale_factor, + None, + ) } /// Like `render_scene_frame_scaled` but with the previous scene's resolved background for transition interpolation. @@ -450,7 +515,15 @@ pub fn render_scene_frame_scaled_with_prev_bg( prev_bg: Option<(&crate::schema::ResolvedBackground, f64)>, ) -> Result> { let children = prepare_scene(scene, config); - render_frame_v2_scaled(config, scene, frame_in_scene, scene_total_frames, &children, scale_factor, prev_bg) + render_frame_v2_scaled( + config, + scene, + frame_in_scene, + scene_total_frames, + &children, + scale_factor, + prev_bg, + ) } /// Compute the per-frame enriched hit-map for a scene at video resolution. @@ -463,7 +536,9 @@ pub fn render_scene_hits( scene: &Scene, frame_in_scene: u32, ) -> Vec { - use rustmotion_components::box_builder::{build_scene_from_refs, component_kind, BuildAnimationCtx}; + use rustmotion_components::box_builder::{ + build_scene_from_refs, component_kind, BuildAnimationCtx, + }; use rustmotion_components::legacy_dispatch::LegacyPaintDispatcher; use rustmotion_core::css::taffy_bridge::ConversionContext; use rustmotion_core::engine::layout_pass::run_layout; @@ -501,7 +576,10 @@ pub fn render_scene_hits( }; let root_css = root_style(scene.layout.as_ref()); - let anim = Some(BuildAnimationCtx { time, scene_duration: scene.duration }); + let anim = Some(BuildAnimationCtx { + time, + scene_duration: scene.duration, + }); let built = build_scene_from_refs(children.iter(), (vw, vh), root_css, anim); let layout = run_layout(&built.root, (vw, vh), &ConversionContext::default()); let dispatcher = LegacyPaintDispatcher::new(&built.components); @@ -517,8 +595,15 @@ pub fn render_scene_hits( hits.into_iter() .filter_map(|h| { - let child = built.components.get(h.node_id as usize).copied().flatten()?; - let pointer = built.root.find(h.node_id).and_then(|n| n.source_path.clone()); + let child = built + .components + .get(h.node_id as usize) + .copied() + .flatten()?; + let pointer = built + .root + .find(h.node_id) + .and_then(|n| n.source_path.clone()); Some(EnrichedHit { node_id: h.node_id, kind: component_kind(&child.component).to_string(), @@ -543,10 +628,13 @@ pub fn render_world_frame_scaled( let time = frame_in_view as f64 / fps as f64; let info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); - let mut surface = surfaces::raster(&info, None, None) - .ok_or(RustmotionError::SurfaceCreation)?; + let mut surface = + surfaces::raster(&info, None, None).ok_or(RustmotionError::SurfaceCreation)?; let canvas = surface.canvas(); if scale_factor != 1.0 { @@ -557,7 +645,11 @@ pub fn render_world_frame_scaled( let vh = config.height as f32; // 1. Draw base background (view-level or video-level) - let bg_color = view.background.color.as_deref().unwrap_or(&config.background); + let bg_color = view + .background + .color + .as_deref() + .unwrap_or(&config.background); canvas.clear(color4f_from_hex(bg_color)); // Pre-compute camera position for background parallax @@ -571,7 +663,8 @@ pub fn render_world_frame_scaled( // Find the "active" scene (the one currently being displayed, latest in sequence) // and optionally a "previous" scene during camera pan for crossfade - let active_scene_idx = visible.iter() + let active_scene_idx = visible + .iter() .filter(|v| !v.is_persisted) .map(|v| v.scene_idx) .max(); @@ -585,9 +678,7 @@ pub fn render_world_frame_scaled( }; // Check if we're during a camera pan (two non-persisted scenes visible) - let non_persisted: Vec<_> = visible.iter() - .filter(|v| !v.is_persisted) - .collect(); + let non_persisted: Vec<_> = visible.iter().filter(|v| !v.is_persisted).collect(); if non_persisted.len() >= 2 { // Crossfade between outgoing and incoming scene backgrounds @@ -612,7 +703,8 @@ pub fn render_world_frame_scaled( let (_, _scene_b_start) = timeline.scene_windows[scene_b_idx]; let pan_start = timeline.scene_windows[scene_b_idx].0 - pan_half; let pan_end = timeline.scene_windows[scene_b_idx].0 + pan_half; - let crossfade = safe_div(time - pan_start, pan_end - pan_start, 1.0).clamp(0.0, 1.0) as f32; + let crossfade = + safe_div(time - pan_start, pan_end - pan_start, 1.0).clamp(0.0, 1.0) as f32; // Draw scene A backgrounds with fading alpha if crossfade < 1.0 { @@ -621,17 +713,30 @@ pub fn render_world_frame_scaled( } } // Draw scene B backgrounds with growing alpha - if crossfade > 0.0 && bgs_a as *const _ != bgs_b as *const _ { + if crossfade > 0.0 && !std::ptr::eq(bgs_a, bgs_b) { // If backgrounds are different, create a temporary surface for B and blend let bg_info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); if let Some(mut bg_surface) = surfaces::raster(&bg_info, None, None) { let bg_canvas = bg_surface.canvas(); - if scale_factor != 1.0 { bg_canvas.scale((scale_factor, scale_factor)); } + if scale_factor != 1.0 { + bg_canvas.scale((scale_factor, scale_factor)); + } bg_canvas.clear(skia_safe::Color4f::new(0.0, 0.0, 0.0, 0.0)); for bg in bgs_b { - draw_world_bg_with_parallax(bg_canvas, bg, time as f32, vw, vh, cam_x, cam_y); + draw_world_bg_with_parallax( + bg_canvas, + bg, + time as f32, + vw, + vh, + cam_x, + cam_y, + ); } let snapshot = bg_surface.image_snapshot(); let mut paint = skia_safe::Paint::default(); @@ -639,7 +744,9 @@ pub fn render_world_frame_scaled( // save()/restore() already bracket the matrix change; an // extra canvas.scale() after restore would compound it. canvas.save(); - if scale_factor != 1.0 { canvas.reset_matrix(); } + if scale_factor != 1.0 { + canvas.reset_matrix(); + } canvas.draw_image(&snapshot, (0.0, 0.0), Some(&paint)); canvas.restore(); } @@ -665,7 +772,9 @@ pub fn render_world_frame_scaled( for vis in &visible { let scene = &view.scenes[vis.scene_idx]; // Use world-position if specified, otherwise fall back to horizontal grid - let (wx, wy) = scene.world_position.as_ref() + let (wx, wy) = scene + .world_position + .as_ref() .map(|p| (p.x, p.y)) .unwrap_or((vw / 2.0 + vis.scene_idx as f32 * vw, vh / 2.0)); @@ -746,10 +855,15 @@ pub fn render_world_frame_scaled( let row_bytes = scaled_w as usize * 4; let mut pixels = vec![0u8; row_bytes * scaled_h as usize]; let dst_info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); - surface.read_pixels(&dst_info, &mut pixels, row_bytes, (0, 0)) - .then_some(()).ok_or(RustmotionError::PixelRead)?; + surface + .read_pixels(&dst_info, &mut pixels, row_bytes, (0, 0)) + .then_some(()) + .ok_or(RustmotionError::PixelRead)?; Ok(pixels) } @@ -765,29 +879,51 @@ pub fn render_scene_bg_scaled( let scaled_h = (config.height as f32 * scale_factor) as i32; let mut time = frame_in_scene as f64 / config.fps as f64; if let Some(freeze_at) = scene.freeze_at { - if time > freeze_at { time = freeze_at; } + if time > freeze_at { + time = freeze_at; + } } let info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); - let mut surface = surfaces::raster(&info, None, None) - .ok_or(RustmotionError::SurfaceCreation)?; + let mut surface = + surfaces::raster(&info, None, None).ok_or(RustmotionError::SurfaceCreation)?; let canvas = surface.canvas(); - if scale_factor != 1.0 { canvas.scale((scale_factor, scale_factor)); } + if scale_factor != 1.0 { + canvas.scale((scale_factor, scale_factor)); + } - let bg = scene.resolved_background.color.as_deref().unwrap_or(&config.background); + let bg = scene + .resolved_background + .color + .as_deref() + .unwrap_or(&config.background); canvas.clear(color4f_from_hex(bg)); for anim_bg in &scene.resolved_background.animated { - draw_animated_background(canvas, anim_bg, time as f32, config.width as f32, config.height as f32); + draw_animated_background( + canvas, + anim_bg, + time as f32, + config.width as f32, + config.height as f32, + ); } let row_bytes = scaled_w as usize * 4; let mut pixels = vec![0u8; row_bytes * scaled_h as usize]; let dst_info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); - surface.read_pixels(&dst_info, &mut pixels, row_bytes, (0, 0)) - .then_some(()).ok_or(RustmotionError::PixelRead)?; + surface + .read_pixels(&dst_info, &mut pixels, row_bytes, (0, 0)) + .then_some(()) + .ok_or(RustmotionError::PixelRead)?; Ok(pixels) } @@ -804,15 +940,22 @@ pub fn render_scene_fg_scaled( let scaled_h = (config.height as f32 * scale_factor) as i32; let mut time = frame_in_scene as f64 / config.fps as f64; if let Some(freeze_at) = scene.freeze_at { - if time > freeze_at { time = freeze_at; } + if time > freeze_at { + time = freeze_at; + } } let info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); - let mut surface = surfaces::raster(&info, None, None) - .ok_or(RustmotionError::SurfaceCreation)?; + let mut surface = + surfaces::raster(&info, None, None).ok_or(RustmotionError::SurfaceCreation)?; let canvas = surface.canvas(); - if scale_factor != 1.0 { canvas.scale((scale_factor, scale_factor)); } + if scale_factor != 1.0 { + canvas.scale((scale_factor, scale_factor)); + } // Transparent background canvas.clear(skia_safe::Color4f::new(0.0, 0.0, 0.0, 0.0)); @@ -829,7 +972,13 @@ pub fn render_scene_fg_scaled( let has_camera = scene.camera.is_some(); if let Some(ref camera) = scene.camera { - apply_camera_transform(canvas, camera, time as f32, config.width as f32, config.height as f32); + apply_camera_transform( + canvas, + camera, + time as f32, + config.width as f32, + config.height as f32, + ); } // Clip scene content to viewport dimensions so scaled elements don't overflow @@ -849,15 +998,22 @@ pub fn render_scene_fg_scaled( ); canvas.restore(); - if has_camera { canvas.restore(); } + if has_camera { + canvas.restore(); + } let row_bytes = scaled_w as usize * 4; let mut pixels = vec![0u8; row_bytes * scaled_h as usize]; let dst_info = ImageInfo::new( - (scaled_w, scaled_h), ColorType::RGBA8888, skia_safe::AlphaType::Premul, None, + (scaled_w, scaled_h), + ColorType::RGBA8888, + skia_safe::AlphaType::Premul, + None, ); - surface.read_pixels(&dst_info, &mut pixels, row_bytes, (0, 0)) - .then_some(()).ok_or(RustmotionError::PixelRead)?; + surface + .read_pixels(&dst_info, &mut pixels, row_bytes, (0, 0)) + .then_some(()) + .ok_or(RustmotionError::PixelRead)?; Ok(pixels) } @@ -913,7 +1069,13 @@ pub(super) fn interpolate_camera_property(camera: &Camera, property: &str, time: } /// Apply camera transform to the canvas: translate, zoom, rotate around scene center. -pub(super) fn apply_camera_transform(canvas: &Canvas, camera: &Camera, time: f32, width: f32, height: f32) { +pub(super) fn apply_camera_transform( + canvas: &Canvas, + camera: &Camera, + time: f32, + width: f32, + height: f32, +) { let x = interpolate_camera_property(camera, "x", time); let y = interpolate_camera_property(camera, "y", time); let zoom = interpolate_camera_property(camera, "zoom", time); diff --git a/crates/rustmotion/src/engine/world.rs b/crates/rustmotion/src/engine/world.rs index e8a9a2f..e597569 100644 --- a/crates/rustmotion/src/engine/world.rs +++ b/crates/rustmotion/src/engine/world.rs @@ -72,7 +72,9 @@ impl WorldTimeline { windows.push((start, end)); // Use world-position if specified, otherwise fall back to horizontal grid - let (wx, wy) = scene.world_position.as_ref() + let (wx, wy) = scene + .world_position + .as_ref() .map(|p| (p.x, p.y)) .unwrap_or((vw / 2.0 + i as f32 * vw, vh / 2.0)); @@ -135,7 +137,8 @@ impl WorldTimeline { // During this pan → interpolate between wp_a and wp_b if time <= pan_end { - let raw_progress = safe_div(time - pan_start, pan_end - pan_start, 1.0).clamp(0.0, 1.0); + let raw_progress = + safe_div(time - pan_start, pan_end - pan_start, 1.0).clamp(0.0, 1.0); let t = ease(raw_progress, easing) as f32; let x = wp_a.x + (wp_b.x - wp_a.x) * t; let y = wp_a.y + (wp_b.y - wp_a.y) * t; @@ -165,11 +168,7 @@ impl WorldTimeline { // The pan to this scene starts at `start - pan_half` and finishes at `start + pan_half` // Animations begin after the pan finishes arriving, so anim_start = start + pan_half // (For the first scene, there's no incoming pan, so anim_start = start) - let anim_start = if i == 0 { - *start - } else { - start + pan_half - }; + let anim_start = if i == 0 { *start } else { start + pan_half }; // Is this scene currently in its active window (including pan margins)? let visible_start = start - pan_half; @@ -183,7 +182,8 @@ impl WorldTimeline { let local_frame = if local_time <= 0.0 { 0 } else { - ((local_time * fps as f64).round() as u32).min(scene_total_frames.saturating_sub(1)) + ((local_time * fps as f64).round() as u32) + .min(scene_total_frames.saturating_sub(1)) }; // Calculate opacity for crossfade during camera pans @@ -202,11 +202,17 @@ impl WorldTimeline { if i > 0 && time >= in_pan_start.max(0.0) && time < in_pan_end { // Fading in: opacity goes 0 → 1 during incoming pan let denom = in_pan_end - in_pan_start.max(0.0); - let progress = safe_div(time - in_pan_start.max(0.0), denom, 1.0).clamp(0.0, 1.0); + let progress = + safe_div(time - in_pan_start.max(0.0), denom, 1.0).clamp(0.0, 1.0); progress as f32 - } else if i < self.scene_windows.len() - 1 && time >= out_pan_start && time <= out_pan_end { + } else if i < self.scene_windows.len() - 1 + && time >= out_pan_start + && time <= out_pan_end + { // Fading out: opacity goes 1 → 0 during outgoing pan - let progress = safe_div(time - out_pan_start, out_pan_end - out_pan_start, 1.0).clamp(0.0, 1.0); + let progress = + safe_div(time - out_pan_start, out_pan_end - out_pan_start, 1.0) + .clamp(0.0, 1.0); 1.0 - progress as f32 } else { 1.0 diff --git a/crates/rustmotion/src/include.rs b/crates/rustmotion/src/include.rs index f66cb05..bbfd7de 100644 --- a/crates/rustmotion/src/include.rs +++ b/crates/rustmotion/src/include.rs @@ -5,8 +5,8 @@ use crate::error::Result; use crate::error::RustmotionError; use crate::schema::{ AnimatedBackground, BackgroundEntry, BackgroundPreset, BackgroundValue, EasingType, - IncludeDirective, ResolvedBackground, ResolvedScenario, ResolvedView, Scene, SceneEntry, - Scenario, ViewType, + IncludeDirective, ResolvedBackground, ResolvedScenario, ResolvedView, Scenario, Scene, + SceneEntry, ViewType, }; const MAX_INCLUDE_DEPTH: u8 = 8; @@ -27,7 +27,7 @@ pub fn resolve_includes(scenario: Scenario, source: &IncludeSource) -> Result Result Result, included_paths: &mut Vec, ) -> Result> { - let is_remote = directive.include.starts_with("http://") - || directive.include.starts_with("https://"); + let is_remote = + directive.include.starts_with("http://") || directive.include.starts_with("https://"); let (json_str, child_source) = if is_remote { let body = fetch_remote(&directive.include)?; @@ -122,9 +129,10 @@ fn fetch_and_resolve( (body, child_source) } else { let path = resolve_local_path(&directive.include, parent_source)?; - let body = std::fs::read_to_string(&path).map_err(|_| { - RustmotionError::IncludeFileNotFound { path: path.display().to_string() } - })?; + let body = + std::fs::read_to_string(&path).map_err(|_| RustmotionError::IncludeFileNotFound { + path: path.display().to_string(), + })?; // Track this included file for watch mode included_paths.push(path.clone()); let child_source = IncludeSource::File(path); @@ -148,7 +156,13 @@ fn fetch_and_resolve( audio.extend(child_scenario.audio); // Recursively resolve any nested includes - let mut scenes = resolve_entries(child_scenario.scenes, &child_source, depth, audio, included_paths)?; + let mut scenes = resolve_entries( + child_scenario.scenes, + &child_source, + depth, + audio, + included_paths, + )?; // Apply scene index filter if specified if let Some(ref indices) = directive.scenes { @@ -159,7 +173,7 @@ fn fetch_and_resolve( index: idx, path: directive.include.clone(), total, - }.into()); + }); } } let mut slots: Vec> = scenes.into_iter().map(Some).collect(); @@ -178,27 +192,30 @@ fn fetch_and_resolve( fn resolve_local_path(relative: &str, source: &IncludeSource) -> Result { match source { IncludeSource::File(parent_path) => { - let parent_dir = parent_path - .parent() - .unwrap_or_else(|| Path::new(".")); + let parent_dir = parent_path.parent().unwrap_or_else(|| Path::new(".")); Ok(parent_dir.join(relative)) } - IncludeSource::Inline => { - return Err(RustmotionError::IncludeInlinePath { - path: relative.to_string(), - }.into()); - } + IncludeSource::Inline => Err(RustmotionError::IncludeInlinePath { + path: relative.to_string(), + }), } } fn fetch_remote(url: &str) -> Result { let response = ureq::get(url) .call() - .map_err(|e| RustmotionError::IncludeRemoteFetch { url: url.to_string(), reason: e.to_string() })?; - let body = response - .into_body() - .read_to_string() - .map_err(|e| RustmotionError::IncludeRemoteFetch { url: url.to_string(), reason: e.to_string() })?; + .map_err(|e| RustmotionError::IncludeRemoteFetch { + url: url.to_string(), + reason: e.to_string(), + })?; + let body = + response + .into_body() + .read_to_string() + .map_err(|e| RustmotionError::IncludeRemoteFetch { + url: url.to_string(), + reason: e.to_string(), + })?; Ok(body) } @@ -212,11 +229,14 @@ fn resolve_entry( templates: &HashMap, ) -> Result { let base = if let Some(ref name) = entry.template_ref { - let tmpl = templates.get(name).ok_or_else(|| { - RustmotionError::UnknownBackgroundTemplate { name: name.clone() } - })?; + let tmpl = templates + .get(name) + .ok_or_else(|| RustmotionError::UnknownBackgroundTemplate { name: name.clone() })?; let mut base = tmpl.clone(); - deep_merge(&mut base, &serde_json::Value::Object(entry.overrides.clone())); + deep_merge( + &mut base, + &serde_json::Value::Object(entry.overrides.clone()), + ); base } else { serde_json::Value::Object(entry.overrides.clone()) @@ -232,8 +252,7 @@ fn validate_animated_bg(bg: &AnimatedBackground) -> Result<()> { if crate::engine::heropatterns::find_pattern(&cfg.pattern).is_none() { return Err(RustmotionError::UnknownHeropattern { name: cfg.pattern.clone(), - } - .into()); + }); } } Ok(()) diff --git a/crates/rustmotion/src/lib.rs b/crates/rustmotion/src/lib.rs index 4b8c480..e88ffbd 100644 --- a/crates/rustmotion/src/lib.rs +++ b/crates/rustmotion/src/lib.rs @@ -10,10 +10,10 @@ pub use rustmotion_components as components; // Re-export engine from core + local extensions pub mod engine { pub use rustmotion_core::engine::*; + pub mod preload; pub mod render; pub mod world; - pub mod preload; - pub use preload::{prefetch_icons, preextract_video_frames}; + pub use preload::{preextract_video_frames, prefetch_icons}; } // Local modules diff --git a/crates/rustmotion/src/loader.rs b/crates/rustmotion/src/loader.rs index 33327f9..c211d7a 100644 --- a/crates/rustmotion/src/loader.rs +++ b/crates/rustmotion/src/loader.rs @@ -4,16 +4,17 @@ use crate::{include, variables}; use std::path::PathBuf; pub fn load_scenario(input: &PathBuf) -> Result { - let json_str = std::fs::read_to_string(input) - .map_err(|e| RustmotionError::FileRead { path: input.display().to_string(), source: e })?; + let json_str = std::fs::read_to_string(input).map_err(|e| RustmotionError::FileRead { + path: input.display().to_string(), + source: e, + })?; let mut json_value: serde_json::Value = serde_json::from_str(&json_str).map_err(RustmotionError::from)?; // Apply variable defaults for standalone rendering variables::apply_defaults(&mut json_value)?; - let scenario: Scenario = - serde_json::from_value(json_value).map_err(RustmotionError::from)?; + let scenario: Scenario = serde_json::from_value(json_value).map_err(RustmotionError::from)?; include::resolve_includes(scenario, &include::IncludeSource::File(input.clone())) } @@ -22,9 +23,7 @@ pub fn load_scenario_from_source( json: Option<&str>, ) -> Result { match (input, json) { - (Some(_), Some(_)) => { - Err(RustmotionError::ConflictingInput.into()) - } + (Some(_), Some(_)) => Err(RustmotionError::ConflictingInput), (Some(path), None) => load_scenario(path), (None, Some(json_str)) => { let mut json_value: serde_json::Value = @@ -34,9 +33,7 @@ pub fn load_scenario_from_source( serde_json::from_value(json_value).map_err(RustmotionError::from)?; include::resolve_includes(scenario, &include::IncludeSource::Inline) } - (None, None) => { - Err(RustmotionError::MissingInput.into()) - } + (None, None) => Err(RustmotionError::MissingInput), } } @@ -44,8 +41,10 @@ pub fn load_scenario_from_source( /// JSON value, deserialize into `Scenario`, then resolve includes — reusing the /// exact same pipeline as the JSON loader. pub fn load_scenario_from_html(input: &PathBuf) -> Result { - let html = std::fs::read_to_string(input) - .map_err(|e| RustmotionError::FileRead { path: input.display().to_string(), source: e })?; + let html = std::fs::read_to_string(input).map_err(|e| RustmotionError::FileRead { + path: input.display().to_string(), + source: e, + })?; let value = rustmotion_html::html_to_scenario_value(&html) .map_err(|e| RustmotionError::HtmlParse(e.to_string()))?; let scenario: Scenario = serde_json::from_value(value).map_err(RustmotionError::from)?; diff --git a/crates/rustmotion/src/tests.rs b/crates/rustmotion/src/tests.rs index c9657b3..3139c3e 100644 --- a/crates/rustmotion/src/tests.rs +++ b/crates/rustmotion/src/tests.rs @@ -131,8 +131,8 @@ mod component_smoke { use rustmotion_core::engine::layout_pass::run_layout; use rustmotion_core::engine::paint_pass::{paint_tree, PaintFrame}; - let mut surface = skia_safe::surfaces::raster_n32_premul((400, 300)) - .expect("raster surface"); + let mut surface = + skia_safe::surfaces::raster_n32_premul((400, 300)).expect("raster surface"); let canvas = surface.canvas(); let frame = PaintFrame { time: 0.5, @@ -159,15 +159,11 @@ mod component_smoke { let scene = vec![child]; let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let built = build_scene(&scene, (400.0, 300.0)); - let layout = run_layout( - &built.root, - (400.0, 300.0), - &ConversionContext::default(), - ); + let layout = run_layout(&built.root, (400.0, 300.0), &ConversionContext::default()); let dispatcher = LegacyPaintDispatcher::new(&built.components); paint_tree(canvas, &built.root, &layout, &frame, &dispatcher); })); - if let Err(_) = result { + if result.is_err() { failures.push(name); } } @@ -191,14 +187,14 @@ mod component_smoke { use rustmotion_components::box_builder::build_scene; use rustmotion_components::card::Card; use rustmotion_components::legacy_dispatch::LegacyPaintDispatcher; + use rustmotion_core::css::style::{CssStyle, Edges, FlexDirection, Gap}; use rustmotion_core::css::taffy_bridge::ConversionContext; + use rustmotion_core::css::units::LengthPercentage; use rustmotion_core::engine::layout_pass::run_layout; use rustmotion_core::engine::paint_pass::{paint_tree, PaintFrame}; - use rustmotion_core::css::style::{CssStyle, Edges, FlexDirection, Gap}; - use rustmotion_core::css::units::LengthPercentage; - let mut surface = skia_safe::surfaces::raster_n32_premul((400, 300)) - .expect("raster surface"); + let mut surface = + skia_safe::surfaces::raster_n32_premul((400, 300)).expect("raster surface"); let canvas = surface.canvas(); let frame = PaintFrame { time: 0.0, @@ -244,15 +240,11 @@ mod component_smoke { let scene = vec![card_child]; let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let built = build_scene(&scene, (400.0, 300.0)); - let layout = run_layout( - &built.root, - (400.0, 300.0), - &ConversionContext::default(), - ); + let layout = run_layout(&built.root, (400.0, 300.0), &ConversionContext::default()); let dispatcher = LegacyPaintDispatcher::new(&built.components); paint_tree(canvas, &built.root, &layout, &frame, &dispatcher); })); - if let Err(_) = result { + if result.is_err() { failures.push(name); } } @@ -288,17 +280,24 @@ mod component_smoke { use rustmotion_core::engine::layout_pass::run_layout; use rustmotion_core::engine::paint_pass::{paint_tree, PaintFrame}; - let mut surface = skia_safe::surfaces::raster_n32_premul((w as i32, h as i32)) - .expect("raster surface"); + let mut surface = + skia_safe::surfaces::raster_n32_premul((w as i32, h as i32)).expect("raster surface"); let canvas = surface.canvas(); canvas.clear(skia_safe::Color4f::new(0.0, 0.0, 0.0, 0.0)); let built = build_scene_with_anim( children, (w as f32, h as f32), - BuildAnimationCtx { time, scene_duration }, + BuildAnimationCtx { + time, + scene_duration, + }, + ); + let layout = run_layout( + &built.root, + (w as f32, h as f32), + &ConversionContext::default(), ); - let layout = run_layout(&built.root, (w as f32, h as f32), &ConversionContext::default()); let dispatcher = LegacyPaintDispatcher::new(&built.components); let frame = PaintFrame { time, @@ -352,7 +351,10 @@ mod component_smoke { let scene = vec![child]; let new_buf = render_new(&scene, 400, 300); let lit = nonzero_pixels(&new_buf); - assert!(lit > 100, "new pipeline produced too few non-zero pixels: {lit}"); + assert!( + lit > 100, + "new pipeline produced too few non-zero pixels: {lit}" + ); } #[test] @@ -405,7 +407,11 @@ mod component_smoke { sum_wx += w * x; sum_w += w; } - if sum_w == 0.0 { None } else { Some(sum_wx / sum_w) } + if sum_w == 0.0 { + None + } else { + Some(sum_wx / sum_w) + } } #[test] @@ -485,5 +491,4 @@ mod component_smoke { "SlideInLeft centroid did not move right (early_cx={early_cx:.1}, late_cx={late_cx:.1}, dx={dx:.1})" ); } - }