641 lines
27 KiB
Rust
641 lines
27 KiB
Rust
//! Texture › Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer.
|
||
//!
|
||
//! Like the Pixelate filters' patterns, theirs (cracks, grain, tiles, squares, panes and the
|
||
//! Texturizer's surfaces, all generated here, no image files) lie in document space around the
|
||
//! object's centre: the same at any zoom or resolution, moving with the object, and the same on
|
||
//! every redraw (hash noise of the pattern's cell indices). Surfaces in relief are height fields
|
||
//! whose slopes, taken in document units, are lit from one side.
|
||
|
||
use std::f64::consts::{FRAC_1_SQRT_2, PI};
|
||
|
||
use vectorcraft_geom::{Point, Vec2};
|
||
|
||
use super::pixelate::{AROUND, Around, Place, Soft, cell, gap, point_in, rand, value_noise};
|
||
use super::{PixelSpace, blur_plane, pick};
|
||
|
||
/// Grain's types, in its Grain Type menu's order: (label, parameter value).
|
||
pub const GRAIN_TYPES: [(&str, &str); 10] = [
|
||
("Regular", "regular"),
|
||
("Soft", "soft"),
|
||
("Sprinkles", "sprinkles"),
|
||
("Clumped", "clumped"),
|
||
("Contrasty", "contrasty"),
|
||
("Enlarged", "enlarged"),
|
||
("Stippled", "stippled"),
|
||
("Horizontal", "horizontal"),
|
||
("Vertical", "vertical"),
|
||
("Speckle", "speckle"),
|
||
];
|
||
|
||
/// A Grain type ([`GRAIN_TYPES`]).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Grain {
|
||
Regular,
|
||
Soft,
|
||
Sprinkles,
|
||
Clumped,
|
||
Contrasty,
|
||
Enlarged,
|
||
Stippled,
|
||
Horizontal,
|
||
Vertical,
|
||
Speckle,
|
||
}
|
||
|
||
impl Grain {
|
||
const ALL: [Grain; 10] = [
|
||
Grain::Regular,
|
||
Grain::Soft,
|
||
Grain::Sprinkles,
|
||
Grain::Clumped,
|
||
Grain::Contrasty,
|
||
Grain::Enlarged,
|
||
Grain::Stippled,
|
||
Grain::Horizontal,
|
||
Grain::Vertical,
|
||
Grain::Speckle,
|
||
];
|
||
|
||
/// The type of Grain Type `value` (a [`GRAIN_TYPES`] value, any case); Regular otherwise.
|
||
pub fn parse(value: &str) -> Self {
|
||
pick(&GRAIN_TYPES, &Self::ALL, value, Grain::Regular)
|
||
}
|
||
}
|
||
|
||
/// Texturizer's textures, in its Texture menu's order: (label, parameter value).
|
||
pub const TEXTURES: [(&str, &str); 4] = [("Brick", "brick"), ("Burlap", "burlap"), ("Canvas", "canvas"), ("Sandstone", "sandstone")];
|
||
|
||
/// A Texturizer surface ([`TEXTURES`]), made in code.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Texture {
|
||
Brick,
|
||
Burlap,
|
||
Canvas,
|
||
Sandstone,
|
||
}
|
||
|
||
impl Texture {
|
||
const ALL: [Texture; 4] = [Texture::Brick, Texture::Burlap, Texture::Canvas, Texture::Sandstone];
|
||
|
||
/// The texture of `value` (a [`TEXTURES`] value, any case); Canvas otherwise.
|
||
pub fn parse(value: &str) -> Self {
|
||
pick(&TEXTURES, &Self::ALL, value, Texture::Canvas)
|
||
}
|
||
|
||
/// The surface's height at `p` (points at 100 %), in points: slopes of about 1 at most.
|
||
pub(super) fn height(self, p: Point) -> f64 {
|
||
match self {
|
||
Texture::Brick => brick(p),
|
||
Texture::Burlap => weave(p, 7.0, true),
|
||
Texture::Canvas => weave(p, 4.0, false),
|
||
Texture::Sandstone => sandstone(p),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Texturizer's light directions, in its Light menu's order: (label, parameter value).
|
||
pub const LIGHT_DIRECTIONS: [(&str, &str); 8] = [
|
||
("Bottom", "bottom"),
|
||
("Bottom Left", "bottomLeft"),
|
||
("Left", "left"),
|
||
("Top Left", "topLeft"),
|
||
("Top", "top"),
|
||
("Top Right", "topRight"),
|
||
("Right", "right"),
|
||
("Bottom Right", "bottomRight"),
|
||
];
|
||
|
||
/// Where a surface is lit from ([`LIGHT_DIRECTIONS`]), on the page.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Light {
|
||
Bottom,
|
||
BottomLeft,
|
||
Left,
|
||
TopLeft,
|
||
Top,
|
||
TopRight,
|
||
Right,
|
||
BottomRight,
|
||
}
|
||
|
||
impl Light {
|
||
const ALL: [Light; 8] =
|
||
[Light::Bottom, Light::BottomLeft, Light::Left, Light::TopLeft, Light::Top, Light::TopRight, Light::Right, Light::BottomRight];
|
||
|
||
/// The direction of `value` (a [`LIGHT_DIRECTIONS`] value, any case); Top otherwise.
|
||
pub fn parse(value: &str) -> Self {
|
||
pick(&LIGHT_DIRECTIONS, &Self::ALL, value, Light::Top)
|
||
}
|
||
|
||
/// The unit vector towards the light (document axes, y down).
|
||
fn toward(self) -> Vec2 {
|
||
let d = FRAC_1_SQRT_2;
|
||
match self {
|
||
Light::Bottom => Vec2::new(0.0, 1.0),
|
||
Light::BottomLeft => Vec2::new(-d, d),
|
||
Light::Left => Vec2::new(-1.0, 0.0),
|
||
Light::TopLeft => Vec2::new(-d, -d),
|
||
Light::Top => Vec2::new(0.0, -1.0),
|
||
Light::TopRight => Vec2::new(d, -d),
|
||
Light::Right => Vec2::new(1.0, 0.0),
|
||
Light::BottomRight => Vec2::new(d, d),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Seeds of the Texture filters' patterns (Pixelate's use smaller salts).
|
||
const SALT_CRACKS: u64 = 64;
|
||
const SALT_GRAIN: u64 = 72;
|
||
const SALT_TILES: u64 = 88;
|
||
const SALT_PATCH: u64 = 96;
|
||
const SALT_GLASS: u64 = 104;
|
||
const SALT_SURFACE: u64 = 112;
|
||
|
||
/// How soft Craquelure's tones are, as a share of the crack spacing: their contours become cracks.
|
||
pub(super) const CONTOUR_SOFTNESS: f64 = 1.0 / 6.0;
|
||
|
||
/// A pixel's straight colour (0..1).
|
||
pub(super) fn straight(p: [u8; 4]) -> [f32; 3] {
|
||
let a = f32::from(p[3]);
|
||
[p[0], p[1], p[2]].map(|v| if a > 0.0 { (f32::from(v) / a).min(1.0) } else { 0.0 })
|
||
}
|
||
|
||
/// The pixel of straight colour `c` at coverage `alpha` (0..1), premultiplied.
|
||
pub(super) fn premultiply(c: [f32; 3], alpha: f32) -> [u8; 4] {
|
||
let a = (alpha.clamp(0.0, 1.0) * 255.0).round();
|
||
let [r, g, b] = c.map(|v| (v.clamp(0.0, 1.0) * a).round().min(a) as u8);
|
||
[r, g, b, a as u8]
|
||
}
|
||
|
||
/// Colour `c` lit by `s`: darkened towards black below 0, lightened towards white above.
|
||
fn lit(c: [f32; 3], s: f64) -> [f32; 3] {
|
||
let s = if s.is_finite() { s.clamp(-1.0, 1.0) as f32 } else { 0.0 };
|
||
c.map(|v| if s < 0.0 { v * (1.0 + s) } else { v + (1.0 - v) * s })
|
||
}
|
||
|
||
/// How a surface whose height rises along `slope` (height per length) faces a light from
|
||
/// `toward`: 0 flat, positive facing it, negative turned away.
|
||
fn facing(slope: Vec2, toward: Vec2) -> f64 {
|
||
-slope.dot(toward)
|
||
}
|
||
|
||
/// The share of a pixel `px` wide that lies within `d` of a line (negative `d`: beyond it).
|
||
fn cover(d: f64, px: f64) -> f64 {
|
||
(d / px + 0.5).clamp(0.0, 1.0)
|
||
}
|
||
|
||
/// The luma of colour `c` (Rec. 601 weights).
|
||
pub(super) fn luma(c: [f32; 3]) -> f32 {
|
||
0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||
}
|
||
|
||
/// Strict distance ordering, with the original hypot comparison at rounding/tie boundaries.
|
||
fn distance_less(a2: f64, b2: f64, exact: impl FnOnce() -> (f64, f64)) -> bool {
|
||
// Cell distances are small and finite. Preserve hypot's ordering at rounding/tie
|
||
// boundaries, without computing it for every candidate of every pixel.
|
||
if b2.is_finite() && (a2 - b2).abs() <= 8.0 * f64::EPSILON * (a2 + b2) {
|
||
let (a, b) = exact();
|
||
a < b
|
||
} else {
|
||
a2 < b2
|
||
}
|
||
}
|
||
|
||
/// The two random points nearest to (`u`, `v`) (in cells), `point(i, j)` being cell (`i`, `j`)'s,
|
||
/// found among the cells [`AROUND`] the one holding it.
|
||
fn nearest_two(u: f64, v: f64, around: &mut Around<Point>, point: impl Fn(i64, i64) -> Point) -> Option<(Point, Point)> {
|
||
let (i, j) = (cell(u)?, cell(v)?);
|
||
let mut best: [(f64, Option<Point>); 2] = [(f64::INFINITY, None); 2];
|
||
for (k, (di, dj)) in AROUND.iter().enumerate() {
|
||
// A cell farther than the second point so far can't hold a nearer one.
|
||
let (gx, gy) = (gap(i.wrapping_add(*di), u), gap(j.wrapping_add(*dj), v));
|
||
let exact = |p: Option<Point>| p.map_or(f64::INFINITY, |p| (p.x - u).hypot(p.y - v));
|
||
if !distance_less(gx * gx + gy * gy, best[1].0, || (gx.hypot(gy), exact(best[1].1))) {
|
||
continue;
|
||
}
|
||
let Some(p) = around.get((i, j), k, &point) else { continue };
|
||
let d2 = (p.x - u) * (p.x - u) + (p.y - v) * (p.y - v);
|
||
if distance_less(d2, best[0].0, || (exact(Some(p)), exact(best[0].1))) {
|
||
best = [(d2, Some(p)), best[0]];
|
||
} else if distance_less(d2, best[1].0, || (exact(Some(p)), exact(best[1].1))) {
|
||
best[1] = (d2, Some(p));
|
||
}
|
||
}
|
||
Some((best[0].1?, best[1].1?))
|
||
}
|
||
|
||
/// How far (`u`, `v`) lies from the border between the cells of `near` and `next` (its nearest
|
||
/// points, [`nearest_two`]), and the way away from it (unit), in cells.
|
||
fn edge(u: f64, v: f64, near: Point, next: Point) -> (f64, Vec2) {
|
||
let q = Point::new(u, v);
|
||
let apart = near - next;
|
||
let len = apart.hypot();
|
||
if !(len.is_finite() && len > 1e-12) {
|
||
return (0.0, Vec2::ZERO);
|
||
}
|
||
(((q - next).hypot2() - (q - near).hypot2()) / (2.0 * len), apart / len)
|
||
}
|
||
|
||
/// The slope of a rise of 1 over `width` from a border at distance `d` (smooth at both ends).
|
||
fn bevel(d: f64, width: f64) -> f64 {
|
||
if !(width.is_finite() && width > 0.0) {
|
||
return 0.0;
|
||
}
|
||
let t = (d / width).clamp(0.0, 1.0);
|
||
6.0 * t * (1.0 - t)
|
||
}
|
||
|
||
/// The object's straight luminance (0..1) softened by a Gaussian of `sigma` pixels, weighted by
|
||
/// coverage so the transparency around it doesn't darken its edges.
|
||
pub(super) struct Tones {
|
||
v: Vec<f32>,
|
||
w: usize,
|
||
h: usize,
|
||
}
|
||
|
||
impl Tones {
|
||
pub(super) fn new(px: &[[u8; 4]], w: usize, h: usize, sigma: f64) -> Self {
|
||
let mut l: Vec<f32> = px.iter().map(|p| luma([p[0], p[1], p[2]].map(f32::from)) / 255.0).collect();
|
||
let mut a: Vec<f32> = px.iter().map(|p| f32::from(p[3]) / 255.0).collect();
|
||
blur_plane(&mut l, w, h, sigma);
|
||
blur_plane(&mut a, w, h, sigma);
|
||
let v = l.iter().zip(&a).map(|(l, a)| if *a > 1e-4 { (l / a).clamp(0.0, 1.0) } else { 0.0 }).collect();
|
||
Self { v, w, h }
|
||
}
|
||
|
||
pub(super) fn get(&self, x: usize, y: usize) -> f64 {
|
||
let (x, y) = (x.min(self.w.saturating_sub(1)), y.min(self.h.saturating_sub(1)));
|
||
self.v.get(y * self.w + x).map_or(0.0, |v| f64::from(*v))
|
||
}
|
||
|
||
/// The tone at pixel (`x`, `y`) and its slope per document unit along the document's axes.
|
||
fn slope(&self, x: usize, y: usize, place: &Place) -> (f64, Vec2) {
|
||
let gx = 0.5 * (self.get(x + 1, y) - self.get(x.saturating_sub(1), y));
|
||
let gy = 0.5 * (self.get(x, y + 1) - self.get(x, y.saturating_sub(1)));
|
||
// Per pixel → per document unit: through the transpose of the document → pixel map.
|
||
let [a, b, c, d, _, _] = place.to_px.as_coeffs();
|
||
(self.get(x, y), Vec2::new(gx * a + gy * b, gx * c + gy * d))
|
||
}
|
||
}
|
||
|
||
/// Craquelure: the object painted on relief plaster that cracks into plates about `spacing` apart
|
||
/// and along the contours of its tones. `depth` (0..10) sets how deep and wide the cracks are and
|
||
/// how high the plates and the tones stand; `brightness` (0..10) how brightly the surface is lit.
|
||
pub(super) fn craquelure(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, spacing: f64, depth: f64, brightness: f64) {
|
||
let Some(place) = Place::new(space, w, h) else { return };
|
||
if !(spacing.is_finite() && spacing > 0.0) {
|
||
return;
|
||
}
|
||
let depth = (depth / 10.0).clamp(0.0, 1.0);
|
||
let light = (0.5 + 0.05 * brightness.clamp(0.0, 10.0)) as f32;
|
||
let soft = CONTOUR_SOFTNESS * spacing;
|
||
let tones = Tones::new(px, w, h, soft / place.px);
|
||
let half = (0.15 + 0.6 * depth).min(0.15 * spacing);
|
||
let rise = (3.0 * half).max(0.12 * spacing);
|
||
let toward = Light::TopLeft.toward();
|
||
let mut around = Around::new();
|
||
place.each(px, |x, y, p| {
|
||
if p[3] == 0 {
|
||
return;
|
||
}
|
||
let q = place.at(x, y);
|
||
let (u, v) = (q.x / spacing, q.y / spacing);
|
||
// The cracks between the plates; each plate's edge bevels down into them.
|
||
let (mut crack, mut slope) = (0.0, Vec2::ZERO);
|
||
if let Some((near, next)) = nearest_two(u, v, &mut around, |i, j| point_in(SALT_CRACKS, i, j)) {
|
||
let (e, away) = edge(u, v, near, next);
|
||
crack = cover(half - e * spacing, place.px);
|
||
slope = away * (0.6 * bevel(e * spacing - half, rise));
|
||
}
|
||
// The cracks along the tones' contours (quarter, half and three-quarter tone).
|
||
let (tone, grad) = tones.slope(x, y, &place);
|
||
let steep = grad.hypot();
|
||
if steep > 1e-4 {
|
||
let level = (tone * 4.0).round().clamp(1.0, 3.0) / 4.0;
|
||
crack = crack.max(cover(0.8 * half - (tone - level).abs() / steep, place.px));
|
||
}
|
||
slope += grad * (0.8 * soft);
|
||
let c = lit(straight(*p), 0.5 * depth * facing(slope, toward));
|
||
let dark = (1.0 - 0.85 * crack * depth) as f32;
|
||
*p = premultiply(c.map(|v| v * light * dark), f32::from(p[3]) / 255.0);
|
||
});
|
||
}
|
||
|
||
/// A random value in [-1, 1) for the `size`-point cell holding `q`; `k` picks one of several.
|
||
pub(super) fn cell_noise(salt: u64, q: Point, size: f64, k: u64) -> f64 {
|
||
match (cell(q.x / size), cell(q.y / size)) {
|
||
(Some(i), Some(j)) => 2.0 * rand(salt, i, j, k) - 1.0,
|
||
_ => 0.0,
|
||
}
|
||
}
|
||
|
||
/// [`value_noise`] in [-1, 1) over cells `size` points wide.
|
||
pub(super) fn smooth_noise(salt: u64, q: Point, size: f64) -> f64 {
|
||
2.0 * value_noise(salt, q.x / size, q.y / size) - 1.0
|
||
}
|
||
|
||
/// Grain: noise of `kind` over the object in 1-point grains (2 for Enlarged, clumps for Clumped),
|
||
/// as strong as `intensity` (0..100); `contrast` (0..100, 50 leaves it) sets the image's
|
||
/// contrast. Sprinkles and Stippled use the background colour, white.
|
||
pub(super) fn grain(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, intensity: f64, contrast: f64, kind: Grain) {
|
||
let Some(place) = Place::new(space, w, h) else { return };
|
||
let a = (intensity / 100.0).clamp(0.0, 1.0);
|
||
let k = (0.5 + contrast / 100.0).clamp(0.5, 1.5) as f32;
|
||
let s = SALT_GRAIN;
|
||
place.each(px, |x, y, p| {
|
||
if p[3] == 0 {
|
||
return;
|
||
}
|
||
let q = place.at(x, y);
|
||
let mut c = straight(*p).map(|v| (v - 0.5) * k + 0.5);
|
||
let chance = 0.5 * (cell_noise(s, q, 1.0, 3) + 1.0);
|
||
match kind {
|
||
Grain::Regular => add(&mut c, |ch| cell_noise(s, q, 1.0, ch), 0.5 * a),
|
||
Grain::Soft => add(&mut c, |ch| smooth_noise(s + ch, q, 1.0), 0.4 * a),
|
||
Grain::Clumped => add(&mut c, |ch| 0.65 * smooth_noise(s + ch, q, 3.0) + 0.35 * cell_noise(s, q, 1.0, ch), 0.6 * a),
|
||
Grain::Enlarged => add(&mut c, |ch| 0.5 * (smooth_noise(s + ch, q, 2.0) + cell_noise(s, q, 2.0, ch)), 0.5 * a),
|
||
Grain::Contrasty => {
|
||
c = c.map(|v| (v - 0.5) * 1.4 + 0.5);
|
||
add(&mut c, |_| cell_noise(s, q, 1.0, 4), 0.6 * a);
|
||
}
|
||
Grain::Horizontal | Grain::Vertical => {
|
||
// Streaks one point thick: a value per row (column) drifting along it.
|
||
let (along, across) = if kind == Grain::Horizontal { (q.x, q.y) } else { (q.y, q.x) };
|
||
let line = cell(across).unwrap_or(0);
|
||
let n = 0.6 * (2.0 * rand(s, 0, line, 6) - 1.0) + 0.4 * (2.0 * value_noise(s + 7, along / 12.0, line as f64) - 1.0);
|
||
add(&mut c, |_| n, 0.5 * a);
|
||
}
|
||
Grain::Sprinkles => {
|
||
if chance < 0.35 * a {
|
||
c = c.map(|v| v + (1.0 - v) * 0.9);
|
||
}
|
||
}
|
||
Grain::Speckle => {
|
||
if chance < 0.25 * a {
|
||
c = c.map(|v| v * 0.25);
|
||
} else {
|
||
add(&mut c, |_| cell_noise(s, q, 1.0, 5), 0.1 * a);
|
||
}
|
||
}
|
||
Grain::Stippled => {
|
||
// Dots of the colour where it is dark, the background between them.
|
||
let dots = f64::from(1.0 - luma(c).clamp(0.0, 1.0)) * (0.35 + 0.65 * a);
|
||
c = if chance < dots { c.map(|v| v * 0.6) } else { [1.0; 3] };
|
||
}
|
||
}
|
||
*p = premultiply(c, f32::from(p[3]) / 255.0);
|
||
});
|
||
}
|
||
|
||
/// Add noise `n(channel)` (-1..1) times `amount` to colour `c`.
|
||
fn add(c: &mut [f32; 3], n: impl Fn(u64) -> f64, amount: f64) {
|
||
for (ch, v) in c.iter_mut().enumerate() {
|
||
*v += (amount * n(ch as u64)) as f32;
|
||
}
|
||
}
|
||
|
||
/// A random point of cell (`i`, `j`) of pattern `salt` within `jitter` (0..1, a share of the
|
||
/// cell) of its centre, in cells.
|
||
fn jittered(salt: u64, i: i64, j: i64, jitter: f64) -> Point {
|
||
Point::new(i as f64 + 0.5 + jitter * (rand(salt, i, j, 0) - 0.5), j as f64 + 0.5 + jitter * (rand(salt, i, j, 1) - 0.5))
|
||
}
|
||
|
||
/// Mosaic Tiles: the object laid in irregular tiles about `tile` across, bevelled at their edges,
|
||
/// with sunken grout between them half a point wide per step of `grout` (1..15), as light as
|
||
/// `lighten` (0..10) says.
|
||
pub(super) fn mosaic_tiles(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, tile: f64, grout: f64, lighten: f64) {
|
||
let Some(place) = Place::new(space, w, h) else { return };
|
||
if !(tile.is_finite() && tile > 0.0 && grout.is_finite()) {
|
||
return;
|
||
}
|
||
let half = (0.25 * grout).clamp(0.0, 0.225 * tile);
|
||
let rise = (1.2 * half).max(0.08 * tile);
|
||
let lighten = (0.08 * lighten.clamp(0.0, 10.0)) as f32;
|
||
let toward = Light::TopLeft.toward();
|
||
let mut around = Around::new();
|
||
place.each(px, |x, y, p| {
|
||
if p[3] == 0 {
|
||
return;
|
||
}
|
||
let q = place.at(x, y);
|
||
let (u, v) = (q.x / tile, q.y / tile);
|
||
let Some((near, next)) = nearest_two(u, v, &mut around, |i, j| jittered(SALT_TILES, i, j, 0.5)) else { return };
|
||
let (e, away) = edge(u, v, near, next);
|
||
let e = e * tile;
|
||
let c = straight(*p);
|
||
let face = lit(c, 0.35 * facing(away * bevel(e - half, rise), toward));
|
||
// Sunk in shade, then lightened.
|
||
let sunk = c.map(|v| 0.5 * v + (1.0 - 0.5 * v) * lighten);
|
||
let g = cover(half - e, place.px) as f32;
|
||
let mut out = face;
|
||
for (o, s) in out.iter_mut().zip(sunk) {
|
||
*o = *o * (1.0 - g) + s * g;
|
||
}
|
||
*p = premultiply(out, f32::from(p[3]) / 255.0);
|
||
});
|
||
}
|
||
|
||
/// Patchwork: the object in squares `square` points across, each filled with the colour around
|
||
/// its centre and raised to a height of its own (brighter squares stand higher, each a little
|
||
/// more or less at random), its bevelled sides lit from the top left as strongly as `relief`
|
||
/// (0..25) says.
|
||
pub(super) fn patchwork(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, square: f64, relief: f64) {
|
||
let Some(place) = Place::new(space, w, h) else { return };
|
||
let side = square.max(1.0);
|
||
if !side.is_finite() {
|
||
return;
|
||
}
|
||
let soft = Soft::new(px, w, side / 3.0 / place.px);
|
||
let relief = (relief / 25.0).clamp(0.0, 1.0);
|
||
let rise = 0.25 * side;
|
||
let toward = Light::TopLeft.toward();
|
||
place.each(px, |x, y, p| {
|
||
let q = place.at(x, y);
|
||
let (Some(i), Some(j)) = (cell(q.x / side), cell(q.y / side)) else { return };
|
||
let (x0, y0) = (i as f64 * side, j as f64 * side);
|
||
let [r, g, b, alpha] = soft.at(place.index(Point::new(x0 + 0.5 * side, y0 + 0.5 * side)));
|
||
if alpha <= 0.0 {
|
||
*p = [0; 4];
|
||
return;
|
||
}
|
||
let c = [r, g, b].map(|v| v / alpha);
|
||
let height = relief * (0.35 + 0.65 * (0.5 * rand(SALT_PATCH, i, j, 0) + 0.5 * f64::from(luma(c))));
|
||
// The bevels rise inwards from each side.
|
||
let (fx, fy) = (q.x - x0, q.y - y0);
|
||
let ramp = |d: f64| cover(rise - d, place.px);
|
||
let slope = Vec2::new(ramp(fx) - ramp(side - fx), ramp(fy) - ramp(side - fy)) * (1.5 * height);
|
||
*p = premultiply(lit(c, 0.9 * facing(slope, toward)), alpha);
|
||
});
|
||
}
|
||
|
||
/// Stained Glass: the object as panes of single colours around random points about `cell_size`
|
||
/// apart, leaded in the foreground colour (black) half a point wide per step of `border` (1..20),
|
||
/// lit from behind at its centre as strongly as `light` (0..10) says, fading out towards its
|
||
/// corners.
|
||
pub(super) fn stained_glass(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, cell_size: f64, border: f64, light: f64) {
|
||
let Some(place) = Place::new(space, w, h) else { return };
|
||
if !(cell_size.is_finite() && cell_size > 0.0 && border.is_finite()) {
|
||
return;
|
||
}
|
||
let soft = Soft::new(px, w, cell_size / place.px / 4.0);
|
||
let half = (0.25 * border).clamp(0.0, 0.25 * cell_size);
|
||
let light = (light / 10.0).clamp(0.0, 1.0);
|
||
let extent = space.extent;
|
||
let mut around = Around::new();
|
||
place.each(px, |x, y, p| {
|
||
let q = place.at(x, y);
|
||
let (u, v) = (q.x / cell_size, q.y / cell_size);
|
||
let Some((near, next)) = nearest_two(u, v, &mut around, |i, j| point_in(SALT_GLASS, i, j)) else { return };
|
||
let pane = |at: Point| soft.at(place.index(Point::new(at.x * cell_size, at.y * cell_size)));
|
||
let (glass, other) = (pane(near), pane(next));
|
||
let (e, _) = edge(u, v, near, next);
|
||
let lead = cover(half - e * cell_size, place.px) as f32;
|
||
let glow = if extent.is_finite() && extent > 0.0 { light * (1.0 - (q.to_vec2().hypot() / extent).powi(2)).max(0.0) } else { 0.0 };
|
||
let [r, g, b, a] = glass;
|
||
let c = if a > 0.0 { lit([r, g, b].map(|v| v / a), 0.6 * glow) } else { [0.0; 3] };
|
||
// The lead holds the panes on both sides of it, as opaque as the more opaque one.
|
||
let alpha = a * (1.0 - lead) + a.max(other[3]) * lead;
|
||
let rgb = c.map(|v| v * a * (1.0 - lead));
|
||
*p = if alpha > 0.0 { premultiply(rgb.map(|v| v / alpha), alpha) } else { [0; 4] };
|
||
});
|
||
}
|
||
|
||
/// Brick in stretcher bond (bricks 20 × 8 points, 1.5-point mortar): each brick raised with
|
||
/// 1-point bevels and a little higher or lower than the next, rough faced, the mortar sandy.
|
||
fn brick(p: Point) -> f64 {
|
||
const LENGTH: f64 = 20.0;
|
||
const COURSE: f64 = 8.0;
|
||
const MORTAR: f64 = 1.5;
|
||
let Some(row) = cell(p.y / COURSE) else { return 0.0 };
|
||
let x = p.x + if row.rem_euclid(2) == 1 { 0.5 * LENGTH } else { 0.0 };
|
||
let Some(col) = cell(x / LENGTH) else { return 0.0 };
|
||
let (fx, fy) = (x - col as f64 * LENGTH, p.y - row as f64 * COURSE);
|
||
let edge = fx.min(LENGTH - fx).min(fy).min(COURSE - fy) - 0.5 * MORTAR;
|
||
if edge <= 0.0 {
|
||
return 0.2 * value_noise(SALT_SURFACE, p.x / 0.6, p.y / 0.6);
|
||
}
|
||
edge.min(1.0) + 0.4 * rand(SALT_SURFACE, col, row, 2) + 0.3 * value_noise(SALT_SURFACE + 1, p.x / 1.5, p.y / 1.5)
|
||
}
|
||
|
||
/// A plain weave of threads `period` points apart, each passing over one thread and under the
|
||
/// next; `coarse` threads (burlap) wander and are hairy, fine ones (canvas) are even.
|
||
fn weave(p: Point, period: f64, coarse: bool) -> f64 {
|
||
let (wander, hair) = if coarse { (0.35, 0.35) } else { (0.08, 0.12) };
|
||
let drift = |salt: u64, t: f64| wander * (2.0 * value_noise(salt, t / (4.0 * period), 0.5) - 1.0);
|
||
let x = p.x / period + drift(SALT_SURFACE + 3, p.y);
|
||
let y = p.y / period + drift(SALT_SURFACE + 4, p.x);
|
||
let (Some(i), Some(j)) = (cell(x), cell(y)) else { return 0.0 };
|
||
let (fx, fy) = (x - i as f64, y - j as f64);
|
||
// Each thread is round across and rises and falls along its length, over one crossing thread
|
||
// and under the next; the higher of the two threads shows.
|
||
let profile = |t: f64| (PI * t).sin().max(0.0).sqrt();
|
||
let over = if i.wrapping_add(j).rem_euclid(2) == 0 { 0.5 } else { -0.5 };
|
||
let down = profile(fx) * (0.5 + over * (PI * fy).sin());
|
||
let across = profile(fy) * (0.5 - over * (PI * fx).sin());
|
||
let thread = down.max(across);
|
||
let fibres = hair * value_noise(SALT_SURFACE + 5, p.x / (0.25 * period), p.y / (0.25 * period));
|
||
(thread + fibres) * period / PI
|
||
}
|
||
|
||
/// Sandstone: four octaves of smooth noise (8 down to 1 point) over a fine sandy grain.
|
||
fn sandstone(p: Point) -> f64 {
|
||
let (mut h, mut size, mut amp) = (0.0, 8.0, 1.6);
|
||
for octave in 0..4 {
|
||
h += amp * value_noise(SALT_SURFACE + 8 + octave, p.x / size, p.y / size);
|
||
size *= 0.5;
|
||
amp *= 0.5;
|
||
}
|
||
h + 0.06 * cell_noise(SALT_SURFACE + 12, p, 0.5, 0)
|
||
}
|
||
|
||
/// Texturizer: the object on a `texture` surface scaled by `scaling` (0.5..2), in relief as
|
||
/// strong as `relief` (0..50) and lit from `light`; `invert` turns the surface's heights over.
|
||
#[allow(clippy::too_many_arguments)] // the effect's five options
|
||
pub(super) fn texturizer(
|
||
px: &mut [[u8; 4]],
|
||
w: usize,
|
||
h: usize,
|
||
space: &PixelSpace,
|
||
texture: Texture,
|
||
scaling: f64,
|
||
relief: f64,
|
||
light: Light,
|
||
invert: bool,
|
||
) {
|
||
let Some(place) = Place::new(space, w, h) else { return };
|
||
if !(scaling.is_finite() && scaling > 0.0) {
|
||
return;
|
||
}
|
||
// Slopes over a quarter point of the texture, or half a pixel where pixels are larger.
|
||
let step = (0.25f64).max(0.5 * place.px / scaling);
|
||
let strength = 0.1 * relief.clamp(0.0, 50.0) * if invert { -1.0 } else { 1.0 };
|
||
let toward = light.toward();
|
||
place.each(px, |x, y, p| {
|
||
if p[3] == 0 {
|
||
return;
|
||
}
|
||
let q = place.at(x, y);
|
||
let t = Point::new(q.x / scaling, q.y / scaling);
|
||
if !(t.x.is_finite() && t.y.is_finite()) {
|
||
return;
|
||
}
|
||
let at = |dx: f64, dy: f64| texture.height(Point::new(t.x + dx, t.y + dy));
|
||
let (left, right, up, down) = (at(-step, 0.0), at(step, 0.0), at(0.0, -step), at(0.0, step));
|
||
let slope = Vec2::new(right - left, down - up) / (2.0 * step);
|
||
// Ridges catch some light from any side and hollows stay in shade, so the surface shows
|
||
// whichever way it runs.
|
||
let ridge = (at(0.0, 0.0) - 0.25 * (left + right + up + down)) / step;
|
||
*p = premultiply(lit(straight(*p), strength * (facing(slope, toward) + ridge)), f32::from(p[3]) / 255.0);
|
||
});
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod nearest_tests {
|
||
use super::*;
|
||
|
||
/// Compare with all 25 candidates, without the production distance-pruning shortcut.
|
||
fn brute_force(u: f64, v: f64, point: &impl Fn(i64, i64) -> Point) -> Option<(Point, Point)> {
|
||
let (i, j) = (cell(u)?, cell(v)?);
|
||
let mut candidates: Vec<_> = AROUND
|
||
.iter()
|
||
.map(|(di, dj)| {
|
||
let p = point(i.wrapping_add(*di), j.wrapping_add(*dj));
|
||
((p.x - u).hypot(p.y - v), p)
|
||
})
|
||
.collect();
|
||
candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||
Some((candidates.first()?.1, candidates.get(1)?.1))
|
||
}
|
||
|
||
#[test]
|
||
fn nearest_points_match_all_candidates_at_cell_and_rounding_boundaries() {
|
||
let mut queries = vec![];
|
||
for y in -20..=20 {
|
||
for x in -24..=24 {
|
||
queries.push((f64::from(x) / 13.0, f64::from(y) / 11.0));
|
||
}
|
||
}
|
||
queries.extend([
|
||
(0.0, 0.0),
|
||
(0.5, 0.5),
|
||
(1.0 - f64::EPSILON, -1.0),
|
||
(1.0 + f64::EPSILON, -1.0 - f64::EPSILON),
|
||
(4.0e6, -4.0e6),
|
||
(1.0e14 + 0.25, -1.0e14 + 0.5),
|
||
(f64::INFINITY, 0.0),
|
||
(f64::NAN, 0.0),
|
||
]);
|
||
for pattern in 0..3 {
|
||
let point = |i: i64, j: i64| match pattern {
|
||
0 => point_in(SALT_CRACKS, i, j),
|
||
1 => Point::new(i as f64 + 0.5, j as f64 + 0.5),
|
||
_ => Point::new(i as f64, j as f64),
|
||
};
|
||
let mut around = Around::new();
|
||
for &(u, v) in &queries {
|
||
assert_eq!(nearest_two(u, v, &mut around, point), brute_force(u, v, &point), "pattern {pattern} at ({u}, {v})");
|
||
}
|
||
}
|
||
}
|
||
}
|