mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 06:07:06 -06:00
Fix Drop Shadow Darkness in source color space
This commit is contained in:
@@ -94,11 +94,11 @@ impl Renderer {
|
||||
let whole = fits && k >= 1.0;
|
||||
let in_view = (!whole).then(|| ViewCrop::of(f.view, room));
|
||||
let stamp = self.stamp;
|
||||
let channels = match f.ink {
|
||||
let channels = match f.ink.base() {
|
||||
Ink::Cmy => effects::pixel::Channels::CmyPlane,
|
||||
Ink::K => effects::pixel::Channels::KPlane,
|
||||
Ink::Display | Ink::RichBlack if f.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk => effects::pixel::Channels::Cmyk,
|
||||
Ink::Display | Ink::RichBlack => effects::pixel::Channels::Rgb,
|
||||
_ => effects::pixel::Channels::Rgb,
|
||||
};
|
||||
// De-Interlace's field lines: the rows of the document's raster grid (1 to 2400 ppi).
|
||||
let ppi = doc_raster_ppi(f.doc);
|
||||
|
||||
@@ -11,21 +11,11 @@ pub(super) fn shadow_filter(dx: f64, dy: f64, blur: f64, color: peniko::Color) -
|
||||
})
|
||||
}
|
||||
|
||||
/// The layer filter of a drop shadow with Darkness: the object's own pixels blurred.
|
||||
/// The layer filter of a drop shadow with Darkness: source paints are adjusted before blur.
|
||||
pub(super) fn shade_filter(blur: f64) -> Filter {
|
||||
Filter::from_primitive(FilterPrimitive::GaussianBlur { std_deviation: (blur / 2.0).max(0.0) as f32, edge_mode: EdgeMode::None })
|
||||
}
|
||||
|
||||
/// How much darker (0..1) a drop shadow with Darkness `darkness` makes the values `ink` paints:
|
||||
/// black added leaves the cyan, magenta and yellow plane of a CMYK document as it is and scales
|
||||
/// the complemented black plane, as it scales screen colors, by 1 − `darkness`.
|
||||
pub(super) fn shade(ink: Ink, darkness: f32) -> f32 {
|
||||
match ink {
|
||||
Ink::Cmy => 0.0,
|
||||
Ink::K | Ink::Display | Ink::RichBlack => darkness,
|
||||
}
|
||||
}
|
||||
|
||||
/// What a shadow or glow raster is made from ([`Made::pixels`]): the silhouette's alpha, or its
|
||||
/// premultiplied RGBA when the shadow takes the object's own colors (Darkness).
|
||||
pub(super) fn silhouette(pm: &vello_cpu::Pixmap, colored: bool) -> Vec<u8> {
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::bounds::{cull_bounds, effected_path};
|
||||
use super::entries::{Content, Drawn, FX_RASTER_BUDGET, FxGeom, Inputs, Kept, Made, Reads, Shadow, ShadowEntry};
|
||||
use super::predicates::{evaluates_live, is_container, rasterize_bounds, reads_outside, shifted};
|
||||
use super::raster::{
|
||||
DRAG_PIXEL_BUDGET, MAX_RASTERIZE, PIXEL_BUDGET, ViewCrop, content_changing, doc_raster_ppi, image_size, linear_key, pan_margin, pcolor, shade,
|
||||
DRAG_PIXEL_BUDGET, MAX_RASTERIZE, PIXEL_BUDGET, ViewCrop, content_changing, doc_raster_ppi, image_size, linear_key, pan_margin, pcolor,
|
||||
shade_filter, shadow_filter, silhouette,
|
||||
};
|
||||
use super::{
|
||||
@@ -115,7 +115,8 @@ impl Renderer {
|
||||
let view = Affine::scale(1.0 / scale) * Affine::translate((-x0, -y0)) * f.view;
|
||||
// Art is culled against the raster's own area: members of a group outside the view cast their shadows in it.
|
||||
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
let crop = Frame { mt: false, view, visible, ..*f };
|
||||
let ink = darkness.map_or(f.ink, |d| f.ink.darkened(d));
|
||||
let crop = Frame { mt: false, view, visible, ink, ..*f };
|
||||
let before = self.stats.moved;
|
||||
self.inside_layer(|r| paint(r, &mut off, &shifted(&crop, dx, dy)));
|
||||
// A silhouette drawn over moved rasters makes a moved shadow.
|
||||
@@ -128,9 +129,8 @@ impl Renderer {
|
||||
Made { pixels: silhouette(&pm, darkness.is_some()), fx: vec![fx.clone()], size: reach.size(), channels: None, outline: None };
|
||||
let (wu, hu) = (w as usize, h as usize);
|
||||
let sigma = blur / 2.0 / f.px / scale;
|
||||
if let Some(darkness) = darkness {
|
||||
if darkness.is_some() {
|
||||
// The object's own colors, blurred, with the black Darkness adds (premultiplied).
|
||||
let keep = 1.0 - shade(f.ink, darkness);
|
||||
let mut planes: [Vec<f32>; 4] = [
|
||||
pm.data().iter().map(|p| p.r as f32).collect(),
|
||||
pm.data().iter().map(|p| p.g as f32).collect(),
|
||||
@@ -143,7 +143,7 @@ impl Renderer {
|
||||
let [r, g, b, a] = &planes;
|
||||
for (k, px) in pm.data_mut().iter_mut().enumerate() {
|
||||
let al = (a[k] * opacity).clamp(0.0, 255.0).round();
|
||||
let value = |plane: &Vec<f32>| (plane[k] * opacity * keep).round().clamp(0.0, al) as u8;
|
||||
let value = |plane: &Vec<f32>| (plane[k] * opacity).round().clamp(0.0, al) as u8;
|
||||
*px = vello_cpu::color::PremulRgba8 { r: value(r), g: value(g), b: value(b), a: al as u8 };
|
||||
}
|
||||
} else {
|
||||
@@ -219,15 +219,9 @@ impl Renderer {
|
||||
self.with_filters(ctx, f, reach, blur, Some((mode, opacity)), |r, c, fr, comp| {
|
||||
c.set_transform(fr.view);
|
||||
c.push_layer(None, comp.map(|m| blend_mode(m.0)), comp.map(|m| m.1), None, Some(filter));
|
||||
paint(r, c, &shifted(fr, dx, dy));
|
||||
// Darkness: black over the object's own colors, keeping their alpha.
|
||||
if let Some(black) = darkness.map(|d| shade(f.ink, d)).filter(|d| *d > 0.0) {
|
||||
c.set_transform(fr.view);
|
||||
c.set_blend_mode(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcAtop));
|
||||
c.set_paint(peniko::Color::new([0.0, 0.0, 0.0, black]));
|
||||
c.fill_rect(&reach);
|
||||
c.set_blend_mode(blend_mode(vectorcraft_doc::color::BlendMode::Normal));
|
||||
}
|
||||
let ink = darkness.map_or(fr.ink, |d| fr.ink.darkened(d));
|
||||
let source = Frame { ink, ..*fr };
|
||||
paint(r, c, &shifted(&source, dx, dy));
|
||||
c.pop_layer();
|
||||
});
|
||||
}
|
||||
@@ -278,7 +272,11 @@ impl Renderer {
|
||||
let view = Affine::scale(1.0 / scale) * Affine::translate((-(p.x + rel.0), -(p.y + rel.1))) * f.view;
|
||||
// Culled against the raster's area, as `shadow_raster` draws the silhouette.
|
||||
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
let crop = Frame { mt: false, view, visible, ..*f };
|
||||
let ink = match fx {
|
||||
RasterFx::DropShadow { darkness: Some(d), .. } => f.ink.darkened(*d),
|
||||
_ => f.ink,
|
||||
};
|
||||
let crop = Frame { mt: false, view, visible, ink, ..*f };
|
||||
self.inside_layer(|r| paint(r, &mut off, &shifted(&crop, dx, dy)));
|
||||
off.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(w, h);
|
||||
@@ -696,3 +694,44 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::RenderOptions;
|
||||
use vectorcraft_color::Color;
|
||||
use vectorcraft_doc::{Document, color::BlendMode};
|
||||
|
||||
#[test]
|
||||
fn uncached_darkness_shadow_subtracts_before_separate_opacity() {
|
||||
let doc = Document::new(100.0, 100.0);
|
||||
let symbols = doc.symbol_boxes().quick();
|
||||
let opts = RenderOptions::default();
|
||||
let frame = Frame {
|
||||
mt: false,
|
||||
doc: &doc,
|
||||
symbols: &symbols,
|
||||
view: Affine::IDENTITY,
|
||||
visible: Rect::new(0.0, 0.0, 100.0, 100.0),
|
||||
px: 1.0,
|
||||
opts: &opts,
|
||||
ink: Ink::Display,
|
||||
};
|
||||
let mut ctx = frame.offscreen_context(100, 100);
|
||||
let mut r = Renderer::new();
|
||||
let fx =
|
||||
RasterFx::DropShadow { mode: BlendMode::Normal, opacity: 0.5, dx: 40.0, dy: 0.0, blur: 0.0, color: Color::BLACK, darkness: Some(0.25) };
|
||||
let source = Rect::new(10.0, 10.0, 30.0, 30.0);
|
||||
r.draw_shadow_filter(&mut ctx, &frame, &fx, source, &mut |_, c, f| {
|
||||
c.set_transform(f.view);
|
||||
c.set_paint(f.ink.color(&Color::rgb(0.8, 0.4, 0.2), 1.0));
|
||||
c.fill_rect(&source);
|
||||
});
|
||||
ctx.flush();
|
||||
let mut pm = vello_cpu::Pixmap::new(100, 100);
|
||||
ctx.render(&mut pm, &mut r.resources);
|
||||
let px = pm.data()[20 * 100 + 60];
|
||||
assert!(px.r.abs_diff(70) <= 1 && px.g.abs_diff(19) <= 1 && px.b == 0 && px.a.abs_diff(128) <= 1, "{px:?}");
|
||||
assert!(r.shadows.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ fn shaded_doc(mode: vectorcraft_doc::ColorMode, fill: Color, shade: &str, darkne
|
||||
d
|
||||
}
|
||||
|
||||
/// Darkness: the shadow is the object's own color, each channel times 1 − darkness in an RGB
|
||||
/// Darkness: the shadow is the object's own color, with darkness subtracted from each RGB channel in an RGB
|
||||
/// document (the reference app's guide gives the object's color at 0%,
|
||||
/// <https://help.adobe.com/archive/en/illustrator/cc/2014/illustrator_reference.pdf> page 428), in the cached
|
||||
/// raster and, at 6000 %, in the filter layer that a raster too large to cache is drawn with.
|
||||
@@ -298,9 +298,8 @@ fn drop_shadow_darkness_darkens_the_objects_own_color() {
|
||||
}
|
||||
}
|
||||
|
||||
/// In a CMYK document Darkness adds black ink and keeps the object's cyan, magenta and yellow
|
||||
/// (same page of the guide: at 100% an object with colors other than black gets a shadow of those colors under
|
||||
/// full black, and a black object's shadow is 100% black).
|
||||
/// Intermediate Darkness adds black ink while preserving C/M/Y. The top endpoint becomes
|
||||
/// gray black before ink conversion; its output below uses this application's gray-color model.
|
||||
#[test]
|
||||
fn drop_shadow_darkness_adds_black_in_a_cmyk_document() {
|
||||
let cmyk = vectorcraft_doc::ColorMode::Cmyk;
|
||||
@@ -312,15 +311,31 @@ fn drop_shadow_darkness_adds_black_in_a_cmyk_document() {
|
||||
let red = Color::cmyk(0.0, 1.0, 1.0, 0.0);
|
||||
for (darkness, k) in [(0.0, 0), (50.0, 128), (100.0, 255)] {
|
||||
let got = inks(&shaded_doc(cmyk, red, "darkness", darkness));
|
||||
assert!(got[0] == 0 && got[1] == 255 && got[2] == 255 && got[3].abs_diff(k) <= 1, "{darkness}%: {got:?}");
|
||||
let my = if darkness == 100.0 { 0 } else { 255 };
|
||||
assert!(got[0] == 0 && got[1] == my && got[2] == my && got[3].abs_diff(k) <= 1, "{darkness}%: {got:?}");
|
||||
}
|
||||
// Half black ink, darkened by half: K 50 + 50 × 50% = K 75.
|
||||
// Half black ink plus 50 percentage points clamps at full black.
|
||||
let got = inks(&shaded_doc(cmyk, Color::cmyk(0.0, 0.0, 0.0, 0.5), "darkness", 50.0));
|
||||
assert!(got[..3] == [0, 0, 0] && got[3].abs_diff(191) <= 1, "{got:?}");
|
||||
assert!(got[..3] == [0, 0, 0] && got[3].abs_diff(255) <= 1, "{got:?}");
|
||||
let got = inks(&shaded_doc(cmyk, Color::cmyk(0.0, 0.0, 0.0, 1.0), "darkness", 100.0));
|
||||
assert_eq!(got, [0, 0, 0, 255]);
|
||||
}
|
||||
|
||||
/// The top endpoint is gray black before document/profile conversion, independently of source model.
|
||||
#[test]
|
||||
fn darkness_black_endpoint_and_gray_source_follow_their_stored_models() {
|
||||
for mode in [vectorcraft_doc::ColorMode::Rgb, vectorcraft_doc::ColorMode::Cmyk] {
|
||||
for source in [Color::rgb(0.8, 0.4, 0.2), Color::cmyk(0.1, 0.3, 0.4, 0.2), Color::gray(0.2)] {
|
||||
let got = render(&shaded_doc(mode, source, "darkness", 100.0)).pixel(66, 66);
|
||||
let want = render(&shaded_doc(mode, Color::gray(1.0), "darkness", 0.0)).pixel(66, 66);
|
||||
assert_eq!(got, want, "{mode:?}, {source:?}");
|
||||
}
|
||||
let got = render(&shaded_doc(mode, Color::gray(0.2), "darkness", 25.0)).pixel(66, 66);
|
||||
let want = render(&shaded_doc(mode, Color::gray(0.45), "darkness", 0.0)).pixel(66, 66);
|
||||
assert_eq!(got, want, "gray intermediate in {mode:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Color stays the default: a shadow without the new keys, and one with Color chosen and a
|
||||
/// Darkness value, draw the same pixels, which a shadow with Darkness does not.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user