feat: add optional animate mode for deep Mandelbrot zoom#14
feat: add optional animate mode for deep Mandelbrot zoom#14dustycyanide wants to merge 2 commits intoPottierLoic:mainfrom
Conversation
|
Hey, thanks for the PR, the animation idea is cool and I like the direction overall. That said, I'm not convinced the extra CLI arguments are the right approach here. Animation feels more like a runtime feature than something that should be configured at startup. I'd rather keep it simple:
This would make it possible to tweak the animation parameters at runtime, it would be more natural than restarting the app with different flags. I'm also not a big fan of hardcoding zoom coordinates. I'd prefer to simply let the user decide to use the |
|
thanks for the review! Just did another pass to move the animation control into the runtime commands and separate the concerns by module.
Validation:
|
|
Hey, thanks for the changes ! |
PottierLoic
left a comment
There was a problem hiding this comment.
Hey, thanks for the updates and sorry for the wait, I was quite busy on some personal things recently.
The feature is much cleaner now and I think we just need to clean a few things before merging it.
The main thing is the removal of the FPS system entirely. Fractal rendering involves heavy computations, and as you probably noticed, the time needed to compute a frame is increasing as we zoom deeper into it. This computation duration will quickly exceed any target FPS interval, making the FPS control meaningless in practice.
The 60fps poll loop in handle_input was already in place, we don't need a second timing layer on top of it.
As a consequence, tick() can be removed too. The zoom can be applied directly in handle_input.
if self.animation.enabled {
f.scale *= self.animation.speed;
self.fractal_view.need_render = true;
}The rest of the comments are mainly to mark where fps related parts are so we don't miss any, once this is cleaned up across all files, this is good to go.
| pub fn set_fps(&mut self, fps: f64) -> Result<(), String> { | ||
| if fps <= 0.0 { | ||
| return Err("Animation FPS must be positive".to_string()); | ||
| } | ||
| self.fps = fps; | ||
| self.last_tick = None; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Alongside with the fps removal, this should go.
| pub fn tick_interval(&self) -> Duration { | ||
| if self.enabled { | ||
| Duration::from_secs_f64(1.0 / self.fps.max(0.1)) | ||
| } else { | ||
| Duration::from_secs_f64(1.0 / 60.0) | ||
| } | ||
| } |
There was a problem hiding this comment.
Alongside with the fps removal, this should go.
| #[derive(Debug, Clone)] | ||
| pub struct AnimationState { | ||
| pub enabled: bool, | ||
| pub fps: f64, |
There was a problem hiding this comment.
This field and all fps related features should be removed. Please read the global review for the reasons.
| pub enabled: bool, | ||
| pub fps: f64, | ||
| pub speed: f64, | ||
| last_tick: Option<Instant>, |
There was a problem hiding this comment.
Should be removed. (edit: on fps and last_tick are concerned, I missclicked)
| RecordJulia(u32, u32, f64, f64, f64, f64), | ||
| CycleSpeed(f64), | ||
| Animate(bool), | ||
| AnimateFps(f64), |
There was a problem hiding this comment.
Same here. (edit: line 21 is no concerned obviously, I missclicked)
| pub fn parse_animate_fps(parts: &[&str]) -> Command { | ||
| if parts.len() != 2 { | ||
| return Command::Unknown(format!( | ||
| "Usage: animate_fps <value>. Got {} arguments", | ||
| parts.len() - 1 | ||
| )); | ||
| } | ||
|
|
||
| match parts[1].parse::<f64>() { | ||
| Ok(v) if v > 0.0 => Command::AnimateFps(v), | ||
| Ok(_) => Command::Unknown("Animation FPS must be positive".to_string()), | ||
| Err(_) => Command::Unknown(format!("Invalid animation FPS: {}", parts[1])), | ||
| } | ||
| } |
| let timeout = Duration::from_secs_f32(1.0 / 60.0); | ||
| let timeout = self.animation.tick_interval(); |
There was a problem hiding this comment.
This can go back to it's original state as we are removing tick_interval.
| - `animate <on|off>` | ||
| Enables or disables continuous zoom animation. | ||
|
|
||
| - `animate_fps <value>` |
| pub fn tick(&mut self, fractal: &mut Fractal) -> bool { | ||
| if !self.enabled { | ||
| return false; | ||
| } | ||
|
|
||
| let now = Instant::now(); | ||
| let interval = self.tick_interval(); | ||
|
|
||
| if let Some(last_tick) = self.last_tick { | ||
| if now.duration_since(last_tick) < interval { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| self.last_tick = Some(now); | ||
| fractal.scale *= self.speed; | ||
| true | ||
| } | ||
| } |
There was a problem hiding this comment.
as we are removing the fps, tick is no longer needed, we will do the zoom in input.rs
| if self.animation.tick(&mut self.fractal) { | ||
| self.fractal_view.need_render = true; | ||
| } |
There was a problem hiding this comment.
The zoom can be done here directly now.
Summary
--animatemode that keeps the originalfractouilledefault behavior unchanged while enabling continuous Mandelbrot zoom animation on demandp--animate-fps,--animate-zoom-factor,--animate-scale-ceiling,--animate-no-cycle)Prompt
Please implement an optional terminal animation mode for Fractouille that preserves existing default behavior (
fractouillestays manual), and enables a new deep Mandelbrot zoom experience only when requested by CLI flag. The mode should continuously zoom, increase iteration depth as zoom deepens, recover/reset gracefully when floating-point depth is exhausted, and optionally cycle curated deep points to keep visuals interesting. Include user-facing controls/help text and documentation for comparing old/manual mode and new animation mode.AI Attribution
This change was written by AI using openai/gpt-5.3-codex in OpenCode.
Verification
cargo buildfractouille --help