normalizing angles to prevent drift

also move the at least one frame check to the AnimationSpeed creation.
This commit is contained in:
Franz Dietrich 2025-10-10 09:26:18 +02:00
parent 5799d2aa07
commit c3f5136359
6 changed files with 84 additions and 41 deletions

View File

@ -23,17 +23,17 @@ async fn main() {
let mut plan = create_turtle(); let mut plan = create_turtle();
// Position turtle // Position turtle
plan.set_speed(1001);
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.set_speed(1200);
} }
plan.hide(); // Hide turtle when done plan.hide(); // Hide turtle when done

View File

@ -8,10 +8,11 @@ async fn main() {
// Create a turtle plan // Create a turtle plan
let mut plan = create_turtle(); let mut plan = create_turtle();
plan.shape(ShapeType::Turtle); plan.shape(ShapeType::Turtle);
plan.set_speed(800); plan.set_speed(1500);
plan.set_pen_width(0.5);
// Draw a square // Draw a 5-pointed star pattern repeatedly
for _ in 0..5 { for _i in 0..50000 {
plan.forward(200.0); plan.forward(200.0);
plan.circle_left(10.0, 72.0, 1000); plan.circle_left(10.0, 72.0, 1000);
plan.circle_right(5.0, 360.0, 1000); plan.circle_right(5.0, 360.0, 1000);

View File

@ -124,6 +124,11 @@ impl TurtlePlan {
self self
} }
pub fn set_heading(&mut self, heading: Precision) -> &mut Self {
self.queue.push(TurtleCommand::SetHeading(heading));
self
}
pub fn pen_up(&mut self) -> &mut Self { pub fn pen_up(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::PenUp); self.queue.push(TurtleCommand::PenUp);
self self

View File

@ -18,32 +18,35 @@ pub type Coordinate = Vec2;
pub type Visibility = bool; pub type Visibility = bool;
/// Execution speed setting /// Execution speed setting
/// - Instant: No animation, commands execute immediately /// - Instant(draw_calls): Fast execution with limited draw calls per frame (speed - 1000, minimum 1)
/// - Animated(speed): Smooth animation at specified pixels/second /// - Animated(speed): Smooth animation at specified pixels/second
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnimationSpeed { pub enum AnimationSpeed {
Instant, Instant(u32), // Number of draw calls per frame (minimum 1)
Animated(f32), // pixels per second Animated(f32), // pixels per second
} }
impl AnimationSpeed { impl AnimationSpeed {
/// Check if this is instant mode /// Check if this is instant mode
pub fn is_instant(&self) -> bool { pub fn is_animating(&self) -> bool {
matches!(self, AnimationSpeed::Instant) matches!(self, AnimationSpeed::Animated(_))
} }
/// Get the speed value (returns a high value for Instant) /// Get the speed value (returns encoded value for Instant)
pub fn value(&self) -> f32 { pub fn value(&self) -> f32 {
match self { match self {
AnimationSpeed::Instant => 9999.0, AnimationSpeed::Instant(calls) => 1000.0 + *calls as f32,
AnimationSpeed::Animated(speed) => *speed, AnimationSpeed::Animated(speed) => *speed,
} }
} }
/// Create from a raw speed value (>= 999 becomes Instant) /// Create from a raw speed value
/// - speed >= 1000 becomes Instant with max(1, speed - 1000) draw calls per frame
/// - speed < 1000 becomes Animated
pub fn from_value(speed: f32) -> Self { pub fn from_value(speed: f32) -> Self {
if speed >= 999.0 { if speed >= 1000.0 {
AnimationSpeed::Instant let draw_calls = (speed - 1000.0).max(1.0) as u32; // Ensure at least 1
AnimationSpeed::Instant(draw_calls)
} else { } else {
AnimationSpeed::Animated(speed.max(1.0)) AnimationSpeed::Animated(speed.max(1.0))
} }

View File

@ -91,10 +91,10 @@ impl TurtleApp {
self.handle_mouse_zoom(); self.handle_mouse_zoom();
if let Some(ref mut controller) = self.tween_controller { if let Some(ref mut controller) = self.tween_controller {
if let Some((completed_cmd, start_state)) = controller.update(&mut self.world.turtle) { let completed_commands = controller.update(&mut self.world.turtle);
// Copy end state before we borrow world mutably
let end_state = self.world.turtle.clone();
// Process all completed commands (multiple in instant mode, 0-1 in animated mode)
for (completed_cmd, start_state, end_state) in completed_commands {
// Add draw commands for the completed tween // Add draw commands for the completed tween
execution::add_draw_for_completed_tween( execution::add_draw_for_completed_tween(
&completed_cmd, &completed_cmd,
@ -105,7 +105,6 @@ impl TurtleApp {
} }
} }
} }
/// Handle mouse click and drag for panning /// Handle mouse click and drag for panning
fn handle_mouse_panning(&mut self) { fn handle_mouse_panning(&mut self) {
let mouse_pos = mouse_position(); let mouse_pos = mouse_position();

View File

@ -74,25 +74,33 @@ impl TweenController {
self.speed = speed; self.speed = speed;
} }
/// Update the tween, returns (command, start_state) if command completed /// Update the tween, returns Vec of (command, start_state, end_state) for all completed commands this frame
pub fn update(&mut self, state: &mut TurtleState) -> Option<(TurtleCommand, TurtleState)> { /// Each command has its own start_state and end_state pair
// In immediate mode, execute all remaining commands instantly pub fn update(
if self.speed.is_instant() { &mut self,
state: &mut TurtleState,
) -> Vec<(TurtleCommand, TurtleState, TurtleState)> {
// In instant mode, execute commands up to the draw calls per frame limit
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
let mut completed_commands = Vec::new();
let mut draw_call_count = 0;
loop { loop {
let command = match self.queue.next() { let command = match self.queue.next() {
Some(cmd) => cmd.clone(), Some(cmd) => cmd.clone(),
None => return None, None => break,
}; };
// Capture start state BEFORE executing this command
let start_state = state.clone(); let start_state = state.clone();
// Handle SetSpeed command to potentially switch modes // Handle SetSpeed command to potentially switch modes
if let TurtleCommand::SetSpeed(new_speed) = &command { if let TurtleCommand::SetSpeed(new_speed) = &command {
state.set_speed(*new_speed); state.set_speed(*new_speed);
self.speed = *new_speed; self.speed = *new_speed;
// If speed dropped below instant, switch to animated mode // If speed switched to animated mode, exit instant mode processing
if !self.speed.is_instant() { if matches!(self.speed, AnimationSpeed::Animated(_)) {
return None; break;
} }
continue; continue;
} }
@ -101,11 +109,22 @@ impl TweenController {
let target_state = self.calculate_target_state(state, &command); let target_state = self.calculate_target_state(state, &command);
*state = target_state.clone(); *state = target_state.clone();
// Return drawable commands for rendering // Capture end state AFTER executing this command
let end_state = state.clone();
// Collect drawable commands with their individual start and end states
if Self::command_creates_drawing(&command) { if Self::command_creates_drawing(&command) {
return Some((command, start_state)); completed_commands.push((command, start_state, end_state));
draw_call_count += 1;
// Stop if we've reached the draw call limit for this frame
if draw_call_count >= max_draw_calls {
break;
}
} }
} }
return completed_commands;
} }
// Process current tween // Process current tween
@ -139,7 +158,7 @@ impl TweenController {
}; };
// Heading changes proportionally with progress for all commands // Heading changes proportionally with progress for all commands
state.heading = match &tween.command { state.heading = normalize_angle(match &tween.command {
TurtleCommand::Circle { TurtleCommand::Circle {
angle, direction, .. angle, direction, ..
} => match direction { } => match direction {
@ -158,7 +177,7 @@ impl TweenController {
let heading_diff = tween.target_state.heading - tween.start_state.heading; let heading_diff = tween.target_state.heading - tween.start_state.heading;
tween.start_state.heading + heading_diff * progress tween.start_state.heading + heading_diff * progress
} }
}; });
state.pen_width = tween.pen_width_tweener.move_to(elapsed); state.pen_width = tween.pen_width_tweener.move_to(elapsed);
// Discrete properties (switch at 50% progress) // Discrete properties (switch at 50% progress)
@ -176,18 +195,19 @@ impl TweenController {
// Tween complete, finalize state // Tween complete, finalize state
let start_state = tween.start_state.clone(); let start_state = tween.start_state.clone();
*state = tween.target_state.clone(); *state = tween.target_state.clone();
let end_state = state.clone();
// Return the completed command and start state to add draw commands // Return the completed command and start/end states to add draw commands
let completed_command = tween.command.clone(); let completed_command = tween.command.clone();
self.current_tween = None; self.current_tween = None;
// Only return command if it creates drawable elements // Only return command if it creates drawable elements
if Self::command_creates_drawing(&completed_command) { if Self::command_creates_drawing(&completed_command) {
return Some((completed_command, start_state)); return vec![(completed_command, start_state, end_state)];
} }
} }
return None; return Vec::new();
} }
// Start next tween // Start next tween
@ -198,9 +218,9 @@ impl TweenController {
if let TurtleCommand::SetSpeed(new_speed) = &command_clone { if let TurtleCommand::SetSpeed(new_speed) = &command_clone {
state.set_speed(*new_speed); state.set_speed(*new_speed);
self.speed = *new_speed; self.speed = *new_speed;
// If switched to immediate mode, process immediately // If switched to instant mode, process commands immediately
if self.speed.is_instant() { if matches!(self.speed, AnimationSpeed::Instant(_)) {
return self.update(state); // Recursively process in immediate mode return self.update(state); // Recursively process in instant mode
} }
// For animated mode speed changes, continue to next command // For animated mode speed changes, continue to next command
return self.update(state); return self.update(state);
@ -246,7 +266,7 @@ impl TweenController {
}); });
} }
None Vec::new()
} }
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
@ -301,7 +321,7 @@ impl TweenController {
target.position = vec2(current.position.x + dx, current.position.y + dy); target.position = vec2(current.position.x + dx, current.position.y + dy);
} }
TurtleCommand::Turn(angle) => { TurtleCommand::Turn(angle) => {
target.heading += angle.to_radians(); target.heading = normalize_angle(current.heading + angle.to_radians());
} }
TurtleCommand::Circle { TurtleCommand::Circle {
radius, radius,
@ -317,16 +337,16 @@ impl TweenController {
angle.to_radians(), angle.to_radians(),
*direction, *direction,
); );
target.heading = match direction { target.heading = normalize_angle(match direction {
CircleDirection::Left => current.heading - angle.to_radians(), CircleDirection::Left => current.heading - angle.to_radians(),
CircleDirection::Right => current.heading + angle.to_radians(), CircleDirection::Right => current.heading + angle.to_radians(),
}; });
} }
TurtleCommand::Goto(coord) => { TurtleCommand::Goto(coord) => {
target.position = *coord; target.position = *coord;
} }
TurtleCommand::SetHeading(heading) => { TurtleCommand::SetHeading(heading) => {
target.heading = *heading; target.heading = normalize_angle(*heading);
} }
TurtleCommand::SetColor(color) => { TurtleCommand::SetColor(color) => {
target.color = *color; target.color = *color;
@ -372,3 +392,18 @@ fn calculate_circle_position(
let geom = CircleGeometry::new(start_pos, start_heading, radius, direction); let geom = CircleGeometry::new(start_pos, start_heading, radius, direction);
geom.position_at_angle(angle_traveled) geom.position_at_angle(angle_traveled)
} }
/// Normalize angle to range [-PI, PI] to prevent floating-point drift
fn normalize_angle(angle: f32) -> f32 {
let two_pi = std::f32::consts::PI * 2.0;
let mut normalized = angle % two_pi;
// Ensure result is in [-PI, PI]
if normalized > std::f32::consts::PI {
normalized -= two_pi;
} else if normalized < -std::f32::consts::PI {
normalized += two_pi;
}
normalized
}