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.
184 lines
5.9 KiB
Rust
184 lines
5.9 KiB
Rust
//! 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)
|
|
}
|