mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-01 22:07:28 -06:00
Merge PR #8903 into integration batch
This commit is contained in:
@@ -1257,8 +1257,7 @@ fn paint_pattern(
|
||||
m,
|
||||
|i| {
|
||||
let (x, y) = (f64::from(big.x0 + (i % w) as i32) + 0.5, f64::from(big.y0 + (i / w) as i32) + 0.5);
|
||||
let (u, v) = place.map(x, y);
|
||||
tile.sample(u, v)
|
||||
tile.sample_area(&place, x, y)
|
||||
},
|
||||
blend,
|
||||
opacity,
|
||||
@@ -1609,8 +1608,7 @@ fn bevel_height(shape: &Map, b: &Bevel, g: &BevelGeom, tex: &TextureCtx, pattern
|
||||
|| (),
|
||||
|_, y, row, _| {
|
||||
for (x, o) in row.iter_mut().enumerate() {
|
||||
let (u, v) = place.map(f64::from(x0 + x as i32) + 0.5, f64::from(y0 + y as i32) + 0.5);
|
||||
let p = tile.sample(u, v);
|
||||
let p = tile.sample_area(&place, f64::from(x0 + x as i32) + 0.5, f64::from(y0 + y as i32) + 0.5);
|
||||
let l = (0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]) * p[3];
|
||||
*o += k * if t.invert { 1.0 - l } else { l };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
//! A pattern tiles the plane from an origin (the layer's frame when linked, else the canvas
|
||||
//! origin, plus the phase), scaled and rotated about that origin. Samples are bilinear with
|
||||
//! wrap-around on premultiplied colour, so integer placements at 100 % reproduce the tile
|
||||
//! exactly and scaled ones stay seamless.
|
||||
//! exactly and scaled ones stay seamless. Below 100 % each document pixel averages an
|
||||
//! `n × n` grid of bilinear taps over its footprint in the tile (see [`pixel_taps`]), so a
|
||||
//! minified tile is box-filtered instead of aliased; at 100 % and above it is a single tap.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
@@ -157,6 +159,33 @@ impl Tile {
|
||||
/// Bilinear, wrapping sample at continuous tile coordinates (pixel centres at `i + 0.5`).
|
||||
/// Returns straight alpha.
|
||||
pub fn sample(&self, u: f64, v: f64) -> [f32; 4] {
|
||||
unpremultiply(self.sample_premul(u, v))
|
||||
}
|
||||
|
||||
/// The pattern at the centre `(x, y)` of a document pixel, filtered over the pixel's
|
||||
/// footprint in the tile when the placement shrinks it. Returns straight alpha.
|
||||
pub fn sample_area(&self, place: &Placement, x: f64, y: f64) -> [f32; 4] {
|
||||
let n = pixel_taps(place.inv_scale);
|
||||
if n == 1 {
|
||||
let (u, v) = place.map(x, y);
|
||||
return self.sample(u, v);
|
||||
}
|
||||
let mut acc = [0.0f32; 4];
|
||||
for j in 0..n {
|
||||
for i in 0..n {
|
||||
let (u, v) = place.map(x + tap_offset(i, n), y + tap_offset(j, n));
|
||||
let q = self.sample_premul(u, v);
|
||||
for c in 0..4 {
|
||||
acc[c] += q[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
let k = 1.0 / (n * n) as f32;
|
||||
unpremultiply([acc[0] * k, acc[1] * k, acc[2] * k, acc[3] * k])
|
||||
}
|
||||
|
||||
/// Bilinear, wrapping sample, premultiplied.
|
||||
fn sample_premul(&self, u: f64, v: f64) -> [f32; 4] {
|
||||
let (u, v) = (u - 0.5, v - 0.5);
|
||||
let (x0, y0) = (u.floor(), v.floor());
|
||||
let (fx, fy) = ((u - x0) as f32, (v - y0) as f32);
|
||||
@@ -177,13 +206,32 @@ impl Tile {
|
||||
}
|
||||
}
|
||||
}
|
||||
if acc[3] <= 0.0 {
|
||||
return [0.0; 4];
|
||||
}
|
||||
[acc[0] / acc[3], acc[1] / acc[3], acc[2] / acc[3], acc[3].min(1.0)]
|
||||
acc
|
||||
}
|
||||
}
|
||||
|
||||
/// Most taps per axis of the minification filter (the WGSL mirror uses the same cap).
|
||||
const MAX_TAPS: usize = 8;
|
||||
|
||||
/// Taps per axis for a placement whose inverse scale is `inv_scale`: one pixel covers
|
||||
/// `inv_scale` tile pixels, so that many taps (capped) box-filter it. 1 at 100 % and above.
|
||||
fn pixel_taps(inv_scale: f64) -> usize {
|
||||
let s = inv_scale as f32;
|
||||
if s.is_finite() && s > 1.0 { (s.ceil() as usize).min(MAX_TAPS) } else { 1 }
|
||||
}
|
||||
|
||||
/// Offset in document pixels of tap `i` of `n`, centred on the pixel.
|
||||
fn tap_offset(i: usize, n: usize) -> f64 {
|
||||
(i as f64 + 0.5) / n as f64 - 0.5
|
||||
}
|
||||
|
||||
fn unpremultiply(acc: [f32; 4]) -> [f32; 4] {
|
||||
if acc[3] <= 0.0 {
|
||||
return [0.0; 4];
|
||||
}
|
||||
[acc[0] / acc[3], acc[1] / acc[3], acc[2] / acc[3], acc[3].min(1.0)]
|
||||
}
|
||||
|
||||
/// Where a pattern sits: document point → tile coordinates.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Placement {
|
||||
@@ -234,8 +282,7 @@ pub fn render(tile: &Tile, place: &Placement, rect: Rect) -> Vec<[f32; 4]> {
|
||||
let mut out = Vec::with_capacity(rect.width() as usize * rect.height() as usize);
|
||||
for y in rect.y0..rect.y1 {
|
||||
for x in rect.x0..rect.x1 {
|
||||
let (u, v) = place.map(f64::from(x) + 0.5, f64::from(y) + 0.5);
|
||||
out.push(tile.sample(u, v));
|
||||
out.push(tile.sample_area(place, f64::from(x) + 0.5, f64::from(y) + 0.5));
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -299,6 +346,29 @@ mod tests {
|
||||
assert!((u - -3.0).abs() < 1e-9 && (v - 1.0).abs() < 1e-9, "{u},{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minified_pattern_is_filtered_not_aliased() {
|
||||
// 1-px dark / light stripes (mean 0.5), shown at 25 %: every pixel covers 4 tile pixels.
|
||||
// The 0.125 px phase puts each pixel centre on a light tile pixel's centre, so a single
|
||||
// bilinear tap would return pure white.
|
||||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||||
s.fill_rect(Rect::new(0, 0, 8, 8), &[1.0, 1.0, 1.0, 1.0]);
|
||||
for x in (0..8).step_by(2) {
|
||||
s.fill_rect(Rect::new(x, 0, x + 1, 8), &[0.0, 0.0, 0.0, 1.0]);
|
||||
}
|
||||
let t = Tile::new(&Pattern::new("stripes", s, 8, 8)).unwrap();
|
||||
let place = Placement::new(Rect::EMPTY, false, (0.125, 0.0), 0.25, 0.0);
|
||||
let (u, _) = place.map(0.5, 0.5);
|
||||
assert_eq!(t.sample(u, 0.5)[0], 1.0, "the single tap lands on a light stripe");
|
||||
for p in render(&t, &place, Rect::new(0, 0, 8, 1)) {
|
||||
assert!((p[0] - 0.5).abs() < 0.05, "{p:?}");
|
||||
}
|
||||
// 100 % stays exact.
|
||||
let one = Placement::new(Rect::EMPTY, false, (0.0, 0.0), 1.0, 0.0);
|
||||
let row = render(&t, &one, Rect::new(0, 0, 2, 1));
|
||||
assert_eq!((row[0][0], row[1][0]), (0.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sampling_at_hostile_phases_does_not_overflow() {
|
||||
let patterns = [checker()];
|
||||
|
||||
@@ -1116,15 +1116,10 @@ fn wrap(i: i32, n: i32) -> i32 {
|
||||
return ((i % n) + n) % n;
|
||||
}
|
||||
|
||||
// compose::pattern::Tile::sample through Placement::map. p3 = (origin, cos, sin),
|
||||
// p4 = (1 / scale, tile w, tile h); `pat_tex` holds premultiplied RGBA.
|
||||
fn pattern_sample(d: vec2<i32>) -> vec4<f32> {
|
||||
let dx = f32(d.x) + 0.5 - op.p3.x;
|
||||
let dy = f32(d.y) + 0.5 - op.p3.y;
|
||||
let c = op.p3.z;
|
||||
let s = op.p3.w;
|
||||
let u = (dx * c - dy * s) * op.p4.x - 0.5;
|
||||
let v = (dx * s + dy * c) * op.p4.x - 0.5;
|
||||
// One bilinear, wrapping, premultiplied tap at tile coordinates (u, v) (Tile::sample_premul).
|
||||
fn pattern_tap(u0: f32, v0: f32) -> vec4<f32> {
|
||||
let u = u0 - 0.5;
|
||||
let v = v0 - 0.5;
|
||||
let x0 = floor(u);
|
||||
let y0 = floor(v);
|
||||
let fx = u - x0;
|
||||
@@ -1143,6 +1138,28 @@ fn pattern_sample(d: vec2<i32>) -> vec4<f32> {
|
||||
acc += textureLoad(pat_tex, vec2(wrap(ix + i, tw), wrap(iy + j, th)), 0) * (wx[i] * wy[j]);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// compose::pattern::Tile::sample_area through Placement::map. p3 = (origin, cos, sin),
|
||||
// p4 = (1 / scale, tile w, tile h); `pat_tex` holds premultiplied RGBA. Below 100 % a pixel
|
||||
// averages n x n taps over its footprint (n = ceil(1 / scale), at most 8), like the CPU.
|
||||
fn pattern_sample(d: vec2<i32>) -> vec4<f32> {
|
||||
var n = 1;
|
||||
if (op.p4.x > 1.0) { n = i32(min(ceil(op.p4.x), 8.0)); }
|
||||
let c = op.p3.z;
|
||||
let s = op.p3.w;
|
||||
var acc = vec4(0.0);
|
||||
for (var j = 0; j < n; j++) {
|
||||
for (var i = 0; i < n; i++) {
|
||||
let ox = (f32(i) + 0.5) / f32(n) - 0.5;
|
||||
let oy = (f32(j) + 0.5) / f32(n) - 0.5;
|
||||
let dx = f32(d.x) + 0.5 + ox - op.p3.x;
|
||||
let dy = f32(d.y) + 0.5 + oy - op.p3.y;
|
||||
acc += pattern_tap((dx * c - dy * s) * op.p4.x, (dx * s + dy * c) * op.p4.x);
|
||||
}
|
||||
}
|
||||
acc = acc / f32(n * n);
|
||||
if (acc.a <= 0.0) { return vec4(0.0); }
|
||||
return vec4(acc.rgb / acc.a, min(acc.a, 1.0));
|
||||
}
|
||||
|
||||
@@ -2658,7 +2658,7 @@ fn pattern_cache_tracks_source_format_and_cow_tiles() {
|
||||
fn pattern_fill_layers() {
|
||||
let Some(mut g) = gpu() else { return };
|
||||
let pat = checker_pattern();
|
||||
for (link, scale, angle, masked) in [(true, 1.0, 0.0, false), (false, 0.7, 25.0, true), (true, 1.6, -40.0, false)] {
|
||||
for (link, scale, angle, masked) in [(true, 1.0, 0.0, false), (false, 0.7, 25.0, true), (true, 1.6, -40.0, false), (true, 0.3, 10.0, false)] {
|
||||
let mut d = base_doc(70, 50);
|
||||
d.patterns.push(pat.clone());
|
||||
let fill = Fill::Pattern { name: pat.name.clone(), id: pat.id.clone(), scale, angle, link, phase: (3.0, -2.0) };
|
||||
|
||||
Reference in New Issue
Block a user