fix: UX issues solved

This commit is contained in:
2026-09-24 19:16:56 +02:00
parent 42dde04945
commit 5206a22bdd
3 changed files with 48 additions and 51 deletions
+41 -43
View File
@@ -992,8 +992,41 @@ impl FractalApp {
self.last_request = Some(key); self.last_request = Some(key);
} }
/// The rendering mode the shaders should use this frame: 3D for as long
/// as the 2D <-> 3D camera transition is in flight (the raymarcher, and
/// the DE heights it reads, stay on until the camera is back top-down),
/// otherwise the selected mode.
fn effective_rendering_mode(&self) -> u32 {
if self.anim.camera_state > 0.0 {
2
} else {
self.rendering_mode
}
}
/// 3D-mode zoom toward the screen point `off` (points from the widget
/// center): unproject it through the camera onto the z = 0 fractal plane,
/// then zoom the 2D view about the matching fractal-texture pixel.
fn zoom_3d_at(&mut self, off: egui::Vec2, rect: egui::Rect, height_px: f64, factor: f64) {
let ndc = (off / rect.size()) * 2.;
let camera_ndc_pos = self.camera.orthographic(self.anim.camera_state).inverse()
* Vec4::new(ndc.x, ndc.y, 0., 1.);
let view_direction = self.camera.direction(self.anim.camera_state);
let z_move = camera_ndc_pos.z / view_direction.z;
let ndc_pos = camera_ndc_pos.xyz() + view_direction * -z_move;
let pos = egui::Vec2::new(ndc_pos.x / self.camera.aspect_ratio, ndc_pos.y) * rect.size()
- rect.center().to_vec2();
self.view
.zoom_at_pixel(pos.x as f64, pos.y as f64, height_px, factor);
}
pub(crate) fn make_uniforms(&self, aspect: f64) -> Uniforms { pub(crate) fn make_uniforms(&self, aspect: f64) -> Uniforms {
let (span_x, span_y) = self.view.span(aspect); let (span_x, span_y) = self.view.span(aspect);
let mode = self.effective_rendering_mode();
Uniforms { Uniforms {
span: [span_x as f32, span_y as f32], span: [span_x as f32, span_y as f32],
max_iter: self.max_iterations.min(MAX_REF_POINTS as u32 - 1), max_iter: self.max_iterations.min(MAX_REF_POINTS as u32 - 1),
@@ -1011,12 +1044,8 @@ impl FractalApp {
phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32], phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32],
lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32], lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32],
complex_power: [self.complex_power.0 as f32, self.complex_power.1 as f32], complex_power: [self.complex_power.0 as f32, self.complex_power.1 as f32],
de_coloring: (self.de_coloring | (self.rendering_mode > 0)) as u32, de_coloring: (self.de_coloring || mode > 0) as u32,
rendering_mode: if self.anim.camera_state > 0.0 { rendering_mode: mode,
2
} else {
self.rendering_mode
},
camera_direction: self.camera.direction(self.anim.camera_state).to_array(), camera_direction: self.camera.direction(self.anim.camera_state).to_array(),
camera_inv_proj: self camera_inv_proj: self
.camera .camera
@@ -2099,36 +2128,19 @@ impl FractalApp {
if let Some(mt) = multi_touch { if let Some(mt) = multi_touch {
let t = mt.translation_delta; let t = mt.translation_delta;
if t.x != 0.0 || t.y != 0.0 { if t.x != 0.0 || t.y != 0.0 {
self.camera.rotate(t.x * ROT_SENS, -t.y * ROT_SENS); self.camera.rotate(-t.x * ROT_SENS, -t.y * ROT_SENS);
interacted = true; interacted = true;
} }
if mt.zoom_delta != 1.0 { if mt.zoom_delta != 1.0 {
let ndc = (mt.center_pos.to_vec2() / rect.size()) * 2.; let off = mt.center_pos - rect.center();
let camera_ndc_pos = self.camera.orthographic(self.anim.camera_state).inverse() self.zoom_3d_at(off, rect, height_px, 1. / (mt.zoom_delta as f64));
* Vec4::new(ndc.x, ndc.y, 0., 1.);
let view_direction = self.camera.direction(self.anim.camera_state);
let z_move = camera_ndc_pos.z / view_direction.z;
let ndc_pos = camera_ndc_pos.xyz() + view_direction * -z_move;
let pos = egui::Vec2::new(ndc_pos.x / self.camera.aspect_ratio, ndc_pos.y)
* rect.size()
- rect.center().to_vec2();
self.view.zoom_at_pixel(
pos.x as f64,
pos.y as f64,
height_px,
1. / (mt.zoom_delta as f64),
);
interacted = true; interacted = true;
} }
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} else if response.dragged() { } else if response.dragged() {
let d = response.drag_delta(); let d = response.drag_delta();
if d.x != 0.0 || d.y != 0.0 { if d.x != 0.0 || d.y != 0.0 {
self.camera.rotate(d.x * ROT_SENS, -d.y * ROT_SENS); self.camera.rotate(-d.x * ROT_SENS, -d.y * ROT_SENS);
} }
} }
} else if let Some(mt) = multi_touch { } else if let Some(mt) = multi_touch {
@@ -2165,21 +2177,7 @@ impl FractalApp {
let factor = (-scroll_y as f64 * 0.0015).exp(); let factor = (-scroll_y as f64 * 0.0015).exp();
let off = pos - rect.center(); let off = pos - rect.center();
if self.rendering_mode == 2 { if self.rendering_mode == 2 {
let ndc = (off / rect.size()) * 2.; self.zoom_3d_at(off, rect, height_px, factor);
let camera_ndc_pos = self.camera.orthographic(self.anim.camera_state).inverse()
* Vec4::new(ndc.x, ndc.y, 0., 1.);
let view_direction = self.camera.direction(self.anim.camera_state);
let z_move = camera_ndc_pos.z / view_direction.z;
let ndc_pos = camera_ndc_pos.xyz() + view_direction * -z_move;
let pos = egui::Vec2::new(ndc_pos.x / self.camera.aspect_ratio, ndc_pos.y)
* rect.size()
- rect.center().to_vec2();
self.view
.zoom_at_pixel(pos.x as f64, pos.y as f64, height_px, factor);
} else { } else {
self.view self.view
.zoom_at_pixel(off.x as f64, off.y as f64, height_px, factor); .zoom_at_pixel(off.x as f64, off.y as f64, height_px, factor);
@@ -2384,7 +2382,7 @@ impl FractalApp {
(((rect.height() * ppp).round() as u32) / downscale).max(1), (((rect.height() * ppp).round() as u32) / downscale).max(1),
]; ];
if self.rendering_mode == 2 { if self.effective_rendering_mode() == 2 {
size_px = [size_px[0] * 2, size_px[1] * 2]; size_px = [size_px[0] * 2, size_px[1] * 2];
} }
+6 -5
View File
@@ -1,4 +1,4 @@
use std::f32::consts::PI; use std::f32::consts::{PI, TAU};
use glam::Vec3; use glam::Vec3;
@@ -32,9 +32,11 @@ impl Camera {
/// Adjust yaw/pitch by the given deltas (radians). Pitch is clamped just /// Adjust yaw/pitch by the given deltas (radians). Pitch is clamped just
/// short of straight up/down to avoid the view flipping past the pole. /// short of straight up/down to avoid the view flipping past the pole.
/// Yaw is wrapped to [-π, π) so the 2D <-> 3D transition (which scales
/// yaw by `t`) always unwinds the short way instead of every past turn.
pub fn rotate(&mut self, dyaw: f32, dpitch: f32) { pub fn rotate(&mut self, dyaw: f32, dpitch: f32) {
const PITCH_LIMIT: f32 = PI / 2.0 - 0.01; const PITCH_LIMIT: f32 = PI / 2.0 - 0.01;
self.yaw += dyaw; self.yaw = (self.yaw + dyaw + PI).rem_euclid(TAU) - PI;
self.pitch = (self.pitch + dpitch).clamp(-PITCH_LIMIT, PITCH_LIMIT); self.pitch = (self.pitch + dpitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);
} }
@@ -64,9 +66,8 @@ impl Camera {
let yaw = self.yaw * t; let yaw = self.yaw * t;
let pitch = self.pitch * t; let pitch = self.pitch * t;
let forward = glam::Mat3::from_rotation_z(-yaw) let forward =
* glam::Mat3::from_rotation_x(-pitch) glam::Mat3::from_rotation_z(-yaw) * glam::Mat3::from_rotation_x(-pitch) * glam::Vec3::Z;
* glam::Vec3::Z;
forward.normalize() forward.normalize()
} }
+1 -3
View File
@@ -123,9 +123,7 @@ fn main() {
match result { match result {
Ok(_) => loading.remove(), Ok(_) => loading.remove(),
Err(e) => { Err(e) => {
loading.set_inner_html( loading.set_inner_html(&format!("<p>The app has crashed.</br>{e:?}</p>"));
&format!("<p>The app has crashed.</br>{e:?}</p>"),
);
log::error!("failed to start eframe: {e:?}"); log::error!("failed to start eframe: {e:?}");
} }
} }