feat: improve UI/UX

This commit is contained in:
2026-09-22 09:59:58 +02:00
parent 66669cd801
commit 7274f19b33
5 changed files with 69 additions and 62 deletions
+35 -28
View File
@@ -261,9 +261,8 @@ pub struct FractalApp {
/// orbit derivative, giving crisp filaments at deep zoom instead of speckle.
de_coloring: bool,
// Use shadow coloring
shadow: bool,
// Use 3D raymarching rendering
dimension3: bool,
rendering_mode: u32,
/// List of enabled lights in the world
lights: Vec<Light>,
@@ -435,8 +434,7 @@ impl FractalApp {
shadow_palette: 0,
antialias: false,
de_coloring: false,
shadow: false,
dimension3: false,
rendering_mode: 0,
lights: vec![Light::default()],
buddha_r_cap: 50,
buddha_g_cap: 500,
@@ -937,14 +935,12 @@ impl FractalApp {
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],
complex_power: [self.complex_power.0 as f32, self.complex_power.1 as f32],
de_coloring: (self.de_coloring | self.shadow | self.dimension3) as u32,
shadow: self.shadow as u32,
dimension3: self.dimension3 as u32,
de_coloring: (self.de_coloring | (self.rendering_mode > 0)) as u32,
rendering_mode: self.rendering_mode,
camera_direction: self.camera.direction().to_array(),
camera_inv_proj: self.camera.orthographic().inverse().to_cols_array(),
_pad: [0; _],
_pad2: [0; _],
_pad3: [0; _],
camera_inv_proj: self.camera.orthographic().inverse().to_cols_array(),
}
}
@@ -1632,6 +1628,13 @@ impl FractalApp {
});
}
ui.label("rendering:");
ui.horizontal(|ui| {
ui.radio_value(&mut self.rendering_mode, 0, "Classic");
ui.radio_value(&mut self.rendering_mode, 1, "Shadow");
ui.radio_value(&mut self.rendering_mode, 2, "3D");
});
ui.add_space(4.);
ui.separator();
ui.add_space(4.);
@@ -1646,15 +1649,29 @@ impl FractalApp {
.logarithmic(true),
);
}
ui.checkbox(&mut self.antialias, "Antialiasing (2×2)")
.on_hover_text("Supersample each pixel for smoother edges (~4× slower).");
if self.rendering_mode == 0 {
ui.checkbox(&mut self.de_coloring, "Distance shading")
.on_hover_text(
"Shade by distance to the set boundary (from the orbit derivative) \
for crisp filaments at deep zoom. Exact for the holomorphic kinds \
(Mandelbrot/Multibrot/Phoenix), approximate for the abs-based kinds \
(Burning Ship/Tricorn/Celtic/Perpendicular/Buffalo).",
);
}
ui.add_space(4.);
ui.separator();
ui.add_space(4.);
if self.rendering_mode == 0 {
ui.add(
egui::Slider::new(&mut self.color_scale, 0.01..=1.0)
.text("color scale")
.logarithmic(true),
);
ui.add(egui::Slider::new(&mut self.color_offset, 0.0..=1.0).text("color offset"));
ui.checkbox(&mut self.shadow, "Shadow rendering");
ui.checkbox(&mut self.dimension3, "3D rendering");
if !self.shadow {
egui::ComboBox::from_label("palette")
.selected_text(PALETTE_NAMES[self.palette as usize])
.show_ui(ui, |ui| {
@@ -1671,7 +1688,8 @@ impl FractalApp {
}
});
}
if self.shadow && self.shadow_palette as usize == SHADOW_PALETTE_NAMES.len() - 1 {
if self.rendering_mode > 0 && self.shadow_palette as usize == SHADOW_PALETTE_NAMES.len() - 1
{
ui.horizontal(|ui| {
ui.label("lights:");
if ui.button("+").clicked() {
@@ -1692,20 +1710,9 @@ impl FractalApp {
ui.add_space(4.);
ui.separator();
ui.add_space(4.);
ui.checkbox(&mut self.antialias, "Antialiasing (2×2)")
.on_hover_text("Supersample each pixel for smoother edges (~4× slower).");
if !self.shadow {
ui.checkbox(&mut self.de_coloring, "Distance shading")
.on_hover_text(
"Shade by distance to the set boundary (from the orbit derivative) \
for crisp filaments at deep zoom. Exact for the holomorphic kinds \
(Mandelbrot/Multibrot/Phoenix), approximate for the abs-based kinds \
(Burning Ship/Tricorn/Celtic/Perpendicular/Buffalo).",
);
}
ui.collapsing("Animation", |ui| {
if !self.shadow {
if self.rendering_mode == 0 {
ui.checkbox(&mut self.anim.color, "Cycle colours")
.on_hover_text("Scroll the palette offset over time.");
if self.anim.color {
@@ -1982,7 +1989,7 @@ impl FractalApp {
// instead of panning/zooming the 2D fractal view.
const ROT_SENS: f32 = 0.002; // radians per dragged pixel
let multi_touch = ui.input(|i| i.multi_touch());
if self.dimension3 {
if self.rendering_mode == 3 {
if let Some(mt) = multi_touch {
let t = mt.translation_delta;
if t.x != 0.0 || t.y != 0.0 {
@@ -2033,7 +2040,7 @@ impl FractalApp {
&& rect.contains(pos)
{
let factor = (-scroll_y as f64 * 0.0015).exp();
if self.dimension3 {
if self.rendering_mode == 3 {
self.camera.zoom(factor as f32);
} else {
let off = pos - rect.center();
@@ -2053,7 +2060,7 @@ impl FractalApp {
if !ui.ctx().egui_wants_keyboard_input() {
let dt = ui.input(|i| i.stable_dt as f64).clamp(0.0, 0.1);
if self.dimension3 {
if self.rendering_mode == 3 {
let (fwd, back, strafe_l, strafe_r, up, down, look_l, look_r, look_u, look_d) = ui
.input(|i| {
(
+4 -7
View File
@@ -50,7 +50,7 @@ fn color_differs(a: &Uniforms, b: &Uniforms) -> bool {
|| a.color_scale != b.color_scale
|| a.palette_id != b.palette_id
|| a.shadow_palette_id != b.shadow_palette_id
|| a.shadow != b.shadow
|| a.rendering_mode != b.rendering_mode
}
/// GPU-side view + coloring parameters. Layout must match `Uniforms` in the
@@ -93,14 +93,11 @@ pub struct Uniforms {
pub complex_power: [f32; 2],
/// 0 = escape-time coloring, 1 = distance-estimation shading.
pub de_coloring: u32,
// 0 = classic colors, 1 = shadows
pub shadow: u32,
// 0 = classic colors, 1 = 3D raymarching rendering
pub dimension3: u32,
pub _pad2: [u32; 3],
// 0 = classic colors, 1 = shadows, 2 = 3D raymarching rendering
pub rendering_mode: u32,
// camera direction vector
pub camera_direction: [f32; 3],
pub _pad3: [u32; 1],
pub _pad2: [u32; 1],
/// Inverse of the camera's view-projection matrix (column-major), for
/// reconstructing a world-space ray origin per pixel in the raymarcher.
pub camera_inv_proj: [f32; 16],
+9 -2
View File
@@ -1,7 +1,7 @@
use std::f32::consts::PI;
use bytemuck::{Pod, Zeroable};
use egui::{Color32, Ui};
use egui::{Button, Color32, Ui};
/// Maximum number of simultaneous lights.
pub const MAX_LIGHT_COUNT: usize = 16;
@@ -28,7 +28,12 @@ impl Default for Light {
impl Light {
pub fn widget(&mut self, ui: &mut Ui) -> bool {
let formater = |v, _| format!("{}°", ((v as f32 * 180. / PI) as u32));
let formater = |v, _| format!("{}°", ((v * 180. / std::f64::consts::PI) as u32));
let parser = |s: &str| {
s.parse::<u32>()
.ok()
.map(|x| x as f64 * std::f64::consts::PI / 180.)
};
ui.horizontal(|ui| {
let del = ui.button("-").clicked();
ui.label("color:");
@@ -38,6 +43,7 @@ impl Light {
egui::DragValue::new(&mut self.azimuth)
.range(0.0..=PI * 2.)
.custom_formatter(formater)
.custom_parser(parser)
.speed(0.02),
);
ui.label("φ:");
@@ -45,6 +51,7 @@ impl Light {
egui::DragValue::new(&mut self.altitude)
.range(0.0..=PI / 2.)
.custom_formatter(formater)
.custom_parser(parser)
.speed(0.02),
);
+3 -5
View File
@@ -35,8 +35,9 @@ fn shadow_fragment(pos: vec3<f32>) -> vec4<f32> {
}
@fragment
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
if u.dimension3 == 0u {
if u.shadow != 0u {
if u.shadow == 2u {
return ray_marching(pos);
} else if u.shadow == 1u {
return shadow_fragment(pos.xyz);
} else {
let d = textureLoad(data_tex, vec2<i32>(i32(pos.x), i32(pos.y)), 0);
@@ -50,9 +51,6 @@ fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
col = col * (1.0 - interior_frac);
return vec4<f32>(col, 1.0);
}
} else {
return ray_marching(pos);
}
}
fn sdf(pos: vec3<f32>) -> f32 {
+1 -3
View File
@@ -31,10 +31,8 @@ struct Uniforms {
complex_power: vec2<f32>,
// 0 = escape-time coloring, 1 = distance-estimation shading.
de_coloring: u32,
// 0 = classic colors, 1 = shadows
// 0 = classic colors, 1 = shadows, 2 = 3D raymarching rendering
shadow: u32,
// Use 3D raymarching rendering
dimension3: u32,
// camera direction vector
camera_direction: vec3<f32>,
// inverse of the camera's view-projection matrix, for reconstructing a