This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

🍱 Continue Schedule Hell, object clipping(wip), animation easing, region fixes

+334 -165
+7
Cargo.lock
··· 800 800 ] 801 801 802 802 [[package]] 803 + name = "easing-function" 804 + version = "0.1.1" 805 + source = "registry+https://github.com/rust-lang/crates.io-index" 806 + checksum = "1ff18235504129e34c411871066cfa4ad108ed6a28d5fb2417057aff7f99bee4" 807 + 808 + [[package]] 803 809 name = "either" 804 810 version = "1.15.0" 805 811 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2698 2704 "chrono", 2699 2705 "console 0.16.1", 2700 2706 "docopt", 2707 + "easing-function", 2701 2708 "env_logger", 2702 2709 "futures-util", 2703 2710 "getrandom 0.2.16",
+8 -1
Cargo.toml
··· 33 33 "dep:tungstenite", 34 34 ] 35 35 web = ["dep:wasm-bindgen", "dep:web-sys"] 36 - video = ["dep:env_logger", "dep:vgv", "dep:notify-rust", "dep:tokio"] 36 + video = [ 37 + "dep:env_logger", 38 + "dep:vgv", 39 + "dep:notify-rust", 40 + "dep:tokio", 41 + "dep:easing-function", 42 + ] 37 43 video-server = ["dep:axum"] 38 44 39 45 [dependencies] ··· 90 96 serde-aux = "4.7.0" 91 97 notify-rust = { version = "4.11.7", optional = true } 92 98 tokio = { version = "1.48.0", optional = true } 99 + easing-function = { version = "0.1.1", optional = true } 93 100 94 101 95 102 [dev-dependencies]
+2 -2
Justfile
··· 3 3 4 4 5 5 s *args: 6 - just schedule-hell --resolution 480 {{args}} 6 + just schedule-hell {{args}} 7 7 8 8 backbone *args: 9 - just schedule-hell --resolution 480 --marker "\"end first break\"" {{args}} 9 + just schedule-hell --marker "\"end first break\"" {{args}} 10 10 11 11 [working-directory: 'examples/schedule-hell'] 12 12 schedule-hell *args:
+18 -17
examples/schedule-hell/src/main.rs
··· 3 3 use anyhow::anyhow; 4 4 use itertools::Itertools; 5 5 use rand::{SeedableRng, rngs::SmallRng}; 6 - use shapemaker::{ui::Log, video::engine::EngineControl, *}; 6 + use shapemaker::{ui::Log, *}; 7 7 use std::{fs, path::PathBuf, time::Duration}; 8 8 9 9 pub struct State { ··· 31 31 let mut video = Video::<State>::new(canvas); 32 32 let mut args = pico_args::Arguments::from_env(); 33 33 34 - video.duration_override = args 35 - .value_from_str("--duration") 36 - .ok() 37 - .map(Duration::from_secs); 38 - 39 - if video.duration_override.is_some_and(|d| d.is_zero()) { 40 - video.duration_override = None; 41 - } 42 - 43 - video.start_rendering_at = args 44 - .value_from_str("--start") 45 - .ok() 46 - .map(Timestamp::from_seconds) 47 - .unwrap_or_default(); 48 - 49 34 video = video 50 35 // Sync inputs // 51 36 .sync_audio_with("schedule-hell.midi") ··· 72 57 marker_end.map(|end| Duration::from_millis((end - marker_start) as _)) 73 58 } 74 59 60 + video.duration_override = Some( 61 + args.value_from_str("--duration") 62 + .map(Duration::from_secs) 63 + .unwrap_or(video.duration_override.unwrap_or_default()), 64 + ); 65 + 66 + if video.duration_override.is_some_and(|d| d.is_zero()) { 67 + video.duration_override = None; 68 + } 69 + 70 + video.start_rendering_at = args 71 + .value_from_str("--start") 72 + .ok() 73 + .map(Timestamp::from_seconds) 74 + .unwrap_or(video.start_rendering_at); 75 + 75 76 video.resolution = args.value_from_str("--resolution").ok().unwrap_or(480); 76 77 video.fps = args.value_from_str("--fps").ok().unwrap_or(30); 77 78 ··· 109 110 video.serve(&destination).await; 110 111 } else { 111 112 let result = if destination.ends_with(".svg") { 112 - let render_ahead = 10; 113 + let render_ahead = 1_000; 113 114 114 115 let frame_no = destination 115 116 .trim_end_matches(".svg")
+16 -10
examples/schedule-hell/src/scenes/backbone.rs
··· 1 1 use anyhow::Result; 2 2 use rand::{Rng, rngs::SmallRng, seq::IteratorRandom}; 3 - use shapemaker::*; 3 + use shapemaker::{context::Context, video::hooks::Hook, *}; 4 4 5 5 use crate::State; 6 6 ··· 23 23 iterate(&mut ctx.extra.rng, canvas)?; 24 24 Ok(()) 25 25 }) 26 - .each_n_frame(3, &|canvas, ctx| { 27 - canvas.clear(); 28 - iterate(&mut ctx.extra.rng, canvas)?; 29 - Ok(()) 30 - }) 26 + // .each_n_frame(3, &|canvas, ctx| { 27 + // canvas.clear(); 28 + // iterate(&mut ctx.extra.rng, canvas)?; 29 + // Ok(()) 30 + // }) 31 31 .on_note("anchor kick", &|canvas, ctx| { 32 32 canvas.clear(); 33 33 iterate(&mut ctx.extra.rng, canvas)?; ··· 42 42 || id == &format!("crosses-NWSE-{point}") 43 43 }); 44 44 45 - ctx.animate(700, &move |t, canvas, _| { 45 + ctx.animate(200, &move |t, canvas, _| { 46 46 canvas 47 47 .layer("flickers")? 48 48 .objects_with_tag("rotate") 49 49 .for_each(|(_, obj)| { 50 50 obj.recolor(Cyan); 51 - obj.set_rotation(Angle::from_degrees(t * 45.0)); 51 + obj.set_rotation(Angle::from_degrees(t * 90.0)); 52 52 }); 53 53 54 54 Ok(()) 55 55 }); 56 56 57 57 Ok(()) 58 + }) 59 + .dump_frame_when(&|_, ctx, _, _| match ctx.since_scene_start() { 60 + Some(t) => t.as_millis() == 500, 61 + None => false, 58 62 }) 59 63 } 60 64 ··· 108 112 format!("crosses-SWNE-{point}"), 109 113 Object::Line(point, point.translated(1, 1), grid_thickness) 110 114 .colored(Color::Purple) 111 - .opacified(0.25 + rng.random_range(0.5..1.0)), 115 + .flickering(rng, 0.25) 116 + .clipped_to((point, point)), 112 117 ); 113 118 flickers.set( 114 119 format!("crosses-NWSE-{point}"), ··· 118 123 grid_thickness, 119 124 ) 120 125 .colored(Color::Purple) 121 - .opacified(0.25 + rng.random_range(0.5..1.0)), 126 + .flickering(rng, 0.25) 127 + .clipped_to((point, point)), 122 128 ); 123 129 } 124 130
+6
src/geometry/point.rs
··· 67 67 } 68 68 } 69 69 70 + impl From<(&usize, &usize)> for Point { 71 + fn from(value: (&usize, &usize)) -> Self { 72 + Self(*value.0, *value.1) 73 + } 74 + } 75 + 70 76 impl From<(i32, i32)> for Point { 71 77 fn from(value: (i32, i32)) -> Self { 72 78 Self(value.0 as usize, value.1 as usize)
+51 -32
src/geometry/region.rs
··· 1 1 use crate::{Object, Point}; 2 - use anyhow::{Error, Result, format_err}; 2 + use anyhow::{Error, Result, anyhow, format_err}; 3 3 use backtrace::Backtrace; 4 4 #[cfg(feature = "web")] 5 5 use wasm_bindgen::prelude::*; ··· 85 85 pub fn rectangle(&self) -> Object { 86 86 Object::Rectangle(self.start, self.end) 87 87 } 88 + 89 + pub fn center_coords(&self, cell_size: usize) -> (f32, f32) { 90 + let (x, y) = self.center().coords(cell_size); 91 + let (h, w) = self.size(cell_size); 92 + 93 + (x + (w / 2.0), y + (h / 2.0)) 94 + } 88 95 } 89 96 90 97 pub struct RegionIterator { ··· 176 183 177 184 impl Region { 178 185 pub fn new( 179 - start_x: usize, 180 - start_y: usize, 181 - end_x: usize, 182 - end_y: usize, 186 + start: impl Into<Point>, 187 + end: impl Into<Point>, 183 188 ) -> Result<Self, Error> { 184 189 let region = Self { 185 - start: (start_x, start_y).into(), 186 - end: (end_x, end_y).into(), 190 + start: start.into(), 191 + end: end.into(), 187 192 }; 188 193 region.ensure_valid() 189 194 } 190 195 191 - pub fn from_points(start: Point, end: Point) -> Result<Self, Error> { 192 - Self::new(start.0, start.1, end.0, end.1) 193 - } 194 - 195 196 pub fn bottomleft(&self) -> Point { 196 197 Point(self.start.0, self.end.1) 197 198 } ··· 230 231 } 231 232 232 233 pub fn from_origin(end: Point) -> Result<Self> { 233 - Self::new(0, 0, end.0, end.1) 234 + Self::new((0, 0), end) 234 235 } 235 236 236 237 pub fn from_topleft(origin: Point, size: (usize, usize)) -> Result<Self> { 237 - Self::from_points( 238 + Self::new( 238 239 origin, 239 240 origin.translated_by(Point::from(size).translated(-1, -1)), 240 241 ) ··· 253 254 } 254 255 255 256 pub fn from_bottomright(origin: Point, size: (usize, usize)) -> Result<Self> { 256 - Self::from_points( 257 + Self::new( 257 258 origin.translated(-(size.0 as i32 - 1), -(size.1 as i32 - 1)), 258 259 origin, 259 260 ) ··· 280 281 ) -> Result<Self> { 281 282 let half_size = (size.0 / 2, size.1 / 2); 282 283 Self::new( 283 - center.0 - half_size.0, 284 - center.1 - half_size.1, 285 - center.0 + half_size.0, 286 - center.1 + half_size.1, 284 + (center.0 - half_size.0, center.1 - half_size.1), 285 + (center.0 + half_size.0, center.1 + half_size.1), 287 286 ) 288 287 } 289 288 ··· 297 296 )); 298 297 } 299 298 299 + // check that no point's coordinate is too close to usize::MAX 300 + if vec![self.start.0, self.start.1, self.end.0, self.end.1] 301 + .iter() 302 + .any(|&coord| coord >= usize::MAX - 10) 303 + { 304 + return Err(format_err!( 305 + "Invalid region: coordinate very close to usize::MAX in region {:?}", 306 + self 307 + )); 308 + } 309 + 300 310 Ok(self) 301 311 } 302 312 ··· 324 334 let resulting = Self { 325 335 start: self.start, 326 336 end: ( 327 - (self.end.0 as i32 + add_x) as usize, 328 - (self.end.1 as i32 + add_y) as usize, 337 + (self.end.0.saturating_add_signed(add_x as _)), 338 + (self.end.1.saturating_add_signed(add_y as _)), 329 339 ) 330 340 .into(), 331 341 }; 332 342 333 - if resulting.ensure_valid().is_err() { 334 - let bt = Backtrace::new(); 335 - println!( 336 - "WARN: Did not enlarge region {self} with ({add_x}, {add_y}), it would result in a non-valid region\n{bt:?}" 337 - ); 338 - return *self; 339 - } 340 - 341 343 resulting 344 + .ensure_valid() 345 + .map_err(|e| { 346 + anyhow!( 347 + "Invalid enlargement of ({add_x}, {add_y}) on {self:?}: {e:?}" 348 + ) 349 + }) 350 + .unwrap() 342 351 } 343 352 344 353 /// resized is like enlarged, but transforms from the center, by first translating the region by (-dx, -dy) ··· 390 399 } 391 400 392 401 pub fn height(&self) -> usize { 393 - if self.end.1 < self.start.1 { 402 + let (Point(_, sy), Point(_, ey)) = (self.start, self.end); 403 + 404 + if ey < sy { 394 405 return 0; 395 406 } 396 407 397 - self.end.1 - self.start.1 + 1 408 + ey.checked_sub(sy) 409 + .expect(&format!("{self:?} overflows when computing height")) 410 + .checked_add(1) 411 + .expect(&format!( 412 + "{self:?} overflows when adjusting height computation" 413 + )) 398 414 } 399 415 400 416 pub fn dimensions(&self) -> (usize, usize) { 401 417 (self.width(), self.height()) 402 418 } 403 419 404 - pub fn size(&self, cell_size: usize) -> (usize, usize) { 405 - (self.width() * cell_size, self.height() * cell_size) 420 + pub fn size(&self, cell_size: usize) -> (f32, f32) { 421 + ( 422 + (self.width() * cell_size) as f32, 423 + (self.height() * cell_size) as f32, 424 + ) 406 425 } 407 426 408 427 // goes from -width to width (inclusive on both ends)
+1 -1
src/graphics/canvas.rs
··· 54 54 font_options: FontOptions::default(), 55 55 colormap: ColorMapping::default(), 56 56 layers: vec![Layer::new("root")], 57 - world_region: Region::new(0, 0, 3, 3).unwrap(), 57 + world_region: Region::new((0, 0), (3, 3)).unwrap(), 58 58 background: None, 59 59 fontdb: None, 60 60 }
+1
src/graphics/mod.rs
··· 5 5 pub mod layer; 6 6 pub mod objects; 7 7 pub mod transform; 8 + pub mod region; 8 9 9 10 pub use canvas::Canvas; 10 11 pub use color::{Color, ColorMapping};
+37 -6
src/graphics/objects.rs
··· 1 - use std::fmt::Display; 2 - 3 1 use crate::{Angle, Fill, Filter, Point, Region, Transformation}; 2 + use anyhow::anyhow; 4 3 use itertools::Itertools; 4 + use std::fmt::Display; 5 5 #[cfg(feature = "web")] 6 6 use wasm_bindgen::prelude::*; 7 7 ··· 57 57 pub filters: Vec<Filter>, 58 58 pub transformations: Vec<Transformation>, 59 59 pub tags: Vec<String>, 60 + pub clip_to: Option<Region> 60 61 } 61 62 62 63 impl ColoredObject { ··· 87 88 self 88 89 } 89 90 91 + pub fn clipped_to(mut self, region: impl Into<Region>) -> Self { 92 + self.clip_to = Some(region.into()); 93 + self 94 + } 95 + 90 96 pub fn clear_filters(&mut self) { 91 97 self.filters.clear(); 92 98 } ··· 137 143 let tag_str = format!("{tag}"); 138 144 self.tags.iter().any(|t| t == &tag_str) 139 145 } 146 + 147 + 140 148 } 141 149 142 150 impl std::fmt::Display for ColoredObject { ··· 147 155 filters, 148 156 transformations, 149 157 tags, 158 + clip_to 150 159 } = self; 151 160 152 161 if fill.is_some() { ··· 167 176 write!(f, "{}", tags.iter().map(|t| format!("#{t}")).join(" "))?; 168 177 } 169 178 179 + if let Some(clip_to) = clip_to { 180 + write!(f, " (clipped to {:?})", clip_to)?; 181 + } 182 + 170 183 Ok(()) 171 184 } 172 185 } ··· 179 192 filters: vec![], 180 193 transformations: vec![], 181 194 tags: vec![], 195 + clip_to: None 182 196 } 183 197 } 184 198 } ··· 191 205 filters: vec![], 192 206 transformations: vec![], 193 207 tags: vec![], 208 + clip_to: None 194 209 } 195 210 } 196 211 } ··· 285 300 // println!("region for {:?} -> {}", self, region); 286 301 region 287 302 } 288 - Object::Line(start, end, _) 289 - | Object::CurveInward(start, end, _) 290 - | Object::CurveOutward(start, end, _) 291 - | Object::Rectangle(start, end) => (start, end).into(), 303 + Object::Line(Point(x1, y1), Point(x2, y2), _) 304 + | Object::CurveInward(Point(x1, y1), Point(x2, y2), _) 305 + | Object::CurveOutward(Point(x1, y1), Point(x2, y2), _) => { 306 + let region = Region::new( 307 + (x1.min(x2), y1.min(y2)), 308 + (x1.max(x2), y1.max(y2)), 309 + ) 310 + .map_err(|e| { 311 + anyhow!("Could not construct region of {self:?}: {e:?}") 312 + }) 313 + .unwrap(); 314 + 315 + region.enlarged( 316 + if region.width() > 1 { -1 } else { 0 }, 317 + if region.height() > 1 { -1 } else { 0 }, 318 + ) 319 + } 320 + Object::Rectangle(start, end) => { 321 + Region::new(*start, *end).unwrap().enlarged(-1, -1) 322 + } 292 323 Object::Text(anchor, _, _) 293 324 | Object::CenteredText(anchor, ..) 294 325 | Object::Dot(anchor)
+7
src/graphics/region.rs
··· 1 + use crate::Region; 2 + 3 + impl Region { 4 + pub fn clip_path_id(&self) -> String { 5 + format!("clip-{}-{}", self.start, self.end) 6 + } 7 + }
+1
src/random/fill.rs
··· 32 32 ) 33 33 } 34 34 } 35 +
+7 -1
src/random/objects.rs
··· 1 1 use rand::{Rng, distr::uniform::SampleRange}; 2 2 3 - use crate::{LineSegment, Object, Point, Region}; 3 + use crate::{ColoredObject, LineSegment, Object, Point, Region}; 4 4 5 5 impl Object { 6 6 pub fn random_starting_at<R: rand::Rng>( ··· 103 103 } 104 104 } 105 105 } 106 + 107 + impl ColoredObject { 108 + pub fn flickering(self, rng: &mut impl Rng, amplitude: f32) -> Self { 109 + self.opacified(rng.random_range((1.0 - amplitude).max(0.0)..1.0)) 110 + } 111 + }
+24 -2
src/rendering/canvas.rs
··· 1 1 use super::renderable::SVGRenderable; 2 2 use crate::{ 3 + ColoredObject, 3 4 graphics::canvas::Canvas, 4 5 rendering::{ 5 6 rasterization::{ ··· 10 11 }, 11 12 }; 12 13 use measure_time::debug_time; 14 + use std::path::PathBuf; 13 15 14 16 impl SVGRenderable for Canvas { 15 17 fn render_to_svg( ··· 59 61 } 60 62 } 61 63 64 + for layer in self.layers.iter() { 65 + for ColoredObject { clip_to, .. } in layer.objects.values() { 66 + if let Some(region) = clip_to { 67 + defs.add( 68 + svg::tag("clipPath") 69 + .attr("id", region.clip_path_id()) 70 + .child( 71 + svg::tag("rect") 72 + .position(region.start, cell_size) 73 + .size(*region, cell_size) 74 + .node(), 75 + ), 76 + ); 77 + } 78 + } 79 + } 80 + 62 81 svg.add(defs); 63 82 64 83 Ok(svg ··· 136 155 Ok(rendered.to_string()) 137 156 } 138 157 139 - pub fn render_to_svg_file(&mut self, at: &str) -> anyhow::Result<()> { 158 + pub fn render_to_svg_file( 159 + &mut self, 160 + at: impl Into<PathBuf>, 161 + ) -> anyhow::Result<()> { 140 162 debug_time!("render_to_svg_file"); 141 163 142 - std::fs::write(at, self.render_to_svg_string()?)?; 164 + std::fs::write(at.into(), self.render_to_svg_string()?)?; 143 165 144 166 Ok(()) 145 167 }
+22 -19
src/rendering/objects.rs
··· 30 30 .fill 31 31 .render_to_css(&colormap.clone(), !self.object.fillable()); 32 32 33 - if !self.transformations.is_empty() || !self.filters.is_empty() { 34 - let start = self.object.region().start.coords(cell_size); 35 - let (w, h) = ( 36 - self.object.region().width() * cell_size, 37 - self.object.region().height() * cell_size, 38 - ); 33 + let object_svg = if !self.transformations.is_empty() 34 + || !self.filters.is_empty() 35 + { 36 + // transform-box is not supported by resvg yet 37 + // css += "transform-box: fill-box; transform-origin: 50% 50%;"; 38 + 39 + let (center_x, center_y) = 40 + self.object.region().center_coords(cell_size); 39 41 40 - css += "transform-box: fill-box;"; 42 + css += &format!("transform-origin: {center_x}px {center_y}px;"); 41 43 42 44 css += self 43 45 .filters ··· 46 48 .join(" ") 47 49 .as_ref(); 48 50 49 - Ok(svg::tag("g") 51 + svg::tag("g") 50 52 .dataset("object", id) 51 - .attr( 52 - "transform-origin", 53 - &format!( 54 - "{} {}", 55 - start.0 + (w as f32 / 2.0), 56 - start.1 + (h as f32 / 2.0) 57 - ), 58 - ) 59 53 .with_attributes(self.transformations.render_to_svg_attributes( 60 54 colormap, 61 55 cell_size, ··· 64 58 )?) 65 59 .wrapping(vec![plain_obj]) 66 60 .attr("style", &css) 67 - .into()) 61 + .into() 68 62 } else { 69 - Ok(match plain_obj { 63 + match plain_obj { 70 64 svg::Node::Element(el) => el.attr("style", &css).into(), 71 65 _ => plain_obj, 72 - }) 66 + } 67 + }; 68 + 69 + if let Some(region) = &self.clip_to { 70 + Ok(svg::tag("g") 71 + .attr("clip-path", region.clip_path_id()) 72 + .child(object_svg) 73 + .into()) 74 + } else { 75 + Ok(object_svg) 73 76 } 74 77 } 75 78 }
+9 -2
src/rendering/svg.rs
··· 111 111 } 112 112 113 113 /// Sets width and height 114 - pub fn dimensions(self, p: impl Into<(usize, usize)>) -> Self { 114 + pub fn dimensions<Coord: Into<f32>>(self, p: impl Into<(Coord, Coord)>) -> Self { 115 115 let (w, h) = p.into(); 116 - self.attr("width", w).attr("height", h) 116 + self.attr("width", w.into()).attr("height", h.into()) 117 117 } 118 118 119 119 /// Sets width and height ··· 157 157 ) -> Self { 158 158 Element { 159 159 children: children.into_iter().map(|n| n.into()).collect(), 160 + ..self 161 + } 162 + } 163 + 164 + pub fn child(self, child: impl Into<Node>) -> Self { 165 + Element { 166 + children: vec![child.into()], 160 167 ..self 161 168 } 162 169 }
+76 -2
src/video/animation.rs
··· 1 + use crate::{Canvas, Layer, context::Context, video::hooks::InnerHook}; 2 + use easing_function::Easing; 3 + pub use easing_function::{EasingFunction, easings}; 4 + use nanoid::nanoid; 1 5 use std::fmt::Display; 2 - 3 - use crate::{Canvas, Layer}; 4 6 5 7 /// Arguments: animation progress (from 0.0 to 1.0), canvas, current ms 6 8 pub type AnimationUpdateFunction = ··· 59 61 Self { name, update: f } 60 62 } 61 63 } 64 + 65 + impl<C: Default> Context<'_, C> { 66 + /// duration is in milliseconds 67 + pub fn start_animation( 68 + &mut self, 69 + duration: usize, 70 + easing: impl Into<EasingFunction>, 71 + animation: Animation, 72 + ) { 73 + let start_ms = self.ms; 74 + let ms_range = start_ms..(start_ms + duration); 75 + let easing = easing.into(); 76 + 77 + self.inner_hooks.push(InnerHook { 78 + once: false, 79 + when: Box::new(move |_, ctx, _| ms_range.contains(&ctx.ms)), 80 + render_function: Box::new(move |canvas, ms| { 81 + let t = (ms - start_ms) as f32 / duration as f32; 82 + (animation.update)(easing.ease(t), canvas, ms) 83 + }), 84 + }) 85 + } 86 + 87 + /// duration is in milliseconds 88 + /// Animates with ease-in-out quadratic easing 89 + /// See animat_linear or animate_eased for other options 90 + pub fn animate( 91 + &mut self, 92 + duration: usize, 93 + f: &'static AnimationUpdateFunction, 94 + ) { 95 + self.animate_eased(duration, easings::EaseInOutQuadradic, f); 96 + } 97 + 98 + pub fn animate_linear( 99 + &mut self, 100 + duration: usize, 101 + f: &'static AnimationUpdateFunction, 102 + ) { 103 + self.animate_eased(duration, easings::Linear, f); 104 + } 105 + 106 + pub fn animate_eased( 107 + &mut self, 108 + duration: usize, 109 + easing: impl Into<EasingFunction>, 110 + f: &'static AnimationUpdateFunction, 111 + ) { 112 + self.start_animation( 113 + duration, 114 + easing.into(), 115 + Animation::new(format!("unnamed animation {}", nanoid!()), f), 116 + ); 117 + } 118 + 119 + pub fn animate_layer( 120 + &mut self, 121 + layer: &'static str, 122 + duration: usize, 123 + f: &'static LayerAnimationUpdateFunction, 124 + ) { 125 + let animation = Animation { 126 + name: format!("unnamed animation {}", nanoid!()), 127 + update: Box::new(move |progress, canvas, ms| { 128 + (f)(progress, canvas.layer_unchecked(layer), ms)?; 129 + Ok(()) 130 + }), 131 + }; 132 + 133 + self.start_animation(duration, easings::EaseInOutQuadradic, animation); 134 + } 135 + }
+6 -43
src/video/context.rs
··· 93 93 Duration::from_millis(self.ms as _) 94 94 } 95 95 96 + pub fn since_scene_start(&self) -> Option<Duration> { 97 + self.scene_started_at_ms 98 + .map(|start_ms| Duration::from_millis((self.ms - start_ms) as _)) 99 + } 100 + 96 101 pub fn notes_of_stem(&self, name: &str) -> impl Iterator<Item = Note> + '_ { 97 102 let stem = &self.syncdata.stems[name]; 98 103 stem.notes ··· 193 198 ); 194 199 } 195 200 196 - /// duration is in milliseconds 197 - pub fn start_animation(&mut self, duration: usize, animation: Animation) { 198 - let start_ms = self.ms; 199 - let ms_range = start_ms..(start_ms + duration); 200 - 201 - self.inner_hooks.push(InnerHook { 202 - once: false, 203 - when: Box::new(move |_, ctx, _| ms_range.contains(&ctx.ms)), 204 - render_function: Box::new(move |canvas, ms| { 205 - let t = (ms - start_ms) as f32 / duration as f32; 206 - (animation.update)(t, canvas, ms) 207 - }), 208 - }) 209 - } 210 - 211 - /// duration is in milliseconds 212 - pub fn animate( 213 - &mut self, 214 - duration: usize, 215 - f: &'static AnimationUpdateFunction, 216 - ) { 217 - self.start_animation( 218 - duration, 219 - Animation::new(format!("unnamed animation {}", nanoid!()), f), 220 - ); 221 - } 222 - 223 - pub fn animate_layer( 224 - &mut self, 225 - layer: &'static str, 226 - duration: usize, 227 - f: &'static LayerAnimationUpdateFunction, 228 - ) { 229 - let animation = Animation { 230 - name: format!("unnamed animation {}", nanoid!()), 231 - update: Box::new(move |progress, canvas, ms| { 232 - (f)(progress, canvas.layer_unchecked(layer), ms)?; 233 - Ok(()) 234 - }), 235 - }; 236 - 237 - self.start_animation(duration, animation); 238 - } 201 + 239 202 240 203 pub fn switch_scene(&mut self, scene_name: impl Display) { 241 204 self.current_scene = Some(scene_name.to_string());
+34 -26
src/video/hooks.rs
··· 53 53 } 54 54 } 55 55 56 - pub trait AttachHooks<AdditionalContext>: Sized { 57 - fn with_hook(self, hook: Hook<AdditionalContext>) -> Self; 56 + pub trait AttachHooks<C>: Sized { 57 + fn with_hook(self, hook: Hook<C>) -> Self; 58 58 59 - fn init( 59 + fn hook( 60 60 self, 61 - render_function: &'static RenderFunction<AdditionalContext>, 61 + when: &'static super::hooks::HookCondition<C>, 62 + render_function: &'static super::hooks::RenderFunction<C>, 62 63 ) -> Self { 63 64 self.with_hook(Hook { 64 - when: Box::new(move |_, context, _, _| context.rendered_frames == 0), 65 + when: Box::new(when), 65 66 render_function: Box::new(render_function), 66 67 }) 67 68 } 68 69 70 + fn init(self, render_function: &'static RenderFunction<C>) -> Self { 71 + self.hook( 72 + &|_, context: &Context<C>, _, _| context.rendered_frames == 0, 73 + render_function, 74 + ) 75 + } 76 + 77 + fn dump_frame_when(self, when: &'static HookCondition<C>) -> Self { 78 + self.hook(when, &|canvas, ctx| { 79 + canvas 80 + .render_to_svg_file(format!("frame-{}.svg", ctx.rendered_frames)) 81 + }) 82 + } 83 + 69 84 // TODO The &'static requirement might be possibly liftable, see https://users.rust-lang.org/t/how-to-store-functions-in-structs/58089 70 85 fn on( 71 86 self, 72 87 marker_text: &'static str, 73 - render_function: &'static RenderFunction<AdditionalContext>, 88 + render_function: &'static RenderFunction<C>, 74 89 ) -> Self { 75 90 self.with_hook(Hook { 76 91 when: Box::new(move |_, context, _, _| { ··· 96 111 }) 97 112 } 98 113 99 - fn each_beat( 100 - self, 101 - render_function: &'static RenderFunction<AdditionalContext>, 102 - ) -> Self { 114 + fn each_beat(self, render_function: &'static RenderFunction<C>) -> Self { 103 115 self.with_hook(Hook { 104 116 when: Box::new( 105 117 move |_, ··· 119 131 self, 120 132 amount: f32, 121 133 unit: MusicalDurationUnit, 122 - render_function: &'static RenderFunction<AdditionalContext>, 134 + render_function: &'static RenderFunction<C>, 123 135 ) -> Self { 124 136 let beats = match unit { 125 137 MusicalDurationUnit::Beats => amount, ··· 138 150 }) 139 151 } 140 152 141 - fn each_frame( 142 - self, 143 - render_function: &'static RenderFunction<AdditionalContext>, 144 - ) -> Self { 153 + fn each_frame(self, render_function: &'static RenderFunction<C>) -> Self { 145 154 self.each_n_frame(1, render_function) 146 155 } 147 156 148 157 fn each_n_frame( 149 158 self, 150 159 n: usize, 151 - render_function: &'static RenderFunction<AdditionalContext>, 160 + render_function: &'static RenderFunction<C>, 152 161 ) -> Self { 153 162 self.with_hook(Hook { 154 163 when: Box::new(move |_, context, _, previous_rendered_frame| { ··· 167 176 self, 168 177 stem_name: &'static str, 169 178 threshold: f32, 170 - above_amplitude: &'static RenderFunction<AdditionalContext>, 171 - below_amplitude: &'static RenderFunction<AdditionalContext>, 179 + above_amplitude: &'static RenderFunction<C>, 180 + below_amplitude: &'static RenderFunction<C>, 172 181 ) -> Self { 173 182 self.with_hook(Hook { 174 183 when: Box::new(move |_, context, _, _| { ··· 188 197 fn on_note( 189 198 self, 190 199 stems: &'static str, 191 - render_function: &'static RenderFunction<AdditionalContext>, 200 + render_function: &'static RenderFunction<C>, 192 201 ) -> Self { 193 202 self.with_hook(Hook { 194 203 when: Box::new(move |_, ctx, _, _| { ··· 205 214 fn on_note_end( 206 215 self, 207 216 stems: &'static str, 208 - render_function: &'static RenderFunction<AdditionalContext>, 217 + render_function: &'static RenderFunction<C>, 209 218 ) -> Self { 210 219 self.with_hook(Hook { 211 220 when: Box::new(move |_, ctx, _, _| { ··· 228 237 create_object: &'static ObjectCreator, 229 238 ) -> Self 230 239 where 231 - ObjectCreator: Fn(&Canvas, &mut Context<AdditionalContext>) -> Result<ColoredObject> 232 - + Send 233 - + Sync, 240 + ObjectCreator: 241 + Fn(&Canvas, &mut Context<C>) -> Result<ColoredObject> + Send + Sync, 234 242 { 235 243 self.with_hook(Hook { 236 244 when: Box::new(move |_, ctx, _, _| { ··· 265 273 fn at_frame( 266 274 self, 267 275 frame: usize, 268 - render_function: &'static RenderFunction<AdditionalContext>, 276 + render_function: &'static RenderFunction<C>, 269 277 ) -> Self { 270 278 self.with_hook(Hook { 271 279 when: Box::new(move |_, context, _, _| context.frame() == frame), ··· 276 284 fn when_remaining( 277 285 self, 278 286 seconds: usize, 279 - render_function: &'static RenderFunction<AdditionalContext>, 287 + render_function: &'static RenderFunction<C>, 280 288 ) -> Self { 281 289 self.with_hook(Hook { 282 290 when: Box::new(move |_, ctx, _, _| { ··· 289 297 fn at_timestamp( 290 298 self, 291 299 timestamp: &'static str, 292 - render_function: &'static RenderFunction<AdditionalContext>, 300 + render_function: &'static RenderFunction<C>, 293 301 ) -> Self { 294 302 let hook = Hook { 295 303 when: Box::new(move |_, context, _, previous_rendered_frame| {
+1 -1
src/video/mod.rs
··· 13 13 #[cfg(feature = "video-server")] 14 14 pub mod server; 15 15 16 - pub use animation::Animation; 16 + pub use animation::{Animation, easings}; 17 17 pub use hooks::AttachHooks; 18 18 pub use scene::Scene; 19 19 pub use video::Timestamp;