mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 04:50:08 +00:00
Merge upstream/main into fix/justify-cjk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -662,7 +662,8 @@ pub struct Glass {
|
||||
pub dispersion: f64,
|
||||
/// Blur of the backdrop (a blur radius, document units, 0–[`Self::MAX_FROST`]).
|
||||
pub frost: f64,
|
||||
/// How widely the light spreads over the glass (0–1).
|
||||
/// Which way the curved edge bends the backdrop: along the edge's normal (0) to away from the
|
||||
/// layer's centre (1).
|
||||
pub splay: f64,
|
||||
}
|
||||
|
||||
|
||||
@@ -254,16 +254,65 @@ fn glass_edges_bend_split_and_catch_the_light() {
|
||||
let (rim, middle) = changed(&split, &bent);
|
||||
assert!(rim > 0 && middle == 0, "dispersion: rim {rim}, middle {middle}");
|
||||
|
||||
// The light (from the top left by default) brightens the top-left rim more than the
|
||||
// bottom-right one, and only near the edge.
|
||||
// The light (from the top left by default) is a line on the 1-unit bevel: the top-left corner
|
||||
// catches it all, the bottom-right one (facing away) 0.6 as much, the top side (45° from it)
|
||||
// 0.574 and the bottom side 0.6 × 0.574, and the top-right corner (side on) none. Further in
|
||||
// only the faint gradient across the curve shows, and nothing past `depth`.
|
||||
let (d, f, _) = glass_scene(Glass { light_intensity: 1.0, ..plain });
|
||||
let lit = render_node(&d, f, 1.0).unwrap();
|
||||
let gain = |x: u32, y: u32| i32::from(px(&lit, x, y)[1]) - i32::from(px(&flat, x, y)[1]);
|
||||
assert!(gain(82, 42) > gain(197, 117), "{} vs {}", gain(82, 42), gain(197, 117));
|
||||
assert!(gain(82, 42) > 40, "{}", gain(82, 42));
|
||||
let (top_left, bottom_right, top, bottom, top_right) = (gain(80, 40), gain(199, 119), gain(140, 40), gain(140, 119), gain(199, 40));
|
||||
assert!(
|
||||
top_left > bottom_right && bottom_right > top && top > bottom && bottom > top_right.abs(),
|
||||
"{top_left} {bottom_right} {top} {bottom} {top_right}"
|
||||
);
|
||||
assert!(top_left > 120 && top_right.abs() <= 2, "{top_left} {top_right}");
|
||||
assert!(gain(140, 44).abs() <= 13, "4 px in: the gradient only, {}", gain(140, 44));
|
||||
assert_eq!(gain(140, 80), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splay_turns_the_bend_towards_the_centre() {
|
||||
// Refraction only, so the bend alone moves the stripes' edges.
|
||||
let bend = Glass { frost: 0.0, refraction: 1.0, dispersion: 0.0, light_intensity: 0.0, depth: 16.0, ..Glass::default() };
|
||||
let (d, f, _) = glass_scene(bend.clone());
|
||||
let edge = render_node(&d, f, 1.0).unwrap();
|
||||
let (d, f, _) = glass_scene(Glass { splay: 1.0, ..bend });
|
||||
let centre = render_node(&d, f, 1.0).unwrap();
|
||||
let diff = |xs: std::ops::Range<u32>, ys: std::ops::Range<u32>| {
|
||||
let mut most = 0;
|
||||
for y in ys {
|
||||
for x in xs.clone() {
|
||||
most = most.max(px(&edge, x, y).iter().zip(px(¢re, x, y)).map(|(a, b)| a.abs_diff(b)).max().unwrap());
|
||||
}
|
||||
}
|
||||
most
|
||||
};
|
||||
// Halfway along the left side the edge's normal and the way from the centre (140, 80) agree:
|
||||
// nearly the same pixels. In the top-left corner they part, and so does what shows there.
|
||||
assert!(diff(80..96, 78..82) <= 8, "mid-left: {}", diff(80..96, 78..82));
|
||||
assert!(diff(80..96, 40..56) > 40, "corner: {}", diff(80..96, 40..56));
|
||||
}
|
||||
|
||||
/// Glass with nothing under it (an export of the pane alone): the light on the bevel still shows,
|
||||
/// the alpha rising with it, and the flat inside and the outside stay clear.
|
||||
#[test]
|
||||
fn glass_over_nothing_still_catches_the_light() {
|
||||
let mut d = Document::new("glass");
|
||||
let p = d.first_page().unwrap();
|
||||
let pane = d.create(p, NodeKind::Rectangle, "Glass", 40.0, 30.0, 120.0, 80.0).unwrap();
|
||||
let n = d.get_mut(pane).unwrap();
|
||||
n.fills.clear();
|
||||
n.effects = vec![Effect { kind: EffectKind::Glass(Glass { light_intensity: 1.0, ..Glass::default() }), visible: true }];
|
||||
let out = render_area(&d, &[pane], Rect::new(0.0, 0.0, 200.0, 140.0), 1.0).unwrap();
|
||||
// The top-left corner faces the light (from the top left): white, as opaque as it is bright.
|
||||
let corner = px(&out, 40, 30);
|
||||
assert!(corner[3] > 100 && corner.iter().all(|v| v.abs_diff(corner[3]) <= 2), "{corner:?}");
|
||||
assert!(out.data().chunks_exact(4).all(|q| q[0] <= q[3] && q[1] <= q[3] && q[2] <= q[3]), "premultiplied");
|
||||
assert_eq!(px(&out, 100, 70), [0; 4]);
|
||||
assert_eq!(px(&out, 10, 10), [0; 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glass_and_backdrop_blur_survive_bands_and_partial_renders() {
|
||||
let (d, _, _) = glass_scene(Glass::default());
|
||||
|
||||
+133
-57
@@ -3,10 +3,12 @@
|
||||
//! Within `depth` of the outline the surface curves like a thick lens: the backdrop there is bent
|
||||
//! towards the inside, most a [`BEVEL`] in from the edge (`refraction` × `depth` far), falling off
|
||||
//! as the 3.5th power of the way out of the curve and gone `depth` further in, with a fifth as
|
||||
//! much on the bevel itself ([`bend_at`]). Each colour channel bends by a different amount
|
||||
//! (`dispersion`, a rainbow fringe: red furthest, blue least, each smeared along the bend), and the
|
||||
//! rim catches the light: brightest where it faces `light_angle`, half as bright on the opposite
|
||||
//! side, spread wider by `splay`.
|
||||
//! much on the bevel itself ([`bend_at`]). `splay` turns which way it bends, from along the edge's
|
||||
//! normal (0) to away from the layer's centre (1). Each colour channel bends by a different amount
|
||||
//! (`dispersion`, a rainbow fringe: red furthest, blue least, each smeared along the bend). The
|
||||
//! bevel catches the light as a thin line added to the backdrop ([`rim_light_at`]): fully where the
|
||||
//! edge faces `light_angle`, none below 0.35 of facing it, 0.6 as much on the opposite side; and a
|
||||
//! faint gradient runs across the curve, 5% darker where it faces the light and lighter opposite.
|
||||
//! `frost` blurs the backdrop first. Distances to the outline come from an exact Euclidean
|
||||
//! distance transform of the shape's coverage.
|
||||
//!
|
||||
@@ -181,11 +183,45 @@ const RIM_BEND: f32 = 0.2;
|
||||
/// by a triangle (the two end steps weigh nothing and are skipped).
|
||||
const DISPERSION_STEPS: usize = 8;
|
||||
|
||||
/// The least an edge faces the light (the cosine between its outward normal and the light) and
|
||||
/// still catches some.
|
||||
const HL_LO: f32 = 0.35;
|
||||
|
||||
/// How much of the light the side opposite it catches.
|
||||
const HL_SECONDARY: f32 = 0.6;
|
||||
|
||||
/// The gradient across the curve at its strongest, a share of the light's intensity.
|
||||
const BROAD: f32 = 0.05;
|
||||
|
||||
/// `x` from `a` to `b` as 0 to 1, clamped (a span under 1e-5 counts as 1e-5).
|
||||
fn lin(a: f32, b: f32, x: f32) -> f32 {
|
||||
((x - a) / (b - a).max(1e-5)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// `x` from `a` to `b` as a smooth 0-to-1 step (Hermite), clamped.
|
||||
fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
|
||||
let t = ((x - a) / (b - a).max(1e-5)).clamp(0.0, 1.0);
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
|
||||
/// How much of the rim light falls `x` pixels in from the edge, for a `bevel` pixels deep: all
|
||||
/// of it within a fifth of the bevel, none a bevel in.
|
||||
pub(crate) fn rim_light_at(x: f32, bevel: f32) -> f32 {
|
||||
smoothstep(-bevel - 0.001, -0.2 * bevel, -x)
|
||||
}
|
||||
|
||||
/// How much light an edge catches, `facing` being the cosine between its outward normal and the
|
||||
/// light: from [`HL_LO`] to fully facing it, and [`HL_SECONDARY`] as much facing away.
|
||||
fn catches(facing: f32) -> f32 {
|
||||
smoothstep(HL_LO, 1.0, facing) + HL_SECONDARY * smoothstep(HL_LO, 1.0, -facing)
|
||||
}
|
||||
|
||||
/// How strongly the gradient across the curve shows `x` pixels in from the edge (`depth` and
|
||||
/// `bevel` in pixels): fully on the bevel, falling as the square of the way in to none at `depth`.
|
||||
pub(crate) fn sheen_at(x: f32, depth: f32, bevel: f32) -> f32 {
|
||||
lin(-depth, -bevel + 1e-4, -x).powi(2)
|
||||
}
|
||||
|
||||
/// How far the backdrop moves at `x` pixels in from the edge, for a glass `depth` and [`BEVEL`]
|
||||
/// `bevel` deep (pixels) and `refraction` (0–1): a thick lens's profile, the most a bevel in from
|
||||
/// the edge, falling off as the 3.5th power of the way out of the curve to nothing `depth` further
|
||||
@@ -229,6 +265,17 @@ fn dispersed(frosted: &Pixmap, x: f32, y: f32, dx: f32, dy: f32, k: f32) -> [f32
|
||||
[(acc[0] / sum).min(a), (acc[1] / sum).min(a), (acc[2] / sum).min(a), a]
|
||||
}
|
||||
|
||||
/// `base`, a premultiplied backdrop pixel (channels 0–255), with `add` (levels, below 0 darkens)
|
||||
/// added to its colour, at coverage `c`. Where that is more than the backdrop's alpha holds (all of
|
||||
/// it over a transparent backdrop, an export with nothing under the glass), the alpha rises with
|
||||
/// it, so the light shows there too.
|
||||
fn lit(base: [f32; 4], add: f32, c: f32) -> [u8; 4] {
|
||||
let [r, g, b, a] = base;
|
||||
let (r, g, b) = (r + add, g + add, b + add);
|
||||
let a = a.max(0.0).max(r).max(g).max(b).min(255.0);
|
||||
[r, g, b, a].map(|v| (v.clamp(0.0, a) * c).round().clamp(0.0, 255.0) as u8)
|
||||
}
|
||||
|
||||
/// Bilinear sample of premultiplied `p` at pixel coordinates `(x, y)` (centres at +0.5), each
|
||||
/// channel 0..=255; transparent outside.
|
||||
fn sample(p: &Pixmap, x: f32, y: f32) -> [f32; 4] {
|
||||
@@ -256,17 +303,6 @@ fn sample(p: &Pixmap, x: f32, y: f32) -> [f32; 4] {
|
||||
})
|
||||
}
|
||||
|
||||
/// The furthest in (pixels) the rim light is worth half a level or more: where
|
||||
/// `255 × intensity × e^(−d / band)` falls to 0.5, at most [`MAX_LIGHT_PX`] (0 for no light).
|
||||
fn light_reach(band: f64, intensity: f64) -> f64 {
|
||||
let k = 255.0 * intensity / 0.5;
|
||||
if k > 1.0 && band.is_finite() { (band * k.ln()).min(MAX_LIGHT_PX) } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The light reaches no further in than this many pixels (a wide light on a deep pane at a high
|
||||
/// zoom would otherwise need a huge margin of coverage around every cut).
|
||||
const MAX_LIGHT_PX: f64 = 512.0;
|
||||
|
||||
/// A shape's coverage (alpha), distance to its edge and smoothed inward normals over a window of
|
||||
/// pixels whose top-left pixel is (`x0`, `y0`).
|
||||
struct Fields {
|
||||
@@ -420,9 +456,19 @@ fn shape_fields(shape: &Outline, xf: Affine, w: u32, h: u32, m: u32, r: usize) -
|
||||
Some((Arc::new(Fields::new(&cov, -i64::from(m), -i64::from(m), r)), (0, 0)))
|
||||
}
|
||||
|
||||
/// [`shape_fields`] for a whole shape, from the cache; `None` when it isn't kept (first asked for
|
||||
/// in this render, too big, or its transform too far out).
|
||||
fn whole_shape(shape: &Outline, xf: Affine, m: u32, r: usize) -> Option<(Arc<Fields>, (i64, i64))> {
|
||||
/// A layer transform split so the same shape at the same zoom lands on the same pixels of its own
|
||||
/// frame wherever a render puts it or cuts it (see [`own_frame`]).
|
||||
struct OwnFrame {
|
||||
/// The whole pixels of the translation: the shift from layer pixels to the shape's own frame.
|
||||
shift: (i64, i64),
|
||||
/// The translation's fraction of a pixel, in steps of 1/65536.
|
||||
frac: (i64, i64),
|
||||
/// Local coordinates → the shape's own frame: the linear part and that fraction.
|
||||
xf: Affine,
|
||||
}
|
||||
|
||||
/// `xf` (local → layer pixels) split into its [`OwnFrame`]; `None` for a translation too far out.
|
||||
fn own_frame(xf: Affine) -> Option<OwnFrame> {
|
||||
let [a, b, c, d, e, f] = xf.as_coeffs();
|
||||
// The translation in steps of 1/65536 px (far finer than the rasteriser's 1/64, coarse enough
|
||||
// that rounding errors in where a render starts land on the same step), its whole pixels (the
|
||||
@@ -435,7 +481,14 @@ fn whole_shape(shape: &Outline, xf: Affine, m: u32, r: usize) -> Option<(Arc<Fie
|
||||
let (qe, qf) = (q(e)?, q(f)?);
|
||||
let shift = (qe.div_euclid(STEPS as i64), qf.div_euclid(STEPS as i64));
|
||||
let frac = (qe.rem_euclid(STEPS as i64), qf.rem_euclid(STEPS as i64));
|
||||
let own = Affine::new([a, b, c, d, frac.0 as f64 / STEPS, frac.1 as f64 / STEPS]);
|
||||
Some(OwnFrame { shift, frac, xf: Affine::new([a, b, c, d, frac.0 as f64 / STEPS, frac.1 as f64 / STEPS]) })
|
||||
}
|
||||
|
||||
/// [`shape_fields`] for a whole shape, from the cache; `None` when it isn't kept (first asked for
|
||||
/// in this render, too big, or its transform too far out).
|
||||
fn whole_shape(shape: &Outline, xf: Affine, m: u32, r: usize) -> Option<(Arc<Fields>, (i64, i64))> {
|
||||
let [a, b, c, d, _, _] = xf.as_coeffs();
|
||||
let OwnFrame { shift, frac, xf: own } = own_frame(xf)?;
|
||||
let bb = (own * shape.0.clone()).bounding_box();
|
||||
let (x0, y0, x1, y1) = (bb.x0.floor(), bb.y0.floor(), bb.x1.ceil(), bb.y1.ceil());
|
||||
if ![x0, y0, x1, y1].iter().all(|v| v.is_finite() && v.abs() < 1e9) {
|
||||
@@ -479,27 +532,25 @@ pub fn glass(g: &Glass, frame: EffectFrame<'_>) -> Option<Pixmap> {
|
||||
let g = g.sanitized();
|
||||
let depth = (g.depth * scale).clamp(1.0, 1024.0);
|
||||
let bevel = (BEVEL * scale).min(1024.0);
|
||||
let band = (depth * (0.15 + 0.35 * g.splay)).max(1.0);
|
||||
let reach = light_reach(band, g.light_intensity);
|
||||
let r = (depth * 0.1).clamp(1.0, 8.0) as usize;
|
||||
// Coverage with a margin, so wherever the layer is cut every distance the pixels read is
|
||||
// exact: the bend's (within `depth` + `bevel`), the light's (within `reach`) and those the
|
||||
// normals are smoothed from (2r + 1 further).
|
||||
let m = (depth + bevel).max(reach).ceil() as u32 + 4 * r as u32 + 4;
|
||||
// exact: the bend's and the gradient's (within `depth` + `bevel`; the light lies on the bevel)
|
||||
// and those the normals are smoothed from (2r + 1 further).
|
||||
let m = (depth + bevel).ceil() as u32 + 4 * r as u32 + 4;
|
||||
let (fields, shift) = shape_fields(shape, xf, w, h, m, r)?;
|
||||
let mut frosted = back.clone();
|
||||
fx::blur(&mut frosted, g.frost / 2.0 * scale);
|
||||
let ang = g.light_angle.to_radians();
|
||||
let light = (ang.sin() as f32, -ang.cos() as f32);
|
||||
// The layer's centre in its own frame (see `own_frame`), which `splay` bends away from: the
|
||||
// direction to it is the same wherever a render puts or cuts the layer.
|
||||
let local_centre = shape.0.bounding_box().center();
|
||||
let ((sx, sy), centre) = match own_frame(xf) {
|
||||
Some(own) => (own.shift, own.xf * local_centre),
|
||||
None => ((0, 0), xf * local_centre),
|
||||
};
|
||||
let (refraction, dispersion, bevel) = (g.refraction as f32, (g.dispersion * Glass::DISPERSION_SPREAD) as f32, bevel as f32);
|
||||
let power = (6.0 - 5.0 * g.splay) as f32;
|
||||
let (band, reach) = (band as f32, reach as f32);
|
||||
// The falloff, lowered by its value at `reach` and scaled back to 1 at the edge, so the light
|
||||
// fades out to 0 at `reach` instead of stopping there.
|
||||
let floor = (-reach / band).exp();
|
||||
let fall = 1.0 - floor;
|
||||
let intensity = g.light_intensity as f32;
|
||||
let depth = depth as f32;
|
||||
let (splay, intensity, depth) = (g.splay as f32, g.light_intensity as f32, depth as f32);
|
||||
let mut out = Pixmap::new(w, h)?;
|
||||
let dst = out.data_mut();
|
||||
for y in 0..h as usize {
|
||||
@@ -508,30 +559,37 @@ pub fn glass(g: &Glass, frame: EffectFrame<'_>) -> Option<Pixmap> {
|
||||
if c <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let bend = bend_at(d, depth, bevel, refraction);
|
||||
let (bx, by) = (x as f32 + pad as f32 + 0.5, y as f32 + pad as f32 + 0.5);
|
||||
let base = if bend == 0.0 {
|
||||
// Nothing bends (past `depth` + `bevel`, or no refraction): the channels' samples
|
||||
// are at the same point, so one sample.
|
||||
let s = sample(&frosted, bx, by);
|
||||
[s[0].min(s[3]), s[1].min(s[3]), s[2].min(s[3]), s[3]]
|
||||
let (base, add) = if d >= depth + bevel {
|
||||
// Past the curve nothing bends and no light falls (the rim light and the gradient
|
||||
// are 0 there): the channels' samples are at the same point, so one sample.
|
||||
(dispersed(&frosted, bx, by, 0.0, 0.0, 0.0), 0.0)
|
||||
} else {
|
||||
dispersed(&frosted, bx, by, inward.0 * bend, inward.1 * bend, dispersion)
|
||||
};
|
||||
// The rim catches the light where it faces it, and half as much opposite. It fades out
|
||||
// by `reach` and is none past it, wherever the layer is cut.
|
||||
let hl = if d < reach && fall > 0.0 {
|
||||
// The curve's outward direction: the edge's normal, turned by `splay` towards the
|
||||
// way away from the centre (not renormalised: where the two part, the bend is less).
|
||||
let away = if splay > 0.0 {
|
||||
let px = ((x as i64 - sx) as f64 + 0.5 - centre.x) as f32;
|
||||
let py = ((y as i64 - sy) as f64 + 0.5 - centre.y) as f32;
|
||||
let len = (px * px + py * py).sqrt();
|
||||
if len > 1e-6 { (px / len, py / len) } else { (0.0, 0.0) }
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let out_dir = (-inward.0 * (1.0 - splay) + away.0 * splay, -inward.1 * (1.0 - splay) + away.1 * splay);
|
||||
// No refraction: nothing bends, so one sample (`dispersed` with no move).
|
||||
let bend = bend_at(d, depth, bevel, refraction);
|
||||
let k = if bend == 0.0 { 0.0 } else { dispersion };
|
||||
let base = dispersed(&frosted, bx, by, -out_dir.0 * bend, -out_dir.1 * bend, k);
|
||||
// The bevel catches the light as a line added to the backdrop, by how much the edge
|
||||
// faces it; across the curve a faint gradient, darker on the side facing it.
|
||||
let facing = -(inward.0 * light.0 + inward.1 * light.1);
|
||||
let spec = facing.max(0.0).powf(power) + 0.5 * (-facing).max(0.0).powf(power);
|
||||
(intensity * spec * ((-d / band).exp() - floor) / fall).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
let rim = rim_light_at(d, bevel) * catches(facing);
|
||||
let sheen = -(out_dir.0 * light.0 + out_dir.1 * light.1) * sheen_at(d, depth, bevel) * BROAD;
|
||||
(base, 255.0 * intensity * (rim + sheen))
|
||||
};
|
||||
let i = (y * w as usize + x) * 4;
|
||||
if let Some(p) = dst.get_mut(i..i + 4) {
|
||||
for (o, v) in p.iter_mut().zip(base) {
|
||||
*o = ((v * (1.0 - hl) + 255.0 * hl) * c).round().clamp(0.0, 255.0) as u8;
|
||||
}
|
||||
p.copy_from_slice(&lit(base, add, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,14 +731,32 @@ mod tests {
|
||||
assert!(c.held <= 100 && !c.map.contains_key(&key(4)));
|
||||
}
|
||||
|
||||
/// The rim light and the gradient across the curve at depth 20 and a 1-unit bevel, x units in
|
||||
/// from the edge: the reference profile's values.
|
||||
#[test]
|
||||
fn light_reach_is_where_the_light_falls_under_half_a_level() {
|
||||
let r = light_reach(10.0, 1.0);
|
||||
assert!((255.0 * (-r / 10.0).exp() - 0.5).abs() < 1e-9, "{r}");
|
||||
// No light, too faint to show, or a band that isn't a number: it reaches nowhere.
|
||||
for (band, intensity) in [(10.0, 0.0), (10.0, 0.5 / 255.0), (f64::NAN, 1.0), (f64::INFINITY, 1.0)] {
|
||||
assert_eq!(light_reach(band, intensity), 0.0, "{band} {intensity}");
|
||||
fn the_light_lies_on_the_bevel_and_the_gradient_across_the_curve() {
|
||||
for (x, rim, sheen) in [
|
||||
(0.10, 1.0, 1.0),
|
||||
(0.25, 0.9888, 1.0),
|
||||
(0.50, 0.6843, 1.0),
|
||||
(0.75, 0.2330, 1.0),
|
||||
(1.00, 0.0, 1.0),
|
||||
(2.0, 0.0, 0.8975),
|
||||
(4.0, 0.0, 0.7091),
|
||||
(6.0, 0.0, 0.5429),
|
||||
(8.0, 0.0, 0.3989),
|
||||
(16.0, 0.0, 0.0443),
|
||||
(20.0, 0.0, 0.0),
|
||||
(30.0, 0.0, 0.0),
|
||||
] {
|
||||
assert!((rim_light_at(x, 1.0) - rim).abs() < 1e-3, "rim at {x}: {}", rim_light_at(x, 1.0));
|
||||
assert!((sheen_at(x, 20.0, 1.0) - sheen).abs() < 1e-3, "gradient at {x}: {}", sheen_at(x, 20.0, 1.0));
|
||||
}
|
||||
assert_eq!(light_reach(1e6, 1.0), MAX_LIGHT_PX);
|
||||
// Facing the light fully, at 45° (cos 0.707) and side on; the opposite side 0.6 as much.
|
||||
assert!((catches(1.0) - 1.0).abs() < 1e-6);
|
||||
assert!((catches(std::f32::consts::FRAC_1_SQRT_2) - 0.574).abs() < 1e-3, "{}", catches(std::f32::consts::FRAC_1_SQRT_2));
|
||||
assert_eq!(catches(0.0), 0.0);
|
||||
assert!((catches(-1.0) - 0.6).abs() < 1e-6);
|
||||
assert!((catches(-std::f32::consts::FRAC_1_SQRT_2) - 0.6 * 0.574).abs() < 1e-3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,25 +59,31 @@ fn cut_diffs(d: &Document, p: NodeId) -> Vec<(u32, (u8, usize))> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A pane deeper than the light may reach, over black and lit from the left: across its middle row
|
||||
/// each pixel is the light alone. It fades out smoothly where it stops, with no ring.
|
||||
/// A deep pane over black, lit from the left, at 2× (its 1-unit bevel 2 px wide): across its middle
|
||||
/// row the left rim catches the light, the right rim 0.6 as much, and in between only the faint
|
||||
/// gradient across the curve shows (black can't darken, so the lit half stays black), changing
|
||||
/// smoothly, with no ring.
|
||||
#[test]
|
||||
fn the_light_fades_out_where_it_stops() {
|
||||
fn the_light_is_a_line_on_the_bevel() {
|
||||
let mut d = Document::new("glass");
|
||||
let p = d.first_page().unwrap();
|
||||
let back = d.create(p, NodeKind::Rectangle, "Back", 0.0, 0.0, 700.0, 700.0).unwrap();
|
||||
d.get_mut(back).unwrap().fills = vec![Paint::solid(Color::BLACK)];
|
||||
let pane = d.create(p, NodeKind::Rectangle, "Glass", 50.0, 50.0, 600.0, 600.0).unwrap();
|
||||
let g = Glass { depth: 300.0, splay: 1.0, light_angle: -90.0, light_intensity: 1.0, frost: 0.0, refraction: 0.0, ..Glass::default() };
|
||||
let g = Glass { depth: 300.0, light_angle: -90.0, light_intensity: 1.0, frost: 0.0, refraction: 0.0, ..Glass::default() };
|
||||
let n = d.get_mut(pane).unwrap();
|
||||
n.fills.clear();
|
||||
n.effects = vec![Effect { kind: EffectKind::Glass(g), visible: true }];
|
||||
let full = render_view_single(&d, p, &View { origin: (0.0, 0.0), scale: 2.0, width: 1400, height: 1400 }, &Options::default()).unwrap();
|
||||
let row = &full.data()[700 * 1400 * 4..][..1400 * 4];
|
||||
let red: Vec<u8> = row.chunks(4).map(|px| px[0]).skip(101).take(598).collect();
|
||||
let step = red.windows(2).map(|w| w[0].abs_diff(w[1])).max().unwrap();
|
||||
assert!(step <= 2, "the light jumps {step} levels between neighbouring pixels");
|
||||
assert!(red[0] > 200 && red[597] < red[0], "lit at the edge, fading in: {} … {}", red[0], red[597]);
|
||||
let red: Vec<u8> = row.chunks(4).map(|px| px[0]).collect();
|
||||
// The pane covers pixels 100..1300 of the row.
|
||||
let (left, right) = (red[100], red[1299]);
|
||||
assert!(left > 230, "the left rim catches the light, less the gradient: {left}");
|
||||
assert!((140..=175).contains(&right), "the right rim 0.6 as much, plus the gradient: {right}");
|
||||
assert!(red[106] <= 14 && red[1293] <= 14, "3 units in, the gradient only: {} {}", red[106], red[1293]);
|
||||
let step = red[104..1296].windows(2).map(|w| w[0].abs_diff(w[1])).max().unwrap();
|
||||
assert!(step <= 2, "the gradient jumps {step} levels between neighbouring pixels");
|
||||
}
|
||||
|
||||
/// Digests of glass renders over a spread of settings, scales and corners: compare two builds'
|
||||
@@ -115,11 +121,13 @@ fn glass_digests() {
|
||||
|
||||
#[test]
|
||||
fn a_cut_through_the_pane_matches_the_whole_render() {
|
||||
// The last look is deep enough that the light is held to `MAX_LIGHT_PX`.
|
||||
// The last look is deep (300 units), so the curve reaches far in from every cut.
|
||||
let looks = [
|
||||
wide_light(),
|
||||
Glass::default(),
|
||||
Glass { splay: 0.5, frost: 0.0, refraction: 0.0, ..Glass::default() },
|
||||
// Splay with the bend and dispersion on: the dispersed samples follow the turned bend.
|
||||
Glass { splay: 1.0, frost: 0.0, ..Glass::default() },
|
||||
Glass { depth: 300.0, ..wide_light() },
|
||||
];
|
||||
// Rounded corners without the pane's fill: a fill's curved edge rasters 1–2 levels apart in a
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user