7603975062
- Extract all apply_* effect methods from screenshot.rs into dedicated
src/effects.rs (blur, vignette, pixelate, swirl) — screenshot.rs
- Rewrite apply_blur: replace broken fastblur::gaussian_blur (horizontal-
only box blur, backbuf dropped) with correct inline two-pass sliding-
window box blur operating directly on Cairo surface bytes — ~10×
faster, no ImageBuffer round-trip, no fastblur dependency
- Remove melting effect entirely (seems to be hard to implement
properely, tried multiple itterations)
- Drop unused dependencies: fastblur, rand
- Add clap_complete dependency + --completions {bash,fish,zsh} flag,
with CompletionShell enum in config.rs
- Fix show_* flags defaulting to true: remove default_value_t=true from
show_media, show_battery, show_network, show_bluetooth, show_album_art,
show_caps_lock_text — all now opt-in (matching user expectation)
- Convert peek toggle to hold: toggle_peek() → set_peek_held(held: bool),
Press calls set_peek_held(true), new Release handler iterates all
surfaces and calls set_peek_held(false), matching Ctrl-hold behavior
- Fix cursor rendering: increase font size 11→14 for peeked chars,
handle cursor_position == 0 (prevent negative t wrapping to far ring
end)
- Fix peek hit-test: use shape-aware point_in_shape() instead of simple
circle pill/square/diamond/hexagon now detect clicks correctly within
their area
- Remove per-frame DEBUG logs: PEEK/DOT mode in lock.rs,
set_password_display/peek_password in render/mod.rs
- Update README: embed rustlock-effects.webp demo, remove melting
references, correct show_* defaults and options table
286 lines
9.7 KiB
Rust
286 lines
9.7 KiB
Rust
//! Visual effects for lock screen backgrounds.
|
|
//!
|
|
//! Each effect takes a `&mut ImageSurface` and processes it in-place.
|
|
//! Effects are applied in order: blur → vignette → pixelate → swirl.
|
|
|
|
use anyhow::{Context, Result};
|
|
use cairo::ImageSurface;
|
|
|
|
/// Apply a swirl effect (radial rotation around the image centre).
|
|
pub fn apply_swirl(surface: &mut ImageSurface, angle: f32) -> Result<()> {
|
|
let width = surface.width();
|
|
let height = surface.height();
|
|
let center_x = width as f32 / 2.0;
|
|
let center_y = height as f32 / 2.0;
|
|
let radius = center_x.min(center_y);
|
|
|
|
let stride = surface.stride() as usize;
|
|
let mut data = vec![0u8; stride * height as usize];
|
|
surface
|
|
.with_data(|src| data.copy_from_slice(src))
|
|
.context("swirl: failed to read surface data")?;
|
|
let original = data.clone();
|
|
|
|
for y in 0..height {
|
|
for x in 0..width {
|
|
let dx = x as f32 - center_x;
|
|
let dy = y as f32 - center_y;
|
|
let d = (dx * dx + dy * dy).sqrt();
|
|
|
|
if d < radius {
|
|
let percent = (radius - d) / radius;
|
|
let theta = percent * percent * angle;
|
|
let s = theta.sin();
|
|
let c = theta.cos();
|
|
|
|
let nx = (c * dx - s * dy + center_x) as i32;
|
|
let ny = (s * dx + c * dy + center_y) as i32;
|
|
|
|
if nx >= 0 && nx < width && ny >= 0 && ny < height {
|
|
let src_idx = (ny as usize * stride) + (nx as usize * 4);
|
|
let dst_idx = (y as usize * stride) + (x as usize * 4);
|
|
data[dst_idx..dst_idx + 4].copy_from_slice(&original[src_idx..src_idx + 4]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut surface_data = surface
|
|
.data()
|
|
.context("swirl: failed to write surface data")?;
|
|
surface_data.copy_from_slice(&data);
|
|
Ok(())
|
|
}
|
|
|
|
/// Pixelate the surface.
|
|
pub fn apply_pixelate(surface: &mut ImageSurface, pixel_size: u32) -> Result<()> {
|
|
if pixel_size <= 1 {
|
|
return Ok(());
|
|
}
|
|
|
|
let width = surface.width();
|
|
let height = surface.height();
|
|
let stride = surface.stride() as usize;
|
|
let mut data = vec![0u8; stride * height as usize];
|
|
surface
|
|
.with_data(|src| data.copy_from_slice(src))
|
|
.context("pixelate: failed to read surface data")?;
|
|
|
|
for y in (0..height).step_by(pixel_size as usize) {
|
|
for x in (0..width).step_by(pixel_size as usize) {
|
|
let mut r = 0u32;
|
|
let mut g = 0u32;
|
|
let mut b = 0u32;
|
|
let mut count = 0u32;
|
|
|
|
// Average pixels in the block
|
|
for py in 0..pixel_size {
|
|
for px in 0..pixel_size {
|
|
let cur_x = x + px as i32;
|
|
let cur_y = y + py as i32;
|
|
if cur_x < width && cur_y < height {
|
|
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
|
|
r += data[index] as u32;
|
|
g += data[index + 1] as u32;
|
|
b += data[index + 2] as u32;
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if count > 0 {
|
|
let r = r.checked_div(count).unwrap_or(0) as u8;
|
|
let g = g.checked_div(count).unwrap_or(0) as u8;
|
|
let b = b.checked_div(count).unwrap_or(0) as u8;
|
|
|
|
// Fill the block
|
|
for py in 0..pixel_size {
|
|
for px in 0..pixel_size {
|
|
let cur_x = x + px as i32;
|
|
let cur_y = y + py as i32;
|
|
if cur_x < width && cur_y < height {
|
|
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
|
|
data[index] = r;
|
|
data[index + 1] = g;
|
|
data[index + 2] = b;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut surface_data = surface
|
|
.data()
|
|
.context("pixelate: failed to write surface data")?;
|
|
surface_data.copy_from_slice(&data);
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply a box blur effect (fast two-pass sliding-window implementation).
|
|
/// Uses multiply-shift to avoid slow integer division in the hot loop.
|
|
pub fn apply_blur(surface: &mut ImageSurface, radius: u32, times: u32) -> Result<()> {
|
|
if radius == 0 || times == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
let width = surface.width() as usize;
|
|
let height = surface.height() as usize;
|
|
let stride = surface.stride() as usize;
|
|
let r = radius as usize;
|
|
|
|
let mut data = vec![0u8; stride * height];
|
|
surface
|
|
.with_data(|src| data.copy_from_slice(src))
|
|
.context("blur: failed to read surface data")?;
|
|
|
|
let mut scratch = vec![0u8; stride * height];
|
|
|
|
// Precompute ceil(2^32 / c) for every possible window size c.
|
|
let max_count = (2 * r + 1).min(width.max(height));
|
|
let factor: Vec<u32> = (0..=max_count)
|
|
.map(|c| {
|
|
if c == 0 {
|
|
0
|
|
} else {
|
|
(1u64 << 32).div_ceil(c as u64) as u32
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
#[inline(always)]
|
|
fn div_mul(n: u32, factor: u32) -> u8 {
|
|
((n as u64 * factor as u64) >> 32) as u8
|
|
}
|
|
|
|
for _ in 0..times {
|
|
// Horizontal box blur: data -> scratch
|
|
for y in 0..height {
|
|
let row = y * stride;
|
|
let init_end = r.min(width - 1);
|
|
let mut b_acc = 0u32;
|
|
let mut g_acc = 0u32;
|
|
let mut r_acc = 0u32;
|
|
for x in 0..=init_end {
|
|
let px = row + x * 4;
|
|
b_acc += data[px] as u32;
|
|
g_acc += data[px + 1] as u32;
|
|
r_acc += data[px + 2] as u32;
|
|
}
|
|
let mut count = (init_end + 1) as u32;
|
|
|
|
for x in 0..width {
|
|
let dst = row + x * 4;
|
|
let f = factor[count as usize];
|
|
scratch[dst] = div_mul(b_acc, f);
|
|
scratch[dst + 1] = div_mul(g_acc, f);
|
|
scratch[dst + 2] = div_mul(r_acc, f);
|
|
scratch[dst + 3] = data[dst + 3];
|
|
|
|
if x >= r {
|
|
let old = row + (x - r) * 4;
|
|
b_acc -= data[old] as u32;
|
|
g_acc -= data[old + 1] as u32;
|
|
r_acc -= data[old + 2] as u32;
|
|
count -= 1;
|
|
}
|
|
if x + r + 1 < width {
|
|
let new = row + (x + r + 1) * 4;
|
|
b_acc += data[new] as u32;
|
|
g_acc += data[new + 1] as u32;
|
|
r_acc += data[new + 2] as u32;
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vertical box blur: scratch -> data
|
|
let init_end = r.min(height - 1);
|
|
let mut b_acc = vec![0u32; width];
|
|
let mut g_acc = vec![0u32; width];
|
|
let mut r_acc = vec![0u32; width];
|
|
for y in 0..=init_end {
|
|
let row = y * stride;
|
|
for x in 0..width {
|
|
let px = row + x * 4;
|
|
b_acc[x] += scratch[px] as u32;
|
|
g_acc[x] += scratch[px + 1] as u32;
|
|
r_acc[x] += scratch[px + 2] as u32;
|
|
}
|
|
}
|
|
let mut count = (init_end + 1) as u32;
|
|
|
|
for y in 0..height {
|
|
let f = factor[count as usize];
|
|
let dst_row = y * stride;
|
|
for x in 0..width {
|
|
let dst = dst_row + x * 4;
|
|
data[dst] = div_mul(b_acc[x], f);
|
|
data[dst + 1] = div_mul(g_acc[x], f);
|
|
data[dst + 2] = div_mul(r_acc[x], f);
|
|
}
|
|
|
|
if y >= r {
|
|
let old_row = (y - r) * stride;
|
|
for x in 0..width {
|
|
let px = old_row + x * 4;
|
|
b_acc[x] -= scratch[px] as u32;
|
|
g_acc[x] -= scratch[px + 1] as u32;
|
|
r_acc[x] -= scratch[px + 2] as u32;
|
|
}
|
|
count -= 1;
|
|
}
|
|
if y + r + 1 < height {
|
|
let new_row = (y + r + 1) * stride;
|
|
for x in 0..width {
|
|
let px = new_row + x * 4;
|
|
b_acc[x] += scratch[px] as u32;
|
|
g_acc[x] += scratch[px + 1] as u32;
|
|
r_acc[x] += scratch[px + 2] as u32;
|
|
}
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut surface_data = surface.data()?;
|
|
surface_data.copy_from_slice(&data);
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply a vignette effect (darken edges).
|
|
pub fn apply_vignette(surface: &mut ImageSurface, base: f32, factor: f32) -> Result<()> {
|
|
let width = surface.width();
|
|
let height = surface.height();
|
|
let center_x = width as f32 / 2.0;
|
|
let center_y = height as f32 / 2.0;
|
|
let max_distance = (center_x * center_x + center_y * center_y).sqrt();
|
|
|
|
let stride = surface.stride() as usize;
|
|
let mut data = vec![0u8; stride * height as usize];
|
|
surface
|
|
.with_data(|src| data.copy_from_slice(src))
|
|
.context("vignette: failed to read surface data")?;
|
|
|
|
for y in 0..height {
|
|
for x in 0..width {
|
|
let dx = x as f32 - center_x;
|
|
let dy = y as f32 - center_y;
|
|
let distance = (dx * dx + dy * dy).sqrt();
|
|
let vignette_factor = base + (1.0 - base) * (distance / max_distance).powf(factor);
|
|
|
|
let index = (y as usize * stride) + (x as usize * 4);
|
|
for i in 0..3 {
|
|
let value = data[index + i] as f32 * vignette_factor;
|
|
data[index + i] = value.clamp(0.0, 255.0) as u8;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut surface_data = surface
|
|
.data()
|
|
.context("vignette: failed to write surface data")?;
|
|
surface_data.copy_from_slice(&data);
|
|
Ok(())
|
|
}
|