diff --git a/turtle-lib-macros/Cargo.toml b/turtle-lib-macros/Cargo.toml index fe9f050..7d677b2 100644 --- a/turtle-lib-macros/Cargo.toml +++ b/turtle-lib-macros/Cargo.toml @@ -10,4 +10,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -syn = { version = "2.0", features = ["full"] } +syn = { version = "3.0", features = ["full"] } diff --git a/turtle-lib/Cargo.toml b/turtle-lib/Cargo.toml index d096df5..9b485c3 100644 --- a/turtle-lib/Cargo.toml +++ b/turtle-lib/Cargo.toml @@ -18,7 +18,7 @@ crossbeam = "0.8" [dev-dependencies] # For examples and testing tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } -dialog = "*" +dialog = "0.3" chrono = "0.4" [features] diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index 9f02241..fb2a505 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -5,12 +5,7 @@ use crate::state::{DrawCommand, TurtleParams, TurtleWorld}; use crate::tessellation; use macroquad::prelude::*; -// Import the easing function from the tween crate -// To change the easing, change both this import and the usage in the draw_tween_arc function below -// Available options: Linear, SineInOut, QuadInOut, CubicInOut, QuartInOut, QuintInOut, -// ExpoInOut, CircInOut, BackInOut, ElasticInOut, BounceInOut, etc. -// See https://easings.net/ for visual demonstrations -use tween::CubicInOut; + /// Render the turtle world with active tween visualization. #[allow(clippy::too_many_lines)] @@ -136,15 +131,11 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { radius.value(), *direction, ); - let elapsed = get_time() - tween.start_time; - let progress = (elapsed / tween.duration).min(1.0); - let eased_progress = CubicInOut.tween(1.0, progress as f32); - // Delegate to the shared arc_points function — same sampling - // strategy as tessellate_arc, eliminating the divergence. + // strategy as tessellate_arc, using tween.progress directly. let samples_to_draw = - (((*steps).max(1) as f32 * eased_progress) as usize).max(1); - let sweep_so_far = angle.as_radians().value() * eased_progress; + (((*steps).max(1) as f32 * tween.progress) as usize).max(1); + let sweep_so_far = angle.as_radians().value() * tween.progress; for pt in arc_points( geom.center, radius.value(), @@ -309,21 +300,15 @@ fn draw_tween_arc( draw_mesh(&mesh); } - // Calculate how much of the arc we've traveled based on tween progress - // Use the same eased progress as the turtle position for synchronized animation - let elapsed = get_time() - tween.start_time; - let t = (elapsed / tween.duration).min(1.0); - let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1 - - // Use Lyon to tessellate and draw the partial arc + // Draw the partial arc traveled based on tween progress if let Ok(mesh) = crate::tessellation::tessellate_arc( geom.center, radius, geom.start_angle_from_center.to_degrees(), - total_angle.value() * progress, + total_angle.value() * tween.progress, tween.start_params.color, tween.start_params.pen_width, - ((steps as f32 * progress).ceil() as usize).max(1), + ((steps as f32 * tween.progress).ceil() as usize).max(1), direction, ) { draw_mesh(&mesh); diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index 6e124e2..c167ebc 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -55,12 +55,13 @@ pub(crate) struct TweenController { #[derive(Clone, Debug)] pub(crate) struct CommandTween { pub(crate) command: TurtleCommand, - pub(crate) start_time: f64, - pub(crate) duration: f64, + pub(crate) progress: f32, pub(crate) start_params: TurtleParams, pub(crate) target_params: TurtleParams, pub(crate) current_position: Vec2, pub(crate) current_heading: f32, + start_time: f64, + duration: f64, position_tweener: Tweener, heading_tweener: Tweener, pen_width_tweener: Tweener, @@ -166,7 +167,8 @@ impl TweenController { // Use tweeners to calculate current values // For circles, calculate position along the arc instead of straight line - let progress = tween.heading_tweener.move_to(elapsed); + let progress = tween.heading_tweener.move_to(elapsed).clamp(0.0, 1.0); + tween.progress = progress; let current_position = match &tween.command { TurtleCommand::Circle { @@ -316,6 +318,7 @@ impl TweenController { self.current_tween = Some(CommandTween { command, + progress: 0.0, start_time: current_time(), duration, start_params: params.clone(), @@ -537,7 +540,42 @@ mod tests { controller.update(0, &mut params, &mut filling, &mut commands, &mut svg_log); assert_eq!(controller.queue.len(), 1, "First command must be popped into current_tween"); - assert!(controller.current_tween().is_some()); + let active = controller.current_tween().expect("Must have active tween"); + assert_eq!(active.progress, 0.0, "New tween must initialize progress to 0.0"); assert!(!controller.is_complete()); } + + #[test] + fn test_animated_mode_progress_advances_and_clamps() { + let mut queue = CommandQueue::new(); + // Circle command with speed 10.0 and radius 100 => duration ~62.8s + queue.push(TurtleCommand::Circle { + radius: Length::new(100.0), + angle: Degrees::new(360.0), + steps: 36, + direction: CircleDirection::Right, + }); + + let mut controller = TweenController::new( + queue, + AnimationSpeed::Animated(10.0), + ); + let mut params = make_test_params(); + let mut filling = None; + let mut commands = Vec::new(); + let mut svg_log = crate::state::SvgLog::default(); + + // Frame 0: pops into current_tween with progress = 0.0 + controller.update(0, &mut params, &mut filling, &mut commands, &mut svg_log); + let initial_progress = controller.current_tween().unwrap().progress; + assert_eq!(initial_progress, 0.0); + + // Advance time by sleeping briefly + std::thread::sleep(std::time::Duration::from_millis(15)); + controller.update(0, &mut params, &mut filling, &mut commands, &mut svg_log); + if let Some(tween) = controller.current_tween() { + assert!(tween.progress >= 0.0 && tween.progress <= 1.0); + assert!(tween.progress >= initial_progress); + } + } }