diff --git a/turtle-lib-macroquad/examples/circle_test.rs b/turtle-lib-macroquad/examples/circle_test.rs index 9d1455d..5a8401a 100644 --- a/turtle-lib-macroquad/examples/circle_test.rs +++ b/turtle-lib-macroquad/examples/circle_test.rs @@ -13,20 +13,23 @@ async fn main() { plan.set_color(RED); plan.set_pen_width(0.5); plan.left(90.0); + plan.set_speed(999); plan.circle_left(100.0, 540.0, 72); // partial circle to the left plan.forward(150.0); - + plan.set_speed(100); plan.set_color(BLUE); plan.circle_right(50.0, 270.0, 72); // partial circle to the right - + // Set animation speed + plan.set_speed(20); plan.forward(150.0); + plan.set_speed(700); plan.set_color(GREEN); plan.circle_left(50.0, 180.0, 36); // Half circle to the left // Create turtle app with animation (speed = 100 pixels/sec) - let mut app = TurtleApp::new().with_commands(plan.build(), 100.0); + let mut app = TurtleApp::new().with_commands(plan.build()); // Main loop loop { diff --git a/turtle-lib-macroquad/examples/direction_test_1.rs b/turtle-lib-macroquad/examples/direction_test_1.rs index 9acb304..bc1756f 100644 --- a/turtle-lib-macroquad/examples/direction_test_1.rs +++ b/turtle-lib-macroquad/examples/direction_test_1.rs @@ -8,14 +8,15 @@ async fn main() { // Create a turtle plan let mut plan = create_turtle(); + // Set animation speed + plan.set_speed(50); plan.right(45.0); plan.forward(100.0); plan.right(45.0); plan.forward(100.0); - //plan.circle_left(100.0, 90.0, 72); // Full circle to the left // Create turtle app with animation (speed = 100 pixels/sec) - let mut app = TurtleApp::new().with_commands(plan.build(), 10.0); + let mut app = TurtleApp::new().with_commands(plan.build()); // Main loop loop { diff --git a/turtle-lib-macroquad/examples/koch.rs b/turtle-lib-macroquad/examples/koch.rs index 8bfd5e3..ea9263a 100644 --- a/turtle-lib-macroquad/examples/koch.rs +++ b/turtle-lib-macroquad/examples/koch.rs @@ -23,22 +23,23 @@ async fn main() { let mut plan = create_turtle(); // Position turtle - plan.set_speed(10); plan.pen_up(); plan.backward(150.0); plan.pen_down(); + plan.set_speed(10000); // Draw Koch snowflake (triangle of Koch curves) for _ in 0..3 { koch(4, &mut plan, 300.0); plan.right(120.0); + plan.set_speed(1000); } plan.hide(); // Hide turtle when done // Create app with animation - let mut app = TurtleApp::new().with_commands(plan.build(), 1000.0); + let mut app = TurtleApp::new().with_commands(plan.build()); loop { clear_background(WHITE); diff --git a/turtle-lib-macroquad/examples/nikolaus.rs b/turtle-lib-macroquad/examples/nikolaus.rs index 7190539..44b9c95 100644 --- a/turtle-lib-macroquad/examples/nikolaus.rs +++ b/turtle-lib-macroquad/examples/nikolaus.rs @@ -58,8 +58,8 @@ async fn main() { nikolaus(&mut plan, 100.0); - // Create turtle app with animation (speed = 100 pixels/sec) - let mut app = TurtleApp::new().with_commands(plan.build(), 100.0); + // Create turtle app with animation + let mut app = TurtleApp::new().with_commands(plan.build()); // Main loop loop { diff --git a/turtle-lib-macroquad/examples/shapes.rs b/turtle-lib-macroquad/examples/shapes.rs index 876fa20..401b972 100644 --- a/turtle-lib-macroquad/examples/shapes.rs +++ b/turtle-lib-macroquad/examples/shapes.rs @@ -31,8 +31,11 @@ async fn main() { plan.shape(ShapeType::Arrow); plan.forward(100.0); + // Set animation speed + plan.set_speed(50); + // Create turtle app with animation (speed = 100 pixels/sec for slower animation) - let mut app = TurtleApp::new().with_commands(plan.build(), 700.0); + let mut app = TurtleApp::new().with_commands(plan.build()); // Main loop loop { diff --git a/turtle-lib-macroquad/examples/square.rs b/turtle-lib-macroquad/examples/square.rs index 36aacf4..c814144 100644 --- a/turtle-lib-macroquad/examples/square.rs +++ b/turtle-lib-macroquad/examples/square.rs @@ -14,8 +14,11 @@ async fn main() { plan.forward(100.0).right(90.0); } - // Create turtle app with animation (speed = 100 pixels/sec) - let mut app = TurtleApp::new().with_commands(plan.build(), 100.0); + // Set animation speed + plan.set_speed(50); + + // Create turtle app with animation + let mut app = TurtleApp::new().with_commands(plan.build()); // Main loop loop { diff --git a/turtle-lib-macroquad/examples/stern.rs b/turtle-lib-macroquad/examples/stern.rs index ceae419..d1ca8f7 100644 --- a/turtle-lib-macroquad/examples/stern.rs +++ b/turtle-lib-macroquad/examples/stern.rs @@ -18,8 +18,11 @@ async fn main() { plan.circle_left(10.0, 72.0, 1000); } + // Set animation speed + plan.set_speed(300); + // Create turtle app with animation (speed = 100 pixels/sec) - let mut app = TurtleApp::new().with_commands(plan.build(), 100.0); + let mut app = TurtleApp::new().with_commands(plan.build()); // Main loop loop { diff --git a/turtle-lib-macroquad/examples/yinyang.rs b/turtle-lib-macroquad/examples/yinyang.rs index cd3fd0e..756fbed 100644 --- a/turtle-lib-macroquad/examples/yinyang.rs +++ b/turtle-lib-macroquad/examples/yinyang.rs @@ -25,8 +25,11 @@ async fn main() { t.pen_down(); t.circle_right(8.0, 360.0, 12); + // Set animation speed + t.set_speed(1000); + // Create turtle app with animation (speed = 100 pixels/sec) - let mut app = TurtleApp::new().with_commands(t.build(), 100.0); + let mut app = TurtleApp::new().with_commands(t.build()); // Main loop loop { diff --git a/turtle-lib-macroquad/src/builders.rs b/turtle-lib-macroquad/src/builders.rs index b74f902..9af3459 100644 --- a/turtle-lib-macroquad/src/builders.rs +++ b/turtle-lib-macroquad/src/builders.rs @@ -1,7 +1,7 @@ //! Builder pattern traits for creating turtle command sequences use crate::commands::{CommandQueue, TurtleCommand}; -use crate::general::{Color, Precision}; +use crate::general::{AnimationSpeed, Color, Precision}; use crate::shapes::{ShapeType, TurtleShape}; /// Trait for adding commands to a queue @@ -106,8 +106,11 @@ impl TurtlePlan { } } - pub fn set_speed(&mut self, speed: u32) -> &mut Self { - self.queue.push(TurtleCommand::SetSpeed(speed)); + /// Set animation speed + /// - Values >= 999 = instant mode (no animation) + /// - Values < 999 = animated mode with specified pixels/second + pub fn set_speed(&mut self, speed: impl Into) -> &mut Self { + self.queue.push(TurtleCommand::SetSpeed(speed.into())); self } diff --git a/turtle-lib-macroquad/src/commands.rs b/turtle-lib-macroquad/src/commands.rs index c2b6c94..48a1669 100644 --- a/turtle-lib-macroquad/src/commands.rs +++ b/turtle-lib-macroquad/src/commands.rs @@ -1,6 +1,6 @@ //! Turtle commands and command queue -use crate::general::{Color, Coordinate, Precision}; +use crate::general::{AnimationSpeed, Color, Coordinate, Precision}; use crate::shapes::TurtleShape; /// Individual turtle commands @@ -28,7 +28,7 @@ pub enum TurtleCommand { SetColor(Color), SetFillColor(Option), SetPenWidth(Precision), - SetSpeed(u32), + SetSpeed(AnimationSpeed), SetShape(TurtleShape), // Position diff --git a/turtle-lib-macroquad/src/execution.rs b/turtle-lib-macroquad/src/execution.rs index a8cf6d8..64b627f 100644 --- a/turtle-lib-macroquad/src/execution.rs +++ b/turtle-lib-macroquad/src/execution.rs @@ -5,6 +5,9 @@ use crate::commands::TurtleCommand; use crate::state::{DrawCommand, TurtleState, TurtleWorld}; use macroquad::prelude::*; +#[cfg(test)] +use crate::general::AnimationSpeed; + /// Execute a single turtle command, updating state and adding draw commands pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) { match command { @@ -129,17 +132,6 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: } } -/// Execute all commands immediately (no animation) -pub fn execute_all_immediate( - queue: &mut crate::commands::CommandQueue, - state: &mut TurtleState, - world: &mut TurtleWorld, -) { - while let Some(command) = queue.next() { - execute_command(command, state, world); - } -} - /// Add drawing command for a completed tween (state transition already occurred) pub fn add_draw_for_completed_tween( command: &TurtleCommand, @@ -228,7 +220,7 @@ mod tests { pen_width: 1.0, color: Color::new(0.0, 0.0, 0.0, 1.0), fill_color: None, - speed: 100, + speed: AnimationSpeed::Animated(100.0), visible: true, shape: TurtleShape::turtle(), }; diff --git a/turtle-lib-macroquad/src/general.rs b/turtle-lib-macroquad/src/general.rs index 2adfdd6..3294d21 100644 --- a/turtle-lib-macroquad/src/general.rs +++ b/turtle-lib-macroquad/src/general.rs @@ -17,8 +17,61 @@ pub type Coordinate = Vec2; /// Visibility flag for turtle pub type Visibility = bool; -/// Speed of animations (higher = faster, >= 999 = instant) -pub type Speed = u32; +/// Execution speed setting +/// - Instant: No animation, commands execute immediately +/// - Animated(speed): Smooth animation at specified pixels/second +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AnimationSpeed { + Instant, + Animated(f32), // pixels per second +} + +impl AnimationSpeed { + /// Check if this is instant mode + pub fn is_instant(&self) -> bool { + matches!(self, AnimationSpeed::Instant) + } + + /// Get the speed value (returns a high value for Instant) + pub fn value(&self) -> f32 { + match self { + AnimationSpeed::Instant => 9999.0, + AnimationSpeed::Animated(speed) => *speed, + } + } + + /// Create from a raw speed value (>= 999 becomes Instant) + pub fn from_value(speed: f32) -> Self { + if speed >= 999.0 { + AnimationSpeed::Instant + } else { + AnimationSpeed::Animated(speed.max(1.0)) + } + } + + /// Create from a u32 value for backward compatibility + pub fn from_u32(speed: u32) -> Self { + Self::from_value(speed as f32) + } +} + +impl Default for AnimationSpeed { + fn default() -> Self { + AnimationSpeed::Animated(100.0) + } +} + +impl From for AnimationSpeed { + fn from(speed: f32) -> Self { + AnimationSpeed::from_value(speed) + } +} + +impl From for AnimationSpeed { + fn from(speed: u32) -> Self { + AnimationSpeed::from_u32(speed) + } +} /// Color type re-export from macroquad pub use macroquad::color::Color; diff --git a/turtle-lib-macroquad/src/lib.rs b/turtle-lib-macroquad/src/lib.rs index a0d3915..f75fcbd 100644 --- a/turtle-lib-macroquad/src/lib.rs +++ b/turtle-lib-macroquad/src/lib.rs @@ -13,7 +13,7 @@ //! let mut plan = create_turtle(); //! plan.forward(100.0).right(90.0).forward(100.0); //! -//! let mut app = TurtleApp::new().with_commands(plan.build(), 100.0); +//! let mut app = TurtleApp::new().with_commands(plan.build()); //! //! loop { //! clear_background(WHITE); @@ -37,7 +37,7 @@ pub mod tweening; // Re-export commonly used types pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands}; pub use commands::{CommandQueue, TurtleCommand}; -pub use general::{Angle, Color, Coordinate, Length, Precision, Speed}; +pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision}; pub use shapes::{ShapeType, TurtleShape}; pub use state::{DrawCommand, TurtleState, TurtleWorld}; pub use tweening::TweenController; @@ -48,7 +48,7 @@ use macroquad::prelude::*; pub struct TurtleApp { world: TurtleWorld, tween_controller: Option, - mode: ExecutionMode, + speed: AnimationSpeed, // Mouse panning state is_dragging: bool, last_mouse_pos: Option, @@ -56,46 +56,31 @@ pub struct TurtleApp { zoom_level: f32, } -enum ExecutionMode { - Immediate, - Animated, -} - impl TurtleApp { - /// Create a new turtle application with default settings + /// Create a new TurtleApp with default settings pub fn new() -> Self { Self { world: TurtleWorld::new(), tween_controller: None, - mode: ExecutionMode::Immediate, + speed: AnimationSpeed::default(), is_dragging: false, last_mouse_pos: None, zoom_level: 1.0, } } - /// Add commands to the turtle with specified speed + /// Add commands to the turtle + /// + /// Speed is controlled by SetSpeed commands in the queue. + /// Use `set_speed()` on the turtle plan to set animation speed. + /// Speed >= 999 = instant mode, speed < 999 = animated mode. /// /// # Arguments /// * `queue` - The command queue to execute - /// * `speed` - Animation speed in pixels/sec (>= 999.0 = instant, 0.5-999.0 = animated) - pub fn with_commands(mut self, queue: CommandQueue, speed: f32) -> Self { - if speed <= 0.5 || speed.is_infinite() || speed.is_nan() { - // Compiler error speed should be between 0.5 and 1000.0 - panic!("Speed must be greater than 0.5 and less than 1000.0"); - } - if speed >= 999.0 { - // Immediate mode - execute all commands instantly - self.mode = ExecutionMode::Immediate; - let mut state = TurtleState::default(); - let mut queue_mut = queue; - execution::execute_all_immediate(&mut queue_mut, &mut state, &mut self.world); - self.world.turtle = state; - } else { - // Animated mode - tween between states - self.mode = ExecutionMode::Animated; - self.tween_controller = Some(TweenController::new(queue, speed)); - } + pub fn with_commands(mut self, queue: CommandQueue) -> Self { + // The TweenController will switch between instant and animated mode + // based on SetSpeed commands encountered + self.tween_controller = Some(TweenController::new(queue, self.speed)); self } diff --git a/turtle-lib-macroquad/src/state.rs b/turtle-lib-macroquad/src/state.rs index 6868be1..74320a3 100644 --- a/turtle-lib-macroquad/src/state.rs +++ b/turtle-lib-macroquad/src/state.rs @@ -1,6 +1,6 @@ //! Turtle state and world state management -use crate::general::{Angle, Color, Coordinate, Precision, Speed}; +use crate::general::{Angle, AnimationSpeed, Color, Coordinate, Precision}; use crate::shapes::TurtleShape; use macroquad::prelude::*; @@ -13,7 +13,7 @@ pub struct TurtleState { pub color: Color, pub fill_color: Option, pub pen_width: Precision, - pub speed: Speed, + pub speed: AnimationSpeed, pub visible: bool, pub shape: TurtleShape, } @@ -27,7 +27,7 @@ impl Default for TurtleState { color: BLACK, fill_color: None, pen_width: 2.0, - speed: 100, // pixels per second + speed: AnimationSpeed::default(), visible: true, shape: TurtleShape::turtle(), } @@ -35,8 +35,8 @@ impl Default for TurtleState { } impl TurtleState { - pub fn set_speed(&mut self, speed: Speed) { - self.speed = speed.max(1); + pub fn set_speed(&mut self, speed: AnimationSpeed) { + self.speed = speed; } pub fn heading_angle(&self) -> Angle { diff --git a/turtle-lib-macroquad/src/tweening.rs b/turtle-lib-macroquad/src/tweening.rs index 46624fc..89edece 100644 --- a/turtle-lib-macroquad/src/tweening.rs +++ b/turtle-lib-macroquad/src/tweening.rs @@ -2,6 +2,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::{CommandQueue, TurtleCommand}; +use crate::general::AnimationSpeed; use crate::state::TurtleState; use macroquad::prelude::*; use tween::{CubicInOut, TweenValue, Tweener}; @@ -46,7 +47,7 @@ impl From for Vec2 { pub struct TweenController { queue: CommandQueue, current_tween: Option, - speed: f32, // pixels per second (or degrees per second for rotations) + speed: AnimationSpeed, } pub(crate) struct CommandTween { @@ -61,20 +62,52 @@ pub(crate) struct CommandTween { } impl TweenController { - pub fn new(queue: CommandQueue, speed: f32) -> Self { + pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self { Self { queue, current_tween: None, - speed: speed.max(1.0), + speed, } } - pub fn set_speed(&mut self, speed: f32) { - self.speed = speed.max(1.0); + pub fn set_speed(&mut self, speed: AnimationSpeed) { + self.speed = speed; } /// Update the tween, returns (command, start_state) if command completed pub fn update(&mut self, state: &mut TurtleState) -> Option<(TurtleCommand, TurtleState)> { + // In immediate mode, execute all remaining commands instantly + if self.speed.is_instant() { + loop { + let command = match self.queue.next() { + Some(cmd) => cmd.clone(), + None => return None, + }; + + let start_state = state.clone(); + + // Handle SetSpeed command to potentially switch modes + if let TurtleCommand::SetSpeed(new_speed) = &command { + state.set_speed(*new_speed); + self.speed = *new_speed; + // If speed dropped below instant, switch to animated mode + if !self.speed.is_instant() { + return None; + } + continue; + } + + // Execute command immediately + let target_state = self.calculate_target_state(state, &command); + *state = target_state.clone(); + + // Return drawable commands for rendering + if Self::command_creates_drawing(&command) { + return Some((command, start_state)); + } + } + } + // Process current tween if let Some(ref mut tween) = self.current_tween { let elapsed = (get_time() - tween.start_time) as f32; @@ -160,6 +193,19 @@ impl TweenController { // Start next tween if let Some(command) = self.queue.next() { let command_clone = command.clone(); + + // Handle SetSpeed command specially + if let TurtleCommand::SetSpeed(new_speed) = &command_clone { + state.set_speed(*new_speed); + self.speed = *new_speed; + // If switched to immediate mode, process immediately + if self.speed.is_instant() { + return self.update(state); // Recursively process in immediate mode + } + // For animated mode speed changes, continue to next command + return self.update(state); + } + let speed = state.speed; // Extract speed before borrowing self let duration = self.calculate_duration(&command_clone, speed); @@ -219,8 +265,8 @@ impl TweenController { ) } - fn calculate_duration(&self, command: &TurtleCommand, speed: u32) -> f64 { - let speed = speed.max(1) as f32; + fn calculate_duration(&self, command: &TurtleCommand, speed: AnimationSpeed) -> f64 { + let speed = speed.value(); let base_time = match command { TurtleCommand::Move(dist) => dist.abs() / speed,