feat: initial project with CPU-rendered ring light overlay
Wayland layer-shell overlay using tiny-skia for rendering a glowing rounded rectangle ring. Includes PID-based lock file toggle via SIGUSR1, keyboard capture (Escape to exit), and pointer tracking for cursor avoidance. Configurable ring geometry and glow parameters.
This commit is contained in:
7 files changed
+1484
No files matched your search
+183
@@ -0,0 +1,183 @@
|
||||
//! 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)
|
||||
}
|
||||
Reference in new issue
Block a user