feat: improve CLI arguments
This commit is contained in:
+56
-13
@@ -14,6 +14,8 @@ use crate::fractal::{
|
||||
compute_set_reference,
|
||||
};
|
||||
use crate::lights::Light;
|
||||
use crate::view::parse_half_height_spec;
|
||||
use crate::view::parse_re_im_spec;
|
||||
use crate::view::{
|
||||
Big, DEFAULT_HALF_HEIGHT, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str,
|
||||
parse_view_spec, precision_for,
|
||||
@@ -356,7 +358,7 @@ fn sig_digits_for(bits: usize) -> usize {
|
||||
}
|
||||
|
||||
/// Format a magnification for the editable field (compact scientific).
|
||||
fn format_magnification(m: f64) -> String {
|
||||
fn format_zoom(m: f64) -> String {
|
||||
format!("{m:.4e}")
|
||||
}
|
||||
|
||||
@@ -415,7 +417,7 @@ impl FractalApp {
|
||||
let sig = sig_digits_for(view.precision_bits());
|
||||
let center_re_edit = big_to_decimal_str(&view.center_re, sig);
|
||||
let center_im_edit = big_to_decimal_str(&view.center_im, sig);
|
||||
let zoom_edit = format_magnification(view.magnification());
|
||||
let zoom_edit = format_zoom(view.zoom());
|
||||
|
||||
Self {
|
||||
view,
|
||||
@@ -532,16 +534,24 @@ impl FractalApp {
|
||||
if let Some(spec) = cli.view {
|
||||
self.apply_view_spec(&spec);
|
||||
}
|
||||
if let Some(iterations) = cli.iterations {
|
||||
self.auto_iterations = false;
|
||||
self.max_iterations = iterations;
|
||||
}
|
||||
if let Some(half_height) = cli.half_height {
|
||||
self.apply_half_height_spec(&half_height);
|
||||
}
|
||||
if let Some(position) = cli.position {
|
||||
self.apply_re_im_spec(&position);
|
||||
}
|
||||
if cli.de {
|
||||
self.de_coloring = true;
|
||||
}
|
||||
if cli.buddhabrot {
|
||||
self.mode = FractalMode::Buddhabrot;
|
||||
}
|
||||
if let Some(p) = cli.buddha_palette {
|
||||
self.buddha_palette = p.min(BUDDHA_PALETTE_NAMES.len() as u32 - 1);
|
||||
}
|
||||
if let Some(p) = cli.palette {
|
||||
self.buddha_palette = p.min(BUDDHA_PALETTE_NAMES.len() as u32 - 1);
|
||||
self.palette = p.min(PALETTE_NAMES.len() as u32 - 1);
|
||||
}
|
||||
self.export_path = cli.export_path;
|
||||
@@ -562,6 +572,27 @@ impl FractalApp {
|
||||
true
|
||||
}
|
||||
|
||||
/// Apply a half_height spec. Used by the native debug env var.
|
||||
#[allow(dead_code)]
|
||||
pub fn apply_half_height_spec(&mut self, spec: &str) -> bool {
|
||||
let Some(half_height) = parse_half_height_spec(spec) else {
|
||||
return false;
|
||||
};
|
||||
self.view.half_height = half_height;
|
||||
true
|
||||
}
|
||||
/// Apply a view spec "re,im" (re/im are decimal,
|
||||
/// parsed at full precision). Used by the native debug env var.
|
||||
#[allow(dead_code)]
|
||||
pub fn apply_re_im_spec(&mut self, spec: &str) -> bool {
|
||||
let Some((re, im)) = parse_re_im_spec(spec, self.view.precision_bits()) else {
|
||||
return false;
|
||||
};
|
||||
self.view.center_re = re;
|
||||
self.view.center_im = im;
|
||||
true
|
||||
}
|
||||
|
||||
/// The current view (center + half-height). Used by headless animation
|
||||
/// to snapshot the start of a camera path.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -586,6 +617,19 @@ impl FractalApp {
|
||||
self.auto_iterations = v;
|
||||
}
|
||||
|
||||
/// Set `max_iterations`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn set_max_iterations(&mut self, i: u32) {
|
||||
self.auto_iterations = false;
|
||||
self.max_iterations = i;
|
||||
}
|
||||
|
||||
/// Get `max_iterations`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn max_iterations(&mut self) -> u32 {
|
||||
self.max_iterations
|
||||
}
|
||||
|
||||
/// Jump to a preset Mandelbrot location: decimal center (parsed at the
|
||||
/// precision the zoom needs), half-height, and a fitting iteration count.
|
||||
fn go_to_place(&mut self, re: &str, im: &str, half_height: f64, iterations: u32) {
|
||||
@@ -1837,7 +1881,7 @@ impl FractalApp {
|
||||
self.center_im_edit = big_to_decimal_str(&self.view.center_im, sig);
|
||||
}
|
||||
|
||||
ui.label("magnification (×):");
|
||||
ui.label("zoom:");
|
||||
let zoom_resp = ui.add(
|
||||
egui::TextEdit::singleline(&mut self.zoom_edit)
|
||||
.desired_width(f32::INFINITY)
|
||||
@@ -1848,18 +1892,17 @@ impl FractalApp {
|
||||
}
|
||||
if zoom_resp.lost_focus() {
|
||||
if self.zoom_edited
|
||||
&& let Ok(m) = self.zoom_edit.trim().parse::<f64>()
|
||||
&& let Ok(hh) = self.zoom_edit.trim().parse::<f64>()
|
||||
&& hh > 0.0
|
||||
&& hh.is_finite()
|
||||
{
|
||||
let hh = DEFAULT_HALF_HEIGHT / m;
|
||||
if m > 0.0 && hh > 0.0 && hh.is_finite() {
|
||||
self.view.half_height = hh;
|
||||
self.view.sync_precision();
|
||||
}
|
||||
self.view.half_height = hh;
|
||||
self.view.sync_precision();
|
||||
}
|
||||
self.zoom_edited = false;
|
||||
}
|
||||
if !zoom_resp.has_focus() {
|
||||
self.zoom_edit = format_magnification(self.view.magnification());
|
||||
self.zoom_edit = format_zoom(self.view.zoom());
|
||||
}
|
||||
ui.label(format!("reference: {} pts", self.reference.len()));
|
||||
ui.label(format!("precision: {} bits", self.view.precision_bits()));
|
||||
|
||||
+26
-14
@@ -13,6 +13,14 @@ pub struct Cli {
|
||||
#[arg(long, value_enum)]
|
||||
pub kind: Option<KindArg>,
|
||||
|
||||
/// Start in Julia mode with this seed constant.
|
||||
#[arg(long, value_name = "RE,IM")]
|
||||
pub julia: Option<String>,
|
||||
|
||||
/// Switch to the Buddhabrot renderer.
|
||||
#[arg(long)]
|
||||
pub buddhabrot: bool,
|
||||
|
||||
/// Exponent for the Multibrot kind (z -> z^power + c), clamped to [2, 8].
|
||||
#[arg(long)]
|
||||
pub power: Option<u32>,
|
||||
@@ -21,14 +29,6 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "RE,IM")]
|
||||
pub complex_power: Option<String>,
|
||||
|
||||
/// Start in Julia mode with this seed constant.
|
||||
#[arg(long, value_name = "RE,IM")]
|
||||
pub julia: Option<String>,
|
||||
|
||||
/// Coloring palette index.
|
||||
#[arg(long, value_name = "INDEX")]
|
||||
pub palette: Option<u32>,
|
||||
|
||||
/// Phoenix constant p for the Phoenix kind (z -> z^2 + c + p*z_prev).
|
||||
#[arg(long, value_name = "RE,IM")]
|
||||
pub phoenix_p: Option<String>,
|
||||
@@ -45,17 +45,25 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "RE,IM,HALF_HEIGHT[,ITERATIONS]")]
|
||||
pub view: Option<String>,
|
||||
|
||||
/// Jump to a specific position on startup.
|
||||
#[arg(long, short('p'), value_name = "RE,IM")]
|
||||
pub position: Option<String>,
|
||||
|
||||
/// Set a maximum iterations count on startup.
|
||||
#[arg(long, short('i'))]
|
||||
pub iterations: Option<u32>,
|
||||
|
||||
/// Set the zoom level on startup.
|
||||
#[arg(long("zoom"), short('z'))]
|
||||
pub half_height: Option<String>,
|
||||
|
||||
/// Enable distance-estimation shading.
|
||||
#[arg(long)]
|
||||
pub de: bool,
|
||||
|
||||
/// Switch to the Buddhabrot renderer.
|
||||
#[arg(long)]
|
||||
pub buddhabrot: bool,
|
||||
|
||||
/// Buddhabrot tonemap palette index.
|
||||
/// Coloring palette index.
|
||||
#[arg(long, value_name = "INDEX")]
|
||||
pub buddha_palette: Option<u32>,
|
||||
pub palette: Option<u32>,
|
||||
|
||||
/// Output path for --headless (default: fractal-<timestamp>.png). When
|
||||
/// animating (--to-view/--to-share), this is a directory of
|
||||
@@ -77,6 +85,10 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "FRAGMENT")]
|
||||
pub to_share: Option<String>,
|
||||
|
||||
/// Set a maximum iterations count at animation end.
|
||||
#[arg(long)]
|
||||
pub to_iterations: Option<u32>,
|
||||
|
||||
/// Number of frames to render for an animation. Alternative to --fps +
|
||||
/// --duration.
|
||||
#[arg(long, value_name = "N")]
|
||||
|
||||
+29
-10
@@ -11,7 +11,8 @@ use crate::app::{FractalApp, unix_timestamp};
|
||||
use crate::cli::Cli;
|
||||
use crate::fractal::{ExportRender, FractalRenderer, ShareState, export_to_png_blocking};
|
||||
use crate::view::{
|
||||
ViewState, big_from_decimal_str, interpolate_view, parse_view_spec, precision_for,
|
||||
ViewState, big_from_decimal_str, interpolate_f64, interpolate_view, parse_view_spec,
|
||||
precision_for,
|
||||
};
|
||||
|
||||
/// Cap on the output image dimension (px), to stay within GPU texture limits.
|
||||
@@ -29,6 +30,7 @@ pub fn run(cli: Cli) -> Result<(), String> {
|
||||
// consumes `cli` to build the start state.
|
||||
let to_view = cli.to_view.clone();
|
||||
let to_share = cli.to_share.clone();
|
||||
let to_iterations = cli.to_iterations;
|
||||
let frames_arg = cli.frames;
|
||||
let fps = cli.fps;
|
||||
let duration = cli.duration;
|
||||
@@ -43,6 +45,7 @@ pub fn run(cli: Cli) -> Result<(), String> {
|
||||
app,
|
||||
to_view,
|
||||
to_share,
|
||||
to_iterations,
|
||||
frames_arg,
|
||||
fps,
|
||||
duration,
|
||||
@@ -97,6 +100,7 @@ fn run_animation(
|
||||
mut app: FractalApp,
|
||||
to_view: Option<String>,
|
||||
to_share: Option<String>,
|
||||
mut to_iterations: Option<u32>,
|
||||
frames_arg: Option<u32>,
|
||||
fps: f64,
|
||||
duration: Option<f64>,
|
||||
@@ -116,11 +120,20 @@ fn run_animation(
|
||||
return Err("animation needs at least 2 frames".into());
|
||||
}
|
||||
|
||||
let to = parse_animation_target(to_view.as_deref(), to_share.as_deref())?;
|
||||
let (to, to_iterations_share) =
|
||||
parse_animation_target(to_view.as_deref(), to_share.as_deref())?;
|
||||
if to_iterations.is_none()
|
||||
&& let Some(to_iterations_share) = to_iterations_share
|
||||
{
|
||||
to_iterations = Some(to_iterations_share);
|
||||
}
|
||||
let from = app.view_state().clone();
|
||||
// Iteration count auto-scales with zoom depth per frame, the same way it
|
||||
// does while zooming interactively — no need to interpolate it by hand.
|
||||
app.set_auto_iterations(true);
|
||||
let from_iterations = app.max_iterations();
|
||||
if to_iterations.is_none() {
|
||||
// Iteration count auto-scales with zoom depth per frame, the same way it
|
||||
// does while zooming interactively — no need to interpolate it by hand.
|
||||
app.set_auto_iterations(true);
|
||||
}
|
||||
|
||||
let out_dir = export_path.unwrap_or_else(|| format!("frames-{}", unix_timestamp()));
|
||||
std::fs::create_dir_all(&out_dir).map_err(|e| format!("failed to create {out_dir}: {e}"))?;
|
||||
@@ -133,6 +146,11 @@ fn run_animation(
|
||||
for i in 0..frames {
|
||||
let raw_t = i as f64 / (frames - 1) as f64;
|
||||
let t = if linear { raw_t } else { smoothstep(raw_t) };
|
||||
if let Some(to) = to_iterations {
|
||||
app.set_max_iterations(
|
||||
interpolate_f64(from_iterations as f64, to as f64, t).round() as u32,
|
||||
);
|
||||
}
|
||||
app.set_view(interpolate_view(&from, &to, t));
|
||||
|
||||
eprintln!("[{:>4}/{frames}] computing reference orbit…", i + 1);
|
||||
@@ -180,11 +198,9 @@ fn run_animation(
|
||||
fn parse_animation_target(
|
||||
to_view: Option<&str>,
|
||||
to_share: Option<&str>,
|
||||
) -> Result<ViewState, String> {
|
||||
) -> Result<(ViewState, Option<u32>), String> {
|
||||
if let Some(spec) = to_view {
|
||||
return parse_view_spec(spec)
|
||||
.map(|(view, _)| view)
|
||||
.ok_or_else(|| format!("invalid --to-view spec: {spec}"));
|
||||
return parse_view_spec(spec).ok_or_else(|| format!("invalid --to-view spec: {spec}"));
|
||||
}
|
||||
let frag = to_share.expect("run_animation only called with one of to_view/to_share set");
|
||||
let state =
|
||||
@@ -194,7 +210,10 @@ fn parse_animation_target(
|
||||
big_from_decimal_str(&state.center_re, bits).ok_or("invalid --to-share center (re)")?;
|
||||
let im =
|
||||
big_from_decimal_str(&state.center_im, bits).ok_or("invalid --to-share center (im)")?;
|
||||
Ok(ViewState::with_center(re, im, state.half_height))
|
||||
Ok((
|
||||
ViewState::with_center(re, im, state.half_height),
|
||||
Some(state.iterations),
|
||||
))
|
||||
}
|
||||
|
||||
/// Ease-in/ease-out pacing: slow at both ends, fast through the middle.
|
||||
|
||||
+33
@@ -58,6 +58,11 @@ impl ViewState {
|
||||
DEFAULT_HALF_HEIGHT / self.half_height
|
||||
}
|
||||
|
||||
/// Current zoom level.
|
||||
pub fn zoom(&self) -> f64 {
|
||||
self.half_height
|
||||
}
|
||||
|
||||
/// Bits of precision the center currently needs for this zoom level.
|
||||
pub fn precision_bits(&self) -> usize {
|
||||
precision_for(self.half_height)
|
||||
@@ -140,6 +145,30 @@ pub fn parse_view_spec(spec: &str) -> Option<(ViewState, Option<u32>)> {
|
||||
Some((ViewState::with_center(re, im, half_height), iterations))
|
||||
}
|
||||
|
||||
/// Parse a half_height spec. Shared by
|
||||
/// `FractalApp::apply_half_height_spec` (the `--zoom` CLI flag) and headless
|
||||
/// animation's `--to-zoom`.
|
||||
pub fn parse_half_height_spec(spec: &str) -> Option<f64> {
|
||||
let half_height = spec.trim().parse::<f64>().ok()?;
|
||||
if !(half_height > 0.0 && half_height.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
Some(half_height)
|
||||
}
|
||||
/// Parse a "re,im" spec (re/im decimal, parsed at
|
||||
/// full precision) into a view. Shared by
|
||||
/// `FractalApp::apply_re_im_spec` (the `--position` CLI flag) and headless
|
||||
/// animation's `--to-position`.
|
||||
pub fn parse_re_im_spec(spec: &str, bits: usize) -> Option<(Big, Big)> {
|
||||
let parts: Vec<&str> = spec.split(',').collect();
|
||||
if parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
let re = big_from_decimal_str(parts[0], bits)?;
|
||||
let im = big_from_decimal_str(parts[1], bits)?;
|
||||
Some((re, im))
|
||||
}
|
||||
|
||||
/// Interpolate between two views for an animation frame, `t` in `[0, 1]`.
|
||||
/// The half-height interpolates geometrically (log-linear), since zoom depth
|
||||
/// spans many decades and a linear sweep would crawl at the start and blow
|
||||
@@ -172,6 +201,10 @@ pub fn interpolate_view(from: &ViewState, to: &ViewState, t: f64) -> ViewState {
|
||||
ViewState::with_center(center_re, center_im, half_height)
|
||||
}
|
||||
|
||||
pub fn interpolate_f64(from: f64, to: f64, t: f64) -> f64 {
|
||||
from + (to - from) * t
|
||||
}
|
||||
|
||||
/// Render a `Big` as a decimal string with `sig_digits` significant digits.
|
||||
pub fn big_to_decimal_str(x: &Big, sig_digits: usize) -> String {
|
||||
let dec = x
|
||||
|
||||
Reference in New Issue
Block a user