315 lines
13 KiB
Rust
315 lines
13 KiB
Rust
//! The Whimsy look (see the vision's K-UI note): a wallpaper glowing with
|
|
//! the theme's orbs, and glass panels over it, the wallpaper blurred and
|
|
//! tinted through them. Both are made once per theme and window size, at
|
|
//! half the size each way (they are soft, and an app's memory is small),
|
|
//! by plain integer code; a frame only copies from them, each pixel
|
|
//! doubled, so it costs about what clearing the window does.
|
|
|
|
use alloc::vec;
|
|
use alloc::vec::Vec;
|
|
|
|
use crate::layout::Rect;
|
|
use crate::theme::{Rgba, Theme};
|
|
|
|
/// The backdrop is made at 1 / `SCALE` of the window, each way.
|
|
const SCALE: usize = 2;
|
|
/// Where the orbs sit, as fractions of the window, and how big they are
|
|
/// (of its larger side).
|
|
const ORBS: [(f32, f32, f32); 3] = [(0.18, 0.22, 0.38), (0.82, 0.38, 0.32), (0.45, 0.92, 0.42)];
|
|
/// An orb is at full strength out to this fraction of its radius, then
|
|
/// fades: a shape with an edge, which the blur softens behind the glass.
|
|
const ORB_CORE: f32 = 0.6;
|
|
|
|
/// An image in pixels of the bytes R, G, B, A (opaque).
|
|
struct Image {
|
|
w: usize,
|
|
h: usize,
|
|
px: Vec<[u8; 4]>,
|
|
}
|
|
|
|
/// The wallpaper, and the glass over it (blurred, tinted), for one theme
|
|
/// and window size.
|
|
pub struct Backdrop {
|
|
key: (Rgba, Rgba, [Rgba; 3], u8, u8, usize, usize),
|
|
wall: Image,
|
|
glass: Image,
|
|
}
|
|
|
|
/// `a` moved toward `b` by `k` 256ths.
|
|
fn mix(a: [u8; 4], b: Rgba, k: u32) -> [u8; 4] {
|
|
let m = |x: u8, y: u8| ((x as u32 * (256 - k) + y as u32 * k) >> 8) as u8;
|
|
[m(a[0], b.0), m(a[1], b.1), m(a[2], b.2), 255]
|
|
}
|
|
|
|
/// How strongly an orb shows (256ths) at a squared distance of `i`/`FALL_STEPS`
|
|
/// of its radius squared.
|
|
const FALL_STEPS: usize = 256;
|
|
|
|
fn falloff() -> [u32; FALL_STEPS + 1] {
|
|
let mut t = [0u32; FALL_STEPS + 1];
|
|
for (i, k) in t.iter_mut().enumerate() {
|
|
let d = libm::sqrtf(i as f32 / FALL_STEPS as f32);
|
|
*k = (((1.0 - d) / (1.0 - ORB_CORE)).clamp(0.0, 1.0) * theme::ORB_STRENGTH * 256.0) as u32;
|
|
}
|
|
t
|
|
}
|
|
|
|
impl Backdrop {
|
|
/// The backdrop for `t` in a window `w` by `h`: `cached` if it still is.
|
|
pub fn for_window(cached: Option<Backdrop>, t: &Theme, w: u32, h: u32) -> Option<Backdrop> {
|
|
let (w, h) = (w as usize, h as usize);
|
|
let key = (t.background, t.surface, t.orbs, t.blur_radius, t.panel_opacity, w, h);
|
|
if let Some(b) = cached.filter(|b| b.key == key) {
|
|
return Some(b);
|
|
}
|
|
let (sw, sh) = (w.div_ceil(SCALE).max(1), h.div_ceil(SCALE).max(1));
|
|
let bg = [t.background.0, t.background.1, t.background.2, 255];
|
|
let mut wall = Image { w: sw, h: sh, px: vec![bg; sw * sh] };
|
|
let big = sw.max(sh) as f32;
|
|
let fall = falloff();
|
|
for (orb, (fx, fy, fr)) in t.orbs.iter().zip(ORBS) {
|
|
let (cx, cy, r) = ((fx * sw as f32) as i64, (fy * sh as f32) as i64, ((fr * big) as i64).max(1));
|
|
let r2 = r * r;
|
|
let rows = (cy - r).max(0) as usize..((cy + r + 1) as usize).min(sh);
|
|
let cols = (cx - r).max(0) as usize..((cx + r + 1) as usize).min(sw);
|
|
for y in rows {
|
|
let dy2 = (y as i64 - cy) * (y as i64 - cy);
|
|
for x in cols.clone() {
|
|
let d2 = (x as i64 - cx) * (x as i64 - cx) + dy2;
|
|
if d2 < r2 {
|
|
let p = &mut wall.px[y * sw + x];
|
|
*p = mix(*p, *orb, fall[(d2 * FALL_STEPS as i64 / r2) as usize]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let mut glass = Image { w: sw, h: sh, px: wall.px.clone() };
|
|
blur(&mut glass, (t.blur_radius as usize).div_ceil(SCALE));
|
|
let opacity = t.panel_opacity as u32 * 256 / 100;
|
|
for p in &mut glass.px {
|
|
*p = mix(*p, t.surface, opacity);
|
|
}
|
|
Some(Backdrop { key, wall, glass })
|
|
}
|
|
|
|
/// The wallpaper over `part` of the window's pixels `pm`
|
|
/// (premultiplied RGBA, `width` pixels a row).
|
|
pub fn wall(&self, pm: &mut [u8], width: usize, part: Rect) {
|
|
copy(&self.wall, pm, width, part, 0.0);
|
|
}
|
|
|
|
/// A glass panel over `r`, its corners rounded by `radius`.
|
|
pub fn glass(&self, pm: &mut [u8], width: usize, r: Rect, radius: f32) {
|
|
copy(&self.glass, pm, width, r, radius);
|
|
}
|
|
}
|
|
|
|
/// The pixels of `r` (whole ones) inside `pm`: columns and rows.
|
|
fn span(r: Rect, width: usize, height: usize) -> (core::ops::Range<usize>, core::ops::Range<usize>) {
|
|
let (x0, y0) = (r.x.max(0.0) as usize, r.y.max(0.0) as usize);
|
|
let (x1, y1) = (((r.x + r.w).max(0.0) as usize).min(width), ((r.y + r.h).max(0.0) as usize).min(height));
|
|
(x0..x1.max(x0), y0..y1.max(y0))
|
|
}
|
|
|
|
/// How much of pixel (`x`, `y`) is inside `r` with its corners rounded by
|
|
/// `radius`, in 256ths: worked out only in the corners.
|
|
fn cover(r: Rect, radius: f32, x: usize, y: usize) -> u32 {
|
|
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
|
let dx = (r.x + radius - fx).max(fx - (r.x + r.w - radius));
|
|
let dy = (r.y + radius - fy).max(fy - (r.y + r.h - radius));
|
|
if dx <= 0.0 || dy <= 0.0 {
|
|
return 256;
|
|
}
|
|
((radius + 0.5 - libm::sqrtf(dx * dx + dy * dy)).clamp(0.0, 1.0) * 256.0) as u32
|
|
}
|
|
|
|
fn blend(d: &mut [u8], s: [u8; 4], k: u32) {
|
|
for (d, s) in d.iter_mut().zip(s) {
|
|
*d = ((s as u32 * k + *d as u32 * (256 - k)) >> 8) as u8;
|
|
}
|
|
}
|
|
|
|
/// Copies `img` (scaled up) into `r` of `pm`, the corners rounded by
|
|
/// `radius` and anti-aliased there.
|
|
fn copy(img: &Image, pm: &mut [u8], width: usize, r: Rect, radius: f32) {
|
|
let radius = radius.min(r.w / 2.0).min(r.h / 2.0).max(0.0);
|
|
let (cols, rows) = span(r, width, pm.len() / 4 / width.max(1));
|
|
let (x0, x1) = (cols.start, cols.end);
|
|
let mut copied: Option<usize> = None;
|
|
for y in rows {
|
|
let corner = radius > 0.0 && ((y as f32) < r.y + radius || (y as f32 + 1.0) > r.y + r.h - radius);
|
|
let at = y * width * 4;
|
|
// A row made from the same source row as the one above: the same.
|
|
if let Some(above) = copied.filter(|a| *a + 1 == y && a / SCALE == y / SCALE && !corner) {
|
|
let from = above * width * 4;
|
|
pm.copy_within(from + x0 * 4..from + x1 * 4, at + x0 * 4);
|
|
copied = Some(y);
|
|
continue;
|
|
}
|
|
let src = &img.px[(y / SCALE).min(img.h - 1) * img.w..][..img.w];
|
|
let row = &mut pm[at + x0 * 4..at + x1 * 4];
|
|
if corner {
|
|
for (i, d) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
|
let s = src[((x0 + i) / SCALE).min(img.w - 1)];
|
|
match cover(r, radius, x0 + i, y) {
|
|
256 => d.copy_from_slice(&s),
|
|
0 => {}
|
|
k => blend(d, s, k),
|
|
}
|
|
}
|
|
copied = None;
|
|
} else {
|
|
for (i, d) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
|
d.copy_from_slice(&src[((x0 + i) / SCALE).min(img.w - 1)]);
|
|
}
|
|
copied = Some(y);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The wet highlight: white along the top of `r`, `alpha` (percent) at
|
|
/// the edge and gone `depth` pixels down.
|
|
pub fn gloss(pm: &mut [u8], width: usize, r: Rect, radius: f32, alpha: u8, depth: f32) {
|
|
let radius = radius.min(r.w / 2.0).min(r.h / 2.0).max(0.0);
|
|
let top = Rect { h: depth.min(r.h), ..r };
|
|
let (cols, rows) = span(top, width, pm.len() / 4 / width.max(1));
|
|
let white = [255u8; 4];
|
|
for y in rows {
|
|
let fade = 1.0 - (y as f32 + 0.5 - r.y) / depth;
|
|
let a = (fade.max(0.0) * alpha as f32 * 256.0 / 100.0) as u32;
|
|
let row = &mut pm[y * width * 4..(y + 1) * width * 4];
|
|
for x in cols.clone() {
|
|
let k = (cover(r, radius, x, y) * a) >> 8;
|
|
if k > 0 {
|
|
blend(&mut row[x * 4..x * 4 + 4], white, k);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// How far (`x`, `y`) is outside `r` with its corners rounded by `radius`
|
|
/// (negative inside).
|
|
fn outside(r: Rect, radius: f32, x: f32, y: f32) -> f32 {
|
|
let (hw, hh) = (r.w / 2.0, r.h / 2.0);
|
|
let qx = libm::fabsf(x - (r.x + hw)) - (hw - radius);
|
|
let qy = libm::fabsf(y - (r.y + hh)) - (hh - radius);
|
|
let (ox, oy) = (qx.max(0.0), qy.max(0.0));
|
|
let out = if ox > 0.0 && oy > 0.0 { libm::sqrtf(ox * ox + oy * oy) } else { ox.max(oy) };
|
|
out + qx.max(qy).min(0.0) - radius
|
|
}
|
|
|
|
/// A soft shadow under `r` (corners rounded by `radius`): cast `down`
|
|
/// pixels below it, `dark` (percent) at its edge and gone `spread` out.
|
|
/// Only where it shows is touched: what `r` covers is drawn over anyway.
|
|
pub fn shadow(pm: &mut [u8], width: usize, r: Rect, radius: f32, down: f32, spread: f32, dark: u8) {
|
|
let radius = radius.min(r.w / 2.0).min(r.h / 2.0).max(0.0);
|
|
let caster = Rect { y: r.y + down, ..r };
|
|
let around = Rect { x: r.x - spread, y: r.y - spread, w: r.w + 2.0 * spread, h: r.h + down + 2.0 * spread };
|
|
let (cols, rows) = span(around, width, pm.len() / 4 / width.max(1));
|
|
// Well inside `r`: covered by it.
|
|
let (in_x, in_y) = (r.x + radius..r.x + r.w - radius, r.y..r.y + r.h);
|
|
for y in rows {
|
|
let fy = y as f32 + 0.5;
|
|
let row = &mut pm[y * width * 4..(y + 1) * width * 4];
|
|
for x in cols.clone() {
|
|
let fx = x as f32 + 0.5;
|
|
if in_x.contains(&fx) && in_y.contains(&fy) {
|
|
continue;
|
|
}
|
|
let d = outside(caster, radius, fx, fy).max(0.0);
|
|
let fade = (1.0 - d / spread).max(0.0);
|
|
let k = (fade * fade * dark as f32 * 256.0 / 100.0) as u32;
|
|
if k > 0 {
|
|
blend(&mut row[x * 4..x * 4 + 4], [0, 0, 0, 255], k);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A box blur run three times over (close to a Gaussian), `r` pixels each
|
|
/// way, rows then columns.
|
|
fn blur(img: &mut Image, r: usize) {
|
|
if r == 0 {
|
|
return;
|
|
}
|
|
let (w, h) = (img.w, img.h);
|
|
let mut line = vec![[0u8; 4]; w.max(h)];
|
|
for _ in 0..3 {
|
|
for y in 0..h {
|
|
pass(&mut img.px, y * w, 1, w, r, &mut line);
|
|
}
|
|
for x in 0..w {
|
|
pass(&mut img.px, x, w, h, r, &mut line);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One box pass along `n` pixels starting at `start`, `step` apart; the
|
|
/// edges are held.
|
|
fn pass(px: &mut [[u8; 4]], start: usize, step: usize, n: usize, r: usize, line: &mut [[u8; 4]]) {
|
|
let at = |i: usize| start + i.min(n - 1) * step;
|
|
let width = (2 * r + 1) as u32;
|
|
for c in 0..4 {
|
|
// The window centred on pixel 0, the edge held.
|
|
let mut sum = px[at(0)][c] as u32 * (r as u32 + 1) + (1..=r).map(|i| px[at(i)][c] as u32).sum::<u32>();
|
|
for (i, out) in line.iter_mut().take(n).enumerate() {
|
|
out[c] = (sum / width) as u8;
|
|
sum = sum + px[at(i + r + 1)][c] as u32 - px[at(i.saturating_sub(r))][c] as u32;
|
|
}
|
|
}
|
|
for (i, p) in line.iter().take(n).enumerate() {
|
|
px[start + i * step] = *p;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn image(w: usize, h: usize, c: [u8; 4]) -> Image {
|
|
Image { w, h, px: vec![c; w * h] }
|
|
}
|
|
|
|
#[test]
|
|
fn blur_spreads_a_shape_and_keeps_flat_colour_flat() {
|
|
let mut img = image(40, 40, [10, 20, 30, 255]);
|
|
blur(&mut img, 3);
|
|
assert!(img.px.iter().all(|p| *p == [10, 20, 30, 255]), "nothing to blur, nothing changes");
|
|
for y in 15..25 {
|
|
for x in 15..25 {
|
|
img.px[y * 40 + x] = [250, 250, 250, 255];
|
|
}
|
|
}
|
|
blur(&mut img, 3);
|
|
let red = |x: usize, y: usize| img.px[y * 40 + x][0];
|
|
assert!(red(20, 20) > 150, "the middle, {}", red(20, 20));
|
|
assert!((40..200).contains(&red(13, 20)), "just outside the edge: between, {}", red(13, 20));
|
|
assert_eq!(red(2, 2), 10, "far away: untouched");
|
|
}
|
|
|
|
#[test]
|
|
fn the_backdrop_is_made_once_per_theme_and_size_and_glass_is_tinted() {
|
|
let t = Theme::WHIMSY_DARK;
|
|
let b = Backdrop::for_window(None, &t, 100, 60).unwrap();
|
|
assert_eq!((b.wall.w, b.wall.h), (50, 30));
|
|
let flat = Theme { orbs: [t.background; 3], ..t };
|
|
let f = Backdrop::for_window(Some(b), &flat, 100, 60).unwrap();
|
|
let bg = [t.background.0, t.background.1, t.background.2, 255];
|
|
assert!(f.wall.px.iter().all(|p| *p == bg), "no orbs, no glow");
|
|
assert_eq!(f.glass.px[0], mix(bg, t.surface, t.panel_opacity as u32 * 256 / 100));
|
|
}
|
|
|
|
#[test]
|
|
fn a_panel_is_copied_with_round_soft_corners() {
|
|
let t = Theme { orbs: [Rgba::rgb(0xffffff); 3], ..Theme::WHIMSY_DARK };
|
|
let b = Backdrop::for_window(None, &t, 40, 40).unwrap();
|
|
let mut pm = vec![0u8; 40 * 40 * 4];
|
|
b.glass(&mut pm, 40, Rect { x: 10.0, y: 10.0, w: 20.0, h: 20.0 }, 8.0);
|
|
let at = |x: usize, y: usize| pm[(y * 40 + x) * 4 + 3];
|
|
assert_eq!(at(20, 20), 255, "inside");
|
|
assert_eq!(at(10, 10), 0, "the very corner is cut off");
|
|
assert!((1..255).contains(&at(12, 12)), "the corner's edge is soft: {}", at(12, 12));
|
|
assert_eq!(at(5, 20), 0, "outside");
|
|
}
|
|
}
|