126 lines
4.4 KiB
Rust
126 lines
4.4 KiB
Rust
//! Raster (painted) effects: descriptions for the renderer.
|
||
|
||
use serde_json::Value;
|
||
use vectorcraft_doc::Effect;
|
||
use vectorcraft_doc::color::{BlendMode, Color};
|
||
|
||
use crate::merged_params;
|
||
use crate::util::*;
|
||
|
||
/// A painted effect, in document units. `blur` is Illustrator's blur distance; the renderer uses
|
||
/// a Gaussian with σ = blur / 2 (so the visible spread is about 1.5 × blur).
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub enum RasterFx {
|
||
DropShadow {
|
||
mode: BlendMode,
|
||
opacity: f32,
|
||
dx: f64,
|
||
dy: f64,
|
||
blur: f64,
|
||
color: Color,
|
||
},
|
||
OuterGlow {
|
||
mode: BlendMode,
|
||
opacity: f32,
|
||
blur: f64,
|
||
color: Color,
|
||
},
|
||
InnerGlow {
|
||
mode: BlendMode,
|
||
opacity: f32,
|
||
blur: f64,
|
||
color: Color,
|
||
center: bool,
|
||
},
|
||
/// Soften the object's edges inward over `radius`.
|
||
Feather {
|
||
radius: f64,
|
||
},
|
||
/// Blur the whole object (σ = radius / 2).
|
||
GaussianBlur {
|
||
radius: f64,
|
||
},
|
||
}
|
||
|
||
impl RasterFx {
|
||
/// Painted below the object (shadows, outer glows)?
|
||
pub fn is_below(&self) -> bool {
|
||
matches!(self, RasterFx::DropShadow { .. } | RasterFx::OuterGlow { .. })
|
||
}
|
||
/// How far this effect reaches beyond the geometry.
|
||
pub fn outset(&self) -> f64 {
|
||
match self {
|
||
RasterFx::DropShadow { dx, dy, blur, .. } => dx.abs().max(dy.abs()) + 1.5 * blur,
|
||
RasterFx::OuterGlow { blur, .. } => 1.5 * blur,
|
||
RasterFx::InnerGlow { .. } | RasterFx::Feather { .. } => 0.0,
|
||
RasterFx::GaussianBlur { radius } => 1.5 * radius,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Parse a blend mode name (`"multiply"`, `"Screen"`, `"color-burn"`…).
|
||
pub fn blend_mode(s: &str) -> BlendMode {
|
||
let key: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_ascii_lowercase();
|
||
BlendMode::ALL.iter().copied().find(|m| format!("{m:?}").to_ascii_lowercase() == key).unwrap_or(BlendMode::Normal)
|
||
}
|
||
|
||
fn color(p: &Value, default: Color) -> Color {
|
||
match p.get("color") {
|
||
Some(Value::String(s)) => Color::from_hex(s).unwrap_or(default),
|
||
Some(Value::Array(a)) if a.len() >= 3 => {
|
||
let f = |i: usize| a[i].as_f64().unwrap_or(0.0) as f32;
|
||
Color::rgb(f(0), f(1), f(2))
|
||
}
|
||
Some(v @ Value::Object(_)) => serde_json::from_value(v.clone()).unwrap_or(default),
|
||
_ => default,
|
||
}
|
||
}
|
||
|
||
fn opacity(p: &Value) -> f32 {
|
||
(num(p, "opacity", 75.0) / 100.0).clamp(0.0, 1.0) as f32
|
||
}
|
||
|
||
/// The visible raster effects of `effects`, in stack order.
|
||
pub fn raster_effects(effects: &[Effect]) -> Vec<RasterFx> {
|
||
effects
|
||
.iter()
|
||
.filter(|e| e.visible)
|
||
.filter_map(|e| {
|
||
let p = merged_params(&e.id, &e.params);
|
||
let blur = num(&p, "blur", 5.0).clamp(0.0, 1000.0);
|
||
Some(match e.id.as_str() {
|
||
"stylize.dropShadow" => RasterFx::DropShadow {
|
||
mode: blend_mode(text(&p, "mode", "multiply")),
|
||
opacity: opacity(&p),
|
||
dx: num(&p, "x", 7.0).clamp(-1e4, 1e4),
|
||
dy: num(&p, "y", 7.0).clamp(-1e4, 1e4),
|
||
blur,
|
||
color: color(&p, Color::BLACK),
|
||
},
|
||
"stylize.outerGlow" => RasterFx::OuterGlow {
|
||
mode: blend_mode(text(&p, "mode", "screen")),
|
||
opacity: opacity(&p),
|
||
blur,
|
||
color: color(&p, Color::rgb(1.0, 1.0, 0.0)),
|
||
},
|
||
"stylize.innerGlow" => RasterFx::InnerGlow {
|
||
mode: blend_mode(text(&p, "mode", "screen")),
|
||
opacity: opacity(&p),
|
||
blur,
|
||
color: color(&p, Color::WHITE),
|
||
center: text(&p, "source", "edge").eq_ignore_ascii_case("center"),
|
||
},
|
||
"stylize.feather" => RasterFx::Feather { radius: num(&p, "radius", 5.0).clamp(0.0, 1000.0) },
|
||
"blur.gaussian" => RasterFx::GaussianBlur { radius: num(&p, "radius", 5.0).clamp(0.0, 1000.0) },
|
||
_ => return None,
|
||
})
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// How far the visible raster effects of `effects` paint beyond the (effected) geometry, in
|
||
/// document units. Geometry effects are accounted for by evaluating them.
|
||
pub fn outset(effects: &[Effect]) -> f64 {
|
||
raster_effects(effects).iter().map(RasterFx::outset).fold(0.0, f64::max)
|
||
}
|