//! Ring light glow rendering. //! //! The ring is a thick stroke centered on a single rounded rectangle SDF. //! This guarantees uniform width everywhere, including at corners. //! Glow bleeds outward and inward from the stroke edges. use tiny_skia::Pixmap; // The rounded rect centerline is inset this far from the screen edge. // The visible ring extends RING_HALF_WIDTH on either side of this line. const CENTERLINE_INSET: f32 = 80.0; // Half the stroke width. Total ring thickness = 2 * RING_HALF_WIDTH. const RING_HALF_WIDTH: f32 = 30.0; // Corner radius of the centerline rounded rect. const CORNER_RADIUS: f32 = 30.0; // Soft glow spread outward and inward from the ring edges. const GLOW_SPREAD: f32 = 50.0; const RING_ALPHA: f32 = 1.00; // Cursor avoidance. const CURSOR_AVOID_RADIUS: f32 = 120.0; const CURSOR_AVOID_FALLOFF: f32 = 60.0; /// Render the ring light glow into `pixmap`, avoiding the cursor at `(cx, cy)`. pub fn render_ring_light(pixmap: &mut Pixmap, cx: f64, cy: f64) { let w = pixmap.width() as f32; let h = pixmap.height() as f32; let cx = cx as f32; let cy = cy as f32; let wi = pixmap.width() as usize; let hi = pixmap.height() as usize; let data = pixmap.data_mut(); data.fill(0); let screen_cx = w / 2.0; let screen_cy = h / 2.0; // Single rounded rect centerline. let hw = w / 2.0 - CENTERLINE_INSET; let hh = h / 2.0 - CENTERLINE_INSET; // The affected band: from (CENTERLINE_INSET - RING_HALF_WIDTH - GLOW_SPREAD) // to (CENTERLINE_INSET + RING_HALF_WIDTH + GLOW_SPREAD) from each screen edge. let band_start = (CENTERLINE_INSET - RING_HALF_WIDTH - GLOW_SPREAD) .max(0.0) .floor() as usize; let band_end = (CENTERLINE_INSET + RING_HALF_WIDTH + GLOW_SPREAD).ceil() as usize + 1; // Top band for py in band_start..band_end.min(hi) { for px in 0..wi { let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy); write_pixel(data, py * wi + px, a); } } // Bottom band let bottom_start = hi.saturating_sub(band_end); let bottom_end = hi.saturating_sub(band_start); for py in bottom_start..bottom_end.min(hi) { if py < band_end { continue; } for px in 0..wi { let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy); write_pixel(data, py * wi + px, a); } } // Left band (between top and bottom) let vert_start = band_end.min(hi); let vert_end = hi.saturating_sub(band_end); for py in vert_start..vert_end { for px in band_start..band_end.min(wi) { let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy); write_pixel(data, py * wi + px, a); } } // Right band (between top and bottom) let right_start = wi.saturating_sub(band_end); let right_end = wi.saturating_sub(band_start); for py in vert_start..vert_end { for px in right_start..right_end.min(wi) { if px < band_end { continue; } let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy); write_pixel(data, py * wi + px, a); } } } /// Convert a tiny-skia `Pixmap` (premultiplied RGBA) to Wayland's ARGB8888 /// format (premultiplied BGRA in memory on little-endian). pub fn pixmap_to_argb8888(pixmap: &Pixmap, out: &mut [u8]) { let src = pixmap.data(); assert_eq!(out.len(), src.len()); for (dst, src) in out.chunks_exact_mut(4).zip(src.chunks_exact(4)) { dst[0] = src[2]; // B dst[1] = src[1]; // G dst[2] = src[0]; // R dst[3] = src[3]; // A } } // ---- Private helpers -------------------------------------------------------- /// Compute glow alpha for a single pixel using a single rounded rect SDF. /// /// The ring is all pixels where |sdf| <= RING_HALF_WIDTH (a thick stroke). /// Glow fades from the ring edges outward/inward over GLOW_SPREAD pixels. #[allow(clippy::too_many_arguments)] fn pixel_alpha( px: f32, py: f32, scx: f32, scy: f32, hw: f32, hh: f32, cursor_x: f32, cursor_y: f32, ) -> u8 { // Signed distance to the rounded rect centerline. let sd = rounded_rect_sdf(px, py, scx, scy, hw, hh, CORNER_RADIUS); let abs_sd = sd.abs(); let glow = if abs_sd <= RING_HALF_WIDTH { // Inside the ring stroke — full brightness. RING_ALPHA } else { // Distance from the nearest ring edge. let dist = abs_sd - RING_HALF_WIDTH; if dist >= GLOW_SPREAD { return 0; } let t = dist / GLOW_SPREAD; smoothstep(1.0 - t) * RING_ALPHA }; // Cursor avoidance. let dx = px - cursor_x; let dy = py - cursor_y; let cursor_dist = (dx * dx + dy * dy).sqrt(); let cursor_factor = smoothstep(((cursor_dist - CURSOR_AVOID_RADIUS) / CURSOR_AVOID_FALLOFF).clamp(0.0, 1.0)); let alpha = glow * cursor_factor; (alpha * 255.0) as u8 } /// Signed distance field for a rounded rectangle. /// /// Returns negative inside, positive outside, zero on the boundary. fn rounded_rect_sdf(px: f32, py: f32, cx: f32, cy: f32, hw: f32, hh: f32, r: f32) -> f32 { let dx = (px - cx).abs() - (hw - r); let dy = (py - cy).abs() - (hh - r); let outside = dx.max(0.0); let outside_y = dy.max(0.0); let outside_len = (outside * outside + outside_y * outside_y).sqrt(); let inside = dx.max(dy).min(0.0); outside_len + inside - r } /// Write a premultiplied white pixel with the given alpha. fn write_pixel(data: &mut [u8], idx: usize, alpha: u8) { if alpha == 0 { return; } let offset = idx * 4; data[offset] = alpha; data[offset + 1] = alpha; data[offset + 2] = alpha; data[offset + 3] = alpha; } /// Hermite smoothstep: 3t^2 - 2t^3 fn smoothstep(t: f32) -> f32 { let t = t.clamp(0.0, 1.0); t * t * (3.0 - 2.0 * t) }