diff --git a/.gitignore b/.gitignore index 8a6e6ac5..a39e4ec6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +target # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Cargo.lock b/Cargo.lock index 7229334a..5a0b2e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2426,6 +2426,15 @@ dependencies = [ "syn", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "io-uring" version = "0.7.10" @@ -2686,26 +2695,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linkme" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b1703c00b2a6a70738920544aa51652532cacddfec2e162d2e29eae01e665c" -dependencies = [ - "linkme-impl", -] - -[[package]] -name = "linkme-impl" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d55ca5d5a14363da83bf3c33874b8feaa34653e760d5216d7ef9829c88001a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -4013,8 +4002,8 @@ dependencies = [ "glam", "image", "indicatif", + "inventory", "itertools 0.14.0", - "linkme", "log", "lru", "pollster", @@ -4064,6 +4053,18 @@ dependencies = [ "toml 0.9.5", ] +[[package]] +name = "ranim-examples" +version = "0.1.3" +dependencies = [ + "itertools 0.14.0", + "rand 0.9.2", + "rand_chacha 0.9.0", + "ranim", + "ranim-macros", + "wasm-bindgen", +] + [[package]] name = "ranim-macros" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 25e3bb2e..6a3655f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,10 @@ readme = "README.md" keywords = ["animation", "manim", "wgpu"] exclude = ["website", "assets", "book"] +# Use RUSTFLAGS="-C prefer-dynamic" reduces link time from 500ms to 100ms +[lib] +crate-type = ["rlib", "dylib"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] @@ -91,13 +95,14 @@ typst-svg = "0.13.1" chrono = "0.4.41" reqwest = "0.12.23" slotmap = "1.0.7" +inventory = "0.3.21" [target.'cfg(not(target_family = "wasm"))'.dependencies] egui-winit = { version = "0.32.1", optional = true } which = "8.0.0" flate2 = "1.1.2" reqwest = { version = "0.12.22", features = ["blocking"] } -linkme = "0.3.33" +# linkme = "0.3.33" # web: [target.'cfg(target_family = "wasm")'.dependencies] @@ -137,102 +142,3 @@ pre-release-hook = [ pre-release-replacements = [ { file = "README.md", search = "ranim = \"[^\"]+\"", replace = "{{crate_name}} = \"{{version}}\"" }, ] - -# MARK: Examples -[[example]] -name = "arc" -path = "examples/arc/main.rs" - -[package.metadata.example.arc] -wasm = true - -[[example]] -name = "arc_between_points" -path = "examples/arc_between_points/main.rs" - -[package.metadata.example.arc_between_points] -wasm = true - -[[example]] -name = "basic" -path = "examples/basic/main.rs" - -[package.metadata.example.basic] -wasm = true - -[[example]] -name = "bubble_sort" -path = "examples/bubble_sort/main.rs" - -[package.metadata.example.bubble_sort] -wasm = true - -[[example]] -name = "getting_started0" -path = "examples/getting_started0/main.rs" - -[package.metadata.example.getting_started0] -wasm = true -hide = true - -[[example]] -name = "getting_started1" -path = "examples/getting_started1/main.rs" - -[package.metadata.example.getting_started1] -wasm = true -hide = true - -[[example]] -name = "getting_started2" -path = "examples/getting_started2/main.rs" - -[package.metadata.example.getting_started2] -wasm = true -hide = true - -[[example]] -name = "hanoi" -path = "examples/hanoi/main.rs" - -[package.metadata.example.hanoi] -wasm = true - -[[example]] -name = "hello_ranim" -path = "examples/hello_ranim/main.rs" - -[package.metadata.example.hello_ranim] -wasm = true - -[[example]] -name = "palettes" -path = "examples/palettes/main.rs" - -[[example]] -name = "perspective_blend" -path = "examples/perspective_blend/main.rs" - -[package.metadata.example.perspective_blend] -wasm = true - -[[example]] -name = "ranim_logo" -path = "examples/ranim_logo/main.rs" - -[package.metadata.example.ranim_logo] -wasm = true - -[[example]] -name = "selective_sort" -path = "examples/selective_sort/main.rs" - -[package.metadata.example.selective_sort] -wasm = true - -[[example]] -name = "extract_vitem_visualize" -path = "examples/extract_vitem_visualize/main.rs" - -[package.metadata.example.extract_vitem_visualize] -wasm = true diff --git a/examples/arc/README.md b/examples/arc/README.md deleted file mode 100644 index 5c67ceb1..00000000 --- a/examples/arc/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Arc - -This example demonstrates `Arc`. - -https://github.com/user-attachments/assets/4f8c6f36-e6b7-429b-8d2a-91c21e17dec2 - -> [arc.mp4](../../assets/arc.mp4) - diff --git a/examples/arc/main.rs b/examples/arc/main.rs deleted file mode 100644 index 2532b253..00000000 --- a/examples/arc/main.rs +++ /dev/null @@ -1,78 +0,0 @@ -use itertools::Itertools; -use log::LevelFilter; -use ranim::{ - animation::{fading::FadingAnim, lagged::LaggedAnim}, - color::HueDirection, - glam::dvec2, - items::{Group, vitem::geometry::Arc}, - prelude::*, - timeline::TimeMark, -}; - -#[scene] -#[preview] -#[output(dir = "arc")] -fn arc(r: &mut RanimScene) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - - // let frame_size = app.camera().size; - let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); - let frame_start = dvec2(frame_size.x / -2.0, frame_size.y / -2.0); - - let start_color = color!("#FF8080FF"); - let end_color = color!("#58C4DDFF"); - - let nrow = 10; - let ncol = 10; - let step_x = frame_size.x / ncol as f64; - let step_y = frame_size.y / nrow as f64; - - let arcs = (0..nrow) - .cartesian_product(0..ncol) - .map(|(i, j)| { - let (i, j) = (i as f64, j as f64); - - let angle = std::f64::consts::PI * (j + 1.0) / ncol as f64 * 360.0 / 180.0; - let radius = step_y / 2.0 * 0.8; - let color = start_color.lerp( - end_color, - i as f32 / (nrow - 1) as f32, - HueDirection::Increasing, - ); - let offset = frame_start + dvec2(j * step_x + step_x / 2.0, i * step_y + step_y / 2.0); - Arc::new(angle, radius).with(|arc| { - arc.stroke_width = 0.12 * (j as f32 + 0.02) / ncol as f32; - arc.set_stroke_color(color) - .put_center_on(offset.extend(0.0)); - }) - }) - .collect::>(); - let r_arcs = r.insert(arcs); - - r.timeline_mut(&r_arcs) - .play_with(|arcs| arcs.lagged(0.2, |arc| arc.fade_in()).with_duration(3.0)); - - r.insert_time_mark( - r.timelines().max_total_secs(), - TimeMark::Capture("preview.png".to_string()), - ); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(arc_scene); - #[cfg(not(feature = "app"))] - render_scene(arc_scene) -} diff --git a/examples/arc_between_points/README.md b/examples/arc_between_points/README.md deleted file mode 100644 index b2dd3d18..00000000 --- a/examples/arc_between_points/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# ArcBetweenPoints Example - -This example demonstrates `ArcBetweenPoints`. - -https://github.com/user-attachments/assets/9595ce1d-2390-4d4d-958f-9299755460b7 - -> [arc_between_points.mp4](../../assets/arc_between_points.mp4) diff --git a/examples/arc_between_points/main.rs b/examples/arc_between_points/main.rs deleted file mode 100644 index dfa74061..00000000 --- a/examples/arc_between_points/main.rs +++ /dev/null @@ -1,76 +0,0 @@ -use itertools::Itertools; -use log::LevelFilter; -use ranim::{ - animation::{fading::FadingAnim, lagged::LaggedAnim}, - color::HueDirection, - glam::{DMat2, dvec2}, - items::{Group, vitem::geometry::ArcBetweenPoints}, - prelude::*, - timeline::TimeMark, -}; - -#[scene] -#[preview] -#[output(dir = "arc_between_points")] -fn arc_between_points(r: &mut RanimScene) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - let center = dvec2(0.0, 0.0); - - let start_color = color!("#FF8080FF"); - let end_color = color!("#58C4DDFF"); - let ntan = 16; - let nrad = 5; - - let arcs = (0..nrad) - .map(|i| { - let radius = 6.0 * (i + 1) as f64 / nrad as f64; - let width = 0.12 * ((nrad - i) as f64 / nrad as f64).powi(2); - let angle = std::f64::consts::PI * 7.0 / 4.0 * (i + 1) as f64 / nrad as f64; - (radius, width, angle) - }) - .cartesian_product(0..ntan) - .map(|((rad, width, angle), j)| { - let color = start_color.lerp( - end_color, - j as f32 / (ntan - 1) as f32, - HueDirection::Increasing, - ); - let vec = DMat2::from_angle(std::f64::consts::PI * 2.0 / ntan as f64 * j as f64) - * dvec2(rad, 0.0); - ArcBetweenPoints::new(center.extend(0.0), (center + vec).extend(0.0), angle).with( - |arc| { - arc.stroke_width = width as f32; - arc.set_stroke_color(color); - }, - ) - }) - .collect::>(); - let r_arcs = r.insert(arcs); - - r.timeline_mut(&r_arcs) - .play_with(|arcs| arcs.lagged(0.2, |arc| arc.fade_in()).with_duration(3.0)); - - r.insert_time_mark( - r.timelines().max_total_secs(), - TimeMark::Capture("preview.png".to_string()), - ); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(arc_between_points_scene); - #[cfg(not(feature = "app"))] - render_scene(arc_between_points_scene) -} diff --git a/examples/basic/README.md b/examples/basic/README.md deleted file mode 100644 index b04745a2..00000000 --- a/examples/basic/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Basic Example - -This example demonstrates the basic usage of `ranim`. - -https://github.com/user-attachments/assets/dadab1ab-65e4-4cd3-bda4-b2ef97c61b0b - -> [basic.mp4](../../assets/basic.mp4) diff --git a/examples/bubble_sort/main.rs b/examples/bubble_sort/main.rs deleted file mode 100644 index d51a1b08..00000000 --- a/examples/bubble_sort/main.rs +++ /dev/null @@ -1,142 +0,0 @@ -use log::LevelFilter; -use rand::{SeedableRng, seq::SliceRandom}; -use ranim::{ - animation::transform::TransformAnim, - color::palettes::manim, - components::Anchor, - glam::{DVec3, dvec2}, - items::vitem::geometry::Rectangle, - prelude::*, - timeline::TimeMark, - utils::rate_functions::linear, -}; - -fn bubble_sort(r: &mut RanimScene, num: usize) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - - let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); - let padded_frame_size = frame_size * 0.9; - - let anim_step_duration = 15.0 / num.pow(2) as f64; - - let width_unit = padded_frame_size.x / num as f64; - let height_unit = padded_frame_size.y / num as f64; - - let mut rng = rand_chacha::ChaChaRng::seed_from_u64(114514); - let mut heights = (1..=num) - .map(|x| x as f64 * height_unit) - .collect::>(); - heights.shuffle(&mut rng); - - let padded_frame_bl = dvec2(padded_frame_size.x / -2.0, padded_frame_size.y / -2.0); - let mut r_rects = heights - .iter() - .enumerate() - .map(|(i, &height)| { - let target_bc_coord = - padded_frame_bl.extend(0.0) + DVec3::X * (width_unit * i as f64 + width_unit / 2.0); - let rect = Rectangle::new(width_unit, height).with(|rect| { - rect.stroke_width = 0.0; - rect.set_fill_color(manim::WHITE.with_alpha(0.5)) - .scale(DVec3::splat(0.8)) - .put_anchor_on(Anchor::edge(0, -1, 0), target_bc_coord); - }); - r.insert_and_show(rect) - }) - .collect::>(); - - let anim_highlight = |rect: Rectangle| { - rect.transform(|data| { - data.set_fill_color(manim::BLUE_C.with_alpha(0.5)); - }) - .with_duration(anim_step_duration) - .with_rate_func(linear) - }; - let anim_unhighlight = |rect: Rectangle| { - rect.transform(|data| { - data.set_fill_color(manim::WHITE.with_alpha(0.5)); - }) - .with_duration(anim_step_duration) - .with_rate_func(linear) - }; - let shift_right = DVec3::X * width_unit; - let swap_shift = [shift_right, -shift_right]; - let anim_swap = |timeline: &mut RanimScene, r_rectab: &[&ItemId; 2]| { - let timelines = timeline.timeline_mut(r_rectab); - timelines - .into_iter() - .zip(swap_shift.iter()) - .for_each(|(timeline, shift)| { - timeline.play_with(|rect| { - rect.transform(|data| { - data.shift(*shift); - }) - .with_duration(anim_step_duration) - .with_rate_func(linear) - }); - }); - }; - - for i in (1..num).rev() { - for j in 0..i { - r.timeline_mut(&[&r_rects[j], &r_rects[j + 1]]) - .into_iter() - .for_each(|timeline| { - timeline.play_with(anim_highlight); - }); - if heights[j] > heights[j + 1] { - anim_swap(r, &[&r_rects[j], &r_rects[j + 1]]); - r.timelines_mut().sync(); - heights.swap(j, j + 1); - r_rects.swap(j, j + 1); - } - r.timeline_mut(&[&r_rects[j], &r_rects[j + 1]]) - .into_iter() - .for_each(|timeline| { - timeline.play_with(anim_unhighlight); - }); - r.timelines_mut().sync(); - } - } - - r.insert_time_mark( - r.timelines().max_total_secs(), - TimeMark::Capture(format!("preview-{num}.png")), - ); -} - -#[scene] -#[preview] -#[output(dir = "bubble_sort")] -fn bubble_sort_10(r: &mut RanimScene) { - bubble_sort(r, 10); -} - -#[scene(name = "bubble_sort")] -#[preview] -#[output(dir = "bubble_sort")] -fn bubble_sort_100(r: &mut RanimScene) { - bubble_sort(r, 100); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(bubble_sort_100_scene); - #[cfg(not(feature = "app"))] - { - render_scene(bubble_sort_10_scene); - render_scene(bubble_sort_100_scene); - } -} diff --git a/examples/getting_started0/main.rs b/examples/getting_started0/main.rs deleted file mode 100644 index f3d7e029..00000000 --- a/examples/getting_started0/main.rs +++ /dev/null @@ -1,49 +0,0 @@ -use log::LevelFilter; -use ranim::{ - animation::fading::FadingAnim, color::palettes::manim, items::vitem::geometry::Square, - prelude::*, -}; - -#[scene] -#[preview] -#[output(dir = "getting_started0")] -fn getting_started0(r: &mut RanimScene) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - // A Square with size 2.0 and color blue - let square = Square::new(2.0).with(|square| { - square.set_color(manim::BLUE_C); - }); - - let r_square = r.insert(square); - { - let timeline = r.timeline_mut(&r_square); - timeline - .play_with(|square| square.fade_in()) - .forward(1.0) - .hide() - .forward(1.0) - .show() - .forward(1.0) - .play_with(|square| square.fade_out()); - } -} -// ANCHOR_END: construct - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(getting_started0_scene); - #[cfg(not(feature = "app"))] - render_scene(getting_started0_scene); -} diff --git a/examples/getting_started1/main.rs b/examples/getting_started1/main.rs deleted file mode 100644 index 621682ac..00000000 --- a/examples/getting_started1/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -use log::LevelFilter; -use ranim::{ - animation::{creation::WritingAnim, transform::TransformAnim}, - color::palettes::manim, - items::vitem::{ - VItem, - geometry::{Circle, Square}, - }, - prelude::*, -}; - -// ANCHOR: construct -#[scene] -#[preview] -#[output(dir = "getting_started1")] -fn getting_started1(r: &mut RanimScene) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - // A Square with size 2.0 and color blue - let square = Square::new(2.0).with(|square| { - square.set_color(manim::BLUE_C); - }); - - let circle = Circle::new(2.0).with(|circle| { - circle.set_color(manim::RED_C); - }); - - // In order to do more low-level opeerations, - // sometimes we need to convert the item to a low-level item. - let r_vitem = r.insert(VItem::from(square)); - { - let timeline = r.timeline_mut(&r_vitem); - timeline.play_with(|vitem| vitem.transform_to(VItem::from(circle.clone()))); - timeline.play_with(|vitem| vitem.unwrite()); - } -} -// ANCHOR_END: construct - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(getting_started1_scene); - #[cfg(not(feature = "app"))] - render_scene(getting_started1_scene); -} diff --git a/examples/getting_started2/main.rs b/examples/getting_started2/main.rs deleted file mode 100644 index 0c524d4a..00000000 --- a/examples/getting_started2/main.rs +++ /dev/null @@ -1,75 +0,0 @@ -use log::LevelFilter; -use ranim::{ - animation::{ - creation::{CreationAnim, WritingAnim}, - transform::TransformAnim, - }, - color::palettes::manim, - items::vitem::{ - VItem, - geometry::{Circle, Rectangle, Square}, - }, - prelude::*, - utils::rate_functions::linear, -}; - -#[scene] -#[preview] -#[output(dir = "getting_started2")] -fn getting_started2(r: &mut RanimScene) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - let rect = Rectangle::new(4.0, 9.0 / 4.0).with(|rect| { - rect.set_stroke_color(manim::GREEN_C); - }); - - // The new initialized timeline is hidden by default, use show to start encoding a static anim and make it show - let r_rect: ItemId = r.insert_and(rect, |timeline| { - timeline.show(); - }); - // or use `insert_and_show` - // let r_rect: ItemId = r.insert_and_show(rect) - - r.timelines_mut().forward(1.0); - - let square = Square::new(2.0).with(|square| { - square.set_color(manim::BLUE_C); - }); - let circle = Circle::new(2.0).with(|circle| { - circle.set_color(manim::RED_C); - }); - let r_vitem = r.insert(VItem::from(square)); - { - let timeline = r.timeline_mut(&r_vitem); - timeline - .forward(1.0) - .play_with(|vitem| vitem.create()) - .play_with(|vitem| { - vitem - .transform_to(VItem::from(circle.clone())) - .with_rate_func(linear) - }) - .play_with(|vitem| vitem.unwrite()); - } - - let r_rect: ItemId = r.map(r_rect, VItem::from); - r.timeline_mut(&r_rect).play_with(|rect| rect.uncreate()); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(getting_started2_scene); - #[cfg(not(feature = "app"))] - render_scene(getting_started2_scene); -} diff --git a/examples/hello_ranim/main.rs b/examples/hello_ranim/main.rs deleted file mode 100644 index 752dc3b1..00000000 --- a/examples/hello_ranim/main.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::f64::consts::PI; - -use log::LevelFilter; -use ranim::{ - animation::{creation::WritingAnim, fading::FadingAnim, transform::TransformAnim}, - color::palettes::manim, - glam::DVec3, - items::vitem::{ - VItem, - geometry::{Circle, Square}, - }, - prelude::*, - timeline::TimeMark, -}; - -#[scene] -#[preview] -#[output(dir = "hello_ranim")] -fn hello_ranim(r: &mut RanimScene) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - let square = Square::new(2.0).with(|square| { - square.set_color(manim::BLUE_C); - }); - - let r_square = r.insert(square); - { - let timeline = r.timeline_mut(&r_square); - timeline.play_with(|square| square.fade_in()); - }; - - let circle = Circle::new(2.0).with(|circle| { - circle - .set_color(manim::RED_C) - .rotate(-PI / 4.0 + PI, DVec3::Z); - }); - - let r_vitem = r.map(r_square, VItem::from); - { - let timeline = r.timeline_mut(&r_vitem); - timeline.play_with(|state| state.transform_to(circle.into())); - timeline.forward(1.0); - let circle = timeline.state().clone(); - timeline.play_with(|circle| circle.unwrite()); - timeline.play(circle.write()); - timeline.play_with(|circle| circle.fade_out()); - }; - - r.insert_time_mark(3.7, TimeMark::Capture("preview.png".to_string())); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(hello_ranim_scene); - #[cfg(not(feature = "app"))] - render_scene(hello_ranim_scene) -} diff --git a/examples/palettes/README.md b/examples/palettes/README.md deleted file mode 100644 index 01fc6e93..00000000 --- a/examples/palettes/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Palettes - -Palettes from manim - -![palette](../../assets/palettes.png) diff --git a/examples/palettes/main.rs b/examples/palettes/main.rs deleted file mode 100644 index e71208fb..00000000 --- a/examples/palettes/main.rs +++ /dev/null @@ -1,77 +0,0 @@ -use log::LevelFilter; -use ranim::{ - color::palettes::manim::*, - components::Anchor, - glam::{dvec2, dvec3}, - items::{Group, vitem::geometry::Rectangle}, - prelude::*, - timeline::TimeMark, -}; - -#[scene] -#[preview] -#[output(dir = "palettes")] -fn palettes(r: &mut RanimScene) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); - let padded_frame_size = frame_size * 0.9; - - let colors = vec![ - vec![BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A], - vec![TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A], - vec![GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A], - vec![YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A], - vec![GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A], - vec![RED_E, RED_D, RED_C, RED_B, RED_A], - vec![MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A], - vec![PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A], - vec![GREY_E, GREY_D, GREY_C, GREY_B, GREY_A], - vec![WHITE, BLACK, GREEN_SCREEN], - vec![GREY_BROWN, LIGHT_BROWN, PINK, LIGHT_PINK, ORANGE], - ]; - - let padded_frame_start = dvec2(padded_frame_size.x / -2.0, padded_frame_size.y / -2.0); - let h_step = padded_frame_size.y / colors.len() as f64; - - let squares = colors - .iter() - .enumerate() - .flat_map(|(i, row)| { - let y = i as f64 * h_step; - let w_step = padded_frame_size.x / row.len() as f64; - row.iter().enumerate().map(move |(j, color)| { - let x = j as f64 * w_step; - Rectangle::new(w_step as f64, h_step as f64).with(|rect| { - rect.stroke_width = 0.0; - - rect.set_color(*color).put_anchor_on( - Anchor::edge(-1, -1, 0), - padded_frame_start.extend(0.0) + dvec3(x, y, 0.0), - ); - }) - }) - }) - .collect::>(); - r.insert_and_show(squares); - r.insert_time_mark(0.0, TimeMark::Capture("preview.png".to_string())); - r.timelines_mut().forward(0.01); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(palettes_scene); - #[cfg(not(feature = "app"))] - render_scene(palettes_scene); -} diff --git a/examples/selective_sort/main.rs b/examples/selective_sort/main.rs deleted file mode 100644 index b0afd495..00000000 --- a/examples/selective_sort/main.rs +++ /dev/null @@ -1,132 +0,0 @@ -use glam::{DVec3, dvec2}; -use log::LevelFilter; -use rand::{SeedableRng, seq::SliceRandom}; -use ranim::{ - animation::transform::TransformAnim, color::palettes::manim, components::Anchor, - items::vitem::geometry::Rectangle, prelude::*, timeline::TimeMark, - utils::rate_functions::linear, -}; - -fn selective_sort(r: &mut RanimScene, num: usize) { - let _r_cam = r.insert_and_show(CameraFrame::default()); - - let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); - let padded_frame_size = frame_size * 0.9; - - let anim_step_duration = 15.0 / num.pow(2) as f64; - - let width_unit = padded_frame_size.x / num as f64; - let height_unit = padded_frame_size.y / num as f64; - - let mut rng = rand_chacha::ChaChaRng::seed_from_u64(114514); - let mut heights = (1..=num) - .map(|x| x as f64 * height_unit) - .collect::>(); - heights.shuffle(&mut rng); - - let padded_frame_bl = dvec2(padded_frame_size.x / -2.0, padded_frame_size.y / -2.0); - let mut r_rects = heights - .iter() - .enumerate() - .map(|(i, &height)| { - let target_bc_coord = - padded_frame_bl.extend(0.0) + DVec3::X * (width_unit * i as f64 + width_unit / 2.0); - let rect = Rectangle::new(width_unit, height).with(|rect| { - rect.fill_rgba = manim::WHITE.with_alpha(0.5); - rect.scale(DVec3::splat(0.8)) - .put_anchor_on(Anchor::edge(0, -1, 0), target_bc_coord); - }); - r.insert_and_show(rect) - }) - .collect::>(); - - let highlight = |rect: Rectangle| { - rect.transform(|data| { - data.set_color(manim::RED_C).set_fill_opacity(0.5); - }) - .with_duration(anim_step_duration) - .with_rate_func(linear) - }; - let unhighlight = |rect: Rectangle| { - rect.transform(|data| { - data.set_color(manim::WHITE).set_fill_opacity(0.5); - }) - .with_duration(anim_step_duration) - .with_rate_func(linear) - }; - - let shift_right = DVec3::X * width_unit; - for i in 0..num - 1 { - r.timeline_mut(&r_rects[i]).play_with(highlight); - for j in i + 1..num { - r.timeline_mut(&r_rects[j]).play_with(highlight); - r.timelines_mut().sync(); - - if heights[i] > heights[j] { - let dir = [shift_right, -shift_right]; - let color = [manim::BLUE_C, manim::RED_C]; - r.timeline_mut(&[&r_rects[i], &r_rects[j]]) - .iter_mut() - .zip(dir) - .zip(color) - .for_each(|((timeline, dir), color)| { - timeline.play_with(|rect| { - rect.transform(|rect| { - rect.shift(dir * (j - i) as f64) - .set_color(color) - .set_fill_opacity(0.5); - }) - .with_duration(anim_step_duration) - .with_rate_func(linear) - }); - }); - heights.swap(i, j); - r_rects.swap(i, j); - } - r.timeline_mut(&r_rects[j]).play_with(unhighlight); - r.timelines_mut().sync(); - } - r.timeline_mut(&r_rects[i]).play_with(unhighlight); - } - - r.insert_time_mark( - r.timelines().max_total_secs() / 2.0, - TimeMark::Capture(format!("preview-{num}.png")), - ); -} - -#[scene] -#[preview] -#[output(dir = "selective_sort")] -fn selective_sort_10(r: &mut RanimScene) { - selective_sort(r, 10); -} - -#[scene(name = "selective_sort")] -#[preview] -#[output(dir = "selective_sort")] -fn selective_sort_100(r: &mut RanimScene) { - selective_sort(r, 100); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(selective_sort_100_scene); - #[cfg(not(feature = "app"))] - { - render_scene(selective_sort_10_scene); - render_scene(selective_sort_100_scene); - } -} diff --git a/flake.lock b/flake.lock index b7313e3f..3c7706af 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1751984180, - "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", + "lastModified": 1757487488, + "narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", + "rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1752201818, - "narHash": "sha256-d8KczaVT8WFEZdWg//tMAbv8EDyn2YTWcJvSY8gqKBU=", + "lastModified": 1757730403, + "narHash": "sha256-Jxl4OZRVsXs8JxEHUVQn3oPu6zcqFyGGKaFrlNgbzp0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "bd8f8329780b348fedcd37b53dbbee48c08c496d", + "rev": "3232f7f8bd07849fc6f4ae77fe695e0abb2eba2c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 48e1bd75..f6607649 100644 --- a/flake.nix +++ b/flake.nix @@ -61,6 +61,7 @@ wasm-pack wasm-bindgen-cli mdbook-i18n-helpers + measureme ]); }; }); diff --git a/justfile b/justfile index 8cbab4c6..b1c1fd90 100644 --- a/justfile +++ b/justfile @@ -30,6 +30,16 @@ doc-nightly: -rm -r website/static/doc/ cp -r target/doc/ website/static/doc/ +doc-examples: + RUSTDOCFLAGS="--cfg docsrs --cfg docrs_dep --html-in-header ./packages/ranim-examples/docs-rs/header.html" \ + RUSTFLAGS="--cfg docsrs_dep" \ + cargo doc --no-deps -p ranim-examples --document-private-items --all-features \ + --target-dir ./packages/ranim-examples/target/ + + cargo build -p ranim-examples --release --target wasm32-unknown-unknown + wasm-bindgen --target web ./target/wasm32-unknown-unknown/release/ranim_examples.wasm \ + --out-dir ./packages/ranim-examples/target/doc/ranim_examples/pkg + doc: cargo doc --no-deps -p ranim --document-private-items --all-features -rm -r website/static/doc/ diff --git a/packages/ranim-cli/src/cli/render.rs b/packages/ranim-cli/src/cli/render.rs index 9443fac9..bff6b964 100644 --- a/packages/ranim-cli/src/cli/render.rs +++ b/packages/ranim-cli/src/cli/render.rs @@ -31,7 +31,7 @@ pub fn render_command(args: &Args, scenes: &[String]) { .unwrap() .expect("Failed on initial build"); - let all_scenes: Vec<&Scene> = lib.scenes().iter().collect::>(); + let all_scenes: Vec<&Scene> = lib.scenes().collect::>(); let scenes_to_render: Vec<&Scene> = if scenes.is_empty() { all_scenes.clone() } else { diff --git a/packages/ranim-cli/src/lib.rs b/packages/ranim-cli/src/lib.rs index b9f09c40..782ba090 100644 --- a/packages/ranim-cli/src/lib.rs +++ b/packages/ranim-cli/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use async_channel::{Receiver, Sender, bounded}; use libloading::{Library, Symbol}; -use log::{debug, error, info}; +use log::{error, info}; use ranim::Scene; use std::{ path::{Path, PathBuf}, @@ -118,6 +118,21 @@ pub struct RanimUserLibrary { temp_path: PathBuf, } +pub struct RanimUserLibrarySceneIter<'a> { + lib: &'a RanimUserLibrary, + idx: usize, +} + +impl<'a> Iterator for RanimUserLibrarySceneIter<'a> { + type Item = &'a Scene; + + fn next(&mut self) -> Option { + let res = self.lib.get_scene(self.idx); + self.idx += 1; + res + } +} + impl RanimUserLibrary { pub fn load(dylib_path: impl AsRef) -> Self { let dylib_path = dylib_path.as_ref(); @@ -146,24 +161,28 @@ impl RanimUserLibrary { } } - /// Safety: dylib has a `scenes` and a `scene_cnt` fn with the correct signature and safe implementation - pub fn scenes(&self) -> &[Scene] { + pub fn scene_cnt(&self) -> usize { let scene_cnt: Symbol usize> = unsafe { self.inner.as_ref().unwrap().get(b"scene_cnt").unwrap() }; + scene_cnt() + } - let scenes: Symbol *const Scene> = - unsafe { self.inner.as_ref().unwrap().get(b"scenes").unwrap() }; - - let scene_cnt = scene_cnt(); - debug!("scene_cnt: {scene_cnt}"); - let scenes = scenes(); + pub fn get_scene(&self, idx: usize) -> Option<&Scene> { + let get_scene: Symbol *const Scene> = + unsafe { self.inner.as_ref().unwrap().get(b"get_scene").unwrap() }; + if self.scene_cnt() <= idx { + None + } else { + Some(unsafe { &*get_scene(idx) }) + } + } - unsafe { std::slice::from_raw_parts(scenes, scene_cnt) } + pub fn scenes(&self) -> impl Iterator { + RanimUserLibrarySceneIter { lib: self, idx: 0 } } pub fn get_preview_func(&self) -> Result<&Scene> { self.scenes() - .iter() .find(|s| s.preview) .context("no scene marked with `#[preview]` found") } diff --git a/packages/ranim-examples/Cargo.toml b/packages/ranim-examples/Cargo.toml new file mode 100644 index 00000000..5bd98121 --- /dev/null +++ b/packages/ranim-examples/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "ranim-examples" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[features] +app = ["ranim/app"] + +[target.'cfg(target_family = "wasm")'.dependencies] +ranim = { workspace = true, features = ["app"] } + +[lib] +crate-type = ["rlib", "cdylib"] # use cdylib to produce .wasm file + +[dependencies] +ranim.workspace = true +ranim-macros.workspace = true +itertools.workspace = true +rand = "0.9.2" +rand_chacha = "0.9.0" +wasm-bindgen = "0.2.100" + +[package.metadata.release] +tag = false + +[package.metadata.docs.rs] +all-features = true +rustc-args = ["--cfg", "docsrs_dep"] +rustdoc-args = ["--cfg", "docsrs_dep", "--html-in-header", "docs-rs/header.html"] + diff --git a/packages/ranim-examples/docs-rs/header.html b/packages/ranim-examples/docs-rs/header.html new file mode 100644 index 00000000..243aee13 --- /dev/null +++ b/packages/ranim-examples/docs-rs/header.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/examples/hanoi/main.rs b/packages/ranim-examples/src/algo/hanoi.rs similarity index 81% rename from examples/hanoi/main.rs rename to packages/ranim-examples/src/algo/hanoi.rs index be2e0b2c..0fcc6ebb 100644 --- a/examples/hanoi/main.rs +++ b/packages/ranim-examples/src/algo/hanoi.rs @@ -1,4 +1,3 @@ -use log::LevelFilter; use ranim::{ animation::transform::TransformAnim, color::{HueDirection, palettes::manim}, @@ -10,7 +9,7 @@ use ranim::{ utils::rate_functions::{ease_in_quad, ease_out_quad, linear}, }; -fn solve_hanoi( +pub fn solve_hanoi( n: usize, idx_src: usize, idx_dst: usize, @@ -26,7 +25,7 @@ fn solve_hanoi( } } -fn hanoi(r: &mut RanimScene, n: usize) { +pub fn hanoi(r: &mut RanimScene, n: usize) { let _r_cam = r.insert_and_show(CameraFrame::default()); let total_sec = 10.0; let rod_width = 0.4; @@ -34,7 +33,6 @@ fn hanoi(r: &mut RanimScene, n: usize) { let rod_section_width = 4.0; let _rods = [-1, 0, 1] - .into_iter() .map(|i| { Rectangle::new(rod_width, rod_height).with(|rect| { rect.set_color(manim::GREY_C).put_anchor_on( @@ -43,8 +41,7 @@ fn hanoi(r: &mut RanimScene, n: usize) { ); }) }) - .map(|rect| r.insert_and_show(rect)) - .collect::>(); + .map(|rect| r.insert_and_show(rect)); let min_disk_width = rod_width * 1.7; let max_disk_width = rod_section_width * 0.8; @@ -111,37 +108,19 @@ fn hanoi(r: &mut RanimScene, n: usize) { } #[scene] +#[wasm_demo_doc] #[preview] #[output(dir = "hanoi")] -fn hanoi_5(r: &mut RanimScene) { +/// An example of solving the Tower of Hanoi problem with 5 disks. +pub fn hanoi_5(r: &mut RanimScene) { hanoi(r, 5); } -#[scene(name = "hanoi")] +#[scene] +#[wasm_demo_doc] #[preview] #[output(dir = "hanoi")] -fn hanoi_10(r: &mut RanimScene) { +/// An example of solving the Tower of Hanoi problem with 10 disks. +pub fn hanoi_10(r: &mut RanimScene) { hanoi(r, 10); } - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(hanoi_10_scene); - #[cfg(not(feature = "app"))] - { - render_scene(hanoi_5_scene); - render_scene(hanoi_10_scene); - } -} diff --git a/packages/ranim-examples/src/algo/sort.rs b/packages/ranim-examples/src/algo/sort.rs new file mode 100644 index 00000000..493f9972 --- /dev/null +++ b/packages/ranim-examples/src/algo/sort.rs @@ -0,0 +1,224 @@ +use rand::{SeedableRng, seq::SliceRandom}; +use ranim::{ + animation::transform::TransformAnim, + color::palettes::manim, + components::Anchor, + glam::{DVec3, dvec2}, + items::vitem::geometry::Rectangle, + prelude::*, + utils::rate_functions::linear, +}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +// MARK: bubble_sort + +pub fn bubble_sort(r: &mut RanimScene, num: usize) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + + let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); + let padded_frame_size = frame_size * 0.9; + + let anim_step_duration = 15.0 / num.pow(2) as f64; + + let width_unit = padded_frame_size.x / num as f64; + let height_unit = padded_frame_size.y / num as f64; + + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(114514); + let mut heights = (1..=num) + .map(|x| x as f64 * height_unit) + .collect::>(); + heights.shuffle(&mut rng); + + let padded_frame_bl = dvec2(padded_frame_size.x / -2.0, padded_frame_size.y / -2.0); + let mut r_rects = heights + .iter() + .enumerate() + .map(|(i, &height)| { + let target_bc_coord = + padded_frame_bl.extend(0.0) + DVec3::X * (width_unit * i as f64 + width_unit / 2.0); + let rect = Rectangle::new(width_unit, height).with(|rect| { + rect.stroke_width = 0.0; + rect.set_fill_color(manim::WHITE.with_alpha(0.5)) + .scale(DVec3::splat(0.8)) + .put_anchor_on(Anchor::edge(0, -1, 0), target_bc_coord); + }); + r.insert_and_show(rect) + }) + .collect::>(); + + let anim_highlight = |rect: Rectangle| { + rect.transform(|data| { + data.set_fill_color(manim::BLUE_C.with_alpha(0.5)); + }) + .with_duration(anim_step_duration) + .with_rate_func(linear) + }; + let anim_unhighlight = |rect: Rectangle| { + rect.transform(|data| { + data.set_fill_color(manim::WHITE.with_alpha(0.5)); + }) + .with_duration(anim_step_duration) + .with_rate_func(linear) + }; + let shift_right = DVec3::X * width_unit; + let swap_shift = [shift_right, -shift_right]; + let anim_swap = |timeline: &mut RanimScene, r_rectab: &[&ItemId; 2]| { + let timelines = timeline.timeline_mut(r_rectab); + timelines + .into_iter() + .zip(swap_shift.iter()) + .for_each(|(timeline, shift)| { + timeline.play_with(|rect| { + rect.transform(|data| { + data.shift(*shift); + }) + .with_duration(anim_step_duration) + .with_rate_func(linear) + }); + }); + }; + + for i in (1..num).rev() { + for j in 0..i { + r.timeline_mut(&[&r_rects[j], &r_rects[j + 1]]) + .into_iter() + .for_each(|timeline| { + timeline.play_with(anim_highlight); + }); + if heights[j] > heights[j + 1] { + anim_swap(r, &[&r_rects[j], &r_rects[j + 1]]); + r.timelines_mut().sync(); + heights.swap(j, j + 1); + r_rects.swap(j, j + 1); + } + r.timeline_mut(&[&r_rects[j], &r_rects[j + 1]]) + .into_iter() + .for_each(|timeline| { + timeline.play_with(anim_unhighlight); + }); + r.timelines_mut().sync(); + } + } +} + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "bubble_sort")] +/// A bubble sort ranim example with input of 10. +pub fn bubble_sort_10(r: &mut RanimScene) { + bubble_sort(r, 10); +} + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "bubble_sort")] +/// A bubble sort ranim example with input of 100. +pub fn bubble_sort_100(r: &mut RanimScene) { + bubble_sort(r, 100); +} + +// MARK: selective_sort + +pub fn selective_sort(r: &mut RanimScene, num: usize) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + + let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); + let padded_frame_size = frame_size * 0.9; + + let anim_step_duration = 15.0 / num.pow(2) as f64; + + let width_unit = padded_frame_size.x / num as f64; + let height_unit = padded_frame_size.y / num as f64; + + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(114514); + let mut heights = (1..=num) + .map(|x| x as f64 * height_unit) + .collect::>(); + heights.shuffle(&mut rng); + + let padded_frame_bl = dvec2(padded_frame_size.x / -2.0, padded_frame_size.y / -2.0); + let mut r_rects = heights + .iter() + .enumerate() + .map(|(i, &height)| { + let target_bc_coord = + padded_frame_bl.extend(0.0) + DVec3::X * (width_unit * i as f64 + width_unit / 2.0); + let rect = Rectangle::new(width_unit, height).with(|rect| { + rect.fill_rgba = manim::WHITE.with_alpha(0.5); + rect.scale(DVec3::splat(0.8)) + .put_anchor_on(Anchor::edge(0, -1, 0), target_bc_coord); + }); + r.insert_and_show(rect) + }) + .collect::>(); + + let highlight = |rect: Rectangle| { + rect.transform(|data| { + data.set_color(manim::RED_C).set_fill_opacity(0.5); + }) + .with_duration(anim_step_duration) + .with_rate_func(linear) + }; + let unhighlight = |rect: Rectangle| { + rect.transform(|data| { + data.set_color(manim::WHITE).set_fill_opacity(0.5); + }) + .with_duration(anim_step_duration) + .with_rate_func(linear) + }; + + let shift_right = DVec3::X * width_unit; + for i in 0..num - 1 { + r.timeline_mut(&r_rects[i]).play_with(highlight); + for j in i + 1..num { + r.timeline_mut(&r_rects[j]).play_with(highlight); + r.timelines_mut().sync(); + + if heights[i] > heights[j] { + let dir = [shift_right, -shift_right]; + let color = [manim::BLUE_C, manim::RED_C]; + r.timeline_mut(&[&r_rects[i], &r_rects[j]]) + .iter_mut() + .zip(dir) + .zip(color) + .for_each(|((timeline, dir), color)| { + timeline.play_with(|rect| { + rect.transform(|rect| { + rect.shift(dir * (j - i) as f64) + .set_color(color) + .set_fill_opacity(0.5); + }) + .with_duration(anim_step_duration) + .with_rate_func(linear) + }); + }); + heights.swap(i, j); + r_rects.swap(i, j); + } + r.timeline_mut(&r_rects[j]).play_with(unhighlight); + r.timelines_mut().sync(); + } + r.timeline_mut(&r_rects[i]).play_with(unhighlight); + } +} + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output] +/// A selective sort ranim example with input of 10. +pub fn selective_sort_10(r: &mut RanimScene) { + selective_sort(r, 10); +} + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output] +/// A selective sort ranim example with input of 100. +pub fn selective_sort_100(r: &mut RanimScene) { + selective_sort(r, 100); +} diff --git a/examples/perspective_blend/main.rs b/packages/ranim-examples/src/camera.rs similarity index 83% rename from examples/perspective_blend/main.rs rename to packages/ranim-examples/src/camera.rs index 4095ec20..7699148d 100644 --- a/examples/perspective_blend/main.rs +++ b/packages/ranim-examples/src/camera.rs @@ -1,4 +1,3 @@ -use log::LevelFilter; use ranim::{ animation::transform::TransformAnim, color::palettes::manim, @@ -13,8 +12,10 @@ use ranim::{ }; #[scene] +#[wasm_demo_doc] #[preview] #[output(dir = "perspective_blend")] +/// An example of perspective blend. fn perspective_blend(r: &mut RanimScene) { let r_cam = r.insert_and_show(CameraFrame::default()); r.timeline_mut(&r_cam).update_with(|cam| { @@ -101,23 +102,3 @@ fn perspective_blend(r: &mut RanimScene) { TimeMark::Capture("preview.png".to_string()), ); } - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(not(feature = "app"))] - render_scene(perspective_blend_scene); - - #[cfg(feature = "app")] - preview(perspective_blend_scene); -} diff --git a/examples/ranim_logo/main.rs b/packages/ranim-examples/src/lib.rs similarity index 54% rename from examples/ranim_logo/main.rs rename to packages/ranim-examples/src/lib.rs index 5d23ecc9..03bbdedb 100644 --- a/examples/ranim_logo/main.rs +++ b/packages/ranim-examples/src/lib.rs @@ -1,17 +1,22 @@ +//! This crate contains all ranim examples + use std::f64::consts::PI; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; -use glam::{DVec3, dvec2, dvec3}; use itertools::Itertools; -use log::LevelFilter; use ranim::{ - animation::{creation::WritingAnim, lagged::LaggedAnim, transform::TransformAnim}, + animation::{ + creation::WritingAnim, fading::FadingAnim, lagged::LaggedAnim, transform::TransformAnim, + }, color::palettes::manim, components::{Anchor, ScaleHint}, + glam::{DVec3, dvec2, dvec3}, items::{ Group, vitem::{ VItem, - geometry::{Polygon, Rectangle, Square}, + geometry::{Circle, Polygon, Rectangle, Square}, svg::SvgItem, typst::typst_svg, }, @@ -21,7 +26,60 @@ use ranim::{ utils::rate_functions::{linear, smooth}, }; -fn build_logo(logo_width: f64) -> [VItem; 6] { +pub mod tutorial { + pub mod extract; + pub mod getting_started; +} +pub mod vitem { + pub mod geometry; + pub mod svg; +} +pub mod algo { + pub mod hanoi; + pub mod sort; +} +pub mod camera; + +// MARK: hello_ranim +#[scene] +#[preview] +#[wasm_demo_doc] +#[output(dir = "hello_ranim")] +/// Hello Ranim! +pub fn hello_ranim(r: &mut RanimScene) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + let square = Square::new(2.0).with(|square| { + square.set_color(manim::BLUE_C); + }); + + let r_square = r.insert(square); + { + let timeline = r.timeline_mut(&r_square); + timeline.play_with(|square| square.fade_in()); + }; + + let circle = Circle::new(2.0).with(|circle| { + circle + .set_color(manim::RED_C) + .rotate(-PI / 4.0 + PI, DVec3::Z); + }); + + let r_vitem = r.map(r_square, VItem::from); + { + let timeline = r.timeline_mut(&r_vitem); + timeline.play_with(|state| state.transform_to(circle.into())); + timeline.forward(1.0); + let circle = timeline.state().clone(); + timeline.play_with(|circle| circle.unwrite()); + timeline.play(circle.write()); + timeline.play_with(|circle| circle.fade_out()); + }; + + r.insert_time_mark(3.7, TimeMark::Capture("preview.png".to_string())); +} + +// MARK: ranim_logo +pub fn build_logo(logo_width: f64) -> [VItem; 6] { let red_bg_rect = Rectangle::new(logo_width / 2.0, logo_width).with(|rect| { rect.set_color(manim::RED_C.with_alpha(0.5)) .put_center_on(dvec3(-logo_width / 4.0, 0.0, 0.0)); @@ -63,10 +121,13 @@ fn build_logo(logo_width: f64) -> [VItem; 6] { VItem::from(blue_triangle), ] } + #[scene] +#[wasm_demo_doc] #[preview] #[output(dir = "ranim_logo")] -fn ranim_logo(r: &mut RanimScene) { +/// A scene shows the logo of ranim +pub fn ranim_logo(r: &mut RanimScene) { let _r_cam = r.insert_and_show(CameraFrame::default()); let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); let logo_width = frame_size.y * 0.618; @@ -153,21 +214,55 @@ fn ranim_logo(r: &mut RanimScene) { }); } -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(ranim_logo_scene); - #[cfg(not(feature = "app"))] - render_scene(ranim_logo_scene); +// MARK: palettes +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "palettes")] +/// An example shows manim's palettes +pub fn palettes(r: &mut RanimScene) { + use ranim::color::palettes::manim::*; + let _r_cam = r.insert_and_show(CameraFrame::default()); + let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); + let padded_frame_size = frame_size * 0.9; + + let colors = vec![ + vec![BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A], + vec![TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A], + vec![GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A], + vec![YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A], + vec![GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A], + vec![RED_E, RED_D, RED_C, RED_B, RED_A], + vec![MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A], + vec![PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A], + vec![GREY_E, GREY_D, GREY_C, GREY_B, GREY_A], + vec![WHITE, BLACK, GREEN_SCREEN], + vec![GREY_BROWN, LIGHT_BROWN, PINK, LIGHT_PINK, ORANGE], + ]; + + let padded_frame_start = dvec2(padded_frame_size.x / -2.0, padded_frame_size.y / -2.0); + let h_step = padded_frame_size.y / colors.len() as f64; + + let squares = colors + .iter() + .enumerate() + .flat_map(|(i, row)| { + let y = i as f64 * h_step; + let w_step = padded_frame_size.x / row.len() as f64; + row.iter().enumerate().map(move |(j, color)| { + let x = j as f64 * w_step; + Rectangle::new(w_step as f64, h_step as f64).with(|rect| { + rect.stroke_width = 0.0; + + rect.set_color(*color).put_anchor_on( + Anchor::edge(-1, -1, 0), + padded_frame_start.extend(0.0) + dvec3(x, y, 0.0), + ); + }) + }) + }) + .collect::>(); + r.insert_and_show(squares); + r.insert_time_mark(0.0, TimeMark::Capture("preview.png".to_string())); + r.timelines_mut().forward(0.01); } diff --git a/examples/extract_vitem_visualize/main.rs b/packages/ranim-examples/src/tutorial/extract.rs similarity index 87% rename from examples/extract_vitem_visualize/main.rs rename to packages/ranim-examples/src/tutorial/extract.rs index d75a581d..f562754e 100644 --- a/examples/extract_vitem_visualize/main.rs +++ b/packages/ranim-examples/src/tutorial/extract.rs @@ -1,10 +1,10 @@ -use log::LevelFilter; use std::f64::consts::PI; use ranim::{ animation::{creation::WritingAnim, fading::FadingAnim, transform::TransformAnim}, color::palettes::manim, components::ScaleHint, + glam::DVec3, items::{ Group, vitem::{ @@ -19,9 +19,9 @@ use ranim::{ timeline::TimeMark, }; -use glam::DVec3; - #[derive(Clone)] +/// A custom item that extract to a vec of [`VItemPrimitive`] that visualizes +/// the underlying points of a vitem pub struct VisualVItem(VItem); impl Interpolatable for VisualVItem { @@ -173,9 +173,10 @@ impl Empty for VisualVItem { // MARK: ranim_text #[scene] +#[wasm_demo_doc] #[preview] -#[output(dir = "extract_vitem_visualize")] -fn ranim_text(r: &mut RanimScene) { +#[output(dir = "ranim_text_extract_vitem_visualize")] +fn ranim_text_extract_vitem_visualize(r: &mut RanimScene) { let r_cam = r.insert_and_show(CameraFrame::default()); let text = SvgItem::new(typst_svg("Ranim")).with(|item| { @@ -210,13 +211,15 @@ fn ranim_text(r: &mut RanimScene) { .play_with(|cam| cam.transform_to(default_cam)); // r.timelines_mut().forward(1.0); - r.insert_time_mark(5.0, TimeMark::Capture("preview-ranim_text.png".to_string())); + r.insert_time_mark(5.0, TimeMark::Capture("preview.png".to_string())); } -#[scene(name = "extract_vitem_visualize")] +#[scene] +#[wasm_demo_doc] #[preview] -#[output(dir = "extract_vitem_visualize")] -pub fn hello_ranim(r: &mut RanimScene) { +#[output(dir = "hello_ranim_extract_vitem_visualize")] +/// [`crate::hello_ranim`] with [`VisualVItem`] +pub fn hello_ranim_extract_vitem_visualize(r: &mut RanimScene) { let _r_cam = r.insert_and_show(CameraFrame::default()); let square = VisualVItem(VItem::from(Square::new(2.0).with(|square| { @@ -243,30 +246,5 @@ pub fn hello_ranim(r: &mut RanimScene) { } r.timelines_mut().sync(); - r.insert_time_mark( - 3.2, - TimeMark::Capture("preview-hello_ranim.png".to_string()), - ); -} - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(hello_ranim_scene); - #[cfg(not(feature = "app"))] - { - render_scene(hello_ranim_scene); - render_scene(ranim_text_scene); - } + r.insert_time_mark(3.2, TimeMark::Capture("preview.png".to_string())); } diff --git a/packages/ranim-examples/src/tutorial/getting_started.rs b/packages/ranim-examples/src/tutorial/getting_started.rs new file mode 100644 index 00000000..6f27826f --- /dev/null +++ b/packages/ranim-examples/src/tutorial/getting_started.rs @@ -0,0 +1,113 @@ +//! Examples to get started with ranim +use ranim::{ + animation::{ + creation::{CreationAnim, WritingAnim}, + fading::FadingAnim, + transform::TransformAnim, + }, + color::palettes::manim, + items::vitem::{ + VItem, + geometry::{Circle, Rectangle, Square}, + }, + prelude::*, + utils::rate_functions::linear, +}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "getting_started0")] +/// This example shows the basic api of [`RanimScene`]. +pub fn getting_started0(r: &mut RanimScene) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + // A Square with size 2.0 and color blue + let square = Square::new(2.0).with(|square| { + square.set_color(manim::BLUE_C); + }); + + let r_square = r.insert(square); + { + let timeline = r.timeline_mut(&r_square); + timeline + .play_with(|square| square.fade_in()) + .forward(1.0) + .hide() + .forward(1.0) + .show() + .forward(1.0) + .play_with(|square| square.fade_out()); + } +} + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "getting_started1")] +/// This example shows the basic api of [`RanimScene`]. +pub fn getting_started1(r: &mut RanimScene) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + // A Square with size 2.0 and color blue + let square = Square::new(2.0).with(|square| { + square.set_color(manim::BLUE_C); + }); + + let circle = Circle::new(2.0).with(|circle| { + circle.set_color(manim::RED_C); + }); + + // In order to do more low-level opeerations, + // sometimes we need to convert the item to a low-level item. + let r_vitem = r.insert(VItem::from(square)); + { + let timeline = r.timeline_mut(&r_vitem); + timeline.play_with(|vitem| vitem.transform_to(VItem::from(circle.clone()))); + timeline.play_with(|vitem| vitem.unwrite()); + } +} + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "getting_started2")] +/// This example shows the basic api of [`RanimScene`]. +fn getting_started2(r: &mut RanimScene) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + let rect = Rectangle::new(4.0, 9.0 / 4.0).with(|rect| { + rect.set_stroke_color(manim::GREEN_C); + }); + + // The new initialized timeline is hidden by default, use show to start encoding a static anim and make it show + let r_rect: ItemId = r.insert_and(rect, |timeline| { + timeline.show(); + }); + // or use `insert_and_show` + // let r_rect: ItemId = r.insert_and_show(rect) + + r.timelines_mut().forward(1.0); + + let square = Square::new(2.0).with(|square| { + square.set_color(manim::BLUE_C); + }); + let circle = Circle::new(2.0).with(|circle| { + circle.set_color(manim::RED_C); + }); + let r_vitem = r.insert(VItem::from(square)); + { + let timeline = r.timeline_mut(&r_vitem); + timeline + .forward(1.0) + .play_with(|vitem| vitem.create()) + .play_with(|vitem| { + vitem + .transform_to(VItem::from(circle.clone())) + .with_rate_func(linear) + }) + .play_with(|vitem| vitem.unwrite()); + } + + let r_rect: ItemId = r.map(r_rect, VItem::from); + r.timeline_mut(&r_rect).play_with(|rect| rect.uncreate()); +} diff --git a/packages/ranim-examples/src/vitem/geometry.rs b/packages/ranim-examples/src/vitem/geometry.rs new file mode 100644 index 00000000..265b8eae --- /dev/null +++ b/packages/ranim-examples/src/vitem/geometry.rs @@ -0,0 +1,112 @@ +use itertools::Itertools; +use ranim::{ + animation::{fading::FadingAnim, lagged::LaggedAnim}, + color::HueDirection, + glam::{DMat2, dvec2}, + items::{ + Group, + vitem::geometry::{Arc, ArcBetweenPoints}, + }, + prelude::*, + timeline::TimeMark, +}; + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "arc")] +/// A scene that shows how to use [`Arc`] item. +pub fn arc(r: &mut RanimScene) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + + // let frame_size = app.camera().size; + let frame_size = dvec2(8.0 * 16.0 / 9.0, 8.0); + let frame_start = dvec2(frame_size.x / -2.0, frame_size.y / -2.0); + + let start_color = color!("#FF8080FF"); + let end_color = color!("#58C4DDFF"); + + let nrow = 10; + let ncol = 10; + let step_x = frame_size.x / ncol as f64; + let step_y = frame_size.y / nrow as f64; + + let arcs = (0..nrow) + .cartesian_product(0..ncol) + .map(|(i, j)| { + let (i, j) = (i as f64, j as f64); + + let angle = std::f64::consts::PI * (j + 1.0) / ncol as f64 * 360.0 / 180.0; + let radius = step_y / 2.0 * 0.8; + let color = start_color.lerp( + end_color, + i as f32 / (nrow - 1) as f32, + HueDirection::Increasing, + ); + let offset = frame_start + dvec2(j * step_x + step_x / 2.0, i * step_y + step_y / 2.0); + Arc::new(angle, radius).with(|arc| { + arc.stroke_width = 0.12 * (j as f32 + 0.02) / ncol as f32; + arc.set_stroke_color(color) + .put_center_on(offset.extend(0.0)); + }) + }) + .collect::>(); + let r_arcs = r.insert(arcs); + + r.timeline_mut(&r_arcs) + .play_with(|arcs| arcs.lagged(0.2, |arc| arc.fade_in()).with_duration(3.0)); + + r.insert_time_mark( + r.timelines().max_total_secs(), + TimeMark::Capture("preview.png".to_string()), + ); +} + +#[scene] +#[wasm_demo_doc] +#[preview] +#[output(dir = "arc_between_points")] +/// A scene that shows how to use [`ArcBetweenPoints`] item. +pub fn arc_between_points(r: &mut RanimScene) { + let _r_cam = r.insert_and_show(CameraFrame::default()); + let center = dvec2(0.0, 0.0); + + let start_color = color!("#FF8080FF"); + let end_color = color!("#58C4DDFF"); + let ntan = 16; + let nrad = 5; + + let arcs = (0..nrad) + .map(|i| { + let radius = 6.0 * (i + 1) as f64 / nrad as f64; + let width = 0.12 * ((nrad - i) as f64 / nrad as f64).powi(2); + let angle = std::f64::consts::PI * 7.0 / 4.0 * (i + 1) as f64 / nrad as f64; + (radius, width, angle) + }) + .cartesian_product(0..ntan) + .map(|((rad, width, angle), j)| { + let color = start_color.lerp( + end_color, + j as f32 / (ntan - 1) as f32, + HueDirection::Increasing, + ); + let vec = DMat2::from_angle(std::f64::consts::PI * 2.0 / ntan as f64 * j as f64) + * dvec2(rad, 0.0); + ArcBetweenPoints::new(center.extend(0.0), (center + vec).extend(0.0), angle).with( + |arc| { + arc.stroke_width = width as f32; + arc.set_stroke_color(color); + }, + ) + }) + .collect::>(); + let r_arcs = r.insert(arcs); + + r.timeline_mut(&r_arcs) + .play_with(|arcs| arcs.lagged(0.2, |arc| arc.fade_in()).with_duration(3.0)); + + r.insert_time_mark( + r.timelines().max_total_secs(), + TimeMark::Capture("preview.png".to_string()), + ); +} diff --git a/examples/basic/main.rs b/packages/ranim-examples/src/vitem/svg.rs similarity index 70% rename from examples/basic/main.rs rename to packages/ranim-examples/src/vitem/svg.rs index 945d8730..1fc7929f 100644 --- a/examples/basic/main.rs +++ b/packages/ranim-examples/src/vitem/svg.rs @@ -1,9 +1,8 @@ -use glam::DVec3; -use log::LevelFilter; use ranim::{ animation::{creation::WritingAnim, fading::FadingAnim, lagged::LaggedAnim}, color::palettes::manim, components::ScaleHint, + glam::DVec3, items::{ Group, vitem::{VItem, svg::SvgItem, typst::typst_svg}, @@ -12,12 +11,14 @@ use ranim::{ timeline::TimeMark, }; -const SVG: &str = include_str!("../../assets/Ghostscript_Tiger.svg"); +const SVG: &str = include_str!("../../../../assets/Ghostscript_Tiger.svg"); #[scene] +#[wasm_demo_doc] #[preview] #[output(dir = "basic")] -fn basic(r: &mut RanimScene) { +/// An example shows [`SvgItem`] +fn svg_item(r: &mut RanimScene) { let _r_cam = r.insert_and_show(CameraFrame::default()); r.timelines_mut().forward(0.2); @@ -56,22 +57,3 @@ fn basic(r: &mut RanimScene) { TimeMark::Capture("preview.png".to_string()), ); } - -fn main() { - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(debug_assertions)] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Trace) - .init(); - #[cfg(not(debug_assertions))] - pretty_env_logger::formatted_timed_builder() - .filter(Some("ranim"), LevelFilter::Info) - .init(); - } - - #[cfg(feature = "app")] - preview(basic_scene); - #[cfg(not(feature = "app"))] - render_scene(basic_scene) -} diff --git a/packages/ranim-macros/src/lib.rs b/packages/ranim-macros/src/lib.rs index 6d27abcb..ba6a485d 100644 --- a/packages/ranim-macros/src/lib.rs +++ b/packages/ranim-macros/src/lib.rs @@ -31,6 +31,7 @@ struct SceneAttrs { name: Option, // #[scene(name = "...")] frame_height: Option, // #[scene(frame_height = 8.0)] preview: bool, // #[preview] + wasm_demo_doc: bool, // #[wasm_demo_doc] outputs: Vec, // #[output(...)] } @@ -44,7 +45,7 @@ struct OutputDef { dir: String, } -// ---------- 入口 ---------- +// MARK: scene #[proc_macro_attribute] pub fn scene(args: TokenStream, input: TokenStream) -> TokenStream { let ranim = ranim_path(); @@ -54,6 +55,18 @@ pub fn scene(args: TokenStream, input: TokenStream) -> TokenStream { let fn_name = &input_fn.sig.ident; let vis = &input_fn.vis; let fn_body = &input_fn.block; + let doc_attrs: Vec<_> = input_fn + .attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .collect(); + + let doc_head = doc_attrs.first(); + let doc_attrs = if doc_attrs.len() > 1 { + &doc_attrs[1..] + } else { + &[] + }; // 场景名称 let scene_name = attrs.name.unwrap_or_else(|| fn_name.to_string()); @@ -93,6 +106,27 @@ pub fn scene(args: TokenStream, input: TokenStream) -> TokenStream { } let preview = attrs.preview; + let mut doc = quote! { + #(#doc_attrs)* + }; + if attrs.wasm_demo_doc { + doc = quote! { + #[doc = concat!(r#"
"#)] + #[doc = concat!(r#" "#)] + #[doc = concat!(r#" "#)] + #[doc = concat!(r#"
"#)] + #doc + }; + } + if let Some(doc_head) = doc_head { + doc = quote! { + #doc_head + #doc + }; + } let static_output_name = syn::Ident::new( &format!("__SCENE_{}_OUTPUTS", fn_name.to_string().to_uppercase()), @@ -118,12 +152,16 @@ pub fn scene(args: TokenStream, input: TokenStream) -> TokenStream { // 构造 Scene 并塞进分布式切片 let expanded = quote! { + #doc #vis fn #fn_name(r: &mut #ranim::timeline::RanimScene) #fn_body + #[doc(hidden)] static #static_output_name: [#ranim::Output; #output_cnt] = [#(#outputs),*]; - #[cfg_attr(not(target_family = "wasm"), #ranim::linkme::distributed_slice(#ranim::SCENES))] - #[cfg_attr(not(target_family = "wasm"), linkme(crate = #ranim::linkme))] + #[doc(hidden)] static #static_name: #ranim::Scene = #scene; + #ranim::inventory::submit!{ + #scene + } #[allow(non_upper_case_globals)] #vis static #static_scene_name: &'static #ranim::Scene = &#static_name; @@ -142,6 +180,11 @@ pub fn preview(_: TokenStream, _: TokenStream) -> TokenStream { TokenStream::new() } +#[proc_macro_attribute] +pub fn wasm_demo_doc(_attr: TokenStream, _: TokenStream) -> TokenStream { + TokenStream::new() +} + // MARK: derive Traits #[proc_macro_derive(Fill)] diff --git a/packages/ranim-macros/src/scene.rs b/packages/ranim-macros/src/scene.rs index 35634fa5..fcabd413 100644 --- a/packages/ranim-macros/src/scene.rs +++ b/packages/ranim-macros/src/scene.rs @@ -41,6 +41,11 @@ pub fn parse_scene_attrs( continue; } + if attr.path().is_ident("wasm_demo_doc") { + res.wasm_demo_doc = true; + continue; + } + if let Meta::List(list) = &attr.meta && list.path.is_ident("output") { diff --git a/src/app/mod.rs b/src/app/mod.rs index 33e5e7a9..cd16b6ba 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -32,6 +32,9 @@ use crate::{ utils::wgpu::WgpuContext, }; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + #[derive(Default, Debug, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] struct OccupiedScreenSpace { @@ -277,9 +280,12 @@ struct WinitApp { window: Option>, app_renderer: Option, wgpu_ctx: Option, + #[cfg(target_arch = "wasm32")] + container_id: String, } impl WinitApp { + #[cfg(not(target_arch = "wasm32"))] fn new(app_state: AppState, event_loop: &EventLoop) -> Self { Self { event_loop_proxy: Some(event_loop.create_proxy()), @@ -291,6 +297,19 @@ impl WinitApp { wgpu_ctx: None, } } + #[cfg(target_arch = "wasm32")] + fn new(app_state: AppState, event_loop: &EventLoop, container_id: String) -> Self { + Self { + event_loop_proxy: Some(event_loop.create_proxy()), + app_state, + + size: (0, 0), + window: None, + app_renderer: None, + wgpu_ctx: None, + container_id, + } + } } // MARK: Redraw @@ -439,18 +458,42 @@ impl ApplicationHandler for WinitApp { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); - log::info!("searching for app-{}", self.app_state.title); + + log::info!("searching for {}", self.container_id); let canvas = document - .get_element_by_id(&format!("app-{}", self.app_state.title)) - .unwrap(); - let canvas = canvas.dyn_into::().ok(); - - log::info!("found canvas: {}", canvas.is_some()); - if let Some(canvas) = canvas.as_ref() { - self.size = (canvas.width(), canvas.height()); - log::info!("canvas size: {:?}", self.size); - } - window_attrs = window_attrs.with_canvas(canvas); + .get_element_by_id(&self.container_id) + .and_then(|canvas| canvas.dyn_into::().ok()); + + let canvas = match canvas { + Some(canvas) => { + log::info!("found canvas"); + self.size = (canvas.width(), canvas.height()); + log::info!("canvas size: {:?}", self.size); + canvas + } + None => { + log::info!("canvas not found, creating a new one"); + let canvas = document + .create_element("canvas") + .unwrap() + .dyn_into::() + .unwrap(); + + // 设置 canvas 的 id 和尺寸 + canvas.set_id(&self.container_id); + canvas.set_width(800); // 默认宽度 + canvas.set_height(600); // 默认高度 + + // 将 canvas 添加到文档中 + document.body().unwrap().append_child(&canvas).unwrap(); + + self.size = (canvas.width(), canvas.height()); + log::info!("created canvas with size: {:?}", self.size); + canvas + } + }; + + window_attrs = window_attrs.with_canvas(Some(canvas)); // window_attrs = // window_attrs.with_prevent_default(window.prevent_default_event_handling); @@ -570,7 +613,7 @@ impl ApplicationHandler for WinitApp { use crate::PUFFIN_GPU_PROFILER; /// Runs an app with the given app state -pub fn run_app(app: AppState) { +pub fn run_app(app: AppState, #[cfg(target_arch = "wasm32")] container_id: String) { #[cfg(feature = "profiling")] let (_cpu_server, _gpu_server) = { puffin::set_scopes_on(true); @@ -592,30 +635,37 @@ pub fn run_app(app: AppState) { .unwrap(); event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); - #[allow(unused_mut)] - let mut app = WinitApp::new(app, &event_loop); - #[cfg(target_arch = "wasm32")] { + let mut app = WinitApp::new(app, &event_loop, container_id); use winit::platform::web::EventLoopExtWebSys; event_loop.spawn_app(app); } #[cfg(not(target_arch = "wasm32"))] { + let mut app = WinitApp::new(app, &event_loop); event_loop.run_app(&mut app).unwrap(); } } -/// Runs a scene preview app on a scene constructor -pub fn run_scene_app(constructor: impl SceneConstructor, name: String) { - #[cfg(target_arch = "wasm32")] - { - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - console_log::init().expect("Failed to initialize console_log"); +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +impl Scene { + pub fn run_app(&self) { + run_scene_app(self.constructor, self.name.to_string()); } + pub fn run_app_with_name(&self, name: String) { + run_scene_app(self.constructor, name); + } +} - let app_state = AppState::new_with_title(constructor, name); - run_app(app_state); +/// Runs a scene preview app on a scene constructor +pub fn run_scene_app(constructor: impl SceneConstructor, name: String) { + let app_state = AppState::new_with_title(constructor, name.clone()); + run_app( + app_state, + #[cfg(target_arch = "wasm32")] + format!("ranim-app-{name}"), + ); } /// Preview a scene diff --git a/src/color/mod.rs b/src/color/mod.rs index f5277dfb..a3c9ba9a 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -1,8 +1,7 @@ -pub use color::{AlphaColor, OpaqueColor, Srgb}; +pub use ::color::*; /// palettes pub mod palettes; -pub use ::color::HueDirection; /// Color preludes pub mod prelude { @@ -36,7 +35,7 @@ pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> AlphaColor { #[macro_export] macro_rules! color { ($color_str:expr) => {{ - use ::color::{Srgb, parse_color}; + use ::ranim::color::{Srgb, parse_color}; parse_color($color_str) .expect("Invalid color string") .to_alpha_color::() diff --git a/src/lib.rs b/src/lib.rs index 5262d7a4..346778e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,9 @@ use animation::EvalResult; use log::{info, trace}; use timeline::{RanimScene, SealedRanimScene}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + #[cfg(not(target_arch = "wasm32"))] use file_writer::{FileWriter, FileWriterBuilder}; #[cfg(not(target_arch = "wasm32"))] @@ -89,23 +92,57 @@ impl SceneConstructor for F { } // MARK: Dylib part -#[cfg(not(target_family = "wasm"))] -pub use linkme; - #[doc(hidden)] #[derive(Clone)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] pub struct Scene { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub name: &'static str, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub constructor: fn(&mut RanimScene), + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub config: SceneConfig, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub outputs: &'static [Output], pub preview: bool, } -#[cfg(not(target_family = "wasm"))] +pub use inventory; + +inventory::collect!(Scene); + +#[doc(hidden)] +#[unsafe(no_mangle)] +pub extern "C" fn get_scene(idx: usize) -> *const Scene { + inventory::iter::().skip(idx).take(1).next().unwrap() +} + #[doc(hidden)] -#[linkme::distributed_slice] -pub static SCENES: [Scene]; +#[unsafe(no_mangle)] +pub extern "C" fn scene_cnt() -> usize { + inventory::iter::().count() +} + +#[cfg(target_arch = "wasm32")] +unsafe extern "C" { + fn __wasm_call_ctors(); +} + +/// Return a scene with matched name +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +pub fn find_scene(name: &str) -> Option { + inventory::iter::().find(|s| s.name == name).cloned() +} + +#[cfg(target_arch = "wasm32")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] +fn wasm_start() { + unsafe { + __wasm_call_ctors(); + } + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init().expect("Failed to initialize console_log"); +} /// Scene config #[derive(Debug, Clone)] @@ -124,6 +161,7 @@ impl Default for SceneConfig { /// The output of a scene #[derive(Debug, Clone)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] pub struct Output { /// The width of the output texture in pixels. pub width: u32, @@ -136,6 +174,7 @@ pub struct Output { /// The directory to save the output /// /// Related to the `output` folder, Or absolute. + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub dir: &'static str, } @@ -156,20 +195,6 @@ impl Output { }; } -#[cfg(not(target_family = "wasm"))] -#[doc(hidden)] -#[unsafe(no_mangle)] -pub extern "C" fn scenes() -> *const Scene { - SCENES.as_ptr() -} - -#[cfg(not(target_family = "wasm"))] -#[doc(hidden)] -#[unsafe(no_mangle)] -pub extern "C" fn scene_cnt() -> usize { - SCENES.len() -} - // MARK: Prelude /// The preludes pub mod prelude { @@ -178,7 +203,7 @@ pub mod prelude { #[cfg(not(target_arch = "wasm32"))] pub use crate::{render, render_scene, render_scene_output}; - pub use ranim_macros::{output, preview, scene}; + pub use ranim_macros::{output, preview, scene, wasm_demo_doc}; pub use crate::items::{ItemId, camera_frame::CameraFrame}; pub use crate::timeline::{RanimScene, TimelineFunc, TimelinesFunc};