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();
|
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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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,13 +109,24 @@ 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
|
||||||
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;
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user