normalizing angles to prevent drift
also move the at least one frame check to the AnimationSpeed creation.
This commit is contained in:
parent
5799d2aa07
commit
c3f5136359
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,11 +109,22 @@ 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
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user