diff --git a/Cargo.lock b/Cargo.lock index de7b312..a7f8636 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -607,6 +616,7 @@ dependencies = [ "indicatif", "itertools", "midly", + "nanoid", "once_cell", "rand", "rust-analyzer", diff --git a/Cargo.toml b/Cargo.toml index 71f13c4..a20b37f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ web-sys = { version = "0.3.4", features = [ 'Window', ] } once_cell = "1.19.0" +nanoid = "0.4.0" [dev-dependencies] diff --git a/examples/schedule-hell-exerpt.mp4 b/examples/schedule-hell-exerpt.mp4 new file mode 100644 index 0000000..c9ba3a4 Binary files /dev/null and b/examples/schedule-hell-exerpt.mp4 differ diff --git a/src/animation.rs b/src/animation.rs new file mode 100644 index 0000000..385aa0c --- /dev/null +++ b/src/animation.rs @@ -0,0 +1,52 @@ +use std::fmt::Display; + +use crate::{Canvas, Context, LaterHookCondition, RenderFunction}; + +/// Arguments: animation progress (from 0.0 to 1.0), canvas, current ms +pub type AnimationUpdateFunction = dyn Fn(f32, &mut Canvas, usize); + +pub struct Animation { + pub name: String, + // pub keyframes: Vec>, + pub update: Box, +} + +// pub struct Keyframe { +// pub at: f32, // from 0 to 1 +// pub action: Box>, +// } + +impl Animation { + /// Example + /// ``` + /// Animation::new("example", &|t, canvas, _| { + /// canvas.root().object("dot").fill(Fill::Translucent(Color::Red, t)) + /// }) + /// ``` + pub fn new(name: N, f: &'static AnimationUpdateFunction) -> Self + where + N: Display, + { + Self { + name: format!("{}", name), + update: Box::new(f), + } + } + + // /// Example: + // /// ``` + // /// animation.at(50.0, Box::new(|canvas, _| canvas.root().set_background(Color::Black))); + // /// ``` + // pub fn at(&mut self, percent: f32, action: Box>) { + // self.keyframes.push(Keyframe { + // at: percent / 100.0, + // action, + // }); + // } +} + +impl From<(String, Box)> for Animation { + fn from((name, f): (String, Box)) -> Self { + Self { name, update: f } + } +} diff --git a/src/layer.rs b/src/layer.rs index e901f39..0e1ad97 100644 --- a/src/layer.rs +++ b/src/layer.rs @@ -67,6 +67,12 @@ impl Layer { panic!("object {} already exists in layer {}", name_str, self.name); } + self.set_object(name_str, object); + } + + pub fn set_object<'a, N: Display>(&mut self, name: N, object: ColoredObject) { + let name_str = format!("{}", name); + self.objects.insert(name_str, object); self.flush(); } diff --git a/src/lib.rs b/src/lib.rs index 843292e..5559841 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,39 +1,44 @@ +pub mod animation; +pub mod audio; +pub mod canvas; pub mod cli; -pub mod video; -pub use video::*; -mod color; +pub mod color; pub mod examples; -mod objects; +pub mod fill; +pub mod filter; +pub mod layer; +pub mod midi; +pub mod objects; +pub mod point; +pub mod preview; +pub mod region; +pub mod sync; +pub mod video; +pub mod web; +pub use animation::*; +pub use audio::*; +pub use canvas::*; pub use color::*; -pub use objects::*; -mod fill; -mod point; pub use fill::*; +pub use filter::*; +pub use layer::*; +pub use midi::MidiSynchronizer; +pub use objects::*; pub use point::*; -mod region; pub use region::*; -mod web; -pub use web::log; -mod audio; -pub use audio::*; -mod sync; -use sync::SyncData; pub use sync::Syncable; -mod layer; -pub use layer::*; -mod canvas; -pub use canvas::*; -mod filter; -pub use filter::*; -mod midi; -mod preview; +pub use video::*; +pub use web::log; + use indicatif::{ProgressBar, ProgressStyle}; -pub use midi::MidiSynchronizer; +use nanoid::nanoid; use std::fs::{self}; -use std::path::{PathBuf}; +use std::ops::{Add, Div, Range, Sub}; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::time; +use sync::SyncData; const PROGRESS_BARS_STYLE: &str = "{spinner:.cyan} {percent:03.bold.cyan}% {msg:<30} [{bar:100.bold.blue/dim.blue}] {eta:.cyan}"; @@ -119,6 +124,7 @@ impl<'a, C> Context<'a, C> { self.later_hooks.insert( 0, LaterHook { + once: true, when: Box::new(move |_, context, _previous_beat| { context.frame >= current_frame + delay }), @@ -133,6 +139,7 @@ impl<'a, C> Context<'a, C> { self.later_hooks.insert( 0, LaterHook { + once: true, when: Box::new(move |_, context, _previous_beat| context.ms >= current_ms + delay), render_function: Box::new(render_function), }, @@ -145,6 +152,7 @@ impl<'a, C> Context<'a, C> { self.later_hooks.insert( 0, LaterHook { + once: true, when: Box::new(move |_, context, _previous_beat| { context.beat_fractional >= current_beat as f32 + delay }), @@ -152,6 +160,29 @@ impl<'a, C> Context<'a, C> { }, ); } + + /// duration is in milliseconds + pub fn start_animation(&mut self, duration: usize, animation: Animation) { + let start_ms = self.ms; + let ms_range = start_ms..(start_ms + duration); + + self.later_hooks.push(LaterHook { + once: false, + when: Box::new(move |_, ctx, _| ms_range.contains(&ctx.ms)), + render_function: Box::new(move |canvas, ms| { + let t = (ms - start_ms) as f32 / duration as f32; + (animation.update)(t, canvas, ms) + }), + }) + } + + /// duration is in milliseconds + pub fn animate(&mut self, duration: usize, f: &'static AnimationUpdateFunction) { + self.start_animation( + duration, + Animation::new(format!("unnamed animation {}", nanoid!()), f), + ); + } } struct SpinState { @@ -191,5 +222,4 @@ impl SpinState { } } - fn main() {} diff --git a/src/main.rs b/src/main.rs index 7b7c632..b66e6e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,14 +52,14 @@ pub fn run(args: cli::Args) { let Point(x, y) = canvas.world_region.end; (x - 2, y - 2) }; - kicks.add_object("top left", circle_at(1, 1).color(fill)); - kicks.add_object("top right", circle_at(end_x, 1).color(fill)); - kicks.add_object("bottom left", circle_at(1, end_y).color(fill)); - kicks.add_object("bottom right", circle_at(end_x, end_y).color(fill)); + kicks.set_object("top left", circle_at(1, 1).color(fill)); + kicks.set_object("top right", circle_at(end_x, 1).color(fill)); + kicks.set_object("bottom left", circle_at(1, end_y).color(fill)); + kicks.set_object("bottom right", circle_at(end_x, end_y).color(fill)); canvas.add_or_replace_layer(kicks); let mut ch = Layer::new("ch"); - ch.add_object("0", Object::Dot(Point(0, 0)).into()); + ch.set_object("0", Object::Dot(Point(0, 0)).into()); canvas.add_or_replace_layer(ch); }) .sync_audio_with(&args.flag_sync_with.unwrap()) @@ -71,7 +71,13 @@ pub fn run(args: cli::Args) { canvas.layer("anchor kick").flush(); - ctx.later_ms(200, &fade_out_kick_circles) + // ctx.later_ms(200, &fade_out_kick_circles) + ctx.animate(200, &|t, canvas, _| { + canvas + .layer("anchor kick") + .paint_all_objects(Fill::Translucent(Color::White, 1.0 - t)); + canvas.layer("anchor kick").flush(); + }); }) .on_note("bass", &|canvas, ctx| { let mut new_layer = canvas.random_layer_within("bass", &ctx.extra.bass_pattern_at); @@ -140,7 +146,7 @@ pub fn run(args: cli::Args) { layer.objects.retain(|name, _| dots_to_keep.contains(name)); let object_name = format!("{}", ctx.ms); - layer.add_object( + layer.set_object( &object_name, Object::Dot(world.resized(-1, -1).random_coordinates_within().into()) .color(Fill::Solid(Color::Cyan)), @@ -150,7 +156,7 @@ pub fn run(args: cli::Args) { canvas.layer("ch").flush(); }) .when_remaining(10, &|canvas, _| { - canvas.root().add_object( + canvas.root().set_object( "credits text", Object::RawSVG(Box::new(svg::node::Text::new("by ewen-lbh"))).into(), ); @@ -167,7 +173,7 @@ pub fn run(args: cli::Args) { } } -fn fade_out_kick_circles(canvas: &mut Canvas) { +fn fade_out_kick_circles(canvas: &mut Canvas, _: usize) { canvas .layer("anchor kick") .paint_all_objects(Fill::Translucent(Color::White, 0.0)); diff --git a/src/video.rs b/src/video.rs index 55869d6..ecaed30 100644 --- a/src/video.rs +++ b/src/video.rs @@ -22,7 +22,8 @@ pub type CommandAction = dyn Fn(String, &mut Canvas, &mut Context); /// Arguments: canvas, context, previous rendered beat, previous rendered frame pub type HookCondition = dyn Fn(&Canvas, &Context, usize, usize) -> bool; -pub type LaterRenderFunction = dyn Fn(&mut Canvas); +/// Arguments: canvas, context, current milliseconds timestamp +pub type LaterRenderFunction = dyn Fn(&mut Canvas, usize); /// Arguments: canvas, context, previous rendered beat pub type LaterHookCondition = dyn Fn(&Canvas, &Context, usize) -> bool; @@ -49,6 +50,8 @@ pub struct Hook { pub struct LaterHook { pub when: Box>, pub render_function: Box, + /// Whether the hook should be run only once + pub once: bool, } impl std::fmt::Debug for Hook { @@ -561,7 +564,11 @@ impl Video { for (i, hook) in context.later_hooks.iter().enumerate() { if (hook.when)(&canvas, &context, previous_rendered_beat) { - (hook.render_function)(&mut canvas); + (hook.render_function)(&mut canvas, context.ms); + if hook.once { + later_hooks_to_delete.push(i); + } + } else if !hook.once { later_hooks_to_delete.push(i); } }