mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 11:26:50 -06:00
- Inner shadows paint over the fills and under the children and strokes, as documented (canvas, PNG, PDF and SVG alike; text fills every run before the inner shadow and the strokes). - Layer and background blur gain a progressive type: the radius ramps linearly from a start radius at a start point to the end radius at an end point (unit-box points, like gradient handles). Rendered by blending a few geometric blur levels, each blurred only around the pixels that use it; bands and partial renders match one piece. Design panel type menu and Start/End fields, on-canvas handles while the settings are open, effect.add/effect.set params, startRadius variable bindings, scale tool, REST import (blurType, startRadius, startOffset, endOffset), Inspect row; CSS leaves progressive blurs out. - SVG export embeds renders for what filters can't match: noise grains over the vector layer (crisp, blended), texture and progressive layer blur whole, background blur and glass from what lies under the layer among the exported layers. PDF and SVG share the renderer's helpers. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.9 KiB
Rust
93 lines
3.9 KiB
Rust
//! The PDF export draws background blur and glass from what lies under the layer on the page, the
|
||
//! way the PNG export does: the images it places match the PNG's pixels.
|
||
|
||
use figcraft_color::{Color, Paint};
|
||
use figcraft_doc::{Document, Effect, EffectKind, Glass, NodeId, NodeKind};
|
||
use figcraft_geom::{Affine, Rect};
|
||
|
||
use crate::PdfOptions;
|
||
use crate::draw::Exporter;
|
||
|
||
fn solid(d: &mut Document, id: NodeId, hex: &str) {
|
||
d.get_mut(id).unwrap().fills = vec![Paint::solid(Color::from_hex(hex).unwrap())];
|
||
}
|
||
|
||
/// A red clipping frame with stripes and a transparent pane (80..200 × 40..120) with `effect`.
|
||
pub(crate) fn scene(effect: EffectKind) -> (Document, NodeId, NodeId) {
|
||
let mut d = Document::new("pdf backdrop");
|
||
let p = d.first_page().unwrap();
|
||
let f = d.create(p, NodeKind::Frame, "Frame", 0.0, 0.0, 240.0, 160.0).unwrap();
|
||
solid(&mut d, f, "E03030");
|
||
d.get_mut(f).unwrap().clip = true;
|
||
for (i, x) in [30.0, 90.0, 150.0].into_iter().enumerate() {
|
||
let s = d.create(f, NodeKind::Rectangle, "Stripe", x, 0.0, 30.0, 160.0).unwrap();
|
||
solid(&mut d, s, if i % 2 == 0 { "2040E0" } else { "20C060" });
|
||
}
|
||
let pane = d.create(f, NodeKind::Rectangle, "Pane", 80.0, 40.0, 120.0, 80.0).unwrap();
|
||
let n = d.get_mut(pane).unwrap();
|
||
n.fills = vec![];
|
||
n.effects = vec![Effect { kind: effect, visible: true }];
|
||
(d, f, pane)
|
||
}
|
||
|
||
/// The PDF's backdrop image of `pane` and the PNG export of `frame` at the same scale: the largest
|
||
/// channel difference over the pane's inside (2 page units in from its edges).
|
||
pub(crate) fn pdf_vs_png(d: &Document, frame: NodeId, pane: NodeId) -> u8 {
|
||
let opts = PdfOptions::default();
|
||
let mut ex = Exporter::new(d, &opts);
|
||
let b = figcraft_render::export_bounds(d, frame).unwrap();
|
||
ex.page_origin = Affine::translate((-b.x0, -b.y0));
|
||
ex.page_rect = b;
|
||
ex.page_ids = vec![frame];
|
||
let (pix, view) = ex.backdrop_pixels(pane).unwrap();
|
||
let png = figcraft_render::render_node(d, frame, view.scale).unwrap();
|
||
let k = view.scale;
|
||
let inside = Rect::new(82.0, 42.0, 198.0, 118.0);
|
||
let mut worst = 0;
|
||
for y in (inside.y0 * k) as u32..(inside.y1 * k) as u32 {
|
||
for x in (inside.x0 * k) as u32..(inside.x1 * k) as u32 {
|
||
// Pixel (x, y) of the PNG (origin at the frame's corner) in the backdrop image.
|
||
let (bx, by) = (x - (view.origin.0 * k).round() as u32, y - (view.origin.1 * k).round() as u32);
|
||
let i = ((y * png.width() + x) * 4) as usize;
|
||
let j = ((by * pix.width() + bx) * 4) as usize;
|
||
for c in 0..4 {
|
||
worst = worst.max(png.data()[i + c].abs_diff(pix.data()[j + c]));
|
||
}
|
||
}
|
||
}
|
||
worst
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_background_blur_matches_the_png() {
|
||
let (d, f, pane) = scene(EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(12.0)));
|
||
let diff = pdf_vs_png(&d, f, pane);
|
||
assert!(diff <= 3, "{diff}");
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_glass_matches_the_png() {
|
||
let (d, f, pane) = scene(EffectKind::Glass(Glass::default()));
|
||
let diff = pdf_vs_png(&d, f, pane);
|
||
assert!(diff <= 3, "{diff}");
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_places_the_backdrop_image() {
|
||
let images = |d: &Document, f: NodeId| {
|
||
let pdf = crate::export_pdf(d, &[f], &PdfOptions::default()).unwrap();
|
||
String::from_utf8_lossy(&pdf).matches("/Subtype/Image").count()
|
||
};
|
||
let (mut d, f, pane) = scene(EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(12.0)));
|
||
let with = images(&d, f);
|
||
d.get_mut(pane).unwrap().effects.clear();
|
||
let without = images(&d, f);
|
||
assert!(with > without, "{with} / {without}");
|
||
// Glass with a layer blur on top: the backdrop image goes under the rasterised layer (an image
|
||
// of its own).
|
||
d.get_mut(pane).unwrap().effects = vec![Effect::glass(), Effect::layer_blur()];
|
||
let both = images(&d, f);
|
||
d.get_mut(pane).unwrap().effects = vec![Effect::layer_blur()];
|
||
assert!(both > images(&d, f), "{both}");
|
||
}
|