diff --git a/src/app.rs b/src/app.rs index df04317..dcf10b5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,8 @@ use crate::theme::{ONE_DARK_PRO, apply_fonts, apply_theme}; use crate::{ matrix::{MATRIX_COLS, MATRIX_ROWS}, render::{ - BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, WgpuBackgroundCallback, + BackgroundRenderResources, MarkerMode, PRESSURE_CELL_COUNT, PressureFrame, + WgpuBackgroundCallback, }, ui::{ ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState, @@ -90,6 +91,7 @@ impl EskinDesktopApp { width, height, pressure: self.pressure_matrix, + marker_mode: MarkerMode::Number, }, )); } @@ -243,7 +245,12 @@ impl EskinDesktopApp { draw_scene_panel(ctx, &mut self.scene_panel); draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state); draw_stats_panel(ctx, &mut self.stats_panel); - draw_export_panel(ctx, &mut self.export_panel, &self.recorder, &mut self.export_path); + draw_export_panel( + ctx, + &mut self.export_panel, + &self.recorder, + &mut self.export_path, + ); draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config); } } diff --git a/src/happy-tree.png b/src/happy-tree.png deleted file mode 100755 index fc86db3..0000000 Binary files a/src/happy-tree.png and /dev/null differ diff --git a/src/main.rs b/src/main.rs index 6f95400..0628942 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ mod app; mod connection; mod matrix; +mod recording; mod render; mod serial_core; mod theme; mod ui; -mod recording; mod utils; +mod model; use app::EskinDesktopApp; use eframe::egui; diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..2c263e3 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,15 @@ +use eframe::{ + egui, + egui_wgpu::{self, wgpu}, + wgpu::util::DeviceExt, +}; + +pub trait Vertex { + fn desc<'a>() -> wgpu::VertexBufferLayout<'a>; +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct ModelVertex { + pub position: [f32; 3], +} \ No newline at end of file diff --git a/src/recording.rs b/src/recording.rs index 4da3ab1..2a5d9f5 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -292,9 +292,11 @@ impl Recorder { let mut pressures = Vec::with_capacity(n_channels); for f in &fields[..n_channels] { - pressures.push(f.parse::().with_context(|| { - format!("line {}: bad channel value '{}'", lineno + 2, f) - })?); + pressures.push( + f.parse::().with_context(|| { + format!("line {}: bad channel value '{}'", lineno + 2, f) + })?, + ); } let raw_ts = fields[n_channels] .parse::() @@ -320,7 +322,8 @@ impl Recorder { // Pretend the recording happened `last.timestamp_ms` ago // so that elapsed_ms() would return that value. // We store a "fake" start by noting the offset. - r.start = Some(Instant::now() - std::time::Duration::from_millis(last.timestamp_ms)); + r.start = + Some(Instant::now() - std::time::Duration::from_millis(last.timestamp_ms)); r.paused_duration_ms = 0; } } diff --git a/src/render.rs b/src/render.rs index 68fb01c..1f2b7ef 100644 --- a/src/render.rs +++ b/src/render.rs @@ -14,6 +14,13 @@ pub struct WgpuBackgroundCallback { pub width: f32, pub height: f32, pub pressure: PressureFrame, + pub marker_mode: MarkerMode, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MarkerMode { + Number, + Dot, } impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback { @@ -26,7 +33,13 @@ impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback { resources: &mut egui_wgpu::CallbackResources, ) -> Vec { let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap(); - resources.prepare(queue, self.width, self.height, &self.pressure); + resources.prepare( + queue, + self.width, + self.height, + &self.pressure, + self.marker_mode, + ); Vec::new() } @@ -51,9 +64,11 @@ pub struct BackgroundRenderResources { uniform_bind_group: wgpu::BindGroup, background_pipeline: wgpu::RenderPipeline, glyph_pipeline: wgpu::RenderPipeline, + dot_pipeline: wgpu::RenderPipeline, glyph_vertex_buffer: wgpu::Buffer, glyph_instance_buffer: wgpu::Buffer, glyph_instances: Vec, + marker_mode: MarkerMode, } impl BackgroundRenderResources { @@ -96,10 +111,20 @@ impl BackgroundRenderResources { }); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Pressure Matrix Shader"), - source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + label: Some("Pressure Matrix Background Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("../static/wgsl/shader.wgsl").into()), }); + // let glyph_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + // label: Some("Pressure Matrix Glyph Shader"), + // source: wgpu::ShaderSource::Wgsl(include_str!("../static/wgsl/shader.wgsl").into()), + // }); + + // let dot_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + // label: Some("Pressure Matrix Dot Shader"), + // source: wgpu::ShaderSource::Wgsl(include_str!("../static/wgsl/shader.wgsl").into()), + // }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Pressure Matrix Pipeline Layout"), bind_group_layouts: &[Some(&uniform_bind_group_layout)], @@ -110,6 +135,8 @@ impl BackgroundRenderResources { create_background_pipeline(device, target_format, &shader, &pipeline_layout); let glyph_pipeline = create_glyph_pipeline(device, target_format, &shader, &pipeline_layout); + let dot_pipeline = + create_dot_pipeline(device, target_format, &shader, &pipeline_layout); let glyph_vertices = [ GlyphVertex { @@ -144,16 +171,26 @@ impl BackgroundRenderResources { uniform_bind_group, background_pipeline, glyph_pipeline, + dot_pipeline, glyph_vertex_buffer, glyph_instance_buffer, glyph_instances, + marker_mode: MarkerMode::Number, } } - fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, pressure: &PressureFrame) { + fn prepare( + &mut self, + queue: &wgpu::Queue, + width: f32, + height: f32, + pressure: &PressureFrame, + marker_mode: MarkerMode, + ) { let aspect = width / height.max(1.0); self.uniform = MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout)); + self.marker_mode = marker_mode; queue.write_buffer( &self.uniform_buffer, 0, @@ -180,7 +217,13 @@ impl BackgroundRenderResources { render_pass.set_pipeline(&self.background_pipeline); render_pass.draw(0..3, 0..1); - render_pass.set_pipeline(&self.glyph_pipeline); + // let marker_pipeline = match self.marker_mode { + // MarkerMode::Number => &self.glyph_pipeline, + // MarkerMode::Dot => &self.dot_pipeline, + // }; + let marker_pipeline = &self.dot_pipeline; + + render_pass.set_pipeline(marker_pipeline); render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..)); render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..)); render_pass.draw(0..6, 0..self.glyph_instances.len() as u32); @@ -253,6 +296,39 @@ fn create_glyph_pipeline( }) } +fn create_dot_pipeline( + device: &wgpu::Device, + target_format: &wgpu::TextureFormat, + shader: &wgpu::ShaderModule, + layout: &wgpu::PipelineLayout, +) -> wgpu::RenderPipeline { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Pressure Matrix Dot Pipeline"), + layout: Some(layout), + vertex: wgpu::VertexState { + module: shader, + entry_point: Some("vs_dot"), + compilation_options: Default::default(), + buffers: &[GlyphVertex::desc(), GlyphInstance::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: shader, + entry_point: Some("fs_dot"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: *target_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }) +} + fn build_glyph_instances( rows: u32, cols: u32, diff --git a/src/theme.rs b/src/theme.rs index 2627c67..dc0be4b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -16,24 +16,24 @@ pub struct AppTheme { } pub const ONE_DARK_PRO: AppTheme = AppTheme { - bg: egui::Color32::from_rgb(40, 44, 52), // #282C34 editor background - panel: egui::Color32::from_rgb(33, 37, 43), // #21252B sidebar/darker + bg: egui::Color32::from_rgb(40, 44, 52), // #282C34 editor background + panel: egui::Color32::from_rgb(33, 37, 43), // #21252B sidebar/darker panel_strong: egui::Color32::from_rgb(44, 49, 58), // #2C313A slightly lighter - panel_deep: egui::Color32::from_rgb(27, 31, 39), // #1B1F27 darkest - border: egui::Color32::from_rgb(62, 68, 81), // #3E4451 border - border_soft: egui::Color32::from_rgb(44, 49, 58), // #2C313A soft border - text: egui::Color32::from_rgb(171, 178, 191), // #ABB2BF default text - text_dim: egui::Color32::from_rgb(92, 99, 112), // #5C6370 dimmed text - accent: egui::Color32::from_rgb(198, 120, 221), // #C678DD purple (signature) + panel_deep: egui::Color32::from_rgb(27, 31, 39), // #1B1F27 darkest + border: egui::Color32::from_rgb(62, 68, 81), // #3E4451 border + border_soft: egui::Color32::from_rgb(44, 49, 58), // #2C313A soft border + text: egui::Color32::from_rgb(171, 178, 191), // #ABB2BF default text + text_dim: egui::Color32::from_rgb(92, 99, 112), // #5C6370 dimmed text + accent: egui::Color32::from_rgb(198, 120, 221), // #C678DD purple (signature) accent_hot: egui::Color32::from_rgb(229, 192, 123), // #E5C07B yellow/gold radius: 3, }; // Additional One Dark Pro accent colors (standalone constants) -pub const ACCENT_GREEN: egui::Color32 = egui::Color32::from_rgb(152, 195, 121); // #98C379 -pub const ACCENT_RED: egui::Color32 = egui::Color32::from_rgb(224, 108, 117); // #E06C75 -pub const ACCENT_BLUE: egui::Color32 = egui::Color32::from_rgb(97, 175, 239); // #61AFEF -pub const ACCENT_CYAN: egui::Color32 = egui::Color32::from_rgb(86, 182, 194); // #56B6C2 +pub const ACCENT_GREEN: egui::Color32 = egui::Color32::from_rgb(152, 195, 121); // #98C379 +pub const ACCENT_RED: egui::Color32 = egui::Color32::from_rgb(224, 108, 117); // #E06C75 +pub const ACCENT_BLUE: egui::Color32 = egui::Color32::from_rgb(97, 175, 239); // #61AFEF +pub const ACCENT_CYAN: egui::Color32 = egui::Color32::from_rgb(86, 182, 194); // #56B6C2 pub const ACCENT_ORANGE: egui::Color32 = egui::Color32::from_rgb(209, 154, 102); // #D19A66 pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) { diff --git a/src/ui.rs b/src/ui.rs index 1f2d03a..9365451 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,7 +3,10 @@ use eframe::egui; use crate::{ connection::{ConnectionManager, ConnectionState}, recording::Recorder, - theme::{ONE_DARK_PRO, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, accent_text, dim_text, group_frame, panel_frame, tag_button}, + theme::{ + ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, ONE_DARK_PRO, accent_text, dim_text, + group_frame, panel_frame, tag_button, + }, utils::serial_enum, }; @@ -977,12 +980,7 @@ fn paint_integrated_center_panel( // ── Signal Chart (sparkline) ─────────────────────────────────────────── /// Draw a mini sparkline chart with current/min/max labels. -pub fn draw_signal_chart( - ui: &mut egui::Ui, - values: &[f32], - label: &str, - color: egui::Color32, -) { +pub fn draw_signal_chart(ui: &mut egui::Ui, values: &[f32], label: &str, color: egui::Color32) { let chart_height = 48.0; let chart_width = ui.available_width().min(200.0); @@ -991,16 +989,20 @@ pub fn draw_signal_chart( ui.colored_label(color, label); if let (Some(&last), Some(&min), Some(&max)) = ( values.last(), - values.iter().min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)), - values.iter().max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)), + values + .iter() + .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)), + values + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)), ) { ui.colored_label(ONE_DARK_PRO.text, format!("{:.0}", last)); ui.colored_label(ONE_DARK_PRO.text_dim, format!("↓{:.0} ↑{:.0}", min, max)); } }); - let (rect, _response) = ui - .allocate_exact_size(egui::vec2(chart_width, chart_height), egui::Sense::hover()); + let (rect, _response) = + ui.allocate_exact_size(egui::vec2(chart_width, chart_height), egui::Sense::hover()); if values.len() < 2 { return; @@ -1038,11 +1040,7 @@ pub fn draw_signal_chart( // ── Recording Toolbar ────────────────────────────────────────────────── /// Draw recording controls inside the connect panel. -pub fn draw_recording_toolbar( - ui: &mut egui::Ui, - recorder: &Recorder, - export_path: &mut String, -) { +pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_path: &mut String) { let is_recording = recorder.is_recording(); let frame_count = recorder.frame_count(); let duration_ms = recorder.duration_ms(); @@ -1203,9 +1201,15 @@ pub fn draw_matrix_config_panel( ui.colored_label(dim_text(), "矩阵尺寸"); ui.add_space(8.0); ui.colored_label(ONE_DARK_PRO.text, "行"); - ui.add_sized(egui::vec2(56.0, 20.0), egui::DragValue::new(&mut config.rows).range(1..=128)); + ui.add_sized( + egui::vec2(56.0, 20.0), + egui::DragValue::new(&mut config.rows).range(1..=128), + ); ui.colored_label(ONE_DARK_PRO.text, "列"); - ui.add_sized(egui::vec2(56.0, 20.0), egui::DragValue::new(&mut config.cols).range(1..=128)); + ui.add_sized( + egui::vec2(56.0, 20.0), + egui::DragValue::new(&mut config.cols).range(1..=128), + ); }); ui.add_space(4.0); @@ -1235,12 +1239,16 @@ pub fn draw_matrix_config_panel( ui.colored_label(ONE_DARK_PRO.text, "最小"); ui.add_sized( egui::vec2(72.0, 20.0), - egui::DragValue::new(&mut config.color_min).range(0.0..=10000.0).speed(100.0), + egui::DragValue::new(&mut config.color_min) + .range(0.0..=10000.0) + .speed(100.0), ); ui.colored_label(ONE_DARK_PRO.text, "最大"); ui.add_sized( egui::vec2(72.0, 20.0), - egui::DragValue::new(&mut config.color_max).range(1.0..=10000.0).speed(100.0), + egui::DragValue::new(&mut config.color_max) + .range(1.0..=10000.0) + .speed(100.0), ); }); diff --git a/src/shader.wgsl b/static/wgsl/shader.wgsl old mode 100755 new mode 100644 similarity index 83% rename from src/shader.wgsl rename to static/wgsl/shader.wgsl index 1ecbf14..180f4e5 --- a/src/shader.wgsl +++ b/static/wgsl/shader.wgsl @@ -8,50 +8,10 @@ struct MatrixUniform { @group(0) @binding(0) var u: MatrixUniform; -struct BackgroundVertexOutput { - @builtin(position) clip_position: vec4f, -} - -struct GlyphVertexInput { - @location(0) local: vec2f, -} - -struct GlyphInstanceInput { - @location(1) world_position: vec4f, - @location(2) style: vec4f, -} - -struct GlyphVertexOutput { - @builtin(position) clip_position: vec4f, - @location(0) local: vec2f, - @location(1) intensity: f32, - @location(2) display_value: f32, -} - fn saturate(value: f32) -> f32 { return clamp(value, 0.0, 1.0); } -fn capsule_distance(point: vec2f, start: vec2f, end: vec2f) -> f32 { - let segment = end - start; - let h = saturate(dot(point - start, segment) / dot(segment, segment)); - return length(point - start - segment * h); -} - -fn rotate2(point: vec2f, angle: f32) -> vec2f { - let s = sin(angle); - let c = cos(angle); - return vec2f(point.x * c - point.y * s, point.x * s + point.y * c); -} - -fn rect_alpha(point: vec2f, center: vec2f, half_size: vec2f) -> f32 { - let delta = abs(point - center) - half_size; - let outside = length(max(delta, vec2f(0.0, 0.0))); - let inside = min(max(delta.x, delta.y), 0.0); - let dist = outside + inside; - return 1.0 - smoothstep(0.015, 0.045, dist); -} - fn range_stop_color(index: u32) -> vec3f { switch index { case 0u: { @@ -86,6 +46,90 @@ fn sample_range_color(value: f32) -> vec3f { return mix(range_stop_color(2u), range_stop_color(3u), local); } + +// background +struct BackgroundVertexOutput { + @builtin(position) clip_position: vec4f, +} + +@vertex +fn vs_background(@builtin(vertex_index) vertex_index: u32) -> BackgroundVertexOutput { + let positions = array( + vec2f(-1.0, -3.0), + vec2f(3.0, 1.0), + vec2f(-1.0, 1.0), + ); + + var out: BackgroundVertexOutput; + out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0); + return out; +} + +@fragment +fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f { + let pixel = frag_coord.xy; + let viewport = u.viewport.xy; + let uv = pixel / max(viewport, vec2f(1.0, 1.0)); + var color = mix(vec3f(0.112, 0.126, 0.160), vec3f(0.145, 0.160, 0.198), 1.0 - uv.y); + let vignette = smoothstep(0.18, 0.92, length((uv - vec2f(0.52, 0.48)) * vec2f(viewport.x / viewport.y, 1.0))); + color *= 1.0 - vignette * 0.30; + color += vec3f(0.035, 0.070, 0.090) * (1.0 - smoothstep(0.0, 0.9, abs(uv.y - 0.52))); + + let track_width = clamp(viewport.x * 0.42, 260.0, 560.0); + let track_height = 12.0; + let track_center = vec2f(viewport.x * 0.5, viewport.y - 38.0); + let local = pixel - track_center; + let half_size = vec2f(track_width * 0.5, track_height * 0.5); + let inside_track = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y); + let border = step(abs(local.x), half_size.x + 1.0) * step(abs(local.y), half_size.y + 1.0) - inside_track; + let t = saturate((local.x + half_size.x) / track_width); + + if (inside_track > 0.5) { + let shade = mix(0.72, 1.0, 1.0 - saturate((local.y + half_size.y) / track_height)); + color = mix(color, sample_range_color(t) * shade, 0.96); + } + + color = max(color, vec3f(0.34, 0.41, 0.46) * border); + + let tick_area = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y + 8.0); + if (tick_area > 0.5) { + let ticks = array(0.0, 0.33, 0.66, 1.0); + for (var index = 0u; index < 4u; index = index + 1u) { + let tick_x = (ticks[index] - 0.5) * track_width; + let tick = step(abs(local.x - tick_x), 1.0) * step(abs(local.y), half_size.y + 7.0); + color = max(color, vec3f(0.78, 0.84, 0.90) * tick); + } + } + + return vec4f(color, 1.0); +} + + +// glyph +struct GlyphVertexInput { + @location(0) local: vec2f, +} + +struct GlyphInstanceInput { + @location(1) world_position: vec4f, + @location(2) style: vec4f, +} + +struct GlyphVertexOutput { + @builtin(position) clip_position: vec4f, + @location(0) local: vec2f, + @location(1) intensity: f32, + @location(2) display_value: f32, +} + +fn rect_alpha(point: vec2f, center: vec2f, half_size: vec2f) -> f32 { + let delta = abs(point - center) - half_size; + let outside = length(max(delta, vec2f(0.0, 0.0))); + let inside = min(max(delta.x, delta.y), 0.0); + let dist = outside + inside; + return 1.0 - smoothstep(0.015, 0.045, dist); +} + fn digit_segment_on(digit: u32, segment: u32) -> bool { switch digit { case 0u: { return segment != 6u; } @@ -183,58 +227,6 @@ fn number_alpha(local: vec2f, display_value: f32) -> f32 { return alpha; } -@vertex -fn vs_background(@builtin(vertex_index) vertex_index: u32) -> BackgroundVertexOutput { - let positions = array( - vec2f(-1.0, -3.0), - vec2f(3.0, 1.0), - vec2f(-1.0, 1.0), - ); - - var out: BackgroundVertexOutput; - out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0); - return out; -} - -@fragment -fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f { - let pixel = frag_coord.xy; - let viewport = u.viewport.xy; - let uv = pixel / max(viewport, vec2f(1.0, 1.0)); - var color = mix(vec3f(0.112, 0.126, 0.160), vec3f(0.145, 0.160, 0.198), 1.0 - uv.y); - let vignette = smoothstep(0.18, 0.92, length((uv - vec2f(0.52, 0.48)) * vec2f(viewport.x / viewport.y, 1.0))); - color *= 1.0 - vignette * 0.30; - color += vec3f(0.035, 0.070, 0.090) * (1.0 - smoothstep(0.0, 0.9, abs(uv.y - 0.52))); - - let track_width = clamp(viewport.x * 0.42, 260.0, 560.0); - let track_height = 12.0; - let track_center = vec2f(viewport.x * 0.5, viewport.y - 38.0); - let local = pixel - track_center; - let half_size = vec2f(track_width * 0.5, track_height * 0.5); - let inside_track = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y); - let border = step(abs(local.x), half_size.x + 1.0) * step(abs(local.y), half_size.y + 1.0) - inside_track; - let t = saturate((local.x + half_size.x) / track_width); - - if (inside_track > 0.5) { - let shade = mix(0.72, 1.0, 1.0 - saturate((local.y + half_size.y) / track_height)); - color = mix(color, sample_range_color(t) * shade, 0.96); - } - - color = max(color, vec3f(0.34, 0.41, 0.46) * border); - - let tick_area = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y + 8.0); - if (tick_area > 0.5) { - let ticks = array(0.0, 0.33, 0.66, 1.0); - for (var index = 0u; index < 4u; index = index + 1u) { - let tick_x = (ticks[index] - 0.5) * track_width; - let tick = step(abs(local.x - tick_x), 1.0) * step(abs(local.y), half_size.y + 7.0); - color = max(color, vec3f(0.78, 0.84, 0.90) * tick); - } - } - - return vec4f(color, 1.0); -} - @vertex fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput { let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0); @@ -256,3 +248,61 @@ fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f { let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity)); return vec4f(color, alpha); } + + +// dot +struct DotVertexInput { + @location(0) local: vec2f, +} + +struct DotInstanceInput { + @location(1) world_position: vec4f, + @location(2) style: vec4f +} + +struct DotVertexOutput { + @builtin(position) clip_position: vec4f, + @location(0) local: vec2f, + @location(1) intensity: f32, +} + +fn circle_alpha(local: vec2f, radius: f32, softness: f32) -> f32 { + let dist = length(local); + return 1.0 - smoothstep(radius, radius + softness, dist); +} + +@vertex +fn vs_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexOutput { + let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0); + let intensity = saturate(instance.style.x); + let shaped = pow(intensity, 0.9); + let pixel_size = u.glyph.x * mix(0.72, 1.85, shaped); + let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0; + var out: DotVertexOutput; + out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w); + out.local = vertex.local; + out.intensity = intensity; + return out; +} + +@fragment +fn fs_dot(in: DotVertexOutput) -> @location(0) vec4f { + let intensity = saturate(in.intensity); + let base_color = sample_range_color(intensity); + + let core = circle_alpha(in.local, 0.42, 0.055); + + let glow = circle_alpha(in.local, 0.78, 0.18) * intensity * 0.45; + + let highlight_pos = in.local - vec2f(-0.18, 0.20); + + let highlight = circle_alpha(highlight_pos, 0.16, 0.08) * 0.45; + + let brightness = mix(0.82, 1.22, intensity); + + let color = base_color * brightness + vec3f(1.0, 1.0, 1.0) * highlight; + + let alpha = max(core, glow); + + return vec4f(color, alpha); +} \ No newline at end of file