mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 23:05:05 +00:00
A linear, radial, conic or diamond gradient has a spread: Pad (the default, as before), Repeat or Reflect, with a count of 1 to 100 (VectorCraft's own limit). Repeat and Reflect run the stops that many times along the vector (around the turn for a conic) and keep going past both ends, Reflect running every other run backwards, as SVG's spreadMethod does. The Gradient panel has a Spread menu and a Repeats field, and the Gradient tool marks where the runs meet. paint.setFill and paint.editGradient take `spread` and `repeats`; bad values are errors. Presets, swatches, blends, strokes along or across a path, Outline Stroke and Object › Expand follow the spread; Expand gives each run the same whole number of objects, and Outline Stroke's mesh keeps the color on each side of a sharp step between two stops at one offset. SVG writes and reads spreadMethod: SVG import now reads a repeating or reflecting gradient as one that repeats or reflects once, instead of expanding its stops over the shape. CSS writes repeating gradients. PDF writes a conic's sweep shading over one run. PDF, EPS, legacy .ai and the .ai editing data write the other kinds with their stops repeated across the shape: at most 256 runs and 16,384 stops (256 stops in .ai files), and for a radial or diamond no farther than 65,536 runs from its center, with a warning past those. PDF import now finds sharp steps in any axial or radial shading instead of blurring them. Docs: docs/mcp.md (Spread), docs/target-app-parity.md, docs/gaps.md.
118 lines
4.7 KiB
Rust
118 lines
4.7 KiB
Rust
//! Command params written by tools and panels: the inverse of the engine's paint parsers, so a
|
|
//! paint read from the document and sent back through a command arrives unchanged.
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_color::{Color, Freeform, FreeformPoint, GradientPaint, GradientStop};
|
|
|
|
/// A colour as a `color` param in its own model: `[r,g,b]`, `{c,m,y,k}`, `{gray}` or `{l,a,b}`.
|
|
pub fn color_json(c: &Color) -> Value {
|
|
match *c {
|
|
Color::Rgb { r, g, b } => json!([r, g, b]),
|
|
Color::Cmyk { c, m, y, k } => json!({"c": c, "m": m, "y": y, "k": k}),
|
|
Color::Gray { k } => json!({"gray": k}),
|
|
Color::Lab { l, a, b } => json!({"l": l, "a": a, "b": b}),
|
|
}
|
|
}
|
|
|
|
/// Gradient stops as the `stops` param of `paint.editGradient` / a `gradient` paint (a linked
|
|
/// stop also carries its `swatch` and `tint` %).
|
|
pub fn stops_json(stops: &[GradientStop]) -> Value {
|
|
Value::Array(
|
|
stops
|
|
.iter()
|
|
.map(|s| {
|
|
let mut v = json!({"offset": s.offset, "color": color_json(&s.color), "opacity": s.opacity, "midpoint": s.midpoint});
|
|
if let Some(n) = &s.swatch {
|
|
v["swatch"] = json!(n);
|
|
v["tint"] = json!(s.tint * 100.0);
|
|
if let Some(p) = s.plates {
|
|
v["plates"] = json!(p);
|
|
}
|
|
}
|
|
v
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
/// A freeform point as its `paint.freeform.addPoint` / `freeform` param fields.
|
|
pub fn freeform_point_json(p: &FreeformPoint) -> Value {
|
|
json!({"at": [p.at.x, p.at.y], "color": color_json(&p.color), "opacity": p.opacity, "spread": p.spread})
|
|
}
|
|
|
|
/// A freeform gradient as the `freeform` param of a `gradient` paint: points, lines and Draw mode.
|
|
pub fn freeform_json(f: &Freeform) -> Value {
|
|
json!({
|
|
"points": f.points.iter().map(freeform_point_json).collect::<Vec<_>>(),
|
|
"lines": f.lines,
|
|
"mode": f.mode.label().to_lowercase(),
|
|
})
|
|
}
|
|
|
|
/// A gradient paint as the `gradient` param of `paint.setFill` / `swatch.new`: kind, stops (with
|
|
/// opacity and midpoint), angle, the linked swatch and, once placed, the vector, aspect (%) and
|
|
/// focal point, and the freeform points.
|
|
pub fn gradient_params(g: &GradientPaint) -> Value {
|
|
let mut v = json!({
|
|
"kind": g.gradient.kind.label().to_lowercase(),
|
|
"angle": g.angle,
|
|
"stops": stops_json(&g.gradient.stops),
|
|
});
|
|
if let Some(geom) = g.geom {
|
|
v["start"] = json!([geom.start.x, geom.start.y]);
|
|
v["end"] = json!([geom.end.x, geom.end.y]);
|
|
v["aspect"] = json!(geom.aspect * 100.0);
|
|
if let Some(f) = geom.focal {
|
|
v["focal"] = json!([f.x, f.y]);
|
|
}
|
|
}
|
|
if let Some(s) = &g.swatch {
|
|
v["swatch"] = json!(s);
|
|
}
|
|
if let Some(f) = &g.freeform {
|
|
v["freeform"] = freeform_json(f);
|
|
}
|
|
if g.gradient.dither {
|
|
v["dither"] = json!(true);
|
|
}
|
|
if !g.gradient.method.is_classic() {
|
|
v["method"] = json!(g.gradient.method.label().to_lowercase());
|
|
}
|
|
// A freeform gradient has no ends to spread past (`paint.setFill` refuses a spread for one).
|
|
if g.gradient.is_spread() {
|
|
v["spread"] = json!(g.gradient.spread.label().to_lowercase());
|
|
}
|
|
// A gradient that pads keeps its count for later.
|
|
if g.gradient.repeat_count() != 1 {
|
|
v["repeats"] = json!(g.gradient.repeat_count());
|
|
}
|
|
v
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use vectorcraft_color::{Gradient, GradientGeom, GradientKind};
|
|
use vectorcraft_geom::Point;
|
|
|
|
#[test]
|
|
fn gradient_params_carry_placement_only_when_placed() {
|
|
let mut g = GradientPaint::new(Gradient { kind: GradientKind::Radial, ..Default::default() });
|
|
let v = gradient_params(&g);
|
|
assert_eq!(v["kind"], "radial");
|
|
assert!(v.get("start").is_none() && v.get("aspect").is_none());
|
|
assert_eq!(v["stops"][0]["midpoint"], json!(0.5));
|
|
g.geom = Some(GradientGeom { start: Point::new(1.0, 2.0), end: Point::new(3.0, 4.0), aspect: 0.5, focal: None });
|
|
g.swatch = Some("Sky".into());
|
|
let v = gradient_params(&g);
|
|
assert_eq!((v["start"].clone(), v["end"].clone(), v["aspect"].clone()), (json!([1.0, 2.0]), json!([3.0, 4.0]), json!(50.0)));
|
|
assert_eq!(v["swatch"], "Sky");
|
|
assert!(v["stops"][0].get("swatch").is_none(), "unlinked stops carry no link");
|
|
g.gradient.stops[1].swatch = Some("Ink".into());
|
|
g.gradient.stops[1].tint = 0.25;
|
|
let v = gradient_params(&g);
|
|
assert_eq!((v["stops"][1]["swatch"].clone(), v["stops"][1]["tint"].clone()), (json!("Ink"), json!(25.0)));
|
|
assert_eq!(color_json(&Color::gray(0.25)), json!({"gray": 0.25}));
|
|
}
|
|
}
|