Shapes: align soft-edge paint and caret opacity (#1600)

* 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>
This commit is contained in:
Fin Warman
2026-11-02 10:53:00 -05:00
committed by GitHub
co-authored by Brandon Thomas
parent 242e850aaa
commit 4f2d0da04c
12 changed files with 468 additions and 7 deletions
+24
View File
@@ -4092,6 +4092,30 @@ fn equation_in_alternate_content_is_read() {
assert!(!p.plain_text().contains("[picture]"));
}
/// A fill's `a:alpha` is its opacity: a translucent white wash over a picture stayed opaque and
/// hid the picture.
#[test]
fn a_shape_fill_keeps_its_transparency() {
let fill =
r#"<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val="FFFFFF"><a:alpha val="30980"/></a:srgbClr></a:solidFill>"#;
let d = read_body(&format!("<w:p>{}</w:p>", anchored_wsp("Wash", 1270000, 635000, fill, "", "<wps:cNvSpPr/>")));
let transparency = |d: &Document| match &paras(d)[0].objects[0] {
InlineObject::Shape { fill: Some(wordcraft_doc::fill::Fill::Solid { color, transparency }), .. } => (Some(*color), *transparency),
o => panic!("{o:?}"),
};
const FILL_ALPHA: u32 = 30_980;
const ALPHA_SCALE: f32 = 100_000.0;
// Canonical Fill stores the percentage computed from opacity, including f32 rounding.
let expected = 100.0 - (FILL_ALPHA as f32 / ALPHA_SCALE) * 100.0;
assert_eq!(transparency(&d), (Some(Rgb(255, 255, 255)), expected));
let bytes = wordcraft_docx::write(&d).unwrap();
let parts = unzip(&bytes);
let xml = String::from_utf8_lossy(parts.get("word/document.xml").unwrap());
assert!(xml.contains(&format!("<a:alpha val=\"{FILL_ALPHA}\"/>")), "{xml}");
let again = wordcraft_docx::read(&bytes).unwrap();
assert_eq!(transparency(&again), (Some(Rgb(255, 255, 255)), expected));
}
/// #1500: gradient fills and transparency on shapes (the cases in the issue), from the shape's
/// own properties, the theme's fill styles (`a:fillRef` 3 and 1003, as Word's cover pages use),
/// a group's fill (`a:grpFill`), an outline gradient and VML's `v:fill`. Each is read as the
+3
View File
@@ -506,3 +506,6 @@ fn canvas_background(rect: Rect, c: &wordcraft_doc::para::CanvasStyle) -> Draw {
cap: Default::default(),
}
}
#[cfg(test)]
mod fill_alpha_tests;
@@ -0,0 +1,83 @@
use super::*;
use wordcraft_doc::connector::ShapeExtra;
use wordcraft_doc::freeform::{FreePath, Freeform, InkTool};
#[test]
fn freeform_fill_has_own_alpha() {
let rect = Rect::new(4.0, 4.0, 24.0, 24.0);
for transparency in [0.0, 50.0, 100.0] {
for closed in [false, true] {
let freeform = Freeform {
w: 24.0,
h: 24.0,
paths: vec![FreePath { pts: vec![[0.0, 0.0], [24.0, 0.0], [24.0, 24.0]], closed }],
alpha: 0.25,
..Default::default()
};
let mut out = Vec::new();
shape_draws(
rect,
&ShapeLook {
kind: ShapeKind::Freeform,
fill: Some(&wordcraft_doc::fill::Fill::Solid { color: Rgb::BLACK, transparency }),
stroke: Some(Rgb::WHITE),
stroke_width: 2.0,
effects: Default::default(),
freeform: Some(&freeform),
extra: &ShapeExtra::default(),
},
&DisplayOptions::default(),
&mut out,
);
let Draw::Path { fill, stroke, stroke_width, .. } = &out[0] else { panic!("expected freeform path") };
match fill {
Some(Paint::Solid(color, alpha)) if closed => {
assert_eq!(*color, Rgb::BLACK);
assert_eq!(*alpha, 1.0 - transparency / 100.0);
}
None if !closed => {}
_ => panic!("unexpected freeform paint: {fill:?}"),
}
assert_eq!((*stroke, *stroke_width), (Some(Rgb::WHITE), 2.0));
}
}
let ink = Freeform::ink(InkTool::Highlighter, 24.0, 24.0, vec![[0.0, 0.0], [24.0, 24.0]]);
let mut out = Vec::new();
shape_draws(
rect,
&ShapeLook {
kind: ShapeKind::Freeform,
fill: Some(&wordcraft_doc::fill::Fill::Solid { color: Rgb::BLACK, transparency: 100.0 }),
stroke: Some(Rgb::WHITE),
stroke_width: 2.0,
effects: Default::default(),
freeform: Some(&ink),
extra: &ShapeExtra::default(),
},
&DisplayOptions::default(),
&mut out,
);
assert!(matches!(out.as_slice(), [Draw::Ink { alpha, .. }] if *alpha == 0.5));
}
#[test]
fn other_path_alpha_defaults() {
let rect = Rect::new(4.0, 4.0, 24.0, 24.0);
let graphic = Graphic {
items: vec![GraphicItem::Path {
segs: vec![PathSeg::Move(0.0, 0.0), PathSeg::Line(24.0, 24.0)],
fill: Some(Rgb::BLACK),
stroke: None,
stroke_width: 0.0,
}],
..Default::default()
};
let mut out = graphic_draws(&Document::new(), &graphic, rect, 1.0);
out.push(path_arrowhead(&[(4.0, 4.0), (28.0, 28.0)], Rgb::BLACK, 2.0, ArrowStyle::default()).unwrap());
let extra = ShapeExtra { arrow_start: true, arrow_end: true, ..Default::default() };
line_draws(rect, ShapeKind::ElbowConnector, Some(Rgb::BLACK), 2.0, Default::default(), &extra, &mut out);
assert_eq!(out.len(), 5);
assert!(out.iter().all(
|item| matches!(item, Draw::Path { fill, .. } if fill.as_ref().is_none_or(|paint| matches!(paint, Paint::Solid(_, alpha) if *alpha == 1.0)))
));
}
+10
View File
@@ -32,6 +32,16 @@ impl Paint {
Paint::Radial { c, r, stops } => Paint::Radial { c: *c, r: *r, stops: fade(stops) },
}
}
/// Inset soft-edge paints. Solid bands composite to the fill's target opacity.
pub fn soft_bands(&self, radius: f32) -> Vec<(f32, Paint)> {
match self {
Paint::Solid(color, alpha) => {
let alpha = if alpha.is_finite() { alpha.clamp(0.0, 1.0) } else { 1.0 };
wordcraft_doc::effects::bands(-radius, 0.0, alpha).into_iter().map(|(grow, band)| (grow, Paint::Solid(*color, band))).collect()
}
_ => wordcraft_doc::effects::bands(-radius, 0.0, 1.0).into_iter().map(|(grow, alpha)| (grow, self.faded(alpha))).collect(),
}
}
/// The colour and opacity this paint gives the point (`x`, `y`): stops blended, padded past
/// the ends.
pub fn at(&self, x: f32, y: f32) -> (Rgb, f32) {
+2
View File
@@ -12762,3 +12762,5 @@ fn header_margin_table_area() {
}
}
}
mod shape_alpha;
+24
View File
@@ -0,0 +1,24 @@
use super::*;
#[test]
fn a_translucent_shape_fill_is_drawn_translucent() {
let mut d = Document::from_text("Wash");
let wash = InlineObject::Shape {
kind: wordcraft_doc::para::ShapeKind::Rectangle,
w: 100.0,
h: 50.0,
fill: Some(wordcraft_doc::fill::Fill::Solid { color: wordcraft_doc::props::Rgb::WHITE, transparency: 69.0 }),
stroke: None,
stroke_width: 0.0,
float: Default::default(),
story: None,
freeform: None,
effects: Default::default(),
extra: Default::default(),
};
d.insert_object(&Pos::body(0, 0), wash, &Default::default()).unwrap();
let draws = flat(display::page_display(&d, &lay(&d).pages[0], &Default::default()));
let alpha =
draws.iter().find_map(|x| if let display::Draw::Shape { fill: Some(display::Paint::Solid(_, alpha)), .. } = x { Some(*alpha) } else { None });
assert!(alpha.is_some_and(|a| (a - 0.31).abs() < 0.001), "fill opacity {alpha:?}");
}
+2 -2
View File
@@ -104,9 +104,9 @@ impl Exporter<'_> {
if let (Some(rad), Some(c), true) = (fx.soft_edge, f.as_ref(), can_fill) {
let rad = rad.min(rect.w.min(rect.h) / 2.0).max(0.0);
s.set_stroke(None);
for (g, a) in wordcraft_doc::effects::bands(-rad, 0.0, 1.0) {
for (g, paint) in c.soft_bands(rad) {
if let Some(p) = shape_path(*kind, &grown(rect, g, 0.0, 0.0), *corner) {
s.set_fill(Some(paint_fill(&c.faded(a))));
s.set_fill(Some(paint_fill(&paint)));
s.draw_path(&p);
}
}
+161
View File
@@ -0,0 +1,161 @@
use super::*;
use hayro_interpret::font::Glyph;
use hayro_interpret::{
BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, SoftMask, interpret_page,
};
use kurbo::Shape;
use wordcraft_doc::effects::ShapeEffects;
use wordcraft_doc::freeform::{FreePath, Freeform};
use wordcraft_doc::para::{Anchor, Float, InlineObject, ShapeKind, Wrap};
use wordcraft_doc::{CharProps, Document, Paragraph, para_block};
const ALPHA_TOLERANCE: f32 = 0.015;
const RED: wordcraft_doc::Rgb = wordcraft_doc::Rgb(255, 0, 0);
const GREEN: wordcraft_doc::Rgb = wordcraft_doc::Rgb(0, 255, 0);
struct PaintedPath {
path: kurbo::BezPath,
color: [u8; 4],
}
struct Paints {
fills: Vec<PaintedPath>,
strokes: Vec<PaintedPath>,
groups: Vec<f32>,
}
impl Device<'_> for Paints {
fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
fn set_blend_mode(&mut self, _: BlendMode) {}
fn draw_path(&mut self, path: &kurbo::BezPath, transform: kurbo::Affine, paint: &Paint<'_>, mode: &PathDrawMode) {
let Paint::Color(color) = paint else { panic!("solid fixture paint") };
let mut color = color.to_rgba().to_rgba8();
color[3] = (f32::from(color[3]) * self.groups.last().copied().unwrap_or(1.0)).round() as u8;
let record = PaintedPath { path: transform * path.clone(), color };
match mode {
PathDrawMode::Fill(_) => self.fills.push(record),
PathDrawMode::Stroke(_) => self.strokes.push(record),
}
}
fn push_clip_path(&mut self, _: &ClipPath) {}
fn pop_clip_path(&mut self) {}
fn push_transparency_group(&mut self, alpha: f32, _: Option<SoftMask<'_>>, _: BlendMode) {
self.groups.push(self.groups.last().copied().unwrap_or(1.0) * alpha);
}
fn pop_transparency_group(&mut self) {
assert!(self.groups.len() > 1);
let _ = self.groups.pop();
}
fn draw_glyph(&mut self, _: &Glyph<'_>, _: kurbo::Affine, _: kurbo::Affine, _: &Paint<'_>, _: &GlyphDrawMode) {}
fn draw_image(&mut self, _: Image<'_, '_>, _: kurbo::Affine) {}
}
impl Paints {
fn red_coverage(&self, point: (f64, f64)) -> f32 {
1.0 - self
.fills
.iter()
.filter(|r| r.color[..3] == [255, 0, 0] && r.path.contains(point.into()))
.map(|r| 1.0 - f32::from(r.color[3]) / 255.0)
.product::<f32>()
}
}
fn paints(shape: InlineObject) -> Paints {
let mut doc = Document::new();
let mut para = Paragraph::new();
para.insert_object(0, shape, &CharProps::default()).unwrap();
doc.body = vec![para_block(para)];
let bytes = export(&doc, &PdfOptions { tagged: false, ..Default::default() }).unwrap();
let pdf = hayro_syntax::Pdf::new(bytes).unwrap();
let cache = InterpreterCache::new();
let mut paints = Paints { fills: Vec::new(), strokes: Vec::new(), groups: vec![1.0] };
for page in pdf.pages().iter() {
let bounds = page.media_box();
let (width, height) = (bounds.width(), bounds.height());
// PDF uses bottom-up coordinates; the fixture points use the layout's top-down frame.
let top_down = kurbo::Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, height]);
let mut ctx = Context::new(top_down, kurbo::Rect::new(0.0, 0.0, width, height), &cache, pdf.xref(), InterpreterSettings::default());
interpret_page(page, &mut ctx, &mut paints);
}
assert_eq!(paints.groups, [1.0]);
paints
}
fn shape(kind: ShapeKind, transparency: f32, effects: ShapeEffects) -> InlineObject {
InlineObject::Shape {
kind,
w: 24.0,
h: 24.0,
fill: Some(wordcraft_doc::fill::Fill::Solid { color: RED, transparency }),
stroke: None,
stroke_width: 0.0,
float: Float { wrap: Wrap::InFrontOfText, h_rel: Anchor::Page, v_rel: Anchor::Page, x: 100.0, y: 100.0, ..Default::default() },
story: None,
freeform: None,
effects,
extra: Default::default(),
}
}
fn close(actual: f32, want: f32) {
assert!((actual - want).abs() < ALPHA_TOLERANCE, "{actual} != {want}");
}
#[test]
fn freeform_fill_keeps_outline() {
for (transparency, alpha) in [(0.0, 1.0), (50.0, 0.5), (100.0, 0.0)] {
let mut item = shape(ShapeKind::Freeform, transparency, ShapeEffects::default());
if let InlineObject::Shape { freeform, stroke, stroke_width, .. } = &mut item {
*freeform = Some(std::sync::Arc::new(Freeform {
w: 24.0,
h: 24.0,
paths: vec![FreePath { pts: vec![[0.0, 0.0], [24.0, 0.0], [24.0, 24.0], [0.0, 24.0]], closed: true }],
alpha: 0.25,
..Default::default()
}));
*stroke = Some(GREEN);
*stroke_width = 2.0;
}
let paints = paints(item);
close(paints.red_coverage((112.0, 112.0)), alpha);
assert_eq!(paints.strokes.len(), 1);
assert_eq!(paints.strokes[0].color, [0, 255, 0, 255]);
}
}
#[test]
fn soft_edge_target_alpha() {
for kind in [ShapeKind::Rectangle, ShapeKind::Ellipse] {
for (radius, count) in [(3.0, 3.0), (6.0, 4.0)] {
for (transparency, alpha) in [(0.0, 1.0), (50.0, 0.5), (100.0, 0.0), (f32::NAN, 1.0)] {
let effects = ShapeEffects { soft_edge: Some(radius), ..Default::default() };
let paints = paints(shape(kind, transparency, effects));
close(paints.red_coverage((112.0, 112.0)), alpha);
close(paints.red_coverage((101.125, 112.0)), alpha / count);
close(paints.red_coverage((99.0, 112.0)), 0.0);
assert!(paints.strokes.is_empty());
}
}
}
}
#[test]
fn soft_edge_keeps_effect_paint() {
let effects = ShapeEffects {
shadow: Some(wordcraft_doc::effects::Shadow::default()),
glow: Some(wordcraft_doc::effects::Glow::default()),
soft_edge: Some(6.0),
};
let opaque = paints(shape(ShapeKind::Rectangle, 0.0, effects));
let clear = paints(shape(ShapeKind::Rectangle, 100.0, effects));
let other = |r: &&PaintedPath| r.color[..3] != [255, 0, 0];
let before: Vec<_> = opaque.fills.iter().filter(other).map(|r| (&r.path, r.color)).collect();
let after: Vec<_> = clear.fills.iter().filter(other).map(|r| (&r.path, r.color)).collect();
assert!(!before.is_empty());
assert_eq!(before, after);
assert!(opaque.strokes.is_empty() && clear.strokes.is_empty());
for (transparency, alpha) in [(0.0, 1.0), (50.0, 0.5), (100.0, 0.0)] {
close(paints(shape(ShapeKind::Rectangle, transparency, ShapeEffects::default())).red_coverage((112.0, 112.0)), alpha);
}
}
+3
View File
@@ -76,3 +76,6 @@ use picture::{load_image, load_image_within};
#[cfg(test)]
mod tests;
#[cfg(test)]
mod fill_alpha_tests;
+123
View File
@@ -0,0 +1,123 @@
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:?}");
}
}
}
}
}
+31 -4
View File
@@ -342,6 +342,28 @@ pub fn background_at(doc: &Document, page: &Page, opts: &RenderOptions, x: f32,
Rgb(c(bg[0]), c(bg[1]), c(bg[2]))
}
// Sample the same inset silhouettes and paints used by the raster and PDF.
fn soft_fill_alpha(
rect: wordcraft_geom::Rect,
kind: ShapeKind,
corner: Option<u32>,
radius: f32,
fill: &wordcraft_layout::display::Paint,
p: wordcraft_geom::Point,
) -> f32 {
let radius = radius.min(rect.w.min(rect.h) / 2.0).max(0.0);
let frame = kurbo::Rect::new(rect.x as f64, rect.y as f64, rect.right() as f64, rect.bottom() as f64);
let point = kurbo::Point::new(p.x as f64, p.y as f64);
let mut opacity = 0.0;
for (grow, paint) in fill.soft_bands(radius) {
let inset = frame.inflate(grow as f64, grow as f64);
if inset.width() > 0.0 && inset.height() > 0.0 && kurbo::Shape::contains(&shape_path(kind, inset, corner), point) {
opacity += (1.0 - opacity) * paint.at(p.x, p.y).1;
}
}
opacity
}
fn blend_backgrounds(items: &[Draw], p: wordcraft_geom::Point, opts: &RenderOptions, bg: &mut [f32; 3], depth: u32) {
if depth > 16 {
return;
@@ -351,13 +373,15 @@ fn blend_backgrounds(items: &[Draw], p: wordcraft_geom::Point, opts: &RenderOpti
Draw::Fill { rect, color, alpha, .. } if rect.contains(p) => (*color, *alpha),
// The shape's own outline (an ellipse's corners are not inside it), in its fill's
// colour and opacity there (#1500: gradients and transparency).
Draw::Shape { rect, kind, fill: Some(fill), corner, .. }
Draw::Shape { rect, kind, fill: Some(fill), corner, effects, .. }
if *kind != ShapeKind::Line && rect.contains(p) && {
let r = kurbo::Rect::new(rect.x as f64, rect.y as f64, rect.right() as f64, rect.bottom() as f64);
kurbo::Shape::contains(&shape_path(*kind, r, *corner), kurbo::Point::new(p.x as f64, p.y as f64))
} =>
{
fill.at(p.x, p.y)
let (color, alpha) = fill.at(p.x, p.y);
let alpha = effects.sanitized().soft_edge.map_or(alpha, |radius| soft_fill_alpha(*rect, *kind, *corner, radius, fill, p));
(color, alpha)
}
Draw::Clip { rect, items } if rect.contains(p) => {
blend_backgrounds(items, p, opts, bg, depth + 1);
@@ -913,10 +937,10 @@ fn shape_shadow_and_glow(ctx: &mut RenderContext, fr: &Frame, (geom, sw): (&Shap
/// Soft edges: the fill fades out toward the outline (which fades with it).
fn shape_soft_edges(ctx: &mut RenderContext, fr: &Frame, geom: &ShapeGeom, (rad, f): (f32, &Paint)) {
for (g, a) in wordcraft_doc::effects::bands(-rad, 0.0, 1.0) {
for (g, paint) in f.soft_bands(rad) {
let rr = geom.r.inflate(g as f64, g as f64);
if rr.width() > 0.0 && rr.height() > 0.0 {
set_paint(ctx, &f.faded(a), fr.opts);
set_paint(ctx, &paint, fr.opts);
ctx.fill_path(&shape_path(geom.kind, rr, geom.corner));
}
}
@@ -1103,3 +1127,6 @@ mod tests;
#[cfg(test)]
mod cache_tests;
#[cfg(test)]
mod fill_alpha_tests;
File diff suppressed because one or more lines are too long