refactor: changed effects, add shell completions
- 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
@@ -14,10 +14,16 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/deps
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Set version
|
||||
id: version
|
||||
run: |
|
||||
@@ -26,10 +32,19 @@ jobs:
|
||||
VERSION=$(grep -m1 '^version =' Cargo.toml | cut -d'"' -f2)
|
||||
echo "VERSION=${VERSION}-nightly.$DATE.$SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build release binary
|
||||
run: cargo build --release
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rustlock-${{ steps.version.outputs.VERSION }}
|
||||
path: target/release/rustlock
|
||||
|
||||
- name: Create nightly release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: nightly-${{ steps.version.outputs.VERSION }}
|
||||
tag_name: ${{ steps.version.outputs.VERSION }}
|
||||
name: Nightly Build ${{ steps.version.outputs.VERSION }}
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
quality-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: ./.github/actions/deps
|
||||
|
||||
@@ -31,9 +31,6 @@ jobs:
|
||||
- name: Run clippy
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
- name: Run unit tests (default features)
|
||||
run: cargo test --quiet
|
||||
|
||||
- name: Run unit tests (all features)
|
||||
run: cargo test --all-features --quiet
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Determine tag
|
||||
id: tag
|
||||
@@ -29,13 +29,21 @@ jobs:
|
||||
publish-crates:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release]
|
||||
if: secrets.CARGO_REGISTRY_TOKEN != ''
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: ./.github/actions/deps
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish to crates.io
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish
|
||||
run: |
|
||||
if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
|
||||
echo "CARGO_REGISTRY_TOKEN not set — skipping publish"
|
||||
exit 0
|
||||
fi
|
||||
cargo publish
|
||||
|
||||
@@ -11,6 +11,14 @@ on:
|
||||
- 'Cargo.lock'
|
||||
- 'deny.toml'
|
||||
- '.github/workflows/security.yml'
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'deny.toml'
|
||||
- '.github/workflows/security.yml'
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -19,7 +27,7 @@ jobs:
|
||||
security-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -8,6 +8,7 @@ repos:
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: check-added-large-files
|
||||
exclude: ^assets/
|
||||
- id: check-merge-conflict
|
||||
|
||||
- repo: local
|
||||
|
||||
@@ -16,6 +16,7 @@ cairo-rs = { version = "0.20", default-features = false, features = ["png"] }
|
||||
gdk-pixbuf = { version = "0.20", default-features = false, features = ["v2_40"] }
|
||||
pangocairo = { version = "0.20" }
|
||||
clap = { version = "4.6", default-features = false, features = ["derive", "std", "help", "usage", "error-context"] }
|
||||
clap_complete = { version = "4.6", default-features = false }
|
||||
toml = { version = "1.1", default-features = false, features = ["parse", "display", "serde"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
|
||||
zeroize = "1.8"
|
||||
@@ -25,11 +26,9 @@ whoami = "1.6"
|
||||
zbus = { version = "5.14", default-features = false, features = ["tokio"] }
|
||||
mpris = "2.0"
|
||||
tokio = { version = "1.51", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] }
|
||||
rand = { version = "0.10" }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"], optional = true }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||
fastblur = "0.1"
|
||||
pam-client = "0.5"
|
||||
pam-client = "0.5"
|
||||
calloop = "0.13"
|
||||
xkbcommon = "0.7"
|
||||
calloop-wayland-source = "0.3"
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# 🔒 RustLock
|
||||
|
||||
[](https://github.com/yourusername/rustlock/blob/main/LICENSE)
|
||||
[](https://github.com/yourusername/rustlock/blob/main/LICENSE)
|
||||
[](https://github.com/yourusername/rustlock/releases)
|
||||
|
||||
A high-performance Wayland screen locker written in Rust, inspired by `swaylock-effects`.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/rustlock-effects.webp" alt="RustLock effects demo" width="800">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
@@ -13,7 +17,7 @@ A high-performance Wayland screen locker written in Rust, inspired by `swaylock-
|
||||
- 🎨 **Visual Effects**:
|
||||
- Gaussian blur (configurable radius and passes)
|
||||
- Vignette effect (configurable base and factor)
|
||||
- Pixelate, Swirl, and Melting effects
|
||||
- Pixelate and Swirl effects
|
||||
- Smooth fade-in animation
|
||||
- 🔐 **Password Indicator**:
|
||||
- Circular ring with configurable radius and thickness
|
||||
@@ -123,7 +127,6 @@ Options can be provided via command line or a configuration file at `~/.config/r
|
||||
| `--effect-vignette <B>:<F>` | — | Vignette: base : factor (e.g., `0.5:0.5`) |
|
||||
| `--effect-pixelate <S>` | — | Pixelate effect with block size in pixels |
|
||||
| `--effect-swirl <A>` | — | Swirl distortion with angle |
|
||||
| `--effect-melting <F>` | — | Melting distortion with factor |
|
||||
| **Colors** (hex `RRGGBB[AA]`) | | |
|
||||
| `--ring-color <HEX>` | `#785412` | Outer ring color |
|
||||
| `--line-color <HEX>` | `#00000000` | Separator line color |
|
||||
@@ -135,14 +138,14 @@ Options can be provided via command line or a configuration file at `~/.config/r
|
||||
| `--caps-lock-color <HEX>` | `#E5A445` | Caps lock indicator ring color |
|
||||
| `--caps-lock-text-color <HEX>` | `#E5A445` | Caps lock text color |
|
||||
| `--verifying-color <HEX>` | `#0072FF` | Verifying feedback ring color |
|
||||
| `--show-caps-lock-text` | `true` | Show "CAPS" text when caps lock is active |
|
||||
| `--show-caps-lock-text` | `false` | Show "CAPS" text when caps lock is active |
|
||||
| **Display** | | |
|
||||
| `--show-media` | `true` | Show MPRIS media player information |
|
||||
| `--show-battery` | `true` | Show battery status |
|
||||
| `--show-network` | `true` | Show WiFi SSID and signal strength |
|
||||
| `--show-bluetooth` | `true` | Show Bluetooth status |
|
||||
| `--show-album-art` | `true` | Show album art for media |
|
||||
| `--show-keyboard-layout` | `true` | Show keyboard layout indicator |
|
||||
| `--show-media` | `false` | Show MPRIS media player information |
|
||||
| `--show-battery` | `false` | Show battery status |
|
||||
| `--show-network` | `false` | Show WiFi SSID and signal strength |
|
||||
| `--show-bluetooth` | `false` | Show Bluetooth status |
|
||||
| `--show-album-art` | `false` | Show album art for media |
|
||||
| `--show-keyboard-layout` | `false` | Show keyboard layout indicator |
|
||||
| **Feedback & Timing** | | |
|
||||
| `--fade-in <SECONDS>` | `0.2` | Fade-in animation duration |
|
||||
| `--grace <SECONDS>` | `0` | Grace period — any key press unlocks within N seconds |
|
||||
|
||||
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 760 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 784 KiB |
|
After Width: | Height: | Size: 443 KiB |
@@ -45,6 +45,13 @@ impl fmt::Display for RingShape {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, clap::ValueEnum)]
|
||||
pub enum CompletionShell {
|
||||
Bash,
|
||||
Fish,
|
||||
Zsh,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone, Serialize, Deserialize)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Config {
|
||||
@@ -91,10 +98,6 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub effect_swirl: Option<f32>,
|
||||
|
||||
#[arg(long)]
|
||||
#[serde(default)]
|
||||
pub effect_melting: Option<f32>,
|
||||
|
||||
#[arg(long, default_value = "785412", value_parser = util::parse_hex_color)]
|
||||
#[serde(
|
||||
deserialize_with = "util::deserialize_hex_color",
|
||||
@@ -144,7 +147,7 @@ pub struct Config {
|
||||
)]
|
||||
pub verifying_color: (f64, f64, f64, f64),
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, default_value_t = true)]
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub show_caps_lock_text: bool,
|
||||
|
||||
#[arg(long, default_value = "00000000", value_parser = util::parse_hex_color)]
|
||||
@@ -196,19 +199,19 @@ pub struct Config {
|
||||
#[arg(long, default_value = "10000")]
|
||||
pub auth_timeout: u64,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, default_value_t = true)]
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub show_media: bool,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, default_value_t = true)]
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub show_battery: bool,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, default_value_t = true)]
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub show_network: bool,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, default_value_t = true)]
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub show_bluetooth: bool,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, default_value_t = true)]
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub show_album_art: bool,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
@@ -292,6 +295,11 @@ pub struct Config {
|
||||
/// Timeout (seconds) for system commands (poweroff, reboot, suspend)
|
||||
#[arg(long, default_value = "5")]
|
||||
pub command_timeout: u64,
|
||||
|
||||
/// Generate shell completions for the given shell
|
||||
#[arg(long, value_enum)]
|
||||
#[serde(skip)]
|
||||
pub completions: Option<CompletionShell>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -299,6 +307,26 @@ impl Config {
|
||||
use clap::CommandFactory;
|
||||
|
||||
let mut config = Config::parse();
|
||||
|
||||
// Handle --completions early (before config file merge so file can't inject it)
|
||||
if let Some(shell) = config.completions {
|
||||
use clap_complete::Shell;
|
||||
let mut cmd = Config::command();
|
||||
let name = "rustlock";
|
||||
match shell {
|
||||
CompletionShell::Bash => {
|
||||
clap_complete::generate(Shell::Bash, &mut cmd, name, &mut std::io::stdout())
|
||||
}
|
||||
CompletionShell::Fish => {
|
||||
clap_complete::generate(Shell::Fish, &mut cmd, name, &mut std::io::stdout())
|
||||
}
|
||||
CompletionShell::Zsh => {
|
||||
clap_complete::generate(Shell::Zsh, &mut cmd, name, &mut std::io::stdout())
|
||||
}
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let cmd = Config::command();
|
||||
let matches = cmd.get_matches();
|
||||
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
//! 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(())
|
||||
}
|
||||
@@ -31,7 +31,8 @@ pub struct LockedSurface {
|
||||
/// Last clock minute (unix-minute) we rendered, to detect %H:%M rollover.
|
||||
last_minute: i64,
|
||||
ctrl_held: bool,
|
||||
/// Set by clicking on the indicator ring. Persists until clicked again.
|
||||
/// True while the pointer button is held on the indicator ring (hold-to-peek).
|
||||
/// Cleared on button release, matching Ctrl-hold transient behavior.
|
||||
peek_toggled: bool,
|
||||
}
|
||||
|
||||
@@ -188,18 +189,8 @@ impl LockedSurface {
|
||||
let buf = self.input_handler.password_buffer();
|
||||
let length = self.input_handler.password_length();
|
||||
if self.ctrl_held || self.peek_toggled {
|
||||
log::debug!(
|
||||
"LockedSurface::update: PEEK mode (ctrl={}, toggled={})",
|
||||
self.ctrl_held,
|
||||
self.peek_toggled
|
||||
);
|
||||
self.renderer.peek_password(buf.as_str());
|
||||
} else {
|
||||
log::debug!(
|
||||
"LockedSurface::update: DOT mode (ctrl={}, toggled={})",
|
||||
self.ctrl_held,
|
||||
self.peek_toggled
|
||||
);
|
||||
self.renderer.set_password_display(length);
|
||||
}
|
||||
}
|
||||
@@ -316,11 +307,12 @@ impl LockedSurface {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle peek mode on/off. Called when the user clicks on the indicator
|
||||
/// ring. Persists until toggled again (unlike Ctrl-hold which is transient).
|
||||
pub fn toggle_peek(&mut self) {
|
||||
self.peek_toggled = !self.peek_toggled;
|
||||
log::debug!("toggle_peek: peek_toggled = {}", self.peek_toggled);
|
||||
/// Set peek mode on/off. Called when the user presses (held=true) or
|
||||
/// releases (held=false) the mouse button on the indicator ring. Works
|
||||
/// like Ctrl-hold — peek only lasts while the button is held.
|
||||
pub fn set_peek_held(&mut self, held: bool) {
|
||||
self.peek_toggled = held;
|
||||
log::debug!("set_peek_held: peek_toggled = {}", self.peek_toggled);
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod auth;
|
||||
mod config;
|
||||
mod effects;
|
||||
mod input;
|
||||
mod lock;
|
||||
mod render;
|
||||
@@ -704,8 +705,8 @@ impl PointerHandler for WaylandLock {
|
||||
events: &[PointerEvent],
|
||||
) {
|
||||
for event in events {
|
||||
if let PointerEventKind::Press { button, .. } = event.kind {
|
||||
if button == 0x110 {
|
||||
match event.kind {
|
||||
PointerEventKind::Press { button: 0x110, .. } => {
|
||||
let (x, y) = event.position;
|
||||
if let Ok(lm) = self.lock_manager.lock() {
|
||||
for surface in &lm.surfaces {
|
||||
@@ -728,14 +729,13 @@ impl PointerHandler for WaylandLock {
|
||||
if handled {
|
||||
return;
|
||||
}
|
||||
// Check indicator ring hit — toggle password peek
|
||||
// Check indicator ring hit — hold to peek (like Ctrl)
|
||||
let cx = surface.renderer.width as f64 / 2.0;
|
||||
let cy = surface.renderer.height as f64 / 2.0;
|
||||
let r = surface.renderer.config.indicator_radius as f64;
|
||||
let dx = x - cx;
|
||||
let dy = y - cy;
|
||||
if dx * dx + dy * dy <= r * r {
|
||||
// Need to reborrow mutably for toggle
|
||||
let shape = surface.renderer.config.ring_shape;
|
||||
if crate::render::ring_shape::point_in_shape(cx, cy, r, shape, x, y)
|
||||
{
|
||||
drop(lm);
|
||||
if let Ok(mut lm) = self.lock_manager.lock() {
|
||||
if let Some(s) = lm
|
||||
@@ -743,7 +743,7 @@ impl PointerHandler for WaylandLock {
|
||||
.iter_mut()
|
||||
.find(|s| s.matches_surface(&event.surface))
|
||||
{
|
||||
s.toggle_peek();
|
||||
s.set_peek_held(true);
|
||||
s.update();
|
||||
let _ = s.commit(&mut self.pool);
|
||||
}
|
||||
@@ -754,6 +754,22 @@ impl PointerHandler for WaylandLock {
|
||||
}
|
||||
}
|
||||
}
|
||||
PointerEventKind::Release { button: 0x110, .. } => {
|
||||
if let Ok(mut lm) = self.lock_manager.lock() {
|
||||
// Clear peek on ALL surfaces. Wayland button release events
|
||||
// can reference a different wl_surface proxy than the press
|
||||
// event (compositor-dependent pointer grab semantics), so
|
||||
// matching by surface might miss the LockedSurface that has
|
||||
// peek_toggled=true. Iterating every surface guarantees peek
|
||||
// always ends on button release — matching Ctrl-hold behavior.
|
||||
for surface in &mut lm.surfaces {
|
||||
surface.set_peek_held(false);
|
||||
surface.update();
|
||||
let _ = surface.commit(&mut self.pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +85,8 @@ impl Renderer {
|
||||
}
|
||||
|
||||
if self.peeking {
|
||||
log::debug!("draw_password_display: PEEKING text ({} chars)", count);
|
||||
// Draw each character at the same ring-perimeter positions as dots
|
||||
self.context.set_font_size(11.0);
|
||||
self.context.set_font_size(14.0);
|
||||
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||
for (i, ch) in self.password_display.chars().enumerate() {
|
||||
let t = (i as f64 / max_dots) + t_offset;
|
||||
@@ -117,9 +116,16 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor indicator (shared between peek and dot modes)
|
||||
// Cursor indicator (shared between peek and dot modes).
|
||||
if self.fade_alpha > 0.0 {
|
||||
let cursor_t = ((self.cursor_position as f64 - 0.5) / max_dots) + t_offset;
|
||||
// At position 0, place the cursor squarely at the first-dot position
|
||||
// (t_offset) instead of subtracting 0.5, which would push t negative
|
||||
// and cause perimeter_point to wrap it to the far end of the ring.
|
||||
let cursor_t = if self.cursor_position == 0 {
|
||||
t_offset
|
||||
} else {
|
||||
((self.cursor_position as f64 - 0.5) / max_dots) + t_offset
|
||||
};
|
||||
let (cx, cy) =
|
||||
ring_shape::perimeter_point(center_x, center_y, dot_radius, shape, cursor_t);
|
||||
let dx = cx - center_x;
|
||||
|
||||
@@ -21,7 +21,7 @@ macro_rules! render_try {
|
||||
mod feedback;
|
||||
mod indicator;
|
||||
mod media_bar;
|
||||
mod ring_shape;
|
||||
pub(crate) mod ring_shape;
|
||||
mod status_bar;
|
||||
|
||||
pub struct Renderer {
|
||||
@@ -128,16 +128,11 @@ impl Renderer {
|
||||
}
|
||||
|
||||
pub fn set_password_display(&mut self, length: usize) {
|
||||
log::debug!("Renderer::set_password_display(length={})", length);
|
||||
self.password_display = ".".repeat(length);
|
||||
self.peeking = false;
|
||||
}
|
||||
|
||||
pub fn peek_password(&mut self, password: &str) {
|
||||
log::debug!(
|
||||
"Renderer::peek_password(len={}, peeking=true)",
|
||||
password.len()
|
||||
);
|
||||
self.password_display = password.to_string();
|
||||
self.peeking = true;
|
||||
}
|
||||
|
||||
@@ -207,6 +207,40 @@ pub(crate) fn build_fill_path(
|
||||
build_ring_path(ctx, cx, cy, inner_r, shape);
|
||||
}
|
||||
|
||||
/// Check whether a point `(px, py)` lies inside the shape's fill area.
|
||||
/// This is used for hit‑testing (e.g. click & hold to peek) so the clickable
|
||||
/// region matches what the user sees on screen.
|
||||
pub(crate) fn point_in_shape(cx: f64, cy: f64, r: f64, shape: RingShape, px: f64, py: f64) -> bool {
|
||||
let dx = (px - cx).abs();
|
||||
let dy = (py - cy).abs();
|
||||
match shape {
|
||||
RingShape::Circle => dx * dx + dy * dy <= r * r,
|
||||
RingShape::Square => dx <= r && dy <= r,
|
||||
RingShape::Diamond => dx + dy <= r,
|
||||
RingShape::Hexagon => {
|
||||
// Regular hexagon, vertex at (r, 0), edge slopes at ±60°.
|
||||
// For |dx| ≥ r/2 the sloping edge bounds: |dy| ≤ √3 (r − |dx|).
|
||||
// For |dx| ≤ r/2 the flat top bounds: |dy| ≤ √3 r / 2.
|
||||
let sqrt3 = 3.0_f64.sqrt();
|
||||
if dx >= r / 2.0 {
|
||||
dy <= sqrt3 * (r - dx)
|
||||
} else {
|
||||
dy <= r * sqrt3 / 2.0
|
||||
}
|
||||
}
|
||||
RingShape::Pill => {
|
||||
// Pill = centre rectangle (2r × 2r) + semicircular caps of
|
||||
// radius r at each end. Total width 4r, height 2r.
|
||||
if dx <= r {
|
||||
dy <= r
|
||||
} else {
|
||||
let ex = dx - r;
|
||||
ex * ex + dy * dy <= r * r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the normalized `t` offset that places the first password dot at the
|
||||
/// visual top-centre of the shape. May be negative; callers should NOT wrap.
|
||||
pub(crate) fn top_centre_offset(shape: RingShape) -> f64 {
|
||||
|
||||
@@ -36,270 +36,17 @@ impl Screenshot {
|
||||
/// Apply configured visual effects to the screenshot.
|
||||
pub fn apply_effects(&mut self, config: &Config) -> Result<()> {
|
||||
if let Some((radius, times)) = config.effect_blur {
|
||||
self.apply_blur(radius, times)?;
|
||||
crate::effects::apply_blur(&mut self.surface, radius, times)?;
|
||||
}
|
||||
if let Some((base, factor)) = config.effect_vignette {
|
||||
self.apply_vignette(base, factor)?;
|
||||
crate::effects::apply_vignette(&mut self.surface, base, factor)?;
|
||||
}
|
||||
if let Some(pixel_size) = config.effect_pixelate {
|
||||
self.apply_pixelate(pixel_size)?;
|
||||
crate::effects::apply_pixelate(&mut self.surface, pixel_size)?;
|
||||
}
|
||||
if let Some(angle) = config.effect_swirl {
|
||||
self.apply_swirl(angle)?;
|
||||
crate::effects::apply_swirl(&mut self.surface, angle)?;
|
||||
}
|
||||
if let Some(factor) = config.effect_melting {
|
||||
self.apply_melting(factor)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a swirl effect.
|
||||
pub fn apply_swirl(&mut self, angle: f32) -> Result<()> {
|
||||
let width = self.surface.width();
|
||||
let height = self.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 = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height as usize];
|
||||
self.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 = self
|
||||
.surface
|
||||
.data()
|
||||
.context("swirl: failed to write surface data")?;
|
||||
surface_data.copy_from_slice(&data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a melting effect (vertical smear).
|
||||
pub fn apply_melting(&mut self, factor: f32) -> Result<()> {
|
||||
let width = self.surface.width();
|
||||
let height = self.surface.height();
|
||||
|
||||
let stride = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height as usize];
|
||||
self.surface
|
||||
.with_data(|src| data.copy_from_slice(src))
|
||||
.context("melting: failed to read surface data")?;
|
||||
|
||||
use rand::RngExt;
|
||||
let mut rng = rand::rng();
|
||||
|
||||
for x in 0..width {
|
||||
let mut melt_amount = 0.0;
|
||||
for y in 0..height {
|
||||
melt_amount += rng.random_range(0.0..factor);
|
||||
let src_y = (y as f32 - melt_amount).max(0.0) as i32;
|
||||
|
||||
let src_idx = (src_y as usize * stride) + (x as usize * 4);
|
||||
let dst_idx = (y as usize * stride) + (x as usize * 4);
|
||||
|
||||
// Copy the pixel from above to create a smear
|
||||
let pixel = [
|
||||
data[src_idx],
|
||||
data[src_idx + 1],
|
||||
data[src_idx + 2],
|
||||
data[src_idx + 3],
|
||||
];
|
||||
data[dst_idx..dst_idx + 4].copy_from_slice(&pixel);
|
||||
}
|
||||
}
|
||||
|
||||
let mut surface_data = self
|
||||
.surface
|
||||
.data()
|
||||
.context("melting: failed to write surface data")?;
|
||||
surface_data.copy_from_slice(&data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pixelate the surface.
|
||||
pub fn apply_pixelate(&mut self, pixel_size: u32) -> Result<()> {
|
||||
if pixel_size <= 1 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let width = self.surface.width();
|
||||
let height = self.surface.height();
|
||||
let stride = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height as usize];
|
||||
self.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 = self
|
||||
.surface
|
||||
.data()
|
||||
.context("pixelate: failed to write surface data")?;
|
||||
surface_data.copy_from_slice(&data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a Gaussian blur effect.
|
||||
pub fn apply_blur(&mut self, radius: u32, times: u32) -> Result<()> {
|
||||
if radius == 0 || times == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let width = self.surface.width() as usize;
|
||||
let height = self.surface.height() as usize;
|
||||
let stride = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height];
|
||||
|
||||
self.surface
|
||||
.with_data(|src| data.copy_from_slice(src))
|
||||
.context("blur: failed to read surface data")?;
|
||||
|
||||
// Convert from stride-padded surface data to tight RgbaImage.
|
||||
// Cairo stride may be larger than width*4 for alignment, so copy
|
||||
// row by row to strip the padding.
|
||||
let tight_stride = width * 4;
|
||||
let mut tight = vec![0u8; tight_stride * height];
|
||||
for y in 0..height {
|
||||
let src_off = y * stride;
|
||||
let dst_off = y * tight_stride;
|
||||
tight[dst_off..dst_off + tight_stride]
|
||||
.copy_from_slice(&data[src_off..src_off + tight_stride]);
|
||||
}
|
||||
|
||||
let mut img: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =
|
||||
image::ImageBuffer::from_raw(width as u32, height as u32, tight)
|
||||
.context("blur: failed to create image buffer")?;
|
||||
|
||||
for _ in 0..times {
|
||||
let mut rgb_data: Vec<[u8; 3]> = Vec::with_capacity(width * height);
|
||||
for pixel in img.pixels() {
|
||||
rgb_data.push([pixel[0], pixel[1], pixel[2]]);
|
||||
}
|
||||
|
||||
fastblur::gaussian_blur(&mut rgb_data, width, height, radius as f32);
|
||||
|
||||
for (i, pixel) in img.pixels_mut().enumerate() {
|
||||
pixel[0] = rgb_data[i][0];
|
||||
pixel[1] = rgb_data[i][1];
|
||||
pixel[2] = rgb_data[i][2];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy back from tight buffer into stride-padded surface data
|
||||
let new_data = img.into_raw();
|
||||
let mut surface_data = self.surface.data()?;
|
||||
for y in 0..height {
|
||||
let src_off = y * tight_stride;
|
||||
let dst_off = y * stride;
|
||||
surface_data[dst_off..dst_off + tight_stride]
|
||||
.copy_from_slice(&new_data[src_off..src_off + tight_stride]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a vignette effect (darken edges).
|
||||
pub fn apply_vignette(&mut self, base: f32, factor: f32) -> Result<()> {
|
||||
let width = self.surface.width();
|
||||
let height = self.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 = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height as usize];
|
||||
self.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 = self
|
||||
.surface
|
||||
.data()
|
||||
.context("vignette: failed to write surface data")?;
|
||||
surface_data.copy_from_slice(&data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||