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();
// Position turtle
plan.set_speed(1001);
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.set_speed(1200);
}
plan.hide(); // Hide turtle when done

View File

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

View File

@ -124,6 +124,11 @@ impl TurtlePlan {
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 {
self.queue.push(TurtleCommand::PenUp);
self

View File

@ -18,32 +18,35 @@ pub type Coordinate = Vec2;
pub type Visibility = bool;
/// 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
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnimationSpeed {
Instant,
Instant(u32), // Number of draw calls per frame (minimum 1)
Animated(f32), // pixels per second
}
impl AnimationSpeed {
/// Check if this is instant mode
pub fn is_instant(&self) -> bool {
matches!(self, AnimationSpeed::Instant)
pub fn is_animating(&self) -> bool {
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 {
match self {
AnimationSpeed::Instant => 9999.0,
AnimationSpeed::Instant(calls) => 1000.0 + *calls as f32,
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 {
if speed >= 999.0 {
AnimationSpeed::Instant
if speed >= 1000.0 {
let draw_calls = (speed - 1000.0).max(1.0) as u32; // Ensure at least 1
AnimationSpeed::Instant(draw_calls)
} else {
AnimationSpeed::Animated(speed.max(1.0))
}

View File

@ -91,10 +91,10 @@ impl TurtleApp {
self.handle_mouse_zoom();
if let Some(ref mut controller) = self.tween_controller {
if let Some((completed_cmd, start_state)) = controller.update(&mut self.world.turtle) {
// Copy end state before we borrow world mutably
let end_state = self.world.turtle.clone();
let completed_commands = controller.update(&mut self.world.turtle);
// 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
execution::add_draw_for_completed_tween(
&completed_cmd,
@ -105,7 +105,6 @@ impl TurtleApp {
}
}
}
/// Handle mouse click and drag for panning
fn handle_mouse_panning(&mut self) {
let mouse_pos = mouse_position();

View File

@ -74,25 +74,33 @@ impl TweenController {
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() {
/// Update the tween, returns Vec of (command, start_state, end_state) for all completed commands this frame
/// Each command has its own start_state and end_state pair
pub fn update(
&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 {
let command = match self.queue.next() {
Some(cmd) => cmd.clone(),
None => return None,
None => break,
};
// Capture start state BEFORE executing this command
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;
// If speed switched to animated mode, exit instant mode processing
if matches!(self.speed, AnimationSpeed::Animated(_)) {
break;
}
continue;
}
@ -101,13 +109,24 @@ impl TweenController {
let target_state = self.calculate_target_state(state, &command);
*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) {
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
if let Some(ref mut tween) = self.current_tween {
let elapsed = (get_time() - tween.start_time) as f32;
@ -139,7 +158,7 @@ impl TweenController {
};
// Heading changes proportionally with progress for all commands
state.heading = match &tween.command {
state.heading = normalize_angle(match &tween.command {
TurtleCommand::Circle {
angle, direction, ..
} => match direction {
@ -158,7 +177,7 @@ impl TweenController {
let heading_diff = tween.target_state.heading - tween.start_state.heading;
tween.start_state.heading + heading_diff * progress
}
};
});
state.pen_width = tween.pen_width_tweener.move_to(elapsed);
// Discrete properties (switch at 50% progress)
@ -176,18 +195,19 @@ impl TweenController {
// Tween complete, finalize state
let start_state = tween.start_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();
self.current_tween = None;
// Only return command if it creates drawable elements
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
@ -198,9 +218,9 @@ impl TweenController {
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
// If switched to instant mode, process commands immediately
if matches!(self.speed, AnimationSpeed::Instant(_)) {
return self.update(state); // Recursively process in instant mode
}
// For animated mode speed changes, continue to next command
return self.update(state);
@ -246,7 +266,7 @@ impl TweenController {
});
}
None
Vec::new()
}
pub fn is_complete(&self) -> bool {
@ -301,7 +321,7 @@ impl TweenController {
target.position = vec2(current.position.x + dx, current.position.y + dy);
}
TurtleCommand::Turn(angle) => {
target.heading += angle.to_radians();
target.heading = normalize_angle(current.heading + angle.to_radians());
}
TurtleCommand::Circle {
radius,
@ -317,16 +337,16 @@ impl TweenController {
angle.to_radians(),
*direction,
);
target.heading = match direction {
target.heading = normalize_angle(match direction {
CircleDirection::Left => current.heading - angle.to_radians(),
CircleDirection::Right => current.heading + angle.to_radians(),
};
});
}
TurtleCommand::Goto(coord) => {
target.position = *coord;
}
TurtleCommand::SetHeading(heading) => {
target.heading = *heading;
target.heading = normalize_angle(*heading);
}
TurtleCommand::SetColor(color) => {
target.color = *color;
@ -372,3 +392,18 @@ fn calculate_circle_position(
let geom = CircleGeometry::new(start_pos, start_heading, radius, direction);
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
}