improve speed to have one single point of truth.

This commit is contained in:
Franz Dietrich 2025-10-09 15:54:53 +02:00
parent cebad0459c
commit 5799d2aa07
15 changed files with 170 additions and 74 deletions

View File

@ -13,20 +13,23 @@ async fn main() {
plan.set_color(RED); plan.set_color(RED);
plan.set_pen_width(0.5); plan.set_pen_width(0.5);
plan.left(90.0); plan.left(90.0);
plan.set_speed(999);
plan.circle_left(100.0, 540.0, 72); // partial circle to the left plan.circle_left(100.0, 540.0, 72); // partial circle to the left
plan.forward(150.0); plan.forward(150.0);
plan.set_speed(100);
plan.set_color(BLUE); plan.set_color(BLUE);
plan.circle_right(50.0, 270.0, 72); // partial circle to the right 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.forward(150.0);
plan.set_speed(700);
plan.set_color(GREEN); plan.set_color(GREEN);
plan.circle_left(50.0, 180.0, 36); // Half circle to the left plan.circle_left(50.0, 180.0, 36); // Half circle to the left
// Create turtle app with animation (speed = 100 pixels/sec) // 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 // Main loop
loop { loop {

View File

@ -8,14 +8,15 @@ async fn main() {
// Create a turtle plan // Create a turtle plan
let mut plan = create_turtle(); let mut plan = create_turtle();
// Set animation speed
plan.set_speed(50);
plan.right(45.0); plan.right(45.0);
plan.forward(100.0); plan.forward(100.0);
plan.right(45.0); plan.right(45.0);
plan.forward(100.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) // 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 // Main loop
loop { loop {

View File

@ -23,22 +23,23 @@ async fn main() {
let mut plan = create_turtle(); let mut plan = create_turtle();
// Position turtle // Position turtle
plan.set_speed(10);
plan.pen_up(); plan.pen_up();
plan.backward(150.0); plan.backward(150.0);
plan.pen_down(); plan.pen_down();
plan.set_speed(10000);
// Draw Koch snowflake (triangle of Koch curves) // Draw Koch snowflake (triangle of Koch curves)
for _ in 0..3 { for _ in 0..3 {
koch(4, &mut plan, 300.0); koch(4, &mut plan, 300.0);
plan.right(120.0); plan.right(120.0);
plan.set_speed(1000);
} }
plan.hide(); // Hide turtle when done plan.hide(); // Hide turtle when done
// Create app with animation // 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 { loop {
clear_background(WHITE); clear_background(WHITE);

View File

@ -58,8 +58,8 @@ async fn main() {
nikolaus(&mut plan, 100.0); nikolaus(&mut plan, 100.0);
// Create turtle app with animation (speed = 100 pixels/sec) // Create turtle app with animation
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0); let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop // Main loop
loop { loop {

View File

@ -31,8 +31,11 @@ async fn main() {
plan.shape(ShapeType::Arrow); plan.shape(ShapeType::Arrow);
plan.forward(100.0); plan.forward(100.0);
// Set animation speed
plan.set_speed(50);
// Create turtle app with animation (speed = 100 pixels/sec for slower animation) // 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 // Main loop
loop { loop {

View File

@ -14,8 +14,11 @@ async fn main() {
plan.forward(100.0).right(90.0); plan.forward(100.0).right(90.0);
} }
// Create turtle app with animation (speed = 100 pixels/sec) // Set animation speed
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0); plan.set_speed(50);
// Create turtle app with animation
let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop // Main loop
loop { loop {

View File

@ -18,8 +18,11 @@ async fn main() {
plan.circle_left(10.0, 72.0, 1000); plan.circle_left(10.0, 72.0, 1000);
} }
// Set animation speed
plan.set_speed(300);
// Create turtle app with animation (speed = 100 pixels/sec) // 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 // Main loop
loop { loop {

View File

@ -25,8 +25,11 @@ async fn main() {
t.pen_down(); t.pen_down();
t.circle_right(8.0, 360.0, 12); t.circle_right(8.0, 360.0, 12);
// Set animation speed
t.set_speed(1000);
// Create turtle app with animation (speed = 100 pixels/sec) // 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 // Main loop
loop { loop {

View File

@ -1,7 +1,7 @@
//! Builder pattern traits for creating turtle command sequences //! Builder pattern traits for creating turtle command sequences
use crate::commands::{CommandQueue, TurtleCommand}; use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::{Color, Precision}; use crate::general::{AnimationSpeed, Color, Precision};
use crate::shapes::{ShapeType, TurtleShape}; use crate::shapes::{ShapeType, TurtleShape};
/// Trait for adding commands to a queue /// Trait for adding commands to a queue
@ -106,8 +106,11 @@ impl TurtlePlan {
} }
} }
pub fn set_speed(&mut self, speed: u32) -> &mut Self { /// Set animation speed
self.queue.push(TurtleCommand::SetSpeed(speed)); /// - Values >= 999 = instant mode (no animation)
/// - Values < 999 = animated mode with specified pixels/second
pub fn set_speed(&mut self, speed: impl Into<AnimationSpeed>) -> &mut Self {
self.queue.push(TurtleCommand::SetSpeed(speed.into()));
self self
} }

View File

@ -1,6 +1,6 @@
//! Turtle commands and command queue //! Turtle commands and command queue
use crate::general::{Color, Coordinate, Precision}; use crate::general::{AnimationSpeed, Color, Coordinate, Precision};
use crate::shapes::TurtleShape; use crate::shapes::TurtleShape;
/// Individual turtle commands /// Individual turtle commands
@ -28,7 +28,7 @@ pub enum TurtleCommand {
SetColor(Color), SetColor(Color),
SetFillColor(Option<Color>), SetFillColor(Option<Color>),
SetPenWidth(Precision), SetPenWidth(Precision),
SetSpeed(u32), SetSpeed(AnimationSpeed),
SetShape(TurtleShape), SetShape(TurtleShape),
// Position // Position

View File

@ -5,6 +5,9 @@ use crate::commands::TurtleCommand;
use crate::state::{DrawCommand, TurtleState, TurtleWorld}; use crate::state::{DrawCommand, TurtleState, TurtleWorld};
use macroquad::prelude::*; use macroquad::prelude::*;
#[cfg(test)]
use crate::general::AnimationSpeed;
/// Execute a single turtle command, updating state and adding draw commands /// Execute a single turtle command, updating state and adding draw commands
pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) { pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) {
match command { 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) /// Add drawing command for a completed tween (state transition already occurred)
pub fn add_draw_for_completed_tween( pub fn add_draw_for_completed_tween(
command: &TurtleCommand, command: &TurtleCommand,
@ -228,7 +220,7 @@ mod tests {
pen_width: 1.0, pen_width: 1.0,
color: Color::new(0.0, 0.0, 0.0, 1.0), color: Color::new(0.0, 0.0, 0.0, 1.0),
fill_color: None, fill_color: None,
speed: 100, speed: AnimationSpeed::Animated(100.0),
visible: true, visible: true,
shape: TurtleShape::turtle(), shape: TurtleShape::turtle(),
}; };

View File

@ -17,8 +17,61 @@ pub type Coordinate = Vec2;
/// Visibility flag for turtle /// Visibility flag for turtle
pub type Visibility = bool; pub type Visibility = bool;
/// Speed of animations (higher = faster, >= 999 = instant) /// Execution speed setting
pub type Speed = u32; /// - 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<f32> for AnimationSpeed {
fn from(speed: f32) -> Self {
AnimationSpeed::from_value(speed)
}
}
impl From<u32> for AnimationSpeed {
fn from(speed: u32) -> Self {
AnimationSpeed::from_u32(speed)
}
}
/// Color type re-export from macroquad /// Color type re-export from macroquad
pub use macroquad::color::Color; pub use macroquad::color::Color;

View File

@ -13,7 +13,7 @@
//! let mut plan = create_turtle(); //! let mut plan = create_turtle();
//! plan.forward(100.0).right(90.0).forward(100.0); //! 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 { //! loop {
//! clear_background(WHITE); //! clear_background(WHITE);
@ -37,7 +37,7 @@ pub mod tweening;
// Re-export commonly used types // Re-export commonly used types
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands}; pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
pub use commands::{CommandQueue, TurtleCommand}; 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 shapes::{ShapeType, TurtleShape};
pub use state::{DrawCommand, TurtleState, TurtleWorld}; pub use state::{DrawCommand, TurtleState, TurtleWorld};
pub use tweening::TweenController; pub use tweening::TweenController;
@ -48,7 +48,7 @@ use macroquad::prelude::*;
pub struct TurtleApp { pub struct TurtleApp {
world: TurtleWorld, world: TurtleWorld,
tween_controller: Option<TweenController>, tween_controller: Option<TweenController>,
mode: ExecutionMode, speed: AnimationSpeed,
// Mouse panning state // Mouse panning state
is_dragging: bool, is_dragging: bool,
last_mouse_pos: Option<Vec2>, last_mouse_pos: Option<Vec2>,
@ -56,46 +56,31 @@ pub struct TurtleApp {
zoom_level: f32, zoom_level: f32,
} }
enum ExecutionMode {
Immediate,
Animated,
}
impl TurtleApp { impl TurtleApp {
/// Create a new turtle application with default settings /// Create a new TurtleApp with default settings
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
world: TurtleWorld::new(), world: TurtleWorld::new(),
tween_controller: None, tween_controller: None,
mode: ExecutionMode::Immediate, speed: AnimationSpeed::default(),
is_dragging: false, is_dragging: false,
last_mouse_pos: None, last_mouse_pos: None,
zoom_level: 1.0, 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 /// # Arguments
/// * `queue` - The command queue to execute /// * `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) -> Self {
pub fn with_commands(mut self, queue: CommandQueue, speed: f32) -> Self { // The TweenController will switch between instant and animated mode
if speed <= 0.5 || speed.is_infinite() || speed.is_nan() { // based on SetSpeed commands encountered
// Compiler error speed should be between 0.5 and 1000.0 self.tween_controller = Some(TweenController::new(queue, self.speed));
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));
}
self self
} }

View File

@ -1,6 +1,6 @@
//! Turtle state and world state management //! 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 crate::shapes::TurtleShape;
use macroquad::prelude::*; use macroquad::prelude::*;
@ -13,7 +13,7 @@ pub struct TurtleState {
pub color: Color, pub color: Color,
pub fill_color: Option<Color>, pub fill_color: Option<Color>,
pub pen_width: Precision, pub pen_width: Precision,
pub speed: Speed, pub speed: AnimationSpeed,
pub visible: bool, pub visible: bool,
pub shape: TurtleShape, pub shape: TurtleShape,
} }
@ -27,7 +27,7 @@ impl Default for TurtleState {
color: BLACK, color: BLACK,
fill_color: None, fill_color: None,
pen_width: 2.0, pen_width: 2.0,
speed: 100, // pixels per second speed: AnimationSpeed::default(),
visible: true, visible: true,
shape: TurtleShape::turtle(), shape: TurtleShape::turtle(),
} }
@ -35,8 +35,8 @@ impl Default for TurtleState {
} }
impl TurtleState { impl TurtleState {
pub fn set_speed(&mut self, speed: Speed) { pub fn set_speed(&mut self, speed: AnimationSpeed) {
self.speed = speed.max(1); self.speed = speed;
} }
pub fn heading_angle(&self) -> Angle { pub fn heading_angle(&self) -> Angle {

View File

@ -2,6 +2,7 @@
use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::{CommandQueue, TurtleCommand}; use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::AnimationSpeed;
use crate::state::TurtleState; use crate::state::TurtleState;
use macroquad::prelude::*; use macroquad::prelude::*;
use tween::{CubicInOut, TweenValue, Tweener}; use tween::{CubicInOut, TweenValue, Tweener};
@ -46,7 +47,7 @@ impl From<TweenVec2> for Vec2 {
pub struct TweenController { pub struct TweenController {
queue: CommandQueue, queue: CommandQueue,
current_tween: Option<CommandTween>, current_tween: Option<CommandTween>,
speed: f32, // pixels per second (or degrees per second for rotations) speed: AnimationSpeed,
} }
pub(crate) struct CommandTween { pub(crate) struct CommandTween {
@ -61,20 +62,52 @@ pub(crate) struct CommandTween {
} }
impl TweenController { impl TweenController {
pub fn new(queue: CommandQueue, speed: f32) -> Self { pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
Self { Self {
queue, queue,
current_tween: None, current_tween: None,
speed: speed.max(1.0), speed,
} }
} }
pub fn set_speed(&mut self, speed: f32) { pub fn set_speed(&mut self, speed: AnimationSpeed) {
self.speed = speed.max(1.0); self.speed = speed;
} }
/// Update the tween, returns (command, start_state) if command completed /// Update the tween, returns (command, start_state) if command completed
pub fn update(&mut self, state: &mut TurtleState) -> Option<(TurtleCommand, TurtleState)> { 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 // Process current tween
if let Some(ref mut tween) = self.current_tween { if let Some(ref mut tween) = self.current_tween {
let elapsed = (get_time() - tween.start_time) as f32; let elapsed = (get_time() - tween.start_time) as f32;
@ -160,6 +193,19 @@ impl TweenController {
// Start next tween // Start next tween
if let Some(command) = self.queue.next() { if let Some(command) = self.queue.next() {
let command_clone = command.clone(); 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 speed = state.speed; // Extract speed before borrowing self
let duration = self.calculate_duration(&command_clone, speed); let duration = self.calculate_duration(&command_clone, speed);
@ -219,8 +265,8 @@ impl TweenController {
) )
} }
fn calculate_duration(&self, command: &TurtleCommand, speed: u32) -> f64 { fn calculate_duration(&self, command: &TurtleCommand, speed: AnimationSpeed) -> f64 {
let speed = speed.max(1) as f32; let speed = speed.value();
let base_time = match command { let base_time = match command {
TurtleCommand::Move(dist) => dist.abs() / speed, TurtleCommand::Move(dist) => dist.abs() / speed,