Files
k-os/crates/k-ui/src/glass.rs

339 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::pixels::{between_corners, blend, corner_row, cover, height, span};
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]);
}
}
}
}
// Blurred at half this size again (it is soft), then brought back.
let mut small = halve(&wall);
blur(&mut small, (t.blur_radius as usize).div_ceil(2 * SCALE));
let mut glass = double(&small, sw, sh);
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);
}
/// `part` of `pm` moved back toward the wallpaper by `k` 256ths: what
/// is drawn there fading in.
pub fn fade(&self, pm: &mut [u8], width: usize, part: Rect, k: u32) {
let (cols, rows) = span(part, width, height(pm, width));
for y in rows {
let src = &self.wall.px[(y / SCALE).min(self.wall.h - 1) * self.wall.w..][..self.wall.w];
let row = pm[(y * width + cols.start) * 4..(y * width + cols.end) * 4].as_chunks_mut::<4>().0;
for (i, d) in row.iter_mut().enumerate() {
blend(d, src[((cols.start + i) / SCALE).min(self.wall.w - 1)], k);
}
}
}
/// 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);
}
}
/// 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, height(pm, width));
let mid = between_corners(r, radius, &cols);
let (x0, x1) = (cols.start, cols.end);
let mut copied: Option<usize> = None;
for y in rows {
let corner = corner_row(r, radius, y);
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 straight = if corner { mid.clone() } else { cols.clone() };
let row = &mut pm[at + straight.start * 4..at + straight.end * 4];
for (i, d) in row.as_chunks_mut::<4>().0.iter_mut().enumerate() {
*d = src[((straight.start + i) / SCALE).min(img.w - 1)];
}
if corner {
let row = pm[at..at + width * 4].as_chunks_mut::<4>().0;
for x in (x0..mid.start).chain(mid.end..x1) {
let s = src[(x / SCALE).min(img.w - 1)];
match cover(r, radius, x, y) {
256 => row[x] = s,
0 => {}
k => blend(&mut row[x], s, k),
}
}
}
copied = (!corner).then_some(y);
}
}
/// `img` at half the size each way, each pixel the average of four.
fn halve(img: &Image) -> Image {
let (w, h) = (img.w.div_ceil(2), img.h.div_ceil(2));
let mut px = vec![[0u8; 4]; w * h];
for y in 0..h {
let (r0, r1) = (2 * y, (2 * y + 1).min(img.h - 1));
for x in 0..w {
let (c0, c1) = (2 * x, (2 * x + 1).min(img.w - 1));
let mut sum = [0u32; 4];
for p in [img.px[r0 * img.w + c0], img.px[r0 * img.w + c1], img.px[r1 * img.w + c0], img.px[r1 * img.w + c1]] {
add(&mut sum, p);
}
px[y * w + x] = sum.map(|s| (s / 4) as u8);
}
}
Image { w, h, px }
}
/// The average of two pixels, all four bytes at once.
fn mid(a: [u8; 4], b: [u8; 4]) -> [u8; 4] {
let (a, b) = (u32::from_le_bytes(a), u32::from_le_bytes(b));
((a & b) + (((a ^ b) & 0xfefe_fefe) >> 1)).to_le_bytes()
}
/// `img` at twice the size (cut to `w` by `h`), the new pixels between
/// the old ones the average of their neighbours: the even rows from the
/// source rows, the odd ones from the rows either side.
fn double(img: &Image, w: usize, h: usize) -> Image {
let mut px = vec![[0u8; 4]; w * h];
for y in (0..h).step_by(2) {
let src = &img.px[(y / 2).min(img.h - 1) * img.w..][..img.w];
let out = &mut px[y * w..(y + 1) * w];
for (x, o) in out.iter_mut().enumerate() {
let s = src[(x / 2).min(img.w - 1)];
*o = if x % 2 == 0 { s } else { mid(s, src[(x / 2 + 1).min(img.w - 1)]) };
}
}
for y in (1..h).step_by(2) {
let below = if y + 1 < h { y + 1 } else { y - 1 };
for x in 0..w {
px[y * w + x] = mid(px[(y - 1) * w + x], px[below * w + x]);
}
}
Image { w, h, px }
}
/// A box blur run three times over (close to a Gaussian), `r` pixels each
/// way: along each row, then down every column at once, so memory is
/// walked in order.
fn blur(img: &mut Image, r: usize) {
if r == 0 {
return;
}
let mut line = vec![[0u8; 4]; img.w];
let mut sums = vec![[0u32; 4]; img.w];
for _ in 0..3 {
for y in 0..img.h {
across(&mut img.px[y * img.w..(y + 1) * img.w], r, &mut line);
}
down(img, r, &mut sums);
}
}
/// Dividing a sum of `2r + 1` bytes by that, as a multiply and a shift:
/// exact for such sums.
fn reciprocal(r: usize) -> u32 {
(65536 + 2 * r as u32) / (2 * r as u32 + 1)
}
fn add(sum: &mut [u32; 4], p: [u8; 4]) {
sum[0] += p[0] as u32;
sum[1] += p[1] as u32;
sum[2] += p[2] as u32;
sum[3] += p[3] as u32;
}
fn sub(sum: &mut [u32; 4], p: [u8; 4]) {
sum[0] -= p[0] as u32;
sum[1] -= p[1] as u32;
sum[2] -= p[2] as u32;
sum[3] -= p[3] as u32;
}
fn average(sum: &[u32; 4], inv: u32) -> [u8; 4] {
[((sum[0] * inv) >> 16) as u8, ((sum[1] * inv) >> 16) as u8, ((sum[2] * inv) >> 16) as u8, ((sum[3] * inv) >> 16) as u8]
}
/// One pass along `row`, its ends held.
fn across(row: &mut [[u8; 4]], r: usize, line: &mut [[u8; 4]]) {
let n = row.len();
let line = &mut line[..n];
line.copy_from_slice(row);
let inv = reciprocal(r);
let mut sum = [0u32; 4];
for i in 0..=2 * r {
add(&mut sum, line[i.saturating_sub(r).min(n - 1)]);
}
for (i, out) in row.iter_mut().enumerate() {
*out = average(&sum, inv);
add(&mut sum, line[(i + r + 1).min(n - 1)]);
sub(&mut sum, line[i.saturating_sub(r)]);
}
}
/// One pass down every column, a row at a time; the top and bottom held.
fn down(img: &mut Image, r: usize, sums: &mut [[u32; 4]]) {
let (w, h) = (img.w, img.h);
let src = img.px.clone();
let row = |i: usize| &src[i.min(h - 1) * w..][..w];
let inv = reciprocal(r);
sums.fill([0; 4]);
for i in 0..=2 * r {
for (s, p) in sums.iter_mut().zip(row(i.saturating_sub(r))) {
add(s, *p);
}
}
for y in 0..h {
for (out, s) in img.px[y * w..(y + 1) * w].iter_mut().zip(sums.iter()) {
*out = average(s, inv);
}
for ((s, a), b) in sums.iter_mut().zip(row(y + r + 1)).zip(row(y.saturating_sub(r))) {
add(s, *a);
sub(s, *b);
}
}
}
#[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");
}
}