mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-12 05:12:44 +00:00
- Background blur sees the whole backdrop under the layer: the renderer keeps the stack of isolated surfaces it draws into (clipping frames, rounded clips, faded groups, masks) and flattens it for the layer's area, reading past the layer's own box. A blur inside a clipping frame now shows the frame's fill instead of the sharp layers below, on the canvas and in PNG exports. The PDF export draws background blur and glass through the same code from a render of what lies under the node (`backdrop::render_beneath`, which the PDF's backdrop now shares), and its images match the PNG's pixels. - Shadow spread offsets the outline: the shape filled and stroked twice the spread wide with mitred joins (grow) or cut away (shrink), the stroked bands cached per outline. Ellipses stay ellipses, stars keep sharp points, text keeps its letterforms, rounded corners grow their radius. Inner shadows shrink the outline the same way. Canvas, PNG and PDF share the shadow code; SVG writes spread shadows as the same geometry and now exports inner shadows. - Glass effect (light angle and intensity, refraction, depth, dispersion, frost, splay): the frosted backdrop bent towards the inside within `depth` of the edge, colour fringes, light on the rim, from an exact distance transform with smoothed normals. - Noise: mono, duo and multi colour modes, grain size, density, opacity and blend mode; grains live in the layer's coordinates (stable when panning and zooming). Texture is now drawn (pixels scattered by the radius, grain size, clip to shape). - Design panel rows for noise mode/colours/opacity, texture clip to shape and every glass value; `effect.add` takes `glass`; REST import of NOISE, TEXTURE and GLASS; Inspect rows and CSS; the scale tool scales grain sizes, texture radius, glass depth and frost. - Tests in new modules: render backdrop/spread/grain tests, PDF backdrop tests, SVG, doc, engine and REST effect tests. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
4.9 KiB
Rust
134 lines
4.9 KiB
Rust
//! Every colour value a document holds, for converting a file between colour profiles: paints
|
|
//! (fills, strokes, text runs), effects, layout grids, page and prototype backgrounds, overlay
|
|
//! backdrops, styles, colour variables and the document colours. Image pixels are not values and
|
|
//! are left alone.
|
|
|
|
use figcraft_color::{Color, ColorProfile, ImageFilters};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::proto::Action;
|
|
use crate::{Document, Effect, EffectKind, LayoutGrid, Node, NodeKind, Style, StyleKind, TextData, TextStyle, VariableValue, Variables};
|
|
|
|
/// A visitor over colour values.
|
|
pub type ColorVisitor<'a> = dyn FnMut(&mut Color) + 'a;
|
|
|
|
/// Image adjustments saved in a document under a name (the image paint's preset menu lists them).
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct ImagePreset {
|
|
pub name: String,
|
|
pub filters: ImageFilters,
|
|
}
|
|
|
|
impl Effect {
|
|
pub fn for_each_color_mut(&mut self, f: &mut ColorVisitor) {
|
|
match &mut self.kind {
|
|
EffectKind::DropShadow(s) | EffectKind::InnerShadow(s) => f(&mut s.color),
|
|
EffectKind::Noise(n) => {
|
|
f(&mut n.color);
|
|
f(&mut n.secondary);
|
|
}
|
|
EffectKind::LayerBlur { .. } | EffectKind::BackgroundBlur { .. } | EffectKind::Texture(_) | EffectKind::Glass(_) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TextStyle {
|
|
pub fn for_each_color_mut(&mut self, f: &mut ColorVisitor) {
|
|
self.fills.iter_mut().flatten().for_each(|p| p.for_each_color_mut(f));
|
|
}
|
|
}
|
|
|
|
impl TextData {
|
|
pub fn for_each_color_mut(&mut self, f: &mut ColorVisitor) {
|
|
self.style.for_each_color_mut(f);
|
|
self.runs.iter_mut().for_each(|r| r.style.for_each_color_mut(f));
|
|
}
|
|
}
|
|
|
|
impl Node {
|
|
/// Visit every colour of this node (not its children).
|
|
pub fn for_each_color_mut(&mut self, f: &mut ColorVisitor) {
|
|
self.fills.iter_mut().chain(self.strokes.iter_mut()).for_each(|p| p.for_each_color_mut(f));
|
|
self.effects.iter_mut().for_each(|e| e.for_each_color_mut(f));
|
|
self.grids.iter_mut().for_each(|g: &mut LayoutGrid| f(&mut g.color));
|
|
match &mut self.kind {
|
|
NodeKind::Page { background, prototype, .. } => {
|
|
f(background);
|
|
f(&mut prototype.background);
|
|
}
|
|
NodeKind::Text(t) => t.for_each_color_mut(f),
|
|
_ => {}
|
|
}
|
|
for it in &mut self.interactions {
|
|
crate::proto::walk_mut(&mut it.actions, &mut |a| {
|
|
if let Action::OpenOverlay { background: Some(c), .. } = a {
|
|
f(c);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Style {
|
|
pub fn for_each_color_mut(&mut self, f: &mut ColorVisitor) {
|
|
match &mut self.kind {
|
|
StyleKind::Paint { paints } => paints.iter_mut().for_each(|p| p.for_each_color_mut(f)),
|
|
StyleKind::Text { style } => style.for_each_color_mut(f),
|
|
StyleKind::Effect { effects } => effects.iter_mut().for_each(|e| e.for_each_color_mut(f)),
|
|
StyleKind::Grid { grids } => grids.iter_mut().for_each(|g| f(&mut g.color)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Variables {
|
|
pub fn for_each_color_mut(&mut self, f: &mut ColorVisitor) {
|
|
for v in self.variables.iter_mut().flat_map(|v| v.values.values_mut()) {
|
|
if let VariableValue::Color(c) = v {
|
|
f(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Document {
|
|
/// Replace every colour value in the document with `map(value)`.
|
|
pub fn map_colors(&mut self, map: impl Fn(Color) -> Color) {
|
|
let mut f = |c: &mut Color| *c = map(*c);
|
|
let ids: Vec<_> = self.ids().collect();
|
|
for id in ids {
|
|
if let Some(n) = self.node_mut(id) {
|
|
n.for_each_color_mut(&mut f);
|
|
}
|
|
}
|
|
self.styles.iter_mut().for_each(|s| s.for_each_color_mut(&mut f));
|
|
self.variables.for_each_color_mut(&mut f);
|
|
self.document_colors.iter_mut().for_each(&mut f);
|
|
}
|
|
|
|
/// Switch the document to `profile`. With `convert`, colour values are converted so that
|
|
/// colours keep their appearance; without, the values stay and are reinterpreted in the new
|
|
/// space. Returns whether anything changed.
|
|
pub fn set_color_profile(&mut self, profile: ColorProfile, convert: bool) -> bool {
|
|
if self.color_profile == profile {
|
|
return false;
|
|
}
|
|
if convert {
|
|
let conv = self.color_profile.conversion_to(profile);
|
|
self.map_colors(|c| conv.apply(c));
|
|
}
|
|
self.color_profile = profile;
|
|
true
|
|
}
|
|
|
|
/// This document with its colour values in sRGB (a converted copy when it is in another
|
|
/// profile), for formats whose colours are always sRGB.
|
|
pub fn in_srgb(&self) -> std::borrow::Cow<'_, Document> {
|
|
if self.color_profile.is_srgb() {
|
|
return std::borrow::Cow::Borrowed(self);
|
|
}
|
|
let mut d = self.clone();
|
|
d.set_color_profile(ColorProfile::Srgb, true);
|
|
std::borrow::Cow::Owned(d)
|
|
}
|
|
}
|