mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-10 20:51:50 +00:00
* Shapes: a solid fill keeps its transparency A shape's fill colour can carry a:alpha, its opacity. WordCraft dropped it, so a translucent white wash laid over a picture was drawn opaque and hid the picture. The transparency is now read into the shape, drawn on screen and in the PDF, and written back. * Fix #1600 freeform and soft-edge fill opacity * #1600: wrap canonical caret sampler signature * #1600: format canonical integration * #1600: assert canonical DOCX alpha round-trip * #1600: document canonical shape opacity API * Adapt alpha fixture to rectangle render bounds * docs: one file-format-parity row for soft-edge fill opacity, status line --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
124 lines
4.9 KiB
Rust
124 lines
4.9 KiB
Rust
use super::*;
|
|
use wordcraft_doc::effects::{Glow, Shadow, ShapeEffects};
|
|
use wordcraft_doc::graphic::PathSeg;
|
|
use wordcraft_geom::Rect;
|
|
|
|
const PIXEL_TOLERANCE: u8 = 3;
|
|
const BLUE: Rgb = Rgb(0, 0, 255);
|
|
const RED: Rgb = Rgb(255, 0, 0);
|
|
const GREEN: Rgb = Rgb(0, 255, 0);
|
|
|
|
fn image(item: Draw) -> Rendered {
|
|
let items = [Draw::Fill { rect: Rect::new(0.0, 0.0, 40.0, 40.0), color: BLUE, alpha: 1.0, text_bg: false }, item];
|
|
render_items(&Document::new(), &items, Rect::new(0.0, 0.0, 40.0, 40.0), 4.0, &RenderOptions::default())
|
|
}
|
|
|
|
fn close(pixel: [u8; 4], alpha: f32) {
|
|
let red = (255.0 * alpha).round() as u8;
|
|
let blue = (255.0 * (1.0 - alpha)).round() as u8;
|
|
let want = [red, 0, blue, 255];
|
|
assert!(pixel.into_iter().zip(want).all(|(a, b)| a.abs_diff(b) <= PIXEL_TOLERANCE), "{pixel:?} != {want:?}");
|
|
}
|
|
|
|
fn soft_shape(kind: ShapeKind, alpha: f32, radius: f32, effects: ShapeEffects) -> Draw {
|
|
Draw::Shape {
|
|
rect: Rect::new(4.0, 4.0, 24.0, 24.0),
|
|
kind,
|
|
fill: Some(wordcraft_layout::display::Paint::Solid(RED, alpha)),
|
|
stroke: None,
|
|
stroke_width: 0.0,
|
|
dash: Vec::new(),
|
|
cap: Default::default(),
|
|
corner: None,
|
|
effects: ShapeEffects { soft_edge: Some(radius), ..effects },
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn path_fill_keeps_stroke_alpha() {
|
|
for alpha in [0.0, 0.5, 1.0] {
|
|
let item = Draw::Path {
|
|
segs: vec![PathSeg::Move(4.0, 4.0), PathSeg::Line(28.0, 4.0), PathSeg::Line(28.0, 28.0), PathSeg::Line(4.0, 28.0), PathSeg::Close],
|
|
fill: Some(wordcraft_layout::display::Paint::Solid(RED, alpha)),
|
|
stroke: Some(GREEN),
|
|
stroke_width: 2.0,
|
|
dash: Vec::new(),
|
|
cap: Default::default(),
|
|
};
|
|
let rendered = image(item);
|
|
close(rendered.pixel(64, 64), alpha);
|
|
assert_eq!(rendered.pixel(16, 64), [0, 255, 0, 255], "stroke at fill alpha {alpha}");
|
|
}
|
|
let ink = image(Draw::Ink { pts: vec![(4.0, 16.0), (28.0, 16.0)], color: RED, width: 4.0, alpha: 0.5 });
|
|
close(ink.pixel(64, 64), 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn soft_edge_has_target_alpha() {
|
|
for kind in [ShapeKind::Rectangle, ShapeKind::Ellipse] {
|
|
for (radius, count) in [(3.0, 3.0), (6.0, 4.0)] {
|
|
for alpha in [0.0, 0.5, 1.0] {
|
|
let rendered = image(soft_shape(kind, alpha, radius, ShapeEffects::default()));
|
|
close(rendered.pixel(64, 64), alpha);
|
|
close(rendered.pixel(20, 64), alpha / count);
|
|
assert_eq!(rendered.pixel(0, 64), [0, 0, 255, 255]);
|
|
}
|
|
}
|
|
let opaque = image(soft_shape(kind, 1.0, 6.0, ShapeEffects::default()));
|
|
for alpha in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 2.0] {
|
|
assert_eq!(image(soft_shape(kind, alpha, 6.0, ShapeEffects::default())).pixels, opaque.pixels);
|
|
}
|
|
assert_eq!(
|
|
image(soft_shape(kind, -1.0, 6.0, ShapeEffects::default())).pixels,
|
|
image(soft_shape(kind, 0.0, 6.0, ShapeEffects::default())).pixels,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn soft_edge_keeps_effects() {
|
|
let effects = ShapeEffects { shadow: Some(Shadow::default()), glow: Some(Glow::default()), soft_edge: None };
|
|
let opaque = image(soft_shape(ShapeKind::Rectangle, 1.0, 6.0, effects));
|
|
let clear = image(soft_shape(ShapeKind::Rectangle, 0.0, 6.0, effects));
|
|
let mut effect_pixels = 0;
|
|
for y in 0..160 {
|
|
for x in 0..160 {
|
|
if (16..112).contains(&x) && (16..112).contains(&y) {
|
|
continue;
|
|
}
|
|
assert_eq!(opaque.pixel(x, y), clear.pixel(x, y));
|
|
effect_pixels += usize::from(opaque.pixel(x, y) != [0, 0, 255, 255]);
|
|
}
|
|
}
|
|
assert!(effect_pixels > 0);
|
|
let stroke = |alpha| {
|
|
let mut item = soft_shape(ShapeKind::Rectangle, alpha, 6.0, ShapeEffects::default());
|
|
if let Draw::Shape { fill, stroke, stroke_width, .. } = &mut item {
|
|
*fill = None;
|
|
*stroke = Some(GREEN);
|
|
*stroke_width = 2.0;
|
|
}
|
|
image(item)
|
|
};
|
|
assert_eq!(stroke(0.0).pixels, stroke(1.0).pixels);
|
|
}
|
|
|
|
#[test]
|
|
fn caret_samples_soft_edge_bands() {
|
|
for kind in [ShapeKind::Rectangle, ShapeKind::Ellipse] {
|
|
for alpha in [0.0, 0.5, 1.0] {
|
|
let shape = soft_shape(kind, alpha, 6.0, ShapeEffects::default());
|
|
let rendered = image(shape.clone());
|
|
for (x, y) in [(18, 64), (22, 64), (30, 64), (64, 64)] {
|
|
let mut bg = [0.0, 0.0, 255.0];
|
|
let point = wordcraft_geom::Point::new((x as f32 + 0.5) / 4.0, (y as f32 + 0.5) / 4.0);
|
|
blend_backgrounds(std::slice::from_ref(&shape), point, &RenderOptions::default(), &mut bg, 0);
|
|
let pixel = rendered.pixel(x, y);
|
|
for (sample, channel) in bg.into_iter().zip(pixel) {
|
|
assert!((sample - channel as f32).abs() <= PIXEL_TOLERANCE as f32, "{kind:?}, alpha={alpha}, ({x},{y}): {bg:?} vs {pixel:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|