mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:20:56 -06:00
Gradients: diamond gradients run the stops from the center out to a square
A new gradient kind, `diamond`, runs its stops from the center (the start) out to a square with a corner at the end of the vector; the aspect stretches the square across the vector into a rhombus. The Gradient panel's Type row has a Diamond button, the Gradient tool draws the square with the aspect handle and center ring, and a gradient fitted to an object covers its box. - Model and canvas: the level is |x| + |y| in the square's frame; a square with no area (no vector, an aspect of 0, numbers out of range) paints the last stop. The canvas draws it as pixels, out to a stroke's miters, caps and arrowheads. Transforms keep the vector and take the mapped size across it as the aspect. - Commands: `paint.setFill`, `paint.editGradient` and `paint.setGradientGeom` take `kind: "diamond"`; a negative aspect or a focal point is an error. A conic or diamond gradient that a plug-in returns with a focal point loses the focal point. - Gradient tool: dragging the aspect handle of a radial or diamond gradient whose file gave it a negative aspect now sets a positive aspect; only a conic's negative aspect mirrors it. - Export: PDF, SVG, EPS and legacy .ai write four linear gradients, one for each quarter of the square, clipped to the shape (PDF as axial shadings). PDF and SVG write an image for translucent stops, a dithered fill or a flat square, and for the Perceptual method unless PDF writes the stops as spot color shadings. SVG writes a diamond on type characters as a radial gradient through the corners, and so does CSS. A swatch library saved as CSS gets four corner layers, a value for the `background` property (a circle when the swatch is turned by an angle that is not a multiple of 90 degrees). The .ai editing data holds a radial gradient through the corners. The document exports give a warning. - SVG export: a stroke drawn as an image, as conic wedges or as diamond quarters now covers the miters, caps and arrowheads that reach past its paint box. - PDF import reads a function-based shading whose color is the same along each square about the origin of its space as a diamond. - Object > Expand gives nested squares or a mesh of rings. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -160,6 +160,7 @@ Generated-in-code art is original and has no file to list. This covers the defau
|
||||
| `assets/icons/dc-fx.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-gamut.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-grad-conic.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-grad-diamond.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-grad-freeform.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-grad-linear.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-grad-radial.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="butt" stroke-linejoin="miter"><defs><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="12" y1="12" x2="3.5" y2="3.5"><stop offset="0" stop-color="white" stop-opacity="0.1"/><stop offset="1" stop-color="white"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="12" y1="12" x2="20.5" y2="3.5"><stop offset="0" stop-color="white" stop-opacity="0.1"/><stop offset="1" stop-color="white"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="12" y1="12" x2="20.5" y2="20.5"><stop offset="0" stop-color="white" stop-opacity="0.1"/><stop offset="1" stop-color="white"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="12" y1="12" x2="3.5" y2="20.5"><stop offset="0" stop-color="white" stop-opacity="0.1"/><stop offset="1" stop-color="white"/></linearGradient></defs><g stroke="none"><rect x="3.5" y="3.5" width="8.5" height="8.5" fill="url(#a)"/><rect x="12" y="3.5" width="8.5" height="8.5" fill="url(#b)"/><rect x="12" y="12" width="8.5" height="8.5" fill="url(#c)"/><rect x="3.5" y="12" width="8.5" height="8.5" fill="url(#d)"/></g><rect x="3.5" y="3.5" width="17" height="17" stroke="currentColor" stroke-width="1"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
+111
-27
@@ -19,13 +19,18 @@ pub enum GradientKind {
|
||||
/// the page from the vector, with a seam where the last stop meets the first. A negative
|
||||
/// `aspect` mirrors the sweep (see [`GradientGeom::transform`]).
|
||||
Conic,
|
||||
/// Diamond gradients: the stops run from the center (`start`) out to a square whose
|
||||
/// half-diagonal is the vector (a diamond when the vector is level), stretched across the
|
||||
/// vector by `aspect` (see [`GradientGeom::diamond_frame`]).
|
||||
Diamond,
|
||||
}
|
||||
|
||||
impl GradientKind {
|
||||
/// Every kind, in the order the Gradient panel lists them.
|
||||
pub const ALL: [GradientKind; 4] = [GradientKind::Linear, GradientKind::Radial, GradientKind::Conic, GradientKind::Freeform];
|
||||
/// Parse a kind name (`linear`, `radial`, `conic`, `freeform`; any case). `angle` and
|
||||
/// `conical` also mean conic.
|
||||
pub const ALL: [GradientKind; 5] =
|
||||
[GradientKind::Linear, GradientKind::Radial, GradientKind::Conic, GradientKind::Diamond, GradientKind::Freeform];
|
||||
/// Parse a kind name (`linear`, `radial`, `conic`, `diamond`, `freeform`; any case). `angle`
|
||||
/// and `conical` also mean conic.
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
if s.eq_ignore_ascii_case("angle") || s.eq_ignore_ascii_case("conical") {
|
||||
return Some(GradientKind::Conic);
|
||||
@@ -38,12 +43,13 @@ impl GradientKind {
|
||||
GradientKind::Radial => "Radial",
|
||||
GradientKind::Freeform => "Freeform",
|
||||
GradientKind::Conic => "Conic",
|
||||
GradientKind::Diamond => "Diamond",
|
||||
}
|
||||
}
|
||||
/// Is it placed by a center (`start`) and an extent ellipse (radial and conic), rather than
|
||||
/// by a line?
|
||||
/// Is it placed by a center (`start`) and an extent around it (radial, conic and diamond),
|
||||
/// rather than by a line?
|
||||
pub fn is_centered(self) -> bool {
|
||||
matches!(self, GradientKind::Radial | GradientKind::Conic)
|
||||
matches!(self, GradientKind::Radial | GradientKind::Conic | GradientKind::Diamond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,13 +674,22 @@ fn one64() -> f64 {
|
||||
impl GradientGeom {
|
||||
/// Default placement for a bounding box: linear spans the box through the centre at `angle_deg`,
|
||||
/// radial and conic are centered with radius = half the larger dimension and their vector at
|
||||
/// `angle_deg`.
|
||||
/// `angle_deg`, and diamond is centered with its vector at `angle_deg`, its square's edge
|
||||
/// running through the box's farthest corner (at 0 or 90 degrees, through all four).
|
||||
pub fn fit(kind: GradientKind, b: Rect, angle_deg: f64) -> Self {
|
||||
let c = b.center();
|
||||
let a = angle_deg.to_radians();
|
||||
let d = Vec2::new(a.cos(), -a.sin());
|
||||
match kind {
|
||||
GradientKind::Radial | GradientKind::Conic => Self { start: c, end: c + d * (b.width().max(b.height()) / 2.0), aspect: 1.0, focal: None },
|
||||
GradientKind::Diamond => {
|
||||
// The square through the box's farthest corner: the largest reach along the vector
|
||||
// plus across it, at one corner.
|
||||
let (w, h) = (b.width() / 2.0, b.height() / 2.0);
|
||||
let level = |x: f64, y: f64| (x * d.x + y * d.y).abs() + (y * d.x - x * d.y).abs();
|
||||
let half = level(w, h).max(level(w, -h));
|
||||
Self { start: c, end: c + d * half, aspect: 1.0, focal: None }
|
||||
}
|
||||
_ => {
|
||||
// Project the box corners onto the direction to cover the whole box.
|
||||
let half = (b.width() * d.x.abs() + b.height() * d.y.abs()) / 2.0;
|
||||
@@ -694,6 +709,10 @@ impl GradientGeom {
|
||||
/// other way: exact for moves, rotations, uniform scales and mirrors, and the nearest frame
|
||||
/// that keeps the seam under a shear or a non-uniform scale. A conic whose vector has no
|
||||
/// length, or whose aspect is 0, still has the sign of its aspect flipped by a mirror.
|
||||
/// Diamond gradients keep their vector (a corner of the square) the same way, with the size
|
||||
/// of the mapped extent across it as their aspect: exact for moves, rotations, scales along or
|
||||
/// across the vector and mirrors, and the nearest square stretched across its vector under a
|
||||
/// shear or another scale.
|
||||
pub fn transform(&mut self, a: Affine, kind: GradientKind) {
|
||||
// The focal point is a point like any other.
|
||||
self.focal = self.focal.map(|f| a * f);
|
||||
@@ -724,6 +743,14 @@ impl GradientGeom {
|
||||
};
|
||||
return;
|
||||
}
|
||||
if kind == GradientKind::Diamond {
|
||||
let (lu, lw) = (lin(u), lin(perp(u) * self.aspect));
|
||||
self.start = start;
|
||||
self.end = start + lu;
|
||||
let aspect = (lw.dot(perp(lu)) / lu.hypot2()).abs();
|
||||
self.aspect = if lu.hypot2() > 1e-18 && aspect.is_finite() { aspect } else { 1.0 };
|
||||
return;
|
||||
}
|
||||
if kind != GradientKind::Radial {
|
||||
let far = a * self.end;
|
||||
let nrm = perp(lin(perp(u)));
|
||||
@@ -768,6 +795,8 @@ impl GradientGeom {
|
||||
/// measured from the focal point (each level is the ellipse scaled about the line from the
|
||||
/// focal point to the centre). For conic gradients it is the turn from the vector in
|
||||
/// [`Self::conic_frame`], clockwise on the page (counterclockwise when mirrored), in 0..=1.
|
||||
/// For diamond gradients it is the size of the square through `p`, as a fraction of the one
|
||||
/// [`Self::diamond_frame`] gives (1 everywhere for a diamond without a frame).
|
||||
pub fn param_at(&self, kind: GradientKind, p: Point) -> f64 {
|
||||
if kind == GradientKind::Conic {
|
||||
let q = (self.conic_frame().inverse() * p).to_vec2();
|
||||
@@ -775,6 +804,13 @@ impl GradientGeom {
|
||||
let t = if t < 0.0 { t + 1.0 } else { t };
|
||||
return if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 };
|
||||
}
|
||||
if kind == GradientKind::Diamond {
|
||||
// Without a frame the square has no inside: every point is past its edge.
|
||||
let Some(m) = self.diamond_frame() else { return 1.0 };
|
||||
let q = (m.inverse() * p).to_vec2();
|
||||
let t = q.x.abs() + q.y.abs();
|
||||
return if t.is_nan() { 1.0 } else { t };
|
||||
}
|
||||
let u = self.end - self.start;
|
||||
if u.hypot2() < 1e-18 {
|
||||
return 0.0;
|
||||
@@ -835,32 +871,52 @@ impl GradientGeom {
|
||||
pub fn conic_wedges(&self, b: Rect, n: usize, overlap: bool) -> Vec<(Vec<Point>, f64)> {
|
||||
let n = n.clamp(1, MAX_CONIC_WEDGES);
|
||||
let frame = self.conic_frame();
|
||||
let to_unit = frame.inverse();
|
||||
let corners = [b.origin(), Point::new(b.x1, b.y0), Point::new(b.x1, b.y1), Point::new(b.x0, b.y1)];
|
||||
let reach = corners.iter().map(|&c| (to_unit * c).to_vec2().hypot()).fold(0.0f64, f64::max).max(1e-6);
|
||||
let reach = if reach.is_finite() { reach * 1.01 } else { 1.0 };
|
||||
// Arc points at most ARC of a turn apart, at a radius where their chords stay outside
|
||||
// `reach`.
|
||||
const ARC: f64 = 1.0 / 32.0;
|
||||
let r = reach / (std::f64::consts::PI * ARC).cos();
|
||||
let at = |turn: f64| {
|
||||
let a = turn * std::f64::consts::TAU;
|
||||
frame * Point::new(r * a.cos(), r * a.sin())
|
||||
};
|
||||
let center = frame * Point::ORIGIN;
|
||||
let r = fan_radius(frame, b);
|
||||
(0..n)
|
||||
.map(|k| {
|
||||
let from = k as f64 / n as f64;
|
||||
let to = if overlap { 1.0 } else { (k + 1) as f64 / n as f64 };
|
||||
let steps = ((to - from) / ARC).ceil().max(1.0) as usize;
|
||||
let mut pts = Vec::with_capacity(steps + 2);
|
||||
pts.push(center);
|
||||
pts.extend((0..=steps).map(|i| at(from + (to - from) * i as f64 / steps as f64)));
|
||||
(pts, (k as f64 + 0.5) / n as f64)
|
||||
(fan(frame, r, from, to), (k as f64 + 0.5) / n as f64)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The frame a diamond gradient is drawn in: its [`Self::unit_frame`], which maps the square
|
||||
/// with corners (±1, 0) and (0, ±1) onto the diamond (its first corner at the end of the
|
||||
/// vector). `None` when the square is flat or not finite (a vector without length, an aspect
|
||||
/// of 0, or a file's out-of-range numbers): such a diamond has no inside, and every point
|
||||
/// takes its last stop.
|
||||
pub fn diamond_frame(&self) -> Option<Affine> {
|
||||
self.unit_frame().filter(|m| m.is_finite() && m.determinant().abs() > 1e-18)
|
||||
}
|
||||
|
||||
/// A diamond gradient as four linear gradients, one for each quadrant of its
|
||||
/// [`Self::diamond_frame`], for formats without diamond gradients: each quadrant's outline (the
|
||||
/// center, then an arc that reaches past every corner of `b`) and the linear gradient that
|
||||
/// equals the diamond there (its stops run across the square's edge in that quadrant), in the
|
||||
/// order they are drawn. Each of the first three runs on past its last edge under the next,
|
||||
/// and the first also back past its first edge under the last, so every edge drawn lies on
|
||||
/// the quadrant before it and no hairline of what lies below shows between them; where they
|
||||
/// overlap, translucent stops would add up, so the stops must be opaque. `None` for a diamond
|
||||
/// without a frame.
|
||||
pub fn diamond_quadrants(&self, b: Rect) -> Option<[(Vec<Point>, GradientGeom); 4]> {
|
||||
let frame = self.diamond_frame()?;
|
||||
let r = fan_radius(frame, b);
|
||||
Some(std::array::from_fn(|k| {
|
||||
let first = k as f64 / 4.0;
|
||||
let (from, to) = match k {
|
||||
0 => (-0.125, 0.375),
|
||||
3 => (0.75, 1.0),
|
||||
_ => (first, first + 0.375),
|
||||
};
|
||||
// The quadrant's corner of the square, halfway along whose edge t is 1/2.
|
||||
let (sx, sy) = [(1.0, 1.0), (-1.0, 1.0), (-1.0, -1.0), (1.0, -1.0)][k];
|
||||
let mut geom = GradientGeom { start: Point::ORIGIN, end: Point::new(sx / 2.0, sy / 2.0), aspect: 1.0, focal: None };
|
||||
geom.transform(frame, GradientKind::Linear);
|
||||
(fan(frame, r, from, to), geom)
|
||||
}))
|
||||
}
|
||||
|
||||
/// The map that squashes a radial gradient's circle (centre `start`, radius the vector's
|
||||
/// length) into its extent ellipse: by `aspect` across the vector, about the centre.
|
||||
pub fn radial_squash(&self) -> Affine {
|
||||
@@ -907,6 +963,34 @@ impl GradientGeom {
|
||||
}
|
||||
}
|
||||
|
||||
/// Arc points of [`fan`] are at most this much of a turn apart.
|
||||
const FAN_ARC: f64 = 1.0 / 32.0;
|
||||
|
||||
/// The radius of the unit circle of `frame` at which the chords of [`fan`]'s arcs stay outside
|
||||
/// every corner of `b`.
|
||||
fn fan_radius(frame: Affine, b: Rect) -> f64 {
|
||||
let to_unit = frame.inverse();
|
||||
let corners = [b.origin(), Point::new(b.x1, b.y0), Point::new(b.x1, b.y1), Point::new(b.x0, b.y1)];
|
||||
let reach = corners.iter().map(|&c| (to_unit * c).to_vec2().hypot()).fold(0.0f64, f64::max).max(1e-6);
|
||||
let reach = if reach.is_finite() { reach * 1.01 } else { 1.0 };
|
||||
reach / (std::f64::consts::PI * FAN_ARC).cos()
|
||||
}
|
||||
|
||||
/// The sector of `frame`'s unit circle, scaled to radius `r`, from turn `from` to turn `to`
|
||||
/// (counted from the frame's x axis toward its y axis) as an outline: the center, then points on
|
||||
/// the arc at most [`FAN_ARC`] of a turn apart.
|
||||
fn fan(frame: Affine, r: f64, from: f64, to: f64) -> Vec<Point> {
|
||||
let at = |turn: f64| {
|
||||
let a = turn * std::f64::consts::TAU;
|
||||
frame * Point::new(r * a.cos(), r * a.sin())
|
||||
};
|
||||
let steps = ((to - from) / FAN_ARC).ceil().max(1.0) as usize;
|
||||
let mut pts = Vec::with_capacity(steps + 2);
|
||||
pts.push(frame * Point::ORIGIN);
|
||||
pts.extend((0..=steps).map(|i| at(from + (to - from) * i as f64 / steps as f64)));
|
||||
pts
|
||||
}
|
||||
|
||||
/// `p` moved from box `from` to box `to`, keeping its position relative to the box (along a side
|
||||
/// `from` has no extent on, its offset from the centre).
|
||||
fn rebase_point(p: Point, from: Rect, to: Rect) -> Point {
|
||||
@@ -1009,7 +1093,7 @@ impl GradientPaint {
|
||||
pub fn draws_as_pixels(&self, fill: bool) -> bool {
|
||||
self.gradient.kind == GradientKind::Freeform || (fill && self.gradient.dither) || !self.gradient.method.is_classic()
|
||||
}
|
||||
/// A linear, radial or conic gradient placed on box `bounds` as a `cols` × `rows` grid of
|
||||
/// A linear, radial, conic or diamond gradient placed on box `bounds` as a `cols` × `rows` grid of
|
||||
/// unpremultiplied RGBA8 pixels over `area`, row by row, each sampled at its centre (colours
|
||||
/// as `rgb` gives them, mixed as the renderer mixes them: [`Ramp`]). Dithered and painting a
|
||||
/// fill (`fill`; dither applies to fills only), each colour channel of pixel (column, row) gets
|
||||
@@ -1040,7 +1124,7 @@ impl GradientPaint {
|
||||
) -> impl Iterator<Item = [u8; 4]> + use<> {
|
||||
self.grid(Ramp::new(&self.gradient, rgb), bounds, area, cols, rows, cells, fill, false)
|
||||
}
|
||||
/// [`Self::pixels`] of a linear, radial or conic gradient whose stops are all CMYK, as C, M, Y, K
|
||||
/// [`Self::pixels`] of a linear, radial, conic or diamond gradient whose stops are all CMYK, as C, M, Y, K
|
||||
/// and opacity levels mixed from the stops' inks; `None` when a stop is in another model,
|
||||
/// there are no stops, or a Perceptual curve has too many stops for [`Gradient::ink_mixes`].
|
||||
/// Dither's noise adds no ink where the ramp has none.
|
||||
|
||||
@@ -880,3 +880,177 @@ fn conic_pixels_follow_the_sweep() {
|
||||
let many = GradientPaint { gradient: Gradient::new(GradientKind::Conic, many), ..g };
|
||||
assert_eq!(many.pixels(b, b, 4, 4, true, &Color::to_rgb).count(), 16);
|
||||
}
|
||||
|
||||
/// A diamond placed at `center` with its vector `dx` long to the right and aspect `aspect`.
|
||||
fn diamond_at(center: Point, dx: f64, aspect: f64) -> GradientGeom {
|
||||
GradientGeom { start: center, end: center + Vec2::new(dx, 0.0), aspect, focal: None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_diamond_runs_from_its_center_out_to_a_square_with_a_corner_at_the_end_of_its_vector() {
|
||||
let g = diamond_at(Point::new(100.0, 100.0), 50.0, 1.0);
|
||||
let t = |x: f64, y: f64| g.param_at(GradientKind::Diamond, Point::new(x, y));
|
||||
assert!(t(100.0, 100.0).abs() < 1e-12);
|
||||
// The square's corners: at the end of the vector and a quarter turn on from it each way.
|
||||
for (x, y) in [(150.0, 100.0), (100.0, 150.0), (50.0, 100.0), (100.0, 50.0)] {
|
||||
assert!((t(x, y) - 1.0).abs() < 1e-12, "({x}, {y})");
|
||||
}
|
||||
// Halfway along an edge, and halfway out along a diagonal (the middle of the edge's half size).
|
||||
assert!((t(125.0, 125.0) - 1.0).abs() < 1e-12);
|
||||
assert!((t(112.5, 87.5) - 0.5).abs() < 1e-12);
|
||||
// Past the square: more than 1 (the last stop pads on).
|
||||
assert!((t(200.0, 200.0) - 4.0).abs() < 1e-12);
|
||||
// The aspect stretches the square across the vector into a rhombus.
|
||||
let tall = diamond_at(Point::new(100.0, 100.0), 50.0, 2.0);
|
||||
assert!((tall.param_at(GradientKind::Diamond, Point::new(100.0, 200.0)) - 1.0).abs() < 1e-12);
|
||||
assert!((tall.param_at(GradientKind::Diamond, Point::new(150.0, 100.0)) - 1.0).abs() < 1e-12);
|
||||
// Turned: the corner follows the vector.
|
||||
let turned = GradientGeom { end: Point::new(130.0, 140.0), ..g };
|
||||
assert!((turned.param_at(GradientKind::Diamond, Point::new(130.0, 140.0)) - 1.0).abs() < 1e-12);
|
||||
assert!((turned.param_at(GradientKind::Diamond, Point::new(115.0, 120.0)) - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diamond_kind_names_and_fit() {
|
||||
for s in ["diamond", "Diamond", "DIAMOND"] {
|
||||
assert_eq!(GradientKind::parse(s), Some(GradientKind::Diamond), "{s}");
|
||||
}
|
||||
assert_eq!(GradientKind::Diamond.label(), "Diamond");
|
||||
assert!(GradientKind::Diamond.is_centered());
|
||||
assert!(GradientKind::ALL.contains(&GradientKind::Diamond));
|
||||
// Level: the square's edges pass through the box's corners, so it covers the box exactly.
|
||||
let b = Rect::new(0.0, 0.0, 200.0, 100.0);
|
||||
let fit = GradientGeom::fit(GradientKind::Diamond, b, 0.0);
|
||||
assert_eq!(fit.start, Point::new(100.0, 50.0));
|
||||
assert!(fit.end.distance(Point::new(250.0, 50.0)) < 1e-9, "{fit:?}");
|
||||
for c in [Point::new(0.0, 0.0), Point::new(200.0, 0.0), Point::new(200.0, 100.0), Point::new(0.0, 100.0)] {
|
||||
assert!((fit.param_at(GradientKind::Diamond, c) - 1.0).abs() < 1e-9, "{c:?}");
|
||||
}
|
||||
// At any angle the square covers the box and runs through its farthest corner, on a square
|
||||
// box too.
|
||||
for b in [b, Rect::new(0.0, 0.0, 100.0, 100.0)] {
|
||||
for angle in [30.0, 45.0, 90.0, -120.0] {
|
||||
let fit = GradientGeom::fit(GradientKind::Diamond, b, angle);
|
||||
let levels = [Point::new(b.x0, b.y0), Point::new(b.x1, b.y0), Point::new(b.x1, b.y1), Point::new(b.x0, b.y1)]
|
||||
.map(|c| fit.param_at(GradientKind::Diamond, c));
|
||||
assert!(levels.iter().all(|&t| t <= 1.0 + 1e-9), "{angle}: {levels:?}");
|
||||
assert!(levels.iter().any(|&t| (t - 1.0).abs() < 1e-9), "{angle}: {levels:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_diamond_follows_moves_turns_scales_and_mirrors() {
|
||||
let c = Point::new(10.0, 20.0);
|
||||
let g0 = diamond_at(c, 30.0, 0.5);
|
||||
let probes = [c + Vec2::new(5.0, 7.0), c + Vec2::new(-9.0, 2.0), c + Vec2::new(3.0, -11.0), c + Vec2::new(-20.0, -4.0)];
|
||||
for a in [
|
||||
Affine::rotate(0.7),
|
||||
Affine::scale(2.5),
|
||||
Affine::translate((4.0, -9.0)) * Affine::rotate(-2.0),
|
||||
Affine::FLIP_X,
|
||||
Affine::FLIP_Y * Affine::rotate(1.1),
|
||||
// Along and across the vector.
|
||||
Affine::scale_non_uniform(2.0, 0.5),
|
||||
Affine::scale_non_uniform(-3.0, 0.25),
|
||||
] {
|
||||
let mut g = g0;
|
||||
g.transform(a, GradientKind::Diamond);
|
||||
assert!(g.start.distance(a * c) < 1e-9, "{a:?}");
|
||||
// A mirror changes nothing a diamond shows: its aspect stays a size.
|
||||
assert!(g.aspect > 0.0, "{a:?}: {g:?}");
|
||||
for p in probes {
|
||||
let (want, got) = (g0.param_at(GradientKind::Diamond, p), g.param_at(GradientKind::Diamond, a * p));
|
||||
assert!((want - got).abs() < 1e-9, "{a:?} at {p:?}: {want} vs {got}");
|
||||
}
|
||||
}
|
||||
// Under a shear the corner stays where the vector maps.
|
||||
let shear = Affine::new([1.0, 0.0, 0.6, 1.0, 0.0, 0.0]);
|
||||
let mut g = g0;
|
||||
g.transform(shear, GradientKind::Diamond);
|
||||
assert!((g.param_at(GradientKind::Diamond, shear * (c + Vec2::new(30.0, 0.0))) - 1.0).abs() < 1e-9, "{g:?}");
|
||||
// Out-of-range numbers: a finite aspect after any transform.
|
||||
for bad in [diamond_at(c, 30.0, f64::NAN), diamond_at(c, f64::INFINITY, 1.0), diamond_at(c, 0.0, 1.0)] {
|
||||
let mut g = bad;
|
||||
g.transform(Affine::rotate(0.4), GradientKind::Diamond);
|
||||
assert!(g.aspect.is_finite(), "{g:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A diamond without a frame (no vector, a flat or a not-finite extent) has no inside: every
|
||||
/// point is past its edge and takes the last stop. Points that aren't numbers do too.
|
||||
#[test]
|
||||
fn a_diamond_with_no_extent_or_bad_numbers_is_its_last_stop() {
|
||||
let c = Point::new(50.0, 50.0);
|
||||
for g in [
|
||||
diamond_at(c, 0.0, 1.0),
|
||||
diamond_at(c, 30.0, 0.0),
|
||||
diamond_at(c, 30.0, f64::NAN),
|
||||
diamond_at(c, f64::INFINITY, 1.0),
|
||||
GradientGeom { start: Point::new(f64::NAN, 0.0), ..diamond_at(c, 30.0, 1.0) },
|
||||
] {
|
||||
assert!(g.diamond_frame().is_none(), "{g:?}");
|
||||
assert!(g.diamond_quadrants(Rect::new(0.0, 0.0, 100.0, 100.0)).is_none(), "{g:?}");
|
||||
assert_eq!(g.param_at(GradientKind::Diamond, Point::new(60.0, 40.0)), 1.0, "{g:?}");
|
||||
}
|
||||
let g = diamond_at(c, 30.0, 1.0);
|
||||
assert_eq!(g.param_at(GradientKind::Diamond, Point::new(f64::NAN, 1.0)), 1.0);
|
||||
assert!(g.param_at(GradientKind::Diamond, Point::new(f64::INFINITY, 1.0)) >= 1.0);
|
||||
}
|
||||
|
||||
/// The four linear gradients a format without diamond gradients gets, drawn in order, equal the
|
||||
/// diamond everywhere, and each edge drawn lies on the quadrant drawn before it.
|
||||
#[test]
|
||||
fn diamond_quadrants_are_linear_gradients_that_equal_the_diamond() {
|
||||
let b = Rect::new(0.0, 0.0, 200.0, 100.0);
|
||||
let probes: Vec<Point> = (0..=20).flat_map(|i| (0..=10).map(move |j| Point::new(i as f64 * 10.0 + 0.3, j as f64 * 10.0 + 0.7))).collect();
|
||||
// Squashed and turned, stretched twice as tall as wide, and stretched by a shear.
|
||||
let mut turned = diamond_at(Point::new(60.0, 40.0), 30.0, 0.5);
|
||||
turned.transform(Affine::rotate_about(0.4, Point::new(60.0, 40.0)), GradientKind::Diamond);
|
||||
let mut sheared = diamond_at(Point::new(90.0, 50.0), 25.0, 1.0);
|
||||
sheared.transform(Affine::new([2.0, 0.0, 0.7, 1.0, -60.0, 0.0]), GradientKind::Diamond);
|
||||
for g in [turned, diamond_at(Point::new(100.0, 50.0), 20.0, 2.0), sheared] {
|
||||
let quadrants = g.diamond_quadrants(b).expect("a frame");
|
||||
for p in &probes {
|
||||
// The last quadrant drawn that holds the point paints it.
|
||||
let Some((_, linear)) = quadrants.iter().rev().find(|(poly, _)| inside(poly, *p)) else { panic!("{p:?} is in no quadrant") };
|
||||
let (want, got) = (g.param_at(GradientKind::Diamond, *p), linear.param_at(GradientKind::Linear, *p));
|
||||
assert!((want - got).abs() < 1e-9, "{g:?} at {p:?}: {want} vs {got}");
|
||||
}
|
||||
}
|
||||
let g = turned;
|
||||
let quadrants = g.diamond_quadrants(b).expect("a frame");
|
||||
// Next to each edge of a quadrant drawn later, on either side, the quadrant drawn before it
|
||||
// lies underneath (the first quadrant's edges are covered by the second and the last).
|
||||
let frame = g.diamond_frame().expect("a frame");
|
||||
for (unit, under) in [
|
||||
// The second quadrant's first edge, on the first.
|
||||
(Point::new(0.01, 0.5), 0),
|
||||
(Point::new(-0.01, 0.5), 0),
|
||||
// The third's, on the second.
|
||||
(Point::new(-0.5, 0.01), 1),
|
||||
(Point::new(-0.5, -0.01), 1),
|
||||
// The last's two, on the third and on the first.
|
||||
(Point::new(-0.01, -0.5), 2),
|
||||
(Point::new(0.01, -0.5), 2),
|
||||
(Point::new(0.5, -0.01), 0),
|
||||
(Point::new(0.5, 0.01), 0),
|
||||
] {
|
||||
assert!(inside(&quadrants[under].0, frame * unit), "{unit:?} lies on quadrant {under}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A diamond's pixels (what SVG and PDF draw a dithered or Perceptual one as, and what the canvas
|
||||
/// draws) follow the squares.
|
||||
#[test]
|
||||
fn diamond_pixels_follow_the_squares() {
|
||||
let b = Rect::new(0.0, 0.0, 5.0, 5.0);
|
||||
let mut g = GradientPaint::new(Gradient::new(GradientKind::Diamond, Gradient::default().stops));
|
||||
g.geom = Some(diamond_at(Point::new(2.5, 2.5), 2.5, 1.0));
|
||||
let px: Vec<[u8; 4]> = g.pixels(b, b, 5, 5, false, &Color::to_rgb).collect();
|
||||
// White at the center, black in the corners, and the same on each side of it.
|
||||
assert!(px[2 * 5 + 2][0] > 200 && px[0][0] < 10 && px[24][0] < 10, "{px:?}");
|
||||
assert_eq!(px[2 * 5 + 1], px[2 * 5 + 3]);
|
||||
assert_eq!(px[5 + 2], px[3 * 5 + 2]);
|
||||
assert_eq!(px[2 * 5 + 1], px[5 + 2]);
|
||||
}
|
||||
|
||||
@@ -205,6 +205,23 @@ fn write_css(lib: &SwatchLibrary, keep: impl Fn(&&Swatch) -> bool) -> String {
|
||||
GradientKind::Conic => {
|
||||
format!("conic-gradient(from {}deg, {})", (90.0 - g.angle).rem_euclid(360.0).round(), stops.join(", "))
|
||||
}
|
||||
// CSS has no diamond gradient: four layers, each a quarter of the box with
|
||||
// the stops running from the center out to its corner at a diagonal angle,
|
||||
// make the diamond its fit gives a box of any shape: the level at a point
|
||||
// is its distance from the center along both axes over the box's
|
||||
// half-width plus half-height
|
||||
// (https://www.w3.org/TR/css-images-3/#linear-gradients). Turned by an angle
|
||||
// that is not a multiple of 90 degrees, its corners leave the box's axes,
|
||||
// and a circle stands in for it, as for a radial swatch.
|
||||
GradientKind::Diamond if (1e-9..90.0 - 1e-9).contains(&g.angle.rem_euclid(90.0)) => {
|
||||
format!("radial-gradient(circle, {})", stops.join(", "))
|
||||
}
|
||||
GradientKind::Diamond => {
|
||||
let stops = stops.join(", ");
|
||||
[("top left", 315), ("top right", 45), ("bottom right", 135), ("bottom left", 225)]
|
||||
.map(|(corner, deg)| format!("linear-gradient({deg}deg, {stops}) {corner} / 50% 50% no-repeat"))
|
||||
.join(", ")
|
||||
}
|
||||
_ => format!("linear-gradient({}deg, {})", (90.0 - g.angle).rem_euclid(360.0).round(), stops.join(", ")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,55 @@ fn css_writes_conic_gradient_swatches() {
|
||||
assert!(text.contains("--back: conic-gradient(from 0deg, #000000 0%, #ffffff 80%);"), "{text}");
|
||||
}
|
||||
|
||||
/// A diamond gradient swatch (CSS has none) is four layered linear gradients, one a quarter of the
|
||||
/// box running from the center out to its corner, for the `background` shorthand.
|
||||
#[test]
|
||||
fn css_writes_diamond_gradient_swatches_as_four_corner_layers() {
|
||||
let lib = SwatchLibrary {
|
||||
name: "x".into(),
|
||||
swatches: vec![Swatch {
|
||||
name: "Gem".into(),
|
||||
paint: Paint::Gradient(Box::new(GradientPaint::new(Gradient { kind: GradientKind::Diamond, ..Gradient::default() }))),
|
||||
global: false,
|
||||
spot: false,
|
||||
}],
|
||||
groups: vec![],
|
||||
};
|
||||
let text = written(&lib, PaletteFormat::Css);
|
||||
// Diagonal angles, not `to <corner>`: on a box that isn't square, `to <corner>` would turn
|
||||
// each layer's lines to run through the quarter's other two corners.
|
||||
let layer = |corner: &str, deg: u32| format!("linear-gradient({deg}deg, #ffffff 0%, #000000 100%) {corner} / 50% 50% no-repeat");
|
||||
let want =
|
||||
format!("--gem: {}, {}, {}, {};", layer("top left", 315), layer("top right", 45), layer("bottom right", 135), layer("bottom left", 225));
|
||||
assert!(text.contains(&want), "{text}");
|
||||
// CSS's gradient line for a diagonal angle on a quarter box w × h is (w + h) / √2 long and
|
||||
// starts at the quarter's corner at the box's center: the level at a point x, y from the
|
||||
// center is (|x| + |y|) / (w + h), the level the diamond fitted to the whole box has there.
|
||||
let (w, h) = (100.0f64, 50.0f64);
|
||||
let length = (w + h) / std::f64::consts::SQRT_2;
|
||||
let fit = crate::GradientGeom::fit(GradientKind::Diamond, kurbo::Rect::new(-w, -h, w, h), 0.0);
|
||||
for (x, y) in [(0.0, -50.0), (-100.0, 0.0), (-30.0, -20.0), (-100.0, -50.0)] {
|
||||
// The top-left quarter's layer runs toward (-1, -1).
|
||||
let css = (-x - y) / std::f64::consts::SQRT_2 / length;
|
||||
let model = fit.param_at(GradientKind::Diamond, kurbo::Point::new(x, y));
|
||||
assert!((css - model).abs() < 1e-9, "({x}, {y}): {css} vs {model}");
|
||||
}
|
||||
// Turned a quarter or a half turn, it is the same diamond; turned 30 degrees, a circle stands
|
||||
// in for it.
|
||||
let turned = |angle: f64| {
|
||||
let mut gp = GradientPaint::new(Gradient { kind: GradientKind::Diamond, ..Gradient::default() });
|
||||
gp.angle = angle;
|
||||
let lib = SwatchLibrary {
|
||||
name: "x".into(),
|
||||
swatches: vec![Swatch { name: "Gem".into(), paint: Paint::Gradient(Box::new(gp)), global: false, spot: false }],
|
||||
groups: vec![],
|
||||
};
|
||||
written(&lib, PaletteFormat::Css)
|
||||
};
|
||||
assert!(turned(90.0).contains(&want) && turned(-180.0).contains(&want), "{}", turned(90.0));
|
||||
assert!(turned(30.0).contains("--gem: radial-gradient(circle, #ffffff 0%, #000000 100%);"), "{}", turned(30.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_preserves_fractional_gradient_stop_positions() {
|
||||
for kind in [GradientKind::Linear, GradientKind::Radial] {
|
||||
|
||||
@@ -444,7 +444,8 @@ impl Appearance {
|
||||
}
|
||||
}
|
||||
/// Map placed gradients through a warp, given its local affine approximation at a point (taken
|
||||
/// at each gradient's center: the start of a radial or conic, the middle of a linear vector).
|
||||
/// at each gradient's center: the start of a radial, conic or diamond, the middle of a linear
|
||||
/// vector).
|
||||
/// Freeform points each follow the warp at their own position.
|
||||
pub fn warp_gradients(&mut self, near: &dyn Fn(vectorcraft_geom::Point) -> vectorcraft_geom::Affine) {
|
||||
for g in self.gradients_mut() {
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand…",
|
||||
["Object"],
|
||||
None,
|
||||
"{object?: true (symbol instances without effects (their fills' and strokes' included) and not in perspective → their art, as Break Link to Symbol places it (the instances that art holds stay instances), type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients, wedges around the center for conic ones) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255)} one undo step; a dimension whose art an option changes becomes a plain group of its art, except one selected with art it measures, directly or in groups without fills, strokes or effects of their own: its lines and label stay as they are, and if the new art no longer fits its link it stays where it was, no longer linked → {ids}",
|
||||
"{object?: true (symbol instances without effects (their fills' and strokes' included) and not in perspective → their art, as Break Link to Symbol places it (the instances that art holds stay instances), type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients, wedges around the center for conic ones, nested squares for diamond ones) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255)} one undo step; a dimension whose art an option changes becomes a plain group of its art, except one selected with art it measures, directly or in groups without fills, strokes or effects of their own: its lines and label stay as they are, and if the new art no longer fits its link it stays where it was, no longer linked → {ids}",
|
||||
has_selection,
|
||||
expand
|
||||
),
|
||||
@@ -1283,9 +1283,12 @@ fn breaks(g: &Gradient, t0: f64, t1: f64, max_gap: f64) -> Vec<(f64, (Color, f32
|
||||
/// cover the box (linear: along the vector `t0..t1` and across it `s0..s1` in points; radial:
|
||||
/// `0..t1`, each level's ellipse centred on the way from the focal point to the start).
|
||||
/// A conic gradient also has its placement, the box and the radius in its frame that reaches
|
||||
/// past the box's corners ([`Frame::pieces`] and [`Frame::mesh`] go around its center).
|
||||
/// past the box's corners ([`Frame::pieces`] and [`Frame::mesh`] go around its center). A
|
||||
/// diamond gradient has its frame ([`vectorcraft_color::GradientGeom::diamond_frame`]; `None`
|
||||
/// when it has no extent), the box and the level of the square that reaches its corners.
|
||||
struct Frame {
|
||||
conic: Option<(vectorcraft_color::GradientGeom, Rect, f64)>,
|
||||
diamond: Option<(Option<Affine>, Rect, f64)>,
|
||||
radial: bool,
|
||||
start: Point,
|
||||
focal: Point,
|
||||
@@ -1323,7 +1326,11 @@ impl Frame {
|
||||
let reach = if reach.is_finite() { reach * 1.01 / (std::f64::consts::PI * CONIC_MESH_GAP).cos() } else { 1.0 };
|
||||
(geom, bounds, reach)
|
||||
});
|
||||
Self { conic, radial, start: geom.start, focal: geom.focal_point(), along, across, t0, t1, s0, s1 }
|
||||
let diamond = (g.gradient.kind == GradientKind::Diamond).then(|| {
|
||||
let reach = corners.iter().map(|c| geom.param_at(GradientKind::Diamond, *c)).fold(0.0f64, f64::max);
|
||||
(geom.diamond_frame(), bounds, if reach.is_finite() { reach.max(1e-6) * 1.01 } else { 1.0 })
|
||||
});
|
||||
Self { conic, diamond, radial, start: geom.start, focal: geom.focal_point(), along, across, t0, t1, s0, s1 }
|
||||
}
|
||||
|
||||
/// Linear: the point at parameter `t` along and `s` across. Radial: the point at parameter `t`
|
||||
@@ -1351,6 +1358,34 @@ impl Frame {
|
||||
};
|
||||
return GradientMesh::from_grid(&us, &[0.0, 1.0], &at, &|_, c, _| br[c].1);
|
||||
}
|
||||
if let Some((frame, b, reach)) = &self.diamond {
|
||||
let Some(frame) = frame else {
|
||||
// No extent: the whole box is the last stop.
|
||||
let last = g.sample_keep_model(1.0);
|
||||
let at = |u: f64, v: f64| Point::new(b.x0 + u * b.width(), b.y0 + v * b.height());
|
||||
return GradientMesh::from_grid(&[0.0, 1.0], &[0.0, 1.0], &at, &|_, _, _| last);
|
||||
};
|
||||
// Rings of squares where the color changes, with columns at the corners: each patch
|
||||
// lies between two rings along one side, where the level runs straight across it.
|
||||
let br = breaks(g, 0.0, *reach, f64::INFINITY);
|
||||
let ts: Vec<f64> = br.iter().map(|(t, _)| t / reach).collect();
|
||||
let at = |u: f64, v: f64| *frame * (diamond_perimeter(u).to_vec2() * (v * reach)).to_point();
|
||||
const CORNERS: [f64; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
|
||||
let mut mesh = GradientMesh::from_grid(&CORNERS, &ts, &at, &|r, _, _| br[r].1);
|
||||
// Every column is at a corner, where `from_grid`'s tangent is the mean of the two
|
||||
// sides: each handle along a ring points a third of the way along its own side, so
|
||||
// the sides stay straight.
|
||||
let side = |v: f64, from: usize, to: Option<&f64>| to.map_or(vectorcraft_geom::Vec2::ZERO, |&u| (at(u, v) - at(CORNERS[from], v)) / 3.0);
|
||||
for (r, &v) in ts.iter().enumerate() {
|
||||
for c in 0..CORNERS.len() {
|
||||
if let Some(point) = mesh.points.get_mut(r * CORNERS.len() + c) {
|
||||
point.handles[0] = side(v, c, CORNERS.get(c + 1));
|
||||
point.handles[1] = side(v, c, c.checked_sub(1).and_then(|i| CORNERS.get(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return mesh;
|
||||
}
|
||||
// Rings as close as the columns around them, so the mesh draws smoothly.
|
||||
let gap = if self.radial { (self.t1 - self.t0) / RADIAL_MESH_COLS as f64 } else { f64::INFINITY };
|
||||
let br = breaks(g, self.t0, self.t1, gap);
|
||||
@@ -1367,9 +1402,9 @@ impl Frame {
|
||||
/// `n` solid shapes covering the box in paint order, each coloured as the gradient in the
|
||||
/// middle of its band: for a linear gradient, rectangles from each band's start to the far
|
||||
/// end, for a radial one, nested ellipses (largest first), for a conic one, wedges around the
|
||||
/// center from each band's start to the seam. Each paints over the ones below except its
|
||||
/// band, so no seam shows between bands. With translucent stops each shape is just its band,
|
||||
/// so nothing doubles up.
|
||||
/// center from each band's start to the seam, for a diamond one, nested squares (largest
|
||||
/// first). Each paints over the ones below except its band, so no seam shows between bands.
|
||||
/// With translucent stops each shape is just its band, so nothing doubles up.
|
||||
fn pieces(&self, g: &Gradient, n: usize) -> Vec<(PathData, (Color, f32))> {
|
||||
let opaque = g.stops.iter().all(|s| s.opacity >= 1.0);
|
||||
if let Some((geom, b, _)) = &self.conic {
|
||||
@@ -1379,6 +1414,29 @@ impl Frame {
|
||||
.map(|(pts, t)| (PathData::single(SubPath::polyline(&pts, true)), g.sample_keep_model(t as f32)))
|
||||
.collect();
|
||||
}
|
||||
if let Some((frame, b, reach)) = &self.diamond {
|
||||
let Some(frame) = frame else {
|
||||
return vec![(shapes::rectangle(*b), g.sample_keep_model(1.0))];
|
||||
};
|
||||
// Nested squares about the center, largest first, as a radial gradient's ellipses.
|
||||
let step = reach / n as f64;
|
||||
let square = |t: f64| -> SubPath {
|
||||
let pts: Vec<Point> = [0.0, 0.25, 0.5, 0.75].iter().map(|&u| *frame * (diamond_perimeter(u).to_vec2() * t).to_point()).collect();
|
||||
SubPath::polyline(&pts, true)
|
||||
};
|
||||
return (0..n)
|
||||
.rev()
|
||||
.map(|k| {
|
||||
let mut shape = PathData::single(square((k + 1) as f64 * step));
|
||||
if !opaque && k > 0 {
|
||||
let mut hole = square(k as f64 * step);
|
||||
hole.reverse();
|
||||
shape.subpaths.push(hole);
|
||||
}
|
||||
(shape, g.sample_keep_model(((k as f64 + 0.5) * step) as f32))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let step = (self.t1 - self.t0) / n as f64;
|
||||
let color = |k: usize| g.sample_keep_model((self.t0 + (k as f64 + 0.5) * step) as f32);
|
||||
if self.radial {
|
||||
@@ -1410,3 +1468,14 @@ impl Frame {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The point a fraction `u` (0..1) of the way around the square with corners (1, 0), (0, 1),
|
||||
/// (-1, 0) and (0, -1), from the first: a diamond gradient's last stop in its frame.
|
||||
fn diamond_perimeter(u: f64) -> Point {
|
||||
const CORNERS: [(f64, f64); 5] = [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0), (1.0, 0.0)];
|
||||
let s = if u.is_finite() { u.clamp(0.0, 1.0) * 4.0 } else { 0.0 };
|
||||
let i = (s.floor() as usize).min(3);
|
||||
let f = s - i as f64;
|
||||
let ((x0, y0), (x1, y1)) = (CORNERS.get(i).copied().unwrap_or((1.0, 0.0)), CORNERS.get(i + 1).copied().unwrap_or((1.0, 0.0)));
|
||||
Point::new(x0 + (x1 - x0) * f, y0 + (y1 - y0) * f)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Gradient",
|
||||
["Window", "Gradient"],
|
||||
None,
|
||||
"{stroke?: bool (default: the targeted item's kind, else the active proxy), kind?: linear|radial|conic|freeform (conic, also accepted as angle or conical, sweeps the stops once around the center, clockwise from the vector, with a seam where the last stop meets the first; freeform places points on each object, coloured along the stops), mode?: points|lines (freeform: how the Gradient tool adds points), method?: classic|perceptual (how the colours between the stops mix: in display RGB, or perceptually (OKLab) so the steps look even; SVG exports draw a Perceptual gradient as an image, and PDF exports do too unless it is linear, radial or conic, has spot color stops, every other stop is paper white or written in CMYK or gray (an export that converts colors to a CMYK destination profile writes RGB and Lab stops in CMYK, and so does a CMYK document unless the export converts colors to an RGB destination profile), it is not a dithered fill and its curve does not have too many stops: then PDF writes tints of one spot ink as a Separation shading in that ink, and spot inks mixed with other spot, CMYK or gray inks as a DeviceN shading; freeform gradients mix their own way), stops?: [{offset 0..1, color? (needed without swatch; #rrggbbaa and #rgba are refused), opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: colour swatch name (a global or spot colour or tint swatch links the stop, so swatch edits recolour it and a spot stop prints on its plate; a process colour just gives its colour), tint?: 0..100 (% of the linked swatch; default 100, or a tint swatch's own)}] (at least 2; a freeform gradient's points are recoloured along them), keepModel?: bool (as paint.setFill; a stop color the gradient already has keeps its model), angle?: deg counter-clockwise (0: the gradient runs left to right; radial and conic: the vector turns about the center), aspect?: % (radial and conic; a negative one mirrors a conic's sweep and is an error for the other kinds), reverse?: bool, dither?: bool (fills only: a linear, radial or conic gradient fill is drawn with fine noise that hides banding, and as pixels in SVG and PDF exports), item?: fill/stroke item index|null (omitted: the Appearance panel's active item when it is of the edited kind), ids?|id?, strokeMode?: within|along|across (strokes only: the gradient lies on the page and shows through the stroke, runs from the start of each subpath to its end, or runs from the stroke's left edge to its right all along it; with nothing selected, for the next object drawn; type characters' own strokes always paint within), target?: \"object\"|\"contents\"} edit the gradient in place (keeps its placement); solid/none paints become the default gradient",
|
||||
"{stroke?: bool (default: the targeted item's kind, else the active proxy), kind?: linear|radial|conic|diamond|freeform (conic, also accepted as angle or conical, sweeps the stops once around the center, clockwise from the vector, with a seam where the last stop meets the first; diamond runs the stops from the center out to a square with a corner at the end of the vector; freeform places points on each object, coloured along the stops), mode?: points|lines (freeform: how the Gradient tool adds points), method?: classic|perceptual (how the colours between the stops mix: in display RGB, or perceptually (OKLab) so the steps look even; SVG exports draw a Perceptual gradient as an image, and PDF exports do too unless it is linear, radial, conic or diamond, has spot color stops, every other stop is paper white or written in CMYK or gray (an export that converts colors to a CMYK destination profile writes RGB and Lab stops in CMYK, and so does a CMYK document unless the export converts colors to an RGB destination profile), it is not a dithered fill and its curve does not have too many stops: then PDF writes tints of one spot ink as a Separation shading in that ink, and spot inks mixed with other spot, CMYK or gray inks as a DeviceN shading; freeform gradients mix their own way), stops?: [{offset 0..1, color? (needed without swatch; #rrggbbaa and #rgba are refused), opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: colour swatch name (a global or spot colour or tint swatch links the stop, so swatch edits recolour it and a spot stop prints on its plate; a process colour just gives its colour), tint?: 0..100 (% of the linked swatch; default 100, or a tint swatch's own)}] (at least 2; a freeform gradient's points are recoloured along them), keepModel?: bool (as paint.setFill; a stop color the gradient already has keeps its model), angle?: deg counter-clockwise (0: the gradient runs left to right; radial, conic and diamond: the vector turns about the center), aspect?: % (radial, conic and diamond; a negative one mirrors a conic's sweep and is an error for the other kinds), reverse?: bool, dither?: bool (fills only: a linear, radial, conic or diamond gradient fill is drawn with fine noise that hides banding, and as pixels in SVG and PDF exports), item?: fill/stroke item index|null (omitted: the Appearance panel's active item when it is of the edited kind), ids?|id?, strokeMode?: within|along|across (strokes only: the gradient lies on the page and shows through the stroke, runs from the start of each subpath to its end, or runs from the stroke's left edge to its right all along it; with nothing selected, for the next object drawn; type characters' own strokes always paint within), target?: \"object\"|\"contents\"} edit the gradient in place (keeps its placement); solid/none paints become the default gradient",
|
||||
has_doc,
|
||||
edit_gradient
|
||||
),
|
||||
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Gradient Vector",
|
||||
[],
|
||||
None,
|
||||
"{start?: [x,y], end?: [x,y] (document coordinates; both or neither: omitted, the vector stays), aspect?: % (radial and conic: the extent ellipse's height / width, a negative one mirroring a conic's sweep and an error for the other kinds; default: kept), focal?: [x,y] (document coordinates) | null (radial: the focal point, where the first stop sits, pulled inside the extent ellipse; null centres it; default: it keeps its place in the ellipse; an error on a conic gradient), ids?|id?, stroke?: bool (default: the targeted item's kind, else the active proxy), item?: fill/stroke item index|null (alias: index; omitted: the Appearance panel's active item when it is of the edited kind), target?: \"object\"|\"contents\", index?: appearance item index (as item)} set the gradient vector, aspect ratio and focal point (solid paints become the default gradient; type objects set it on their runs, in text space)",
|
||||
"{start?: [x,y], end?: [x,y] (document coordinates; both or neither: omitted, the vector stays), aspect?: % (radial, conic and diamond: the extent's height / width across the vector, a negative one mirroring a conic's sweep and an error for the other kinds; default: kept), focal?: [x,y] (document coordinates) | null (radial: the focal point, where the first stop sits, pulled inside the extent ellipse; null centres it; default: it keeps its place in the ellipse; an error on a conic or diamond gradient), ids?|id?, stroke?: bool (default: the targeted item's kind, else the active proxy), item?: fill/stroke item index|null (alias: index; omitted: the Appearance panel's active item when it is of the edited kind), target?: \"object\"|\"contents\", index?: appearance item index (as item)} set the gradient vector, aspect ratio and focal point (solid paints become the default gradient; type objects set it on their runs, in text space)",
|
||||
has_doc,
|
||||
set_gradient_geom
|
||||
),
|
||||
@@ -118,7 +118,7 @@ type Parsed<T> = std::result::Result<T, String>;
|
||||
|
||||
/// Parse a kind name.
|
||||
fn parse_kind(k: &str) -> Parsed<GradientKind> {
|
||||
GradientKind::parse(k).ok_or_else(|| format!("unknown gradient kind `{k}` (linear, radial, conic, freeform)"))
|
||||
GradientKind::parse(k).ok_or_else(|| format!("unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform)"))
|
||||
}
|
||||
|
||||
/// The `method` param of `p`, if given: how the colours between the stops mix.
|
||||
@@ -275,7 +275,7 @@ pub(crate) fn parse_gradient(g: &Value) -> Parsed<GradientPaint> {
|
||||
gp.swatch = str_param(g, "swatch").map(str::to_string);
|
||||
let geom = GeomEdit::parse(g)?;
|
||||
let aspect = geom.aspect.map(|a| aspect_for(kind, a)).transpose()?;
|
||||
if kind == GradientKind::Conic {
|
||||
if matches!(kind, GradientKind::Conic | GradientKind::Diamond) {
|
||||
no_focal(&geom)?;
|
||||
}
|
||||
if let Some((start, end)) = geom.vector {
|
||||
@@ -315,7 +315,7 @@ fn aspect_for(kind: GradientKind, a: f64) -> Parsed<f64> {
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
/// An error when `e` gives a focal point: a conic gradient has none (`null` is allowed).
|
||||
/// An error when `e` gives a focal point: a conic or diamond gradient has none (`null` is allowed).
|
||||
fn no_focal(e: &GeomEdit) -> Parsed<()> {
|
||||
match e.focal {
|
||||
Some(Some(_)) => Err("`focal` applies to radial gradients".into()),
|
||||
@@ -645,7 +645,7 @@ impl GeomEdit {
|
||||
/// the paint's space (text space for type runs) to the document and `bounds` (in that space) fits
|
||||
/// an unplaced gradient, so what the document shows carries over where `e` keeps it. An error
|
||||
/// when `e` gives what the gradient's kind can't take (a negative aspect other than a conic's, or
|
||||
/// a conic's focal point).
|
||||
/// a conic's or diamond's focal point).
|
||||
fn vector_paint(
|
||||
cur: &Paint,
|
||||
e: &GeomEdit,
|
||||
@@ -656,7 +656,7 @@ fn vector_paint(
|
||||
let mut gp = gradient_for_edit(cur, model);
|
||||
let kind = gp.gradient.kind;
|
||||
let edit_aspect = e.aspect.map(|a| aspect_for(kind, a)).transpose()?;
|
||||
if kind == GradientKind::Conic {
|
||||
if matches!(kind, GradientKind::Conic | GradientKind::Diamond) {
|
||||
no_focal(e)?;
|
||||
}
|
||||
let Some(from_doc) = invert(to_doc) else { return Ok(None) };
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Fill",
|
||||
[],
|
||||
None,
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab; #rrggbbaa and #rgba are refused), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|conic|freeform (conic, also accepted as angle or conical: the stops sweep once around the center `start`, clockwise from the vector), method?: classic|perceptual (as paint.editGradient; default classic), stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg counter-clockwise (0: the gradient runs left to right), start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial and conic; a negative one mirrors a conic's sweep, counterclockwise, and is an error for the other kinds; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits; an error with a conic), dither?: bool (linear, radial and conic fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?|id?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear, radial or conic `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} sets the selection's fill and the default (new art fits a gradient to itself); a solid colour recolours only the gradient mesh point last clicked with the Mesh tool or Direct Selection; while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only; without `item`, a grayscale or 1-bit image takes a solid color as the color it is drawn in (white pixels transparent; `none` takes it off; an image with an alpha channel takes only a spot color, black or white); with an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, that object's fill changes for this instance only (solid, none, gradient or pattern) → {objects: how many objects took the paint (the objects inside a group, unless an appearance item of the group's own is targeted; 0 when only the default changed)}",
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab; #rrggbbaa and #rgba are refused), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|conic|diamond|freeform (conic, also accepted as angle or conical: the stops sweep once around the center `start`, clockwise from the vector; diamond: the stops run from the center `start` out to a square with a corner at `end`), method?: classic|perceptual (as paint.editGradient; default classic), stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg counter-clockwise (0: the gradient runs left to right), start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial, conic and diamond; a negative one mirrors a conic's sweep, counterclockwise, and is an error for the other kinds; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits; an error with a conic or diamond), dither?: bool (linear, radial, conic and diamond fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?|id?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear, radial, conic or diamond `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} sets the selection's fill and the default (new art fits a gradient to itself); a solid colour recolours only the gradient mesh point last clicked with the Mesh tool or Direct Selection; while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only; without `item`, a grayscale or 1-bit image takes a solid color as the color it is drawn in (white pixels transparent; `none` takes it off; an image with an alpha channel takes only a spot color, black or white); with an object of a dynamic symbol instance selected by Direct Selection and no ids, id or item, that object's fill changes for this instance only (solid, none, gradient or pattern) → {objects: how many objects took the paint (the objects inside a group, unless an appearance item of the group's own is targeted; 0 when only the default changed)}",
|
||||
has_doc,
|
||||
|s, p| set_paint(s, p, true)
|
||||
),
|
||||
@@ -641,7 +641,7 @@ fn paint_new_art(s: &mut Session, paint: &Paint, fill: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `p` ask for `span: "selection"` (#2127)? Only a linear, radial or conic gradient the targets
|
||||
/// Does `p` ask for `span: "selection"` (#2127)? Only a linear, radial, conic or diamond gradient the targets
|
||||
/// would each fit to their own bounds can span them: one with its own vector (`start`/`end`),
|
||||
/// a freeform one or a gradient swatch already has its placement.
|
||||
fn spans_targets(p: &Value, paint: &Paint) -> std::result::Result<bool, String> {
|
||||
@@ -656,7 +656,7 @@ fn spans_targets(p: &Value, paint: &Paint) -> std::result::Result<bool, String>
|
||||
return Err("span: \"selection\" places a `gradient`, not a swatch".into());
|
||||
}
|
||||
if g.gradient.kind == vectorcraft_color::GradientKind::Freeform || g.freeform.is_some() {
|
||||
return Err("span: \"selection\" places a linear, radial or conic gradient, not a freeform one".into());
|
||||
return Err("span: \"selection\" places a linear, radial, conic or diamond gradient, not a freeform one".into());
|
||||
}
|
||||
if g.geom.is_some() {
|
||||
return Err(
|
||||
|
||||
@@ -404,6 +404,7 @@ fn gradient_key(g: &Gradient) -> Option<GradientKey<'_>> {
|
||||
GradientKind::Radial => 1,
|
||||
GradientKind::Freeform => 2,
|
||||
GradientKind::Conic => 3,
|
||||
GradientKind::Diamond => 4,
|
||||
};
|
||||
let method = match g.method {
|
||||
GradientMethod::Classic => 0,
|
||||
|
||||
@@ -32,7 +32,7 @@ fn fill_gradient(s: &Session, id: NodeId) -> GradientPaint {
|
||||
|
||||
#[test]
|
||||
fn new_gradient_defaults_follow_the_cmyk_document_mode() {
|
||||
for kind in ["linear", "radial", "conic", "freeform"] {
|
||||
for kind in ["linear", "radial", "conic", "diamond", "freeform"] {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width":200,"height":200,"colorMode":"cmyk"})).unwrap();
|
||||
let id = rect(&mut s, 10.0, 10.0, 100.0, 100.0);
|
||||
@@ -586,7 +586,7 @@ fn a_conic_gradient_is_set_edited_transformed_and_saved() {
|
||||
assert_eq!(fill_gradient(&s, id).gradient.kind, GradientKind::Conic, "{name}");
|
||||
}
|
||||
let e = s.execute("paint.editGradient", &json!({"kind": "spiral"})).unwrap_err().to_string();
|
||||
assert!(e.contains("(linear, radial, conic, freeform)"), "{e}");
|
||||
assert!(e.contains("(linear, radial, conic, diamond, freeform)"), "{e}");
|
||||
// Placed, an angle turns the vector about the center.
|
||||
s.execute("paint.setGradientGeom", &json!({"start": [200, 150], "end": [260, 150], "aspect": 50})).unwrap();
|
||||
s.execute("paint.editGradient", &json!({"angle": 90})).unwrap();
|
||||
@@ -747,3 +747,137 @@ fn making_a_gradient_conic_is_one_undo_step() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(fill_gradient(&s, id), before);
|
||||
}
|
||||
|
||||
/// A diamond gradient through `paint.setFill`, the Gradient panel's edits, transforms, the params
|
||||
/// agents and swatches read, and the native format.
|
||||
#[test]
|
||||
fn a_diamond_gradient_is_set_edited_transformed_and_saved() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "diamond"}})).unwrap();
|
||||
let g = fill_gradient(&s, id);
|
||||
assert_eq!(g.gradient.kind, GradientKind::Diamond);
|
||||
// Unplaced, it runs from the box's center out to a square whose edges pass through the box's
|
||||
// corners.
|
||||
let geom = g.resolve(node(&s, id).geometric_bounds().unwrap());
|
||||
assert_eq!((geom.start, geom.end), (Point::new(200.0, 150.0), Point::new(350.0, 150.0)));
|
||||
for (p, t) in [
|
||||
(Point::new(300.0, 200.0), 1.0),
|
||||
(Point::new(100.0, 100.0), 1.0),
|
||||
(Point::new(250.0, 150.0), 1.0 / 3.0),
|
||||
(Point::new(200.0, 175.0), 1.0 / 6.0),
|
||||
] {
|
||||
assert!((geom.param_at(GradientKind::Diamond, p) - t).abs() < 1e-9, "{p:?}");
|
||||
}
|
||||
s.execute("paint.editGradient", &json!({"kind": "linear"})).unwrap();
|
||||
s.execute("paint.editGradient", &json!({"kind": "DIAMOND"})).unwrap();
|
||||
assert_eq!(fill_gradient(&s, id).gradient.kind, GradientKind::Diamond);
|
||||
let e = s.execute("paint.editGradient", &json!({"kind": "spiral"})).unwrap_err().to_string();
|
||||
assert!(e.contains("(linear, radial, conic, diamond, freeform)"), "{e}");
|
||||
// Placed, an angle turns the vector about the center.
|
||||
s.execute("paint.setGradientGeom", &json!({"start": [200, 150], "end": [260, 150], "aspect": 50})).unwrap();
|
||||
s.execute("paint.editGradient", &json!({"angle": 90})).unwrap();
|
||||
let g = fill_geom(&s, id);
|
||||
assert!(g.start.distance(Point::new(200.0, 150.0)) < 1e-9 && g.end.distance(Point::new(200.0, 90.0)) < 1e-9, "{g:?}");
|
||||
// A reflection keeps each point's color.
|
||||
let p = Point::new(230.0, 170.0);
|
||||
let before = g.param_at(GradientKind::Diamond, p);
|
||||
s.execute("object.reflect", &json!({"axis": "vertical"})).unwrap();
|
||||
let m = fill_geom(&s, id);
|
||||
assert!(m.aspect > 0.0, "{m:?}");
|
||||
assert!((m.param_at(GradientKind::Diamond, Point::new(170.0, 170.0)) - before).abs() < 1e-9);
|
||||
// A rotation carries it along.
|
||||
let before = m.param_at(GradientKind::Diamond, p);
|
||||
s.execute("object.rotate", &json!({"angle": 30, "origin": [200, 150]})).unwrap();
|
||||
let r = vectorcraft_geom::Affine::rotate_about(-30f64.to_radians(), Point::new(200.0, 150.0));
|
||||
let after = fill_geom(&s, id).param_at(GradientKind::Diamond, r * p);
|
||||
assert!((after - before).abs() < 1e-9, "{before} vs {after}");
|
||||
// The params give it back, and the native format keeps it.
|
||||
s.execute("paint.editGradient", &json!({"aspect": 60})).unwrap();
|
||||
let want = fill_gradient(&s, id);
|
||||
let params = vectorcraft_tools::params::gradient_params(&want);
|
||||
assert_eq!((params["kind"].as_str(), params["aspect"].as_f64()), (Some("diamond"), Some(60.0)));
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "linear"}})).unwrap();
|
||||
s.execute("paint.setFill", &json!({ "gradient": params })).unwrap();
|
||||
let got = fill_gradient(&s, id);
|
||||
assert_eq!((&got.gradient, got.geom), (&want.gradient, want.geom));
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
let Paint::Gradient(g) = back.node(id).unwrap().appearance.fill_paint() else { panic!("the gradient is lost") };
|
||||
assert_eq!((g.gradient.kind, g.geom), (GradientKind::Diamond, want.geom));
|
||||
}
|
||||
|
||||
/// A diamond refuses a negative aspect and a focal point, from every command that gives one, and
|
||||
/// the gradient stays as it was; a null focal point is allowed.
|
||||
#[test]
|
||||
fn a_diamond_refuses_a_negative_aspect_and_a_focal_point() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "diamond", "start": [200, 150], "end": [260, 150], "aspect": 50}})).unwrap();
|
||||
let before = fill_gradient(&s, id);
|
||||
for p in [
|
||||
json!({"gradient": {"kind": "diamond", "aspect": -50}}),
|
||||
json!({"gradient": {"kind": "diamond", "start": [0, 0], "end": [10, 0], "aspect": -50}}),
|
||||
] {
|
||||
assert!(s.execute("paint.setFill", &p).unwrap_err().to_string().contains("aspect"), "{p}");
|
||||
}
|
||||
assert!(s.execute("paint.editGradient", &json!({"aspect": -50})).is_err());
|
||||
assert!(s.execute("paint.setGradientGeom", &json!({"aspect": -50})).is_err());
|
||||
assert!(s.execute("paint.editGradient", &json!({"kind": "radial", "aspect": -50})).is_err());
|
||||
let e = s.execute("paint.setFill", &json!({"gradient": {"kind": "diamond", "start": [200, 150], "end": [260, 150], "focal": [210, 150]}}));
|
||||
assert!(e.unwrap_err().to_string().contains("focal"));
|
||||
assert!(s.execute("paint.setGradientGeom", &json!({"focal": [210, 150]})).unwrap_err().to_string().contains("focal"));
|
||||
assert_eq!(fill_gradient(&s, id), before);
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "diamond", "start": [200, 150], "end": [260, 150], "focal": null}})).unwrap();
|
||||
assert_eq!(fill_gradient(&s, id).geom.unwrap().focal, None);
|
||||
}
|
||||
|
||||
/// A free distort that is not affine moves a diamond by the warp near its center, as it moves a
|
||||
/// radial gradient.
|
||||
#[test]
|
||||
fn a_warp_moves_a_diamond_by_its_center() {
|
||||
let warped = |kind: &str| {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": kind, "start": [20, 30], "end": [90, 30], "aspect": 60}})).unwrap();
|
||||
s.execute("object.distort", &json!({"corners": [[0, 0], [100, 0], [140, 130], [-20, 100]]})).unwrap();
|
||||
fill_geom(&s, id)
|
||||
};
|
||||
let (diamond, radial) = (warped("diamond"), warped("radial"));
|
||||
assert!(diamond.start.distance(radial.start) < 1e-9, "{diamond:?} vs {radial:?}");
|
||||
}
|
||||
|
||||
/// Saved as `.ai`, as PDF with its editing data and as SVG with its editing data, then opened, a
|
||||
/// turned, squashed diamond comes back as it was.
|
||||
#[test]
|
||||
fn a_diamond_survives_saving_as_ai_pdf_and_svg() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "diamond", "start": [180, 140], "end": [240, 110], "aspect": 60}})).unwrap();
|
||||
let want = fill_gradient(&s, id);
|
||||
for format in ["ai", "pdf", "svg"] {
|
||||
let r = s.execute("file.saveCopy", &json!({"format": format, "preserveEditing": true})).unwrap();
|
||||
let mut back = session();
|
||||
back.execute("document.open", &json!({"name": format!("t.{format}"), "dataBase64": r["dataBase64"]})).unwrap();
|
||||
let mut found = None;
|
||||
back.doc().unwrap().doc.walk(|n| {
|
||||
if let (None, Paint::Gradient(g)) = (&found, n.appearance.fill_paint()) {
|
||||
found = Some(*g.clone());
|
||||
}
|
||||
});
|
||||
let got = found.unwrap_or_else(|| panic!("{format}: the gradient is lost"));
|
||||
assert_eq!((got.gradient.kind, got.geom), (GradientKind::Diamond, want.geom), "{format}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Turning a gradient diamond is one undo step, and undoing it gives the gradient back as it was.
|
||||
#[test]
|
||||
fn making_a_gradient_diamond_is_one_undo_step() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
s.execute("paint.setFill", &json!({"gradient": {"kind": "radial"}})).unwrap();
|
||||
let (before, steps) = (fill_gradient(&s, id), s.doc().unwrap().history.undo.len());
|
||||
s.execute("paint.editGradient", &json!({"kind": "diamond", "angle": 45})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(fill_gradient(&s, id), before);
|
||||
}
|
||||
|
||||
@@ -393,6 +393,77 @@ fn a_mirrored_or_flat_conic_expands_as_the_canvas_draws_it() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Three opaque stops running from the center out to the square.
|
||||
fn diamond_stops() -> Value {
|
||||
json!({"kind": "diamond", "stops": [{"offset": 0, "color": "#ffff00"}, {"offset": 0.5, "color": "#ff00ff"}, {"offset": 1, "color": "#004080"}]})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_diamond_gradient_becomes_nested_squares_or_a_mesh_that_paint_it() {
|
||||
// Objects: nested squares about the center, largest first, together painting what the
|
||||
// canvas painted.
|
||||
let mut s = session();
|
||||
gradient_rect(&mut s, Rect::new(30.0, 40.0, 170.0, 160.0), diamond_stops(), false);
|
||||
let before = render(&s);
|
||||
run(&mut s, "object.expand", json!({"steps": 120}));
|
||||
let g = selected(&s);
|
||||
let (_, squares) = clip_parts(&g);
|
||||
assert_eq!(squares.len(), 120);
|
||||
let corners = |n: &Node| n.path_data().unwrap().subpaths[0].anchors.iter().map(|a| a.p).collect::<Vec<_>>();
|
||||
let (outer, inner) = (corners(squares[0]), corners(squares[119]));
|
||||
assert_eq!((outer.len(), inner.len()), (4, 4));
|
||||
let reach = |pts: &[vectorcraft_geom::Point]| pts.iter().map(|p| p.distance(vectorcraft_geom::Point::new(100.0, 100.0))).fold(0.0, f64::max);
|
||||
assert!(reach(&outer) > reach(&inner) * 100.0, "largest first: {outer:?} then {inner:?}");
|
||||
let after = render(&s);
|
||||
assert!(differing(&before, &after, 48) < 200, "{}", differing(&before, &after, 48));
|
||||
assert!(mean_diff(&before, &after) < 2.0, "{}", mean_diff(&before, &after));
|
||||
// Translucent stops: each square is a ring with the next one cut out, so none doubles up.
|
||||
let mut s = session();
|
||||
let gr = json!({"kind": "diamond", "stops": [{"offset": 0, "color": "#ff0000", "opacity": 0.5}, {"offset": 1, "color": "#0000ff"}]});
|
||||
gradient_rect(&mut s, Rect::new(30.0, 40.0, 170.0, 160.0), gr, false);
|
||||
let before = render(&s);
|
||||
run(&mut s, "object.expand", json!({"steps": 60}));
|
||||
let g = selected(&s);
|
||||
let (_, squares) = clip_parts(&g);
|
||||
assert!(squares[..59].iter().all(|n| n.path_data().unwrap().subpaths.len() == 2), "rings");
|
||||
assert!(mean_diff(&before, &render(&s)) < 4.0, "{}", mean_diff(&before, &render(&s)));
|
||||
// Mesh: rings of patches about the center.
|
||||
let mut s = session();
|
||||
gradient_rect(&mut s, Rect::new(30.0, 40.0, 170.0, 160.0), diamond_stops(), false);
|
||||
let before = render(&s);
|
||||
run(&mut s, "object.expand", json!({"gradient": "mesh"}));
|
||||
let g = selected(&s);
|
||||
let (_, content) = clip_parts(&g);
|
||||
let NodeKind::Mesh(m) = &content[0].kind else { panic!("a mesh node: {:?}", content[0].kind) };
|
||||
assert!(m.is_valid());
|
||||
let after = render(&s);
|
||||
assert!(differing(&before, &after, 48) < 200, "{}", differing(&before, &after, 48));
|
||||
assert!(mean_diff(&before, &after) < 2.5, "{}", mean_diff(&before, &after));
|
||||
}
|
||||
|
||||
/// A turned, squashed diamond, and one whose square is flat (as a file can hold it), expand as
|
||||
/// the canvas draws them.
|
||||
#[test]
|
||||
fn a_turned_or_flat_diamond_expands_as_the_canvas_draws_it() {
|
||||
for aspect in [0.6, 0.0] {
|
||||
for how in [json!({"steps": 120}), json!({"gradient": "mesh"})] {
|
||||
let mut s = session();
|
||||
let id = gradient_rect(&mut s, Rect::new(30.0, 40.0, 170.0, 160.0), diamond_stops(), false);
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let n = d.node_mut(id).unwrap();
|
||||
let Paint::Gradient(mut g) = n.appearance.fill_paint().clone() else { panic!("a gradient fill") };
|
||||
let (start, end) = (vectorcraft_geom::Point::new(90.0, 110.0), vectorcraft_geom::Point::new(130.0, 80.0));
|
||||
g.geom = Some(vectorcraft_color::GradientGeom { start, end, aspect, focal: None });
|
||||
n.appearance.set_fill(Paint::Gradient(g));
|
||||
let before = render(&s);
|
||||
run(&mut s, "object.expand", how.clone());
|
||||
let after = render(&s);
|
||||
assert!(differing(&before, &after, 48) < 200, "{aspect} {how}: {}", differing(&before, &after, 48));
|
||||
assert!(mean_diff(&before, &after) < 2.5, "{aspect} {how}: {}", mean_diff(&before, &after));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mesh_option_makes_a_mesh_that_paints_the_gradient() {
|
||||
for gradient in [
|
||||
|
||||
@@ -7575,3 +7575,45 @@ fn conic_gradients_are_written_as_linear_gradients_with_their_stops() {
|
||||
let Some(Paint::Gradient(swatch)) = back.swatch("Sweep").map(|s| &s.paint) else { panic!("the swatch is lost") };
|
||||
assert_eq!(swatch.gradient.kind, GradientKind::Linear);
|
||||
}
|
||||
|
||||
/// The data has no diamond gradient: a diamond fill is the radial gradient through its square's
|
||||
/// corners (centered on it, its vector and the aspect across it), with the same stops, and so is
|
||||
/// a diamond gradient swatch, with a warning.
|
||||
#[test]
|
||||
fn diamond_gradients_are_written_as_radial_gradients_through_their_corners() {
|
||||
let stops =
|
||||
vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(0.6, SKY), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
|
||||
let mut g = GradientPaint::new(Gradient::new(GradientKind::Diamond, stops.clone()));
|
||||
let (start, end) = (vectorcraft_geom::Point::new(100.0, 50.0), vectorcraft_geom::Point::new(150.0, 50.0));
|
||||
// A focal point (a plug-in's or a file's) draws nothing on a diamond and isn't written.
|
||||
let focal = Some(vectorcraft_geom::Point::new(110.0, 55.0));
|
||||
g.geom = Some(vectorcraft_color::GradientGeom { start, end, aspect: 0.5, focal });
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let id = doc.alloc_id();
|
||||
let l = doc.layers[0].id;
|
||||
let fill = Paint::Gradient(Box::new(g.clone()));
|
||||
doc.insert(Some(l), 0, Node::path(id, shapes::rectangle(Rect::new(20.0, 10.0, 180.0, 90.0)), Appearance::basic(fill, Paint::None, 0.0))).unwrap();
|
||||
let mut swatches: Vec<Swatch> = doc.swatches.iter().cloned().collect();
|
||||
swatches.push(Swatch { name: "Gem".into(), paint: Paint::Gradient(Box::new(GradientPaint { geom: None, ..g })), global: false, spot: false });
|
||||
doc.swatches = swatches.into();
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert!(out.warnings.iter().any(|w| w.contains("diamond gradients are written as radial gradients")), "{:?}", out.warnings);
|
||||
let back = read_back(&doc);
|
||||
let mut read = None;
|
||||
back.walk(|n| {
|
||||
if let Paint::Gradient(g) = n.appearance.fill_paint() {
|
||||
read = Some(g.clone());
|
||||
}
|
||||
});
|
||||
let read = read.expect("a gradient fill");
|
||||
assert_eq!(read.gradient.kind, GradientKind::Radial);
|
||||
assert_eq!(
|
||||
read.gradient.stops.iter().map(|s| (s.offset, s.color.to_hex())).collect::<Vec<_>>(),
|
||||
stops.iter().map(|s| (s.offset, s.color.to_hex())).collect::<Vec<_>>()
|
||||
);
|
||||
let geom = read.geom.unwrap();
|
||||
assert!(geom.start.distance(start) < 0.01 && geom.end.distance(end) < 0.01 && (geom.aspect - 0.5).abs() < 0.01, "{geom:?}");
|
||||
assert!(geom.focal.is_none_or(|f| f.distance(start) < 0.01), "{geom:?}");
|
||||
let Some(Paint::Gradient(swatch)) = back.swatch("Gem").map(|s| &s.paint) else { panic!("the swatch is lost") };
|
||||
assert_eq!(swatch.gradient.kind, GradientKind::Radial);
|
||||
}
|
||||
|
||||
@@ -45,12 +45,22 @@ pub(super) struct PatternOut {
|
||||
used: bool,
|
||||
}
|
||||
|
||||
/// Can `g` be written as a gradient (a linear, radial or conic one with stops, no more than the
|
||||
/// `.ai` reader reads; a conic is written linear, [`CONIC`])?
|
||||
pub(in crate::ai_editing) const DIAMOND: &str =
|
||||
"diamond gradients are written as radial gradients through their corners, with the same stops, in the editing data for other apps";
|
||||
|
||||
/// Can `g` be written as a gradient (a linear, radial, conic or diamond one with stops, no more
|
||||
/// than the `.ai` reader reads; a conic is written linear, [`CONIC`], and a diamond radial,
|
||||
/// [`DIAMOND`])?
|
||||
fn writable_gradient(g: &Gradient) -> bool {
|
||||
g.kind != GradientKind::Freeform && !g.stops.is_empty() && g.stops.len() <= crate::import::MAX_GRADIENT_STOPS
|
||||
}
|
||||
|
||||
/// Is a gradient of `kind` written as a radial one: a radial gradient, or a diamond (the ellipse
|
||||
/// through its square's corners, [`DIAMOND`])?
|
||||
fn written_radial(kind: GradientKind) -> bool {
|
||||
matches!(kind, GradientKind::Radial | GradientKind::Diamond)
|
||||
}
|
||||
|
||||
/// The first name of `Gradient`, `Gradient 2`, `Gradient 3`, … numbered `next` or higher for
|
||||
/// which `taken` returns false (`Gradient` is number 1); `next` is set past it.
|
||||
pub(in crate::ai_editing) fn free_gradient_name(next: &mut u64, mut taken: impl FnMut(&str) -> bool) -> String {
|
||||
@@ -128,7 +138,7 @@ impl Writer<'_> {
|
||||
let Paint::Gradient(g) = p else { return p.clone() };
|
||||
let geom = g.resolve(bounds);
|
||||
let finite = [geom.start.x, geom.start.y, geom.end.x, geom.end.y, geom.aspect].iter().all(|v| v.is_finite());
|
||||
let radial = g.gradient.kind == GradientKind::Radial;
|
||||
let radial = written_radial(g.gradient.kind);
|
||||
if g.gradient.kind == GradientKind::Freeform {
|
||||
self.warn(GRADIENTS);
|
||||
return Paint::solid(average(&g.gradient));
|
||||
@@ -244,7 +254,7 @@ impl Writer<'_> {
|
||||
self.instance_head(g, clip, path);
|
||||
let m = self.grad_space;
|
||||
let u = geom.end - geom.start;
|
||||
let radial = g.gradient.kind == GradientKind::Radial;
|
||||
let radial = written_radial(g.gradient.kind);
|
||||
// The unit vector along the gradient, and across it as long.
|
||||
let frame = |from: f64, to: f64| {
|
||||
let p = geom.start + u * from;
|
||||
@@ -275,10 +285,15 @@ impl Writer<'_> {
|
||||
/// A gradient instance's focal point (`Bh`, radial gradients only) and its `Bg` line, as
|
||||
/// [`Self::instance`] writes them; also what a fill's art style holds after its `Bb`.
|
||||
pub(super) fn instance_head(&mut self, g: &GradientPaint, clip: bool, path: &BezPath) {
|
||||
let Some(geom) = g.geom else { return };
|
||||
let Some(mut geom) = g.geom else { return };
|
||||
if g.gradient.kind != GradientKind::Radial {
|
||||
// Only a radial gradient has a focal point (a plug-in's or a file's other kinds can
|
||||
// carry one that draws nothing): a diamond's circle stays centered.
|
||||
geom.focal = None;
|
||||
}
|
||||
let name = self.define_gradient(&g.gradient, g.swatch.as_deref());
|
||||
let m = self.grad_space;
|
||||
let radial = g.gradient.kind == GradientKind::Radial;
|
||||
let radial = written_radial(g.gradient.kind);
|
||||
let place = placement(&geom, radial, &(self.canvas * path.clone()), self.canvas);
|
||||
if radial {
|
||||
let c = m * geom.start;
|
||||
@@ -305,8 +320,11 @@ impl Writer<'_> {
|
||||
if d.gradient.kind == GradientKind::Conic {
|
||||
self.warn(CONIC);
|
||||
}
|
||||
if d.gradient.kind == GradientKind::Diamond {
|
||||
self.warn(DIAMOND);
|
||||
}
|
||||
let name = ps::bytes(d.name.as_bytes());
|
||||
let radial = d.gradient.kind == GradientKind::Radial;
|
||||
let radial = written_radial(d.gradient.kind);
|
||||
let _ = writeln!(s, "%AI5_BeginGradient: {name}\n{name} {} {} Bd\n[", u8::from(radial), d.gradient.stops.len());
|
||||
let mut stops = sorted_stops(&d.gradient);
|
||||
if !radial {
|
||||
@@ -508,7 +526,7 @@ impl Writer<'_> {
|
||||
let swatch = Some(sw.name.as_str());
|
||||
let name = self.define_gradient(&g.gradient, swatch);
|
||||
s.push_str("Bb\n");
|
||||
if g.gradient.kind == GradientKind::Radial {
|
||||
if written_radial(g.gradient.kind) {
|
||||
s.push_str("0 0 0 0 Bh\n");
|
||||
}
|
||||
let _ = writeln!(s, "2 {} 0 0 0 1 1 0 0 1 0 0 1 Bg\n0 BB", ps::bytes(name.as_bytes()));
|
||||
|
||||
@@ -108,6 +108,7 @@ pub const NAMES: &str = "object names are left out (legacy .ai files name only l
|
||||
const PATTERNS: &str = "pattern fills are written as their tiles, clipped to the shape";
|
||||
const FREEFORM: &str = "freeform gradients are written as their average colour";
|
||||
const CONIC: &str = "conic gradients are written as solid wedges clipped to the shape (legacy .ai files have no conic gradient)";
|
||||
const DIAMOND: &str = "diamond gradients are written as four linear gradients clipped to the shape (legacy .ai files have no diamond gradient)";
|
||||
/// Wedges a conic gradient is written as: one a degree.
|
||||
const CONIC_WEDGES: usize = 360;
|
||||
const STROKES: &str =
|
||||
|
||||
@@ -201,7 +201,9 @@ pub(super) fn segment_point(path: &BezPath, fraction: f64) -> f64 {
|
||||
pub(super) fn direct_fill(p: &Paint) -> bool {
|
||||
match p {
|
||||
Paint::Solid { .. } => true,
|
||||
Paint::Gradient(g) => !matches!(g.gradient.kind, GradientKind::Freeform | GradientKind::Conic) && !g.gradient.stops.is_empty(),
|
||||
Paint::Gradient(g) => {
|
||||
!matches!(g.gradient.kind, GradientKind::Freeform | GradientKind::Conic | GradientKind::Diamond) && !g.gradient.stops.is_empty()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,35 @@ fn conic_gradients_are_written_as_wedges_clipped_to_the_shape() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The format has no diamond gradient: a diamond fill, and a diamond stroke (written as its filled
|
||||
/// outline), are four path objects clipped to the shape, each a linear gradient over a quadrant
|
||||
/// of the diamond's frame, with a warning; the definition is not named after the swatch.
|
||||
#[test]
|
||||
fn diamond_gradients_are_written_as_four_linear_gradients_clipped_to_the_shape() {
|
||||
let stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
|
||||
let mut g = GradientPaint::new(Gradient::new(GradientKind::Diamond, stops));
|
||||
g.geom = Some(vectorcraft_color::GradientGeom {
|
||||
start: vectorcraft_geom::Point::new(100.0, 50.0),
|
||||
end: vectorcraft_geom::Point::new(140.0, 30.0),
|
||||
aspect: 0.7,
|
||||
focal: None,
|
||||
});
|
||||
g.swatch = Some("Gem".into());
|
||||
let diamond = Paint::Gradient(Box::new(g));
|
||||
for (fill, stroke) in [(diamond.clone(), Paint::None), (Paint::None, diamond)] {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let n = shape(&mut doc, shapes::ellipse(Rect::new(20.0, 10.0, 180.0, 90.0)), fill.clone(), stroke.clone());
|
||||
let l = doc.layers[0].id;
|
||||
doc.insert(Some(l), 0, n).unwrap();
|
||||
let out = write(&doc);
|
||||
assert!(out.warnings.iter().any(|w| w == DIAMOND), "{:?}", out.warnings);
|
||||
let text = String::from_utf8_lossy(&out.bytes);
|
||||
assert_eq!(text.matches("%AI5_BeginGradient: (Gradient)").count(), 1, "one linear definition: {text}");
|
||||
assert!(!text.contains("(Gem)"), "not named after the swatch");
|
||||
assert_eq!(text.matches(" Bg\n").count(), 4, "a gradient a quadrant");
|
||||
}
|
||||
}
|
||||
|
||||
/// `doc` written with options `o`. Every file the tests write is also run as a PostScript program
|
||||
/// ([`prints_as`]) and must draw what `doc` draws.
|
||||
fn write_with(doc: &Document, o: LegacyAiOptions) -> LegacyAiOutput {
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::{
|
||||
ROTATED_TYPE, Rect, STROKES, SUBLAYERS, Shape, StrokeLayer, SubPath, TITLE_AND_COLOR_NAMES, TRANSPARENCY, TYPE, TYPE_ATTRIBUTES, TextKind,
|
||||
TextObject, UNDECODED, Vec2, Write, ai_ops, latin, ps, stroke,
|
||||
};
|
||||
use super::{CONIC, CONIC_WEDGES};
|
||||
use super::{CONIC, CONIC_WEDGES, DIAMOND};
|
||||
|
||||
pub(super) struct Writer<'a> {
|
||||
doc: &'a Document,
|
||||
@@ -429,6 +429,11 @@ impl<'a> Writer<'a> {
|
||||
{
|
||||
return self.conic_object(path, rule, f, g);
|
||||
}
|
||||
if let Paint::Gradient(g) = &f.paint
|
||||
&& g.gradient.kind == GradientKind::Diamond
|
||||
{
|
||||
return self.diamond_object(path, rule, f, g);
|
||||
}
|
||||
let Paint::Pattern { pattern, xf } = &f.paint else {
|
||||
// A freeform gradient: its average colour.
|
||||
self.warn(FREEFORM);
|
||||
@@ -497,6 +502,41 @@ impl<'a> Writer<'a> {
|
||||
self.line("Q");
|
||||
}
|
||||
|
||||
/// A diamond gradient fill (the format has no diamond gradient) as four linear gradients
|
||||
/// clipped to `path` (filled by `rule`), one for each quadrant of its frame and equal to the
|
||||
/// diamond there, each running on under the next
|
||||
/// ([`vectorcraft_color::GradientGeom::diamond_quadrants`]). A diamond without an extent is its
|
||||
/// last stop.
|
||||
fn diamond_object(&mut self, path: &PathData, rule: FillRule, f: &FillLayer, g: &GradientPaint) {
|
||||
let area = path.to_bezpath().bounding_box();
|
||||
let Some(quadrants) = g.resolve(area).diamond_quadrants(area) else {
|
||||
let color = g.gradient.stops.last().map_or(Color::gray(0.5), |s| s.color);
|
||||
let flat = FillLayer { paint: Paint::solid(color), ..f.clone() };
|
||||
return self.path_object(path, rule, Some(&flat), None);
|
||||
};
|
||||
self.warn(DIAMOND);
|
||||
self.line("q");
|
||||
self.fill_rule(rule);
|
||||
let several = path.subpaths.len() > 1;
|
||||
if several {
|
||||
self.line("*u");
|
||||
}
|
||||
for sp in path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()) {
|
||||
self.mask(sp);
|
||||
}
|
||||
if several {
|
||||
self.line("*U");
|
||||
}
|
||||
let gradient = Gradient { kind: GradientKind::Linear, ..g.gradient.clone() };
|
||||
for (pts, geom) in quadrants {
|
||||
// Not named after the swatch: the definition is a linear gradient, not the swatch's.
|
||||
let linear = GradientPaint { gradient: gradient.clone(), geom: Some(geom), swatch: None, ..g.clone() };
|
||||
let quadrant = FillLayer { paint: Paint::Gradient(Box::new(linear)), ..f.clone() };
|
||||
self.path_object(&PathData::single(SubPath::polyline(&pts, true)), FillRule::NonZero, Some(&quadrant), None);
|
||||
}
|
||||
self.line("Q");
|
||||
}
|
||||
|
||||
/// A path object painted with `fill` (a colour or a linear or radial gradient) and `stroke`
|
||||
/// (a plain line of a colour) over it; several subpaths make a compound path.
|
||||
fn path_object(&mut self, path: &PathData, rule: FillRule, fill: Option<&FillLayer>, stroke: Option<&StrokeLayer>) {
|
||||
|
||||
+42
-3
@@ -43,6 +43,7 @@ const SPOT_GRADIENTS_L2: &str =
|
||||
const SPOT_GRADIENTS_SHARED: &str = "gradients whose spot inks can't share a DeviceN color space are written without the spot inks";
|
||||
const SPOT_GRADIENTS_PERCEPTUAL: &str = "Perceptual gradients of spot colors are written in process colors";
|
||||
const CONIC_WEDGES: &str = "conic gradients are written as solid wedges (EPS has no conic shading)";
|
||||
const DIAMOND_QUADRANTS: &str = "diamond gradients are written as four linear gradients (EPS has no diamond shading)";
|
||||
/// Wedges a conic gradient is written as: one a degree.
|
||||
const CONIC_WEDGE_COUNT: usize = 360;
|
||||
/// The most colorants a DeviceN color space may have (PostScript's limit).
|
||||
@@ -934,9 +935,9 @@ impl<'a> Scene<'a> {
|
||||
(space, out)
|
||||
}
|
||||
|
||||
/// Paint all of `area` (inside the current clip) with linear, radial or conic gradient `g`,
|
||||
/// placed on `bounds`: a smooth shading at Level 3, else stepped fills (a conic: wedges at
|
||||
/// both levels).
|
||||
/// Paint all of `area` (inside the current clip) with linear, radial, conic or diamond gradient
|
||||
/// `g`, placed on `bounds`: a smooth shading at Level 3, else stepped fills (a conic: wedges at
|
||||
/// both levels; a diamond: four linear gradients, [`Self::diamond_quadrants`]).
|
||||
fn gradient(&mut self, g: &GradientPaint, bounds: Rect, area: Rect) {
|
||||
let geom = g.resolve(bounds);
|
||||
let (space, stops) = self.gradient_stops(&g.gradient);
|
||||
@@ -951,6 +952,7 @@ impl<'a> Scene<'a> {
|
||||
let smooth = self.o.level == Level::Three && !self.o.compatible_gradients;
|
||||
match g.gradient.kind {
|
||||
GradientKind::Conic => self.conic_steps(&geom, area, space, &stops),
|
||||
GradientKind::Diamond => self.diamond_quadrants(&geom, area, space, &stops, smooth),
|
||||
GradientKind::Radial => {
|
||||
let r = geom.length().max(1e-6);
|
||||
let squash = geom.radial_squash();
|
||||
@@ -999,6 +1001,43 @@ impl<'a> Scene<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A diamond gradient over `area` as four linear gradients, each clipped to a quadrant of its
|
||||
/// frame and equal to the diamond there (PostScript has no diamond shading;
|
||||
/// [`vectorcraft_color::GradientGeom::diamond_quadrants`]): smooth axial shadings, else
|
||||
/// stepped fills. Each quadrant but the last runs on under the next (stops are opaque here),
|
||||
/// so no hairline shows between them. A diamond without an extent is its last stop.
|
||||
fn diamond_quadrants(
|
||||
&mut self,
|
||||
geom: &vectorcraft_color::GradientGeom,
|
||||
area: Rect,
|
||||
space: &GradientSpace,
|
||||
stops: &[(f64, Vec<f32>)],
|
||||
smooth: bool,
|
||||
) {
|
||||
let Some(quadrants) = geom.diamond_quadrants(area) else {
|
||||
if let Some((_, c)) = stops.last() {
|
||||
self.set_comps(space, c);
|
||||
self.rect_fill(area);
|
||||
}
|
||||
return;
|
||||
};
|
||||
self.warn(DIAMOND_QUADRANTS);
|
||||
for (pts, q) in quadrants {
|
||||
self.line("q");
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
push_nums(&mut self.body, &[p.x, p.y]);
|
||||
self.line(if i == 0 { "newpath moveto" } else { "lineto" });
|
||||
}
|
||||
self.line("closepath clip newpath");
|
||||
if smooth {
|
||||
self.shading(2, space, &[q.start.x, q.start.y, q.end.x, q.end.y], stops);
|
||||
} else {
|
||||
self.linear_steps(q.start, q.end, area, space, stops);
|
||||
}
|
||||
self.line("Q");
|
||||
}
|
||||
}
|
||||
|
||||
/// A Level 3 smooth shading of `kind` (2 axial, 3 radial) at `coords`, extended both ways.
|
||||
fn shading(&mut self, kind: u8, space: &GradientSpace, coords: &[f64], stops: &[(f64, Vec<f32>)]) {
|
||||
let n = space.n();
|
||||
|
||||
@@ -217,6 +217,33 @@ fn conic_gradients_are_solid_wedges_at_both_levels() {
|
||||
}
|
||||
}
|
||||
|
||||
/// PostScript has no diamond shading: a diamond is four axial shadings at Level 3 (stepped fills at
|
||||
/// Level 2), each clipped to a quadrant of its frame, and read back from the PostScript it looks
|
||||
/// as the canvas paints it, with no hairline between the quadrants.
|
||||
#[test]
|
||||
fn diamond_gradients_are_four_linear_gradients_clipped_to_quadrants() {
|
||||
let stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
|
||||
let mut g = GradientPaint::new(Gradient::new(GradientKind::Diamond, stops));
|
||||
g.geom = Some(vectorcraft_color::GradientGeom { start: Point::new(100.0, 70.0), end: Point::new(140.0, 40.0), aspect: 0.6, focal: None });
|
||||
let paint = Paint::Gradient(Box::new(g));
|
||||
let d = doc_with(|d| vec![path(d, shapes::rectangle(Rect::new(20.0, 20.0, 180.0, 120.0)), paint, Paint::None, 0.0)]);
|
||||
for (o, shadings) in [(opts(&d), 4), (EpsOptions { level: Level::Two, ..opts(&d) }, 0)] {
|
||||
let out = export(&d, &o, None, None).unwrap();
|
||||
assert!(out.warnings.iter().any(|w| w.contains("diamond gradients are written as four linear gradients")), "{:?}", out.warnings);
|
||||
let page = page_of(&d, &o);
|
||||
assert_eq!(page.matches("shfill").count(), shadings);
|
||||
assert_eq!(page.matches("closepath clip newpath").count(), 4, "a clip a quadrant");
|
||||
}
|
||||
let out = export(&d, &opts(&d), None, None).unwrap();
|
||||
let back = import_with(&out.bytes, false).unwrap().document;
|
||||
vectorcraft_testkit::raster::assert_similar(
|
||||
&vectorcraft_testkit::raster::render_artboard(&d),
|
||||
&vectorcraft_testkit::raster::render_artboard(&back),
|
||||
6.0,
|
||||
0.01,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strokes_keep_their_caps_joins_and_dashes() {
|
||||
let d = doc_with(|d| {
|
||||
|
||||
@@ -433,14 +433,17 @@ impl Exporter<'_> {
|
||||
/// Is `p` a paint PDF has no paint for when it paints a fill (`fill`) or a stroke, which
|
||||
/// [`Self::area`] draws as art: a pattern swatch or a gradient drawn as pixels
|
||||
/// ([`vectorcraft_color::GradientPaint::draws_as_pixels`]), but not a Perceptual one of spot
|
||||
/// inks ([`Self::perceptual_spot`]), or a conic gradient that can't be a sweep shading (in
|
||||
/// PDF/A, or with more than [`super::paint::MAX_SWEEP_STOPS`] stops)?
|
||||
/// inks ([`Self::perceptual_spot`]), a conic gradient that can't be a sweep shading (in
|
||||
/// PDF/A, or with more than [`super::paint::MAX_SWEEP_STOPS`] stops), or a diamond gradient
|
||||
/// (four linear gradients, [`Self::diamond`])?
|
||||
pub(super) fn area_paint(&self, p: &Paint, fill: bool) -> bool {
|
||||
match p {
|
||||
Paint::Pattern { pattern, .. } => self.doc.pattern(pattern).is_some(),
|
||||
Paint::Gradient(g) => {
|
||||
let sweep_unwritable = || !self.sweeps || g.gradient.expanded().nth(super::paint::MAX_SWEEP_STOPS).is_some();
|
||||
(g.draws_as_pixels(fill) && !self.perceptual_spot(g, fill)) || (g.gradient.kind == GradientKind::Conic && sweep_unwritable())
|
||||
(g.draws_as_pixels(fill) && !self.perceptual_spot(g, fill))
|
||||
|| (g.gradient.kind == GradientKind::Conic && sweep_unwritable())
|
||||
|| g.gradient.kind == GradientKind::Diamond
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
@@ -448,9 +451,10 @@ impl Exporter<'_> {
|
||||
|
||||
/// Paint the area `clip` (filled by `rule`; bounds `area`) with `p` at `transparency` (opacity,
|
||||
/// blend mode) when it is an [area paint](Self::area_paint) of a fill (`fill`) or a stroke: a
|
||||
/// pattern's [tiling pattern](Self::tiling) (else its tile instances covering it), or an image
|
||||
/// of a gradient drawn as pixels placed on `bounds` (the painted object's box, as on the
|
||||
/// canvas), clipped to it. False (nothing drawn) for other paints.
|
||||
/// pattern's [tiling pattern](Self::tiling) (else its tile instances covering it), a diamond
|
||||
/// gradient's four linear gradients, or an image of a gradient drawn as pixels, placed on
|
||||
/// `bounds` (the painted object's box, as on the canvas), clipped to it. False (nothing drawn)
|
||||
/// for other paints.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn area(
|
||||
&mut self,
|
||||
@@ -498,7 +502,12 @@ impl Exporter<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Paint::Gradient(g) => self.field_image(s, g, bounds, area, fill),
|
||||
Paint::Gradient(g) => {
|
||||
let pixels = g.draws_as_pixels(fill) && !self.perceptual_spot(g, fill);
|
||||
if pixels || g.gradient.kind != GradientKind::Diamond || !self.diamond(s, g, bounds, area) {
|
||||
self.field_image(s, g, bounds, area, fill);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,10 +338,14 @@ impl Exporter<'_> {
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
// Painted areas get an image of the field ([`Self::area`]); elsewhere (gradient
|
||||
// slices) the average colour stands in.
|
||||
GradientKind::Freeform => {
|
||||
self.warn("freeform gradients are exported as their average colour");
|
||||
// Painted areas get an image of the field, or a diamond's four linear gradients
|
||||
// ([`Self::area`]); elsewhere (gradient slices) the average color stands in.
|
||||
GradientKind::Freeform | GradientKind::Diamond => {
|
||||
self.warn(if g.gradient.kind == GradientKind::Diamond {
|
||||
"diamond gradients in gradient slices are exported as their average color"
|
||||
} else {
|
||||
"freeform gradients are exported as their average colour"
|
||||
});
|
||||
let n = g.gradient.stops.len().max(1) as f32;
|
||||
let mut acc = [0.0f32; 3];
|
||||
for s in &g.gradient.stops {
|
||||
@@ -476,10 +480,43 @@ impl Exporter<'_> {
|
||||
g.stops.iter().any(|s| s.swatch.as_deref().is_some_and(|n| doc.swatch(n).is_some_and(|w| w.spot)))
|
||||
}
|
||||
|
||||
/// Draw diamond gradient `g`, placed on `bounds`, over `area` (inside the current clip) as four
|
||||
/// linear gradients, one for each quadrant of its frame, each equal to the diamond there (PDF
|
||||
/// has no diamond shading; [`vectorcraft_color::GradientGeom::diamond_quadrants`]). The
|
||||
/// quadrants overlap so no hairline shows between them, which needs opaque stops. False
|
||||
/// (nothing drawn) for a diamond with translucent stops or without a frame, which
|
||||
/// [`Self::field_image`] draws instead.
|
||||
pub(super) fn diamond(&mut self, s: &mut Surface, g: &vectorcraft_color::GradientPaint, bounds: Rect, area: Rect) -> bool {
|
||||
if g.gradient.stops.iter().any(|st| st.opacity < 1.0) {
|
||||
return false;
|
||||
}
|
||||
let Some(quadrants) = g.resolve(bounds).diamond_quadrants(area.abs()) else { return false };
|
||||
self.warn("diamond gradients are exported as four linear gradients");
|
||||
let gradient = vectorcraft_color::Gradient { kind: GradientKind::Linear, ..g.gradient.clone() };
|
||||
for (outline, geom) in quadrants {
|
||||
let linear = Paint::Gradient(Box::new(vectorcraft_color::GradientPaint { gradient: gradient.clone(), geom: Some(geom), ..g.clone() }));
|
||||
let quadrant = kurbo::BezPath::from_vec(
|
||||
outline
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &p)| if i == 0 { kurbo::PathEl::MoveTo(p) } else { kurbo::PathEl::LineTo(p) })
|
||||
.chain([kurbo::PathEl::ClosePath])
|
||||
.collect(),
|
||||
);
|
||||
if let (Some(paint), Some(path)) = (self.paint(s, &linear, bounds), to_path(s, &quadrant)) {
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(krilla::paint::Fill { paint, opacity: krilla::num::NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }));
|
||||
s.draw_path(&path);
|
||||
s.set_fill(None);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Draw gradient `g`, placed on `bounds`, as an image over `area`, sampled at the document's
|
||||
/// raster effects resolution where it is drawn (`area` as the current transform sizes it, so
|
||||
/// scaled type gets its pixels too): a freeform gradient's colour field (at most
|
||||
/// [`MAX_FIELD_PX`] a side), or a linear, radial or conic gradient (at most [`MAX_GRADIENT_PX`] a side),
|
||||
/// [`MAX_FIELD_PX`] a side), or a linear, radial, conic or diamond gradient (at most [`MAX_GRADIENT_PX`] a side),
|
||||
/// with dither's noise when it paints a fill (`fill`). A linear or radial gradient whose stops
|
||||
/// are all CMYK and not linked to a spot swatch is a CMYK image, unless its Perceptual curve
|
||||
/// has too many stops; any other is an RGB image.
|
||||
@@ -505,6 +542,8 @@ impl Exporter<'_> {
|
||||
let cmyk_doc = self.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk;
|
||||
if g.gradient.kind == GradientKind::Conic {
|
||||
self.warn("conic gradients drawn as pixels (dithered fills, and Perceptual ones other than spot ink shadings), in a PDF/A file or with more than 97 stops are exported as images");
|
||||
} else if g.gradient.kind == GradientKind::Diamond {
|
||||
self.warn("diamond gradients drawn as pixels (dithered fills, and Perceptual ones other than spot ink shadings), with translucent stops or without an extent are exported as images");
|
||||
} else if !freeform {
|
||||
self.warn("linear and radial gradients drawn as pixels are exported as images");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Shadings: axial and radial ones become gradients (their stops sampled from the shading
|
||||
//! function in its own colour model; an end that doesn't extend clips the painted area), a
|
||||
//! function-based one whose color changes along one axis a linear gradient, and one whose color
|
||||
//! turns around the origin of its space a conic gradient (else its average color; all of them
|
||||
//! function-based one whose color changes along one axis a linear gradient, one whose color
|
||||
//! turns around the origin of its space a conic gradient, and one whose color is the same along
|
||||
//! each square about that origin a diamond gradient (else its average color; all of them
|
||||
//! within one budget of function evaluations per import), patch and triangle
|
||||
//! meshes become gradient meshes, a gradient drawn through a luminosity mask that is
|
||||
//! the same gradient in grey (how stop opacity is written) gets its opacity stops back, and a shape
|
||||
@@ -180,7 +181,9 @@ pub(crate) fn function_paint(
|
||||
if !matches!(sp.shading.shading_type.as_ref(), ShadingType::FunctionBased { .. }) {
|
||||
return None;
|
||||
}
|
||||
let gradient = function_gradient(sp, colors, inks, budget).or_else(|| function_sweep(sp, colors, inks, budget));
|
||||
let gradient = function_gradient(sp, colors, inks, budget)
|
||||
.or_else(|| function_sweep(sp, colors, inks, budget))
|
||||
.or_else(|| function_diamond(sp, colors, inks, budget));
|
||||
Some(match gradient {
|
||||
Some(g) => FunctionPaint::Gradient(Box::new(g)),
|
||||
None => function_average(sp, budget).map_or(FunctionPaint::Unread, FunctionPaint::Average),
|
||||
@@ -362,6 +365,59 @@ fn function_sweep(sp: &ShadingPattern, colors: &mut Colors<'_>, inks: Option<&[S
|
||||
Some(g)
|
||||
}
|
||||
|
||||
/// A function-based shading (type 1) whose domain holds the origin of its space and whose color
|
||||
/// is the same along each square about it with corners on the axes (a function of `abs(x) +
|
||||
/// abs(y)`, as a calculator function gives it, ISO 32000-1 7.10.5) as a diamond gradient, its
|
||||
/// vector along the space's x axis to the corner of the largest such square the domain reaches
|
||||
/// (through the domain's farthest corner): stops sampled from the origin out to that corner, as
|
||||
/// [`function_stops`] samples them. `None` when the domain does not hold the origin, the color
|
||||
/// differs between grid points on one square or doesn't change from square to square, the stops
|
||||
/// would be more than [`MAX_FUNCTION_STOPS`], or `budget` runs out (leaving
|
||||
/// [`AVERAGE_EVALUATIONS`] for [`function_average`]).
|
||||
fn function_diamond(sp: &ShadingPattern, colors: &mut Colors<'_>, inks: Option<&[String]>, budget: &FunctionBudget) -> Option<GradientPaint> {
|
||||
let ShadingType::FunctionBased { matrix, .. } = sp.shading.shading_type.as_ref() else { return None };
|
||||
let (function, domain) = function_domain(sp)?;
|
||||
let [x0, x1, y0, y1] = domain;
|
||||
if !(x0 < 0.0 && x1 > 0.0 && y0 < 0.0 && y1 > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let rgb = |x: f32, y: f32| function_rgb(sp, function, x, y, budget, AVERAGE_EVALUATIONS);
|
||||
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
|
||||
// The farthest corner of the domain, and a point on each square up to it inside the domain:
|
||||
// along the x axis toward that corner, then up its side.
|
||||
let (fx, fy) = (if x1 >= -x0 { x1 } else { x0 }, if y1 >= -y0 { y1 } else { y0 });
|
||||
let reach = fx.abs() + fy.abs();
|
||||
let on_square = |level: f32| {
|
||||
let x = level.clamp(0.0, fx.abs());
|
||||
(x.copysign(fx), (level - x).clamp(0.0, fy.abs()).copysign(fy))
|
||||
};
|
||||
// Each grid point has the color of that point on its square, and the color changes from
|
||||
// square to square.
|
||||
let mut turns = false;
|
||||
let origin = rgb(0.0, 0.0)?;
|
||||
for j in 0..=FUNCTION_GRID {
|
||||
for i in 0..=FUNCTION_GRID {
|
||||
let (x, y) = (lerp(x0, x1, grid_at(i)), lerp(y0, y1, grid_at(j)));
|
||||
let (sx, sy) = on_square(x.abs() + y.abs());
|
||||
let on_path = rgb(sx, sy)?;
|
||||
if !same_rgb(rgb(x, y)?, on_path) {
|
||||
return None;
|
||||
}
|
||||
turns |= !same_rgb(on_path, origin);
|
||||
}
|
||||
}
|
||||
if !turns {
|
||||
return None;
|
||||
}
|
||||
let out = function_stops(sp, function, colors, inks, budget, &|s: f32| on_square(s * reach))?;
|
||||
let mut geom = GradientGeom { start: Point::ZERO, end: Point::new(f64::from(reach), 0.0), aspect: 1.0, focal: None };
|
||||
geom.transform(sp.matrix * *matrix, GradientKind::Diamond);
|
||||
let mut g = GradientPaint::new(Gradient::new(GradientKind::Diamond, out));
|
||||
g.geom = Some(geom);
|
||||
// It paints only over its domain ([`extend_clip`]).
|
||||
Some(g)
|
||||
}
|
||||
|
||||
/// How far short of a whole turn [`function_sweep`] reads the last stop: just before the seam.
|
||||
const SWEEP_SEAM: f32 = 1e-4;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ mod colorize;
|
||||
mod compression;
|
||||
mod conic;
|
||||
mod dashalign;
|
||||
mod diamond;
|
||||
mod editing;
|
||||
mod encrypt;
|
||||
mod file_info;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Diamond gradients (PDF has no diamond shading): four axial shadings, one for each quadrant of
|
||||
//! the diamond's frame and equal to it there, drawn as the canvas draws the diamond; an image
|
||||
//! where the gradient is drawn as pixels or has no extent.
|
||||
|
||||
use vectorcraft_color::{Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint};
|
||||
use vectorcraft_doc::{Appearance, Document, Node};
|
||||
use vectorcraft_geom::{Point, Rect, shapes};
|
||||
use vectorcraft_testkit::raster::{assert_similar, render_artboard};
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// A page-sized square filled with `gradient` placed on `geom`.
|
||||
fn doc(gradient: Gradient, geom: GradientGeom) -> Document {
|
||||
let mut gp = GradientPaint::new(gradient);
|
||||
gp.geom = Some(geom);
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let ap = Appearance::basic(Paint::Gradient(Box::new(gp)), Paint::None, 0.0);
|
||||
d.insert(Some(l), 0, Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 200.0)), ap)).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// Red to green to blue from the center out.
|
||||
fn rgb_diamond() -> Gradient {
|
||||
let stops = vec![
|
||||
GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)),
|
||||
GradientStop::new(0.5, Color::rgb(0.0, 1.0, 0.0)),
|
||||
GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0)),
|
||||
];
|
||||
Gradient::new(GradientKind::Diamond, stops)
|
||||
}
|
||||
|
||||
/// Turned and stretched across its vector, off the square's center.
|
||||
fn placed() -> GradientGeom {
|
||||
GradientGeom { start: Point::new(90.0, 110.0), end: Point::new(150.0, 70.0), aspect: 0.6, focal: None }
|
||||
}
|
||||
|
||||
fn uncompressed(d: &Document, o: &PdfOptions) -> (String, Vec<String>) {
|
||||
let report = export_with_report(d, o).unwrap();
|
||||
(String::from_utf8_lossy(&report.bytes).into_owned(), report.warnings)
|
||||
}
|
||||
|
||||
/// Written as four axial shadings and read back, a diamond draws as the canvas draws it, with no
|
||||
/// hairline between the quadrants: level and placed, and in PDF/A, which allows axial shadings.
|
||||
#[test]
|
||||
fn a_diamond_is_written_as_four_axial_shadings() {
|
||||
let level = GradientGeom::fit(GradientKind::Diamond, Rect::new(0.0, 0.0, 200.0, 200.0), 0.0);
|
||||
let mut pdfa = PdfOptions::uncompressed();
|
||||
(pdfa.settings.standard, pdfa.settings.compatibility) = (Standard::PdfA2b, Standard::PdfA2b.version());
|
||||
for (case, d, o) in [
|
||||
("level", doc(rgb_diamond(), level), PdfOptions::uncompressed()),
|
||||
("placed", doc(rgb_diamond(), placed()), PdfOptions::uncompressed()),
|
||||
("PDF/A", doc(rgb_diamond(), placed()), pdfa),
|
||||
] {
|
||||
let (text, warnings) = uncompressed(&d, &o);
|
||||
assert_eq!(text.matches("/ShadingType 2").count(), 4, "{case}");
|
||||
for other in ["/ShadingType 1", "/ShadingType 3", "/Subtype /Image"] {
|
||||
assert!(!text.contains(other), "{case}: {other}");
|
||||
}
|
||||
assert!(warnings.iter().any(|w| w.contains("diamond gradients are exported as four linear gradients")), "{case}: {warnings:?}");
|
||||
let back = import(&export(&d, &o).unwrap()).unwrap();
|
||||
assert_similar(&render_artboard(&back), &render_artboard(&d), 4.0, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Perceptual diamond of one spot ink running out to paper white is not drawn as pixels: its
|
||||
/// four quadrants are axial shadings in that ink's Separation space, as a Perceptual linear
|
||||
/// gradient of spot inks is.
|
||||
#[test]
|
||||
fn a_perceptual_diamond_of_a_spot_ink_is_four_separation_shadings() {
|
||||
let gold = Color::cmyk(0.0, 0.2, 0.8, 0.1);
|
||||
let mut stops = vec![];
|
||||
for (offset, tint) in [(0.0, 1.0), (0.5, 0.4)] {
|
||||
let mut s = GradientStop::new(offset, gold.tinted(tint));
|
||||
(s.swatch, s.tint) = (Some("Gold".into()), tint);
|
||||
stops.push(s);
|
||||
}
|
||||
stops.push(GradientStop::new(1.0, Color::cmyk(0.0, 0.0, 0.0, 0.0)));
|
||||
let g = Gradient { method: vectorcraft_color::GradientMethod::Perceptual, ..Gradient::new(GradientKind::Diamond, stops) };
|
||||
let mut d = doc(g, placed());
|
||||
d.color_mode = vectorcraft_doc::ColorMode::Cmyk;
|
||||
d.swatches.push(vectorcraft_color::Swatch { name: "Gold".into(), paint: Paint::solid(gold), global: true, spot: true });
|
||||
let (text, warnings) = uncompressed(&d, &PdfOptions::uncompressed());
|
||||
assert_eq!(text.matches("/ShadingType 2").count(), 4, "{text}");
|
||||
assert!(text.contains("[/Separation/Gold"), "{text}");
|
||||
assert!(!text.contains("/Subtype /Image"), "{text}");
|
||||
assert!(warnings.iter().any(|w| w.contains("diamond gradients are exported as four linear gradients")), "{warnings:?}");
|
||||
}
|
||||
|
||||
/// A dithered fill, a Perceptual gradient of process colors, one with translucent stops (whose
|
||||
/// quadrants would add up their opacity where they overlap) and a diamond without an extent are
|
||||
/// images of what the canvas draws, with a warning.
|
||||
#[test]
|
||||
fn a_diamond_drawn_as_pixels_or_without_an_extent_is_an_image() {
|
||||
let c = Point::new(100.0, 100.0);
|
||||
let dithered = Gradient { dither: true, ..rgb_diamond() };
|
||||
let perceptual = Gradient { method: vectorcraft_color::GradientMethod::Perceptual, ..rgb_diamond() };
|
||||
let mut translucent = rgb_diamond();
|
||||
translucent.stops[1].opacity = 0.4;
|
||||
for (case, d) in [
|
||||
("dithered", doc(dithered, placed())),
|
||||
("perceptual", doc(perceptual, placed())),
|
||||
("translucent", doc(translucent, placed())),
|
||||
("no vector", doc(rgb_diamond(), GradientGeom { start: c, end: c, aspect: 1.0, focal: None })),
|
||||
("flat", doc(rgb_diamond(), GradientGeom { aspect: 0.0, ..placed() })),
|
||||
] {
|
||||
let (text, warnings) = uncompressed(&d, &PdfOptions::uncompressed());
|
||||
assert!(text.contains("/Image") && !text.contains("/ShadingType"), "{case}");
|
||||
assert!(warnings.iter().any(|w| w.contains("diamond gradients drawn as pixels")), "{case}: {warnings:?}");
|
||||
let back = import(&export(&d, &PdfOptions::uncompressed()).unwrap()).unwrap();
|
||||
let (want, got) = (render_artboard(&d), render_artboard(&back));
|
||||
for (x, y) in [(150, 120), (100, 160), (40, 100), (100, 40), (90, 110), (30, 30), (170, 170)] {
|
||||
let (a, b) = (want.over_white(x, y), got.over_white(x, y));
|
||||
assert!(a.iter().zip(b).all(|(p, q)| p.abs_diff(q) <= 12), "{case} at ({x}, {y}): {a:?} vs {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -769,6 +769,47 @@ fn a_function_based_shading_beside_the_origin_becomes_a_conic_gradient() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A function-based shading whose calculator function turns `abs(x) + abs(y)` into a gray ramp
|
||||
/// (ISO 32000-1 7.10.5; the ramp clipped to 1 past the square with corners at the domain's edges)
|
||||
/// comes in as a diamond gradient about the origin's place on the page. A ramp along a circle
|
||||
/// about the origin does not.
|
||||
#[test]
|
||||
fn a_function_based_shading_of_squares_about_the_origin_becomes_a_diamond_gradient() {
|
||||
use vectorcraft_geom::Point;
|
||||
let _held = shadings();
|
||||
let fill = |r: &ImportReport| {
|
||||
all(&r.document).into_iter().find_map(|n| match n.appearance.fill().map(|f| &f.paint) {
|
||||
Some(Paint::Gradient(g)) => Some(g.clone()),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
// The origin at the square's center, a unit 50 pt.
|
||||
let r = function_shading("-1 1 -1 1", "50 0 0 50 50 50", "{ abs exch abs add dup dup }");
|
||||
let g = fill(&r).unwrap_or_else(|| panic!("a gradient fill, {:?}", r.warnings));
|
||||
assert_eq!(g.gradient.kind, GradientKind::Diamond);
|
||||
let geom = g.geom.unwrap();
|
||||
assert!(geom.start.distance(Point::new(50.0, 50.0)) < 0.01, "{geom:?}");
|
||||
for (x, y) in [(50.0, 50.0), (75.0, 50.0), (50.0, 20.0), (70.0, 70.0), (35.0, 60.0), (95.0, 95.0), (10.0, 80.0)] {
|
||||
let gray = g.gradient.sample(geom.param_at(GradientKind::Diamond, Point::new(x, y)) as f32).0.to_rgb()[0];
|
||||
let want = (((x - 50.0) as f32).abs() / 50.0 + ((y - 50.0) as f32).abs() / 50.0).min(1.0);
|
||||
assert!((gray - want).abs() < 0.03, "({x}, {y}): {gray} vs {want}");
|
||||
}
|
||||
// A radial ramp (the distance from the origin) is not one.
|
||||
let r = function_shading("-1 1 -1 1", "50 0 0 50 50 50", "{ dup mul exch dup mul add sqrt dup dup }");
|
||||
assert!(fill(&r).is_none_or(|g| g.gradient.kind != GradientKind::Diamond));
|
||||
// A domain wider than it is tall: the ramp keeps changing out to its far corners, past the
|
||||
// largest square the domain holds whole.
|
||||
let r = function_shading("-2 2 -1 1", "50 0 0 50 50 50", "{ abs exch abs add 3 div dup dup }");
|
||||
let g = fill(&r).unwrap_or_else(|| panic!("a gradient fill, {:?}", r.warnings));
|
||||
assert_eq!(g.gradient.kind, GradientKind::Diamond);
|
||||
let geom = g.geom.unwrap();
|
||||
for (x, y) in [(50.0, 50.0), (125.0, 90.0), (-20.0, 10.0), (140.0, 50.0), (60.0, 95.0), (0.0, 30.0)] {
|
||||
let gray = g.gradient.sample(geom.param_at(GradientKind::Diamond, Point::new(x, y)) as f32).0.to_rgb()[0];
|
||||
let want = (((x - 50.0) as f32).abs() / 50.0 + ((y - 50.0) as f32).abs() / 50.0) / 3.0;
|
||||
assert!((gray - want).abs() < 0.03, "({x}, {y}): {gray} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A function-based shading paints only over its domain: a square filled with one whose domain
|
||||
/// covers its left half is clipped to that half; one whose domain covers it all isn't clipped.
|
||||
#[test]
|
||||
|
||||
@@ -617,12 +617,17 @@ fn decode_paint(v: &Value, at: &str) -> Result<Paint> {
|
||||
stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
|
||||
g.angle = g.angle.rem_euclid(360.0);
|
||||
let conic = g.gradient.kind == vectorcraft_color::GradientKind::Conic;
|
||||
let no_focal = conic || g.gradient.kind == vectorcraft_color::GradientKind::Diamond;
|
||||
if let Some(geom) = &mut g.geom {
|
||||
check_point(geom.start, at)?;
|
||||
check_point(geom.end, at)?;
|
||||
if let Some(f) = geom.focal {
|
||||
check_point(f, at)?;
|
||||
}
|
||||
// Conic and diamond gradients have no focal point (the commands refuse one).
|
||||
if no_focal {
|
||||
geom.focal = None;
|
||||
}
|
||||
let a = geom.aspect;
|
||||
// A conic's negative aspect mirrors its sweep, so its sign stays.
|
||||
geom.aspect = match (a.is_finite(), conic) {
|
||||
|
||||
@@ -89,6 +89,26 @@ fn a_mirrored_conic_keeps_its_sign_through_a_plug_in() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A focal point handed back on a conic or diamond gradient, which have none, is dropped; a
|
||||
/// radial's is kept.
|
||||
#[test]
|
||||
fn only_a_radial_keeps_a_focal_point_through_a_plug_in() {
|
||||
let (d, ids, _) = doc();
|
||||
let a = ids[0].0;
|
||||
let focal = Point::new(3.0, 1.0);
|
||||
for (kind, want) in [("Conic", None), ("Diamond", None), ("Radial", Some(focal))] {
|
||||
let mut g = vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient::default());
|
||||
g.geom = Some(vectorcraft_color::GradientGeom { start: Point::new(0.0, 0.0), end: Point::new(10.0, 0.0), aspect: 1.0, focal: Some(focal) });
|
||||
let mut p = serde_json::to_value(Paint::Gradient(Box::new(g))).unwrap();
|
||||
p["gradient"]["kind"] = json!(kind);
|
||||
let mut d = d.clone();
|
||||
let out = decode_json(json!({"objects": [{"id": a, "fills": [p]}]})).unwrap();
|
||||
apply_output(&mut d, &ids, out, None, &keep).unwrap();
|
||||
let Paint::Gradient(g) = d.node(ids[0]).unwrap().appearance.fill_paint() else { panic!("a gradient fill") };
|
||||
assert_eq!(g.geom.unwrap().focal, want, "{kind}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A path handed back as it was stays the live shape it was (a rectangle keeps its corner
|
||||
/// widgets); a path that changed is a plain path.
|
||||
#[test]
|
||||
|
||||
+26
-19
@@ -1,6 +1,7 @@
|
||||
//! Dithered gradient fills in the renderer.
|
||||
//! Dithered gradient fills, and diamond gradients, in the renderer.
|
||||
//!
|
||||
//! A dithered linear or radial gradient is drawn as pixels: the gradient sampled on a grid over
|
||||
//! A dithered linear or radial gradient, and a diamond gradient (which has no vector gradient to
|
||||
//! draw it), is drawn as pixels: the gradient sampled on a grid over
|
||||
//! the painted box with a cell per device pixel or finer (in quarter-octave steps, so a pan reuses
|
||||
//! the grid and a zoom rarely misses), each colour channel given fine noise of up to a level
|
||||
//! ([`vectorcraft_color::gradient::dither_noise`]) before it is rounded, and drawn as an image
|
||||
@@ -50,18 +51,19 @@ fn cells(px: f64) -> u16 {
|
||||
2f64.powf(steps / 4.0).ceil().clamp(1.0, MAX_CELLS) as u16
|
||||
}
|
||||
|
||||
/// Cache key of gradient `g` placed on box `b` as a `cols` × `rows` grid, cells `win` of it,
|
||||
/// painted with `ink` (and the active colour settings, which turn colours into display RGB or
|
||||
/// inks).
|
||||
fn key(g: &GradientPaint, b: Rect, cols: u16, rows: u16, win: &[Range<u16>; 2], ink: Ink) -> u64 {
|
||||
/// Cache key of gradient `g` placed on box `place`, sampled as a `cols` × `rows` grid over box
|
||||
/// `b`, cells `win` of it, painting a fill (`fill`) with `ink` (and the active color settings,
|
||||
/// which turn colors into display RGB or inks).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn key(g: &GradientPaint, place: Rect, b: Rect, cols: u16, rows: u16, win: &[Range<u16>; 2], ink: Ink, fill: bool) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
let gr = &g.gradient;
|
||||
(gr.kind as u8, gr.dither, gr.method as u8).hash(&mut h);
|
||||
(gr.kind as u8, gr.dither && fill, gr.method as u8).hash(&mut h);
|
||||
for s in &gr.stops {
|
||||
(s.offset.to_bits(), s.opacity.to_bits(), s.midpoint.to_bits()).hash(&mut h);
|
||||
s.color.key().hash(&mut h);
|
||||
}
|
||||
let geom = g.resolve(b);
|
||||
let geom = g.resolve(place);
|
||||
(geom.start.x.to_bits(), geom.start.y.to_bits(), geom.end.x.to_bits(), geom.end.y.to_bits(), geom.aspect.to_bits()).hash(&mut h);
|
||||
geom.focal.map(|f| (f.x.to_bits(), f.y.to_bits())).hash(&mut h);
|
||||
(b.x0.to_bits(), b.y0.to_bits(), b.x1.to_bits(), b.y1.to_bits(), cols, rows).hash(&mut h);
|
||||
@@ -89,15 +91,17 @@ fn window(xf: Affine, w: u16, h: u16, b: Rect, cols: u16, rows: u16) -> Option<[
|
||||
Some([span(seen.x0, seen.x1, b.x0, b.width(), cols)?, span(seen.y0, seen.y1, b.y0, b.height(), rows)?])
|
||||
}
|
||||
|
||||
/// Cells `win` (columns, rows) of `g` placed on `b` as a `cols` × `rows` grid, as a
|
||||
/// premultiplied pixmap, colours as `ink` paints them.
|
||||
pub(crate) fn rasterize(g: &GradientPaint, b: Rect, cols: u16, rows: u16, win: [Range<u16>; 2], ink: Ink) -> Pixmap {
|
||||
/// Cells `win` (columns, rows) of `g` placed on `place`, sampled as a `cols` × `rows` grid over
|
||||
/// `b`, as a premultiplied pixmap, colors as `ink` paints them, with dither's noise when it
|
||||
/// paints a fill (`fill`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn rasterize(g: &GradientPaint, place: Rect, b: Rect, cols: u16, rows: u16, win: [Range<u16>; 2], ink: Ink, fill: bool) -> Pixmap {
|
||||
let (w, h) = (win[0].len() as u16, win[1].len() as u16);
|
||||
let adjusted = ink.gradient(&g.gradient).map(|gradient| GradientPaint { gradient, ..g.clone() });
|
||||
let ink = if adjusted.is_some() { ink.base() } else { ink };
|
||||
let g = adjusted.as_ref().unwrap_or(g);
|
||||
let data = g
|
||||
.pixels_within(b, b, cols, rows, win, true, &|c| ink.rgb(c))
|
||||
.pixels_within(place, b, cols, rows, win, fill, &|c| ink.rgb(c))
|
||||
.map(|[r, gr, bl, a]| {
|
||||
let pm = |v: u8| ((u16::from(v) * u16::from(a) + 127) / 255) as u8;
|
||||
vello_cpu::color::PremulRgba8 { r: pm(r), g: pm(gr), b: pm(bl), a }
|
||||
@@ -106,11 +110,14 @@ pub(crate) fn rasterize(g: &GradientPaint, b: Rect, cols: u16, rows: u16, win: [
|
||||
Pixmap::from_parts(data, w, h)
|
||||
}
|
||||
|
||||
/// Set dithered gradient `g` on box `bounds` (in the paint's space, which the context's current
|
||||
/// transform maps to pixels) as the context paint. Returns false for an empty box or one off the
|
||||
/// render target.
|
||||
pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bounds: Rect, ink: Ink) -> bool {
|
||||
let Some(b) = painted_box(bounds) else { return false };
|
||||
/// Set gradient `g` placed on box `bounds` (in the paint's space, which the context's current
|
||||
/// transform maps to pixels) as the context paint, drawn as pixels over that box and box `area`
|
||||
/// (a stroke's outline, whose miters, caps and arrowheads can reach past `bounds`), with dither's
|
||||
/// noise when it paints a fill (`fill`). Returns false for an empty box or one off the render
|
||||
/// target. Past both boxes, the pixels at their edge continue.
|
||||
pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bounds: Rect, area: Rect, ink: Ink, fill: bool) -> bool {
|
||||
let Some(place) = painted_box(bounds) else { return false };
|
||||
let b = Some(place.union(area.abs())).filter(|r| r.is_finite()).unwrap_or(place);
|
||||
// Device pixels per unit along the box's sides.
|
||||
let [m0, m1, m2, m3, _, _] = ctx.transform().as_coeffs();
|
||||
let (sx, sy) = (m0.hypot(m1), m2.hypot(m3));
|
||||
@@ -121,7 +128,7 @@ pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bou
|
||||
let (cols, rows) = (cells(wpx), cells(hpx));
|
||||
let Some(win) = window(*ctx.transform(), ctx.width(), ctx.height(), b, cols, rows) else { return false };
|
||||
let (first_col, first_row) = (win[0].start, win[1].start);
|
||||
let k = key(g, b, cols, rows, &win, ink);
|
||||
let k = key(g, place, b, cols, rows, &win, ink, fill);
|
||||
let pm = GRIDS
|
||||
.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
@@ -129,7 +136,7 @@ pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bou
|
||||
c.get(&k, t).cloned()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let pm = Arc::new(rasterize(g, b, cols, rows, win, ink));
|
||||
let pm = Arc::new(rasterize(g, place, b, cols, rows, win, ink, fill));
|
||||
GRIDS.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
let t = c.tick();
|
||||
|
||||
@@ -46,6 +46,8 @@ pub(crate) fn set_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Fr
|
||||
ctx.set_paint_transform(geom.conic_frame());
|
||||
peniko::Gradient::new_sweep((0.0, 0.0), 0.0, std::f32::consts::TAU).with_stops(stops.as_slice())
|
||||
}
|
||||
// peniko has no diamond gradient: it is drawn as pixels, as a dithered fill is.
|
||||
GradientKind::Diamond => return crate::dither::set_dithered_paint(ctx, g, bounds, bounds, f.ink, false),
|
||||
_ => {
|
||||
ctx.reset_paint_transform();
|
||||
let (s, e) =
|
||||
@@ -67,7 +69,19 @@ pub(crate) fn set_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Fr
|
||||
pub(crate) fn set_fill_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Frame) -> bool {
|
||||
match p {
|
||||
Paint::Gradient(g) if g.gradient.dither && g.gradient.kind != GradientKind::Freeform && !g.gradient.stops.is_empty() => {
|
||||
crate::dither::set_dithered_paint(ctx, g, bounds, f.ink)
|
||||
crate::dither::set_dithered_paint(ctx, g, bounds, bounds, f.ink, true)
|
||||
}
|
||||
_ => set_paint(ctx, p, bounds, f),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`set_paint`] for a stroke whose outline (miters, caps and arrowheads included) fits the box
|
||||
/// `area` gives: a diamond gradient, drawn as pixels, is sampled over that box too, so the parts
|
||||
/// of the outline past `bounds` get the diamond's colors there. `area` runs only for a diamond.
|
||||
pub(crate) fn set_stroke_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, area: impl FnOnce() -> Rect, f: &Frame) -> bool {
|
||||
match p {
|
||||
Paint::Gradient(g) if g.gradient.kind == GradientKind::Diamond && !g.gradient.stops.is_empty() => {
|
||||
crate::dither::set_dithered_paint(ctx, g, bounds, area(), f.ink, false)
|
||||
}
|
||||
_ => set_paint(ctx, p, bounds, f),
|
||||
}
|
||||
|
||||
@@ -144,15 +144,27 @@ impl Renderer {
|
||||
let paint_bounds = bounds.inflate(st.width / 2.0, st.width / 2.0);
|
||||
if let Some(g) = st.path_gradient() {
|
||||
self.fill_path_gradient(ctx, f, bp, rule, st, chars, &pieces, width, &g.gradient);
|
||||
} else if paint::set_paint(ctx, &st.paint, paint_bounds, f) {
|
||||
self.fold_alpha(ctx, f.ink, &st.paint);
|
||||
ctx.set_fill_rule(peniko::Fill::NonZero);
|
||||
match self.cached_stroke(f, chars, bp, &pieces.line, st, width) {
|
||||
Some(o) => ctx.fill_path(&o),
|
||||
None => ctx.fill_path(&crate::stroke::outline(&pieces.line, st, width, f.px * 0.25)),
|
||||
}
|
||||
for head in &pieces.heads {
|
||||
ctx.fill_path(&head.outline);
|
||||
} else {
|
||||
let line = match self.cached_stroke(f, chars, bp, &pieces.line, st, width) {
|
||||
Some(o) => o,
|
||||
None => Arc::new(crate::stroke::outline(&pieces.line, st, width, f.px * 0.25)),
|
||||
};
|
||||
// Miters, caps and heads can reach past the paint box: a paint drawn as pixels covers
|
||||
// them too.
|
||||
let reach = || {
|
||||
std::iter::once(&*line)
|
||||
.chain(pieces.heads.iter().map(|h| &h.outline))
|
||||
.filter(|o| !o.elements().is_empty())
|
||||
.map(|o| o.bounding_box())
|
||||
.fold(paint_bounds, |r, b| r.union(b))
|
||||
};
|
||||
if paint::set_stroke_paint(ctx, &st.paint, paint_bounds, reach, f) {
|
||||
self.fold_alpha(ctx, f.ink, &st.paint);
|
||||
ctx.set_fill_rule(peniko::Fill::NonZero);
|
||||
ctx.fill_path(&line);
|
||||
for head in &pieces.heads {
|
||||
ctx.fill_path(&head.outline);
|
||||
}
|
||||
}
|
||||
}
|
||||
if inside {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Diamond gradients: the pixels run from the center out to a square with a corner at the end of
|
||||
//! the vector, and follow the model's parameter when the square is stretched or turned, on fills
|
||||
//! and strokes; a stroke gets no dither noise, and a diamond without an extent draws its last stop.
|
||||
|
||||
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, Paint};
|
||||
use vectorcraft_doc::{Appearance, Document};
|
||||
use vectorcraft_geom::{Point, Rect, Vec2, shapes};
|
||||
use vectorcraft_testkit::fixtures::DocBuilder;
|
||||
use vectorcraft_testkit::raster::{Image, render_view};
|
||||
|
||||
/// A white-to-black diamond on `geom`, dithered when `dither`.
|
||||
fn paint(geom: GradientGeom, dither: bool) -> Paint {
|
||||
let mut gp = GradientPaint::new(Gradient { kind: GradientKind::Diamond, dither, ..Gradient::default() });
|
||||
gp.geom = Some(geom);
|
||||
Paint::Gradient(Box::new(gp))
|
||||
}
|
||||
|
||||
/// A 100 pt square filled with `fill` and stroked `width` wide with `stroke`.
|
||||
fn square(fill: Paint, stroke: Paint, width: f64) -> Document {
|
||||
let mut b = DocBuilder::new(100.0, 100.0);
|
||||
b.path(shapes::rectangle(Rect::new(10.0, 10.0, 90.0, 90.0)), Appearance::basic(fill, stroke, width), |_| {});
|
||||
b.build()
|
||||
}
|
||||
|
||||
/// Every 7th pixel inside `inside` has the white-to-black ramp's gray at the model's parameter
|
||||
/// there (pixel centers).
|
||||
fn assert_follows_model(img: &Image, geom: &GradientGeom, inside: Rect) {
|
||||
for y in (3..100).step_by(7) {
|
||||
for x in (3..100).step_by(7) {
|
||||
let p = Point::new(x as f64 + 0.5, y as f64 + 0.5);
|
||||
if !inside.contains(p) {
|
||||
continue;
|
||||
}
|
||||
let t = geom.param_at(GradientKind::Diamond, p).clamp(0.0, 1.0);
|
||||
let want = ((1.0 - t) * 255.0).round() as u8;
|
||||
let got = img.over_white(x, y)[0];
|
||||
assert!(got.abs_diff(want) <= 6, "({x},{y}) t={t:.3}: got {got}, want {want}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_diamond_fill_follows_the_squares() {
|
||||
let c = Point::new(50.0, 50.0);
|
||||
let level = GradientGeom { start: c, end: c + Vec2::new(40.0, 0.0), aspect: 1.0, focal: None };
|
||||
let stretched = GradientGeom { aspect: 0.5, ..level };
|
||||
let turned = GradientGeom { end: c + Vec2::new(30.0, 30.0), aspect: 0.7, ..level };
|
||||
for geom in [level, stretched, turned] {
|
||||
let img = render_view(&square(paint(geom, false), Paint::None, 0.0), 100, 100);
|
||||
assert_follows_model(&img, &geom, Rect::new(12.0, 12.0, 88.0, 88.0));
|
||||
}
|
||||
// Level: white at the center, mid gray halfway to a corner of the square, along an axis and
|
||||
// along a diagonal alike (pixel centers 20 pt from the center, counted along the axes).
|
||||
let img = render_view(&square(paint(level, false), Paint::None, 0.0), 100, 100);
|
||||
assert!(img.over_white(50, 50)[0] > 240, "{:?}", img.over_white(50, 50));
|
||||
for (x, y) in [(69, 49), (49, 69), (60, 59), (39, 40)] {
|
||||
assert!(img.over_white(x, y)[0].abs_diff(128) <= 6, "({x},{y}): {:?}", img.over_white(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// A stroke follows the squares too, without the noise dither puts on fills.
|
||||
#[test]
|
||||
fn a_diamond_stroke_follows_the_squares_without_dither() {
|
||||
let c = Point::new(50.0, 50.0);
|
||||
let geom = GradientGeom { start: c, end: c + Vec2::new(60.0, 0.0), aspect: 1.0, focal: None };
|
||||
let doc = square(Paint::None, paint(geom, true), 6.0);
|
||||
let img = render_view(&doc, 100, 100);
|
||||
// Along the top edge (y 10, inside the stroke's inner half), the gray follows the model and
|
||||
// two pixels the same distance from the center are the same.
|
||||
for x in [20, 35, 50, 65, 80] {
|
||||
let p = Point::new(x as f64 + 0.5, 10.5);
|
||||
let want = ((1.0 - geom.param_at(GradientKind::Diamond, p).clamp(0.0, 1.0)) * 255.0).round() as u8;
|
||||
assert!(img.over_white(x, 10)[0].abs_diff(want) <= 6, "x {x}: {:?} vs {want}", img.over_white(x, 10));
|
||||
}
|
||||
assert_eq!(img.over_white(30, 10), img.over_white(69, 10));
|
||||
}
|
||||
|
||||
/// A stroke's miter reaches past its paint box (the path's box grown by half the weight): the
|
||||
/// diamond's grays run on there, as the model gives them, instead of repeating the box's edge.
|
||||
#[test]
|
||||
fn a_diamond_stroke_follows_the_squares_past_its_paint_box() {
|
||||
let c = Point::new(50.0, 50.0);
|
||||
let geom = GradientGeom { start: c, end: c + Vec2::new(45.0, 0.0), aspect: 1.0, focal: None };
|
||||
let mut ap = Appearance::basic(Paint::None, paint(geom, false), 8.0);
|
||||
for it in &mut ap.items {
|
||||
if let vectorcraft_doc::AppearanceItem::Stroke(s) = it {
|
||||
s.miter_limit = 10.0;
|
||||
}
|
||||
}
|
||||
let mut b = DocBuilder::new(100.0, 100.0);
|
||||
let peak = vectorcraft_geom::SubPath::polyline(&[Point::new(20.0, 80.0), Point::new(50.0, 20.0), Point::new(80.0, 80.0)], false);
|
||||
b.path(vectorcraft_geom::PathData::single(peak), ap, |_| {});
|
||||
let img = render_view(&b.build(), 100, 100);
|
||||
// The miter's tip runs up to y 11.06; the paint box starts at y 16. Rows 13 and 14 are
|
||||
// covered across pixel column 50.
|
||||
for y in [13, 14] {
|
||||
let p = Point::new(50.5, y as f64 + 0.5);
|
||||
let want = ((1.0 - geom.param_at(GradientKind::Diamond, p).clamp(0.0, 1.0)) * 255.0).round() as u8;
|
||||
assert!(img.over_white(50, y)[0].abs_diff(want) <= 6, "y {y}: {:?} vs {want}", img.over_white(50, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// Without an extent (no vector, or a flat square) every point is past the square: the last stop.
|
||||
#[test]
|
||||
fn a_diamond_without_an_extent_draws_its_last_stop() {
|
||||
let c = Point::new(50.0, 50.0);
|
||||
for geom in [
|
||||
GradientGeom { start: c, end: c, aspect: 1.0, focal: None },
|
||||
GradientGeom { start: c, end: c + Vec2::new(30.0, 0.0), aspect: 0.0, focal: None },
|
||||
GradientGeom { start: c, end: c + Vec2::new(f64::INFINITY, 0.0), aspect: 1.0, focal: None },
|
||||
] {
|
||||
let img = render_view(&square(paint(geom, false), Paint::None, 0.0), 100, 100);
|
||||
for (x, y) in [(50, 50), (20, 80), (85, 15)] {
|
||||
assert!(img.over_white(x, y)[0] < 6, "{geom:?} ({x},{y}): {:?}", img.over_white(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
mod conic;
|
||||
mod dash_align;
|
||||
mod diamond;
|
||||
mod golden;
|
||||
mod gradient_alpha;
|
||||
mod gradient_text;
|
||||
|
||||
@@ -555,8 +555,8 @@ impl Rules<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A linear, radial or conic gradient over `b` at `opacity`, with the stops SVG export writes
|
||||
/// (midpoints as stops of their own). `None` for freeform gradients.
|
||||
/// A linear, radial, conic or diamond gradient over `b` at `opacity`, with the stops SVG export
|
||||
/// writes (midpoints as stops of their own). `None` for freeform gradients.
|
||||
fn gradient(&mut self, g: &GradientPaint, b: Rect, opacity: f32) -> Option<String> {
|
||||
let geom = g.resolve(b);
|
||||
let stops = |pos: &dyn Fn(f32) -> f64| {
|
||||
@@ -580,9 +580,12 @@ impl Rules<'_> {
|
||||
let pos = |t: f32| if line > 1e-9 { (from + t as f64 * length) / line + 0.5 } else { t as f64 };
|
||||
Some(format!("linear-gradient({}deg, {})", fmt_num(angle, 2), stops(&pos)))
|
||||
}
|
||||
GradientKind::Radial => {
|
||||
// CSS has no diamond gradient: the ellipse through the square's corners stands in.
|
||||
GradientKind::Radial | GradientKind::Diamond => {
|
||||
let r = (geom.end - geom.start).hypot();
|
||||
if geom.focal.is_some() {
|
||||
if g.gradient.kind == GradientKind::Diamond {
|
||||
self.warn("diamond gradients are written as radial gradients through their corners");
|
||||
} else if geom.focal.is_some() {
|
||||
self.warn("radial gradients start at their centre (an off-centre focal point is left out)");
|
||||
}
|
||||
let shape = if (geom.aspect - 1.0).abs() < 1e-9 {
|
||||
|
||||
@@ -226,7 +226,7 @@ impl Writer<'_> {
|
||||
if let (Some(g), Some(b)) = (pixel_gradient(&f.paint, true), bounds) {
|
||||
p.extend((f.opacity < 1.0).then(|| ("opacity", fmt_num(f.opacity as f64, 3))));
|
||||
p.extend(blend);
|
||||
self.raster_paint(g, b, &[d.to_string()], rule, &p, "", true);
|
||||
self.raster_paint(g, b, b, &[d.to_string()], rule, &p, "", true);
|
||||
} else {
|
||||
self.fill_props(f, rule, bounds, &mut p);
|
||||
p.extend(blend);
|
||||
@@ -263,13 +263,19 @@ impl Writer<'_> {
|
||||
let clip = self.path_d(&PathData::from_bezpath(&outline), self.xf);
|
||||
p.extend(opacity);
|
||||
p.extend(blend);
|
||||
self.raster_paint(g, b, &[clip], FillRule::NonZero, &p, &side, false);
|
||||
let reach =
|
||||
if outline.elements().is_empty() { b } else { b.union(vectorcraft_geom::kurbo::Shape::bounding_box(&outline)) };
|
||||
self.raster_paint(g, b, reach, &[clip], FillRule::NonZero, &p, &side, false);
|
||||
}
|
||||
(WrittenShape::Fill(outlines), Some((g, b))) if s.path_gradient().is_none() => {
|
||||
let clip: Vec<String> = outlines.iter().map(|o| self.path_d(&PathData::from_bezpath(o), self.xf)).collect();
|
||||
p.extend(opacity);
|
||||
p.extend(blend);
|
||||
self.raster_paint(g, b, &clip, FillRule::NonZero, &p, &side, false);
|
||||
let reach = outlines
|
||||
.iter()
|
||||
.filter(|o| !o.elements().is_empty())
|
||||
.fold(b, |r, o| r.union(vectorcraft_geom::kurbo::Shape::bounding_box(o)));
|
||||
self.raster_paint(g, b, reach, &clip, FillRule::NonZero, &p, &side, false);
|
||||
}
|
||||
(WrittenShape::Stroke { width }, _) => {
|
||||
p.push(("fill", "none".into()));
|
||||
|
||||
@@ -269,13 +269,23 @@ impl Writer<'_> {
|
||||
// `raster_paint`).
|
||||
self.warn("conic gradients on characters are written as linear gradients");
|
||||
}
|
||||
if g.gradient.kind == GradientKind::Diamond {
|
||||
// Object fills and strokes are four linear gradients or images (see `raster_paint`).
|
||||
self.warn("diamond gradients on characters are written as radial gradients");
|
||||
}
|
||||
let mut geom = g.resolve(bounds);
|
||||
geom.transform(self.xf, g.gradient.kind);
|
||||
if g.gradient.kind != GradientKind::Radial {
|
||||
// Only a radial gradient has a focal point (a plug-in's or a file's other kinds can
|
||||
// carry one that draws nothing).
|
||||
geom.focal = None;
|
||||
}
|
||||
let (s, e) = (geom.start, geom.end);
|
||||
// An off-centre focal point: `fx`/`fy`, in the gradient's own (unsquashed) space.
|
||||
let focal = |w: &Self, f: Point| format!(" fx=\"{}\" fy=\"{}\"", w.num(f.x), w.num(f.y));
|
||||
let (tag, prefix, attrs) = match g.gradient.kind {
|
||||
GradientKind::Radial => {
|
||||
// A diamond's circle runs through its square's corners.
|
||||
GradientKind::Radial | GradientKind::Diamond => {
|
||||
let r = self.num((e - s).hypot());
|
||||
let attrs = if (geom.aspect - 1.0).abs() < 1e-9 {
|
||||
let f = geom.focal.map(|f| focal(self, f)).unwrap_or_default();
|
||||
@@ -432,10 +442,12 @@ impl Writer<'_> {
|
||||
|
||||
/// A gradient `g` painted over `bounds` (the space [`Self::xf`] maps into user space) that is
|
||||
/// drawn as pixels ([`GradientPaint::draws_as_pixels`]) when it paints a fill (`fill`) or a
|
||||
/// stroke: an `<image>` over its painted box, clipped to `clip` (outlines in user space filled
|
||||
/// by `rule`), in a group with `props` and the `side` attribute. A freeform gradient's colour
|
||||
/// stroke: an `<image>` over its painted box and over `area` (a stroke's outline, whose miters,
|
||||
/// caps and arrowheads can reach past the box), clipped to `clip` (outlines in user space
|
||||
/// filled by `rule`), in a group with `props` and the `side` attribute. A freeform gradient's
|
||||
/// image covers the painted box only. A freeform gradient's colour
|
||||
/// field, which SVG can't express, is sampled as the canvas samples it (at twice the box's size
|
||||
/// in points); a linear, radial or conic gradient at the document's raster effects resolution
|
||||
/// in points); a linear, radial, conic or diamond gradient at the document's raster effects resolution
|
||||
/// (at most [`MAX_GRADIENT_PX`] a side), with dither's noise on a fill. Nothing is written for
|
||||
/// an empty box.
|
||||
///
|
||||
@@ -444,16 +456,42 @@ impl Writer<'_> {
|
||||
/// running on to the seam under the next ([`vectorcraft_color::GradientGeom::conic_wedges`]):
|
||||
/// vector art with a sharp seam. With translucent stops, where wedges drawn over each other
|
||||
/// would add up their opacity, it is an image.
|
||||
///
|
||||
/// A diamond gradient (SVG has none) with opaque stops that isn't drawn as pixels is written
|
||||
/// as four linear gradients in the same clip group, one for each quadrant of its frame and
|
||||
/// equal to the diamond there, overlapping so no hairline shows between them
|
||||
/// ([`vectorcraft_color::GradientGeom::diamond_quadrants`]). With translucent stops, where
|
||||
/// the overlaps would add up their opacity, or without an extent, it is an image.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn raster_paint(&mut self, g: &GradientPaint, bounds: Rect, clip: &[String], rule: FillRule, props: &Props, side: &str, fill: bool) {
|
||||
pub(super) fn raster_paint(
|
||||
&mut self,
|
||||
g: &GradientPaint,
|
||||
bounds: Rect,
|
||||
area: Rect,
|
||||
clip: &[String],
|
||||
rule: FillRule,
|
||||
props: &Props,
|
||||
side: &str,
|
||||
fill: bool,
|
||||
) {
|
||||
use vectorcraft_color::freeform::{grid_size, painted_box, spread_scale};
|
||||
let Some(b) = painted_box(bounds) else { return };
|
||||
let freeform = g.gradient.kind == GradientKind::Freeform;
|
||||
// The box the paint covers: the painted box, and a stroke's outline, whose miters, caps
|
||||
// and arrowheads can reach past it (a freeform gradient's field covers the painted box).
|
||||
let cover = if freeform { b } else { Some(b.union(area.abs())).filter(|r| r.is_finite()).unwrap_or(b) };
|
||||
let conic = g.gradient.kind == GradientKind::Conic;
|
||||
let wedges = conic && !g.draws_as_pixels(fill) && g.gradient.stops.iter().all(|s| s.opacity >= 1.0);
|
||||
let opaque = g.gradient.stops.iter().all(|s| s.opacity >= 1.0);
|
||||
let wedges = conic && !g.draws_as_pixels(fill) && opaque;
|
||||
let quadrants = (g.gradient.kind == GradientKind::Diamond && !g.draws_as_pixels(fill) && opaque)
|
||||
.then(|| g.resolve(bounds).diamond_quadrants(cover))
|
||||
.flatten();
|
||||
let href = if wedges {
|
||||
self.warn("conic gradients are written as solid wedges clipped to their shapes");
|
||||
None
|
||||
} else if quadrants.is_some() {
|
||||
self.warn("diamond gradients are written as four linear gradients clipped to their shapes");
|
||||
None
|
||||
} else {
|
||||
let (rgba, cols, rows): (Vec<u8>, u16, u16) = if freeform {
|
||||
let (cols, rows) = grid_size(b, 2.0 * b.width().max(b.height()));
|
||||
@@ -463,8 +501,8 @@ impl Writer<'_> {
|
||||
} else {
|
||||
let k = self.doc.raster_effects_ppi / 72.0;
|
||||
let along = |len: f64| (len * k).ceil().clamp(1.0, MAX_GRADIENT_PX) as u16;
|
||||
let (cols, rows) = (along(b.width()), along(b.height()));
|
||||
(g.pixels(b, b, cols, rows, fill, &Color::to_rgb).flatten().collect(), cols, rows)
|
||||
let (cols, rows) = (along(cover.width()), along(cover.height()));
|
||||
(g.pixels(b, cover, cols, rows, fill, &Color::to_rgb).flatten().collect(), cols, rows)
|
||||
};
|
||||
let mut png = Vec::new();
|
||||
let Some(img) = image::RgbaImage::from_raw(u32::from(cols), u32::from(rows), rgba) else { return };
|
||||
@@ -475,6 +513,8 @@ impl Writer<'_> {
|
||||
"freeform gradients are written as images clipped to their shapes"
|
||||
} else if conic {
|
||||
"conic gradients with translucent stops, dither or the Perceptual method are written as images clipped to their shapes"
|
||||
} else if g.gradient.kind == GradientKind::Diamond {
|
||||
"diamond gradients with translucent stops, dither, the Perceptual method or no extent are written as images clipped to their shapes"
|
||||
} else {
|
||||
"linear and radial gradients drawn as pixels are written as images clipped to their shapes"
|
||||
});
|
||||
@@ -499,20 +539,32 @@ impl Writer<'_> {
|
||||
self.depth += 1;
|
||||
match href {
|
||||
Some(href) => {
|
||||
let (x, y, w, h) = (self.num(b.x0), self.num(b.y0), self.num(b.width()), self.num(b.height()));
|
||||
let (x, y, w, h) = (self.num(cover.x0), self.num(cover.y0), self.num(cover.width()), self.num(cover.height()));
|
||||
let tr = if self.xf == Affine::IDENTITY { String::new() } else { format!(" transform=\"{}\"", self.matrix(self.xf)) };
|
||||
self.line(&format!(
|
||||
"<image x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\"{tr} preserveAspectRatio=\"none\" xlink:href=\"{}\"/>",
|
||||
xml_escape(&href)
|
||||
));
|
||||
}
|
||||
None => {
|
||||
let sample = g.gradient.sampler();
|
||||
for (pts, t) in g.resolve(bounds).conic_wedges(b, CONIC_WEDGES, true) {
|
||||
let d = self.path_d(&PathData::single(vectorcraft_geom::SubPath::polyline(&pts, true)), self.xf);
|
||||
self.line(&format!("<path d=\"{d}\" fill=\"{}\"/>", sample(t as f32).0.to_hex()));
|
||||
None => match quadrants {
|
||||
Some(quadrants) => {
|
||||
let gradient = vectorcraft_color::Gradient { kind: GradientKind::Linear, ..g.gradient.clone() };
|
||||
for (pts, geom) in quadrants {
|
||||
// Not named after the swatch: four defs, none of them the swatch's gradient.
|
||||
let linear = GradientPaint { gradient: gradient.clone(), geom: Some(geom), swatch: None, ..g.clone() };
|
||||
let id = self.gradient_def(&linear, b);
|
||||
let d = self.path_d(&PathData::single(vectorcraft_geom::SubPath::polyline(&pts, true)), self.xf);
|
||||
self.line(&format!("<path d=\"{d}\" fill=\"url(#{id})\"/>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let sample = g.gradient.sampler();
|
||||
for (pts, t) in g.resolve(bounds).conic_wedges(cover, CONIC_WEDGES, true) {
|
||||
let d = self.path_d(&PathData::single(vectorcraft_geom::SubPath::polyline(&pts, true)), self.xf);
|
||||
self.line(&format!("<path d=\"{d}\" fill=\"{}\"/>", sample(t as f32).0.to_hex()));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.depth -= 1;
|
||||
self.line("</g>");
|
||||
@@ -700,18 +752,19 @@ impl Writer<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Most pixels along a side of a linear, radial or conic gradient's image.
|
||||
/// Most pixels along a side of a linear, radial, conic or diamond gradient's image.
|
||||
const MAX_GRADIENT_PX: f64 = 4096.0;
|
||||
|
||||
/// Wedges a conic gradient with opaque stops is written as: one a degree.
|
||||
const CONIC_WEDGES: usize = 360;
|
||||
|
||||
/// The gradient a fill (`fill`) or stroke paint is, if it is drawn as pixels
|
||||
/// ([`GradientPaint::draws_as_pixels`]; SVG has no freeform gradients, for one) or is a conic
|
||||
/// gradient (SVG has none either: [`Writer::raster_paint`] draws it as wedges or an image).
|
||||
/// ([`GradientPaint::draws_as_pixels`]; SVG has no freeform gradients, for one) or is a conic or
|
||||
/// diamond gradient (SVG has neither: [`Writer::raster_paint`] draws a conic as wedges or an
|
||||
/// image, a diamond as four linear gradients or an image).
|
||||
pub(super) fn pixel_gradient(p: &Paint, fill: bool) -> Option<&GradientPaint> {
|
||||
match p {
|
||||
Paint::Gradient(g) if g.draws_as_pixels(fill) || g.gradient.kind == GradientKind::Conic => Some(g),
|
||||
Paint::Gradient(g) if g.draws_as_pixels(fill) || matches!(g.gradient.kind, GradientKind::Conic | GradientKind::Diamond) => Some(g),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,24 @@ fn conic_gradients_give_css_conic_gradients() {
|
||||
assert!(sheet.warnings.iter().any(|w| w.contains("conic gradients are written circular")), "{:?}", sheet.warnings);
|
||||
}
|
||||
|
||||
/// CSS has no diamond gradient: a diamond is the radial gradient through its square's corners (its
|
||||
/// vector, and the aspect across it), at its center, with a warning.
|
||||
#[test]
|
||||
fn diamond_gradients_give_css_radial_gradients_through_their_corners() {
|
||||
let mut d = doc();
|
||||
let mut g = GradientPaint::new(Gradient { kind: GradientKind::Diamond, ..Gradient::default() });
|
||||
g.geom = Some(GradientGeom { start: Point::new(40.0, 30.0), end: Point::new(70.0, 30.0), aspect: 0.5, focal: None });
|
||||
let mut n = rect(&mut d, (0.0, 0.0, 100.0, 60.0), 0.0);
|
||||
n.appearance = Appearance::basic(Paint::Gradient(Box::new(g)), Paint::None, 0.0).into();
|
||||
let id = add(&mut d, n);
|
||||
let sheet = css_rules(&d, &[id], &CssOptions::default());
|
||||
assert!(sheet.warnings.iter().any(|w| w.contains("diamond gradients are written as radial gradients")), "{:?}", sheet.warnings);
|
||||
assert_eq!(
|
||||
prop(&one(&d, id, &CssOptions::default()), "background-image"),
|
||||
Some("radial-gradient(ellipse 30px 15px at 40px 30px, #ffffff 0%, #000000 100%)")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadows_and_unsupported_art() {
|
||||
let mut d = doc();
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Diamond gradients, which SVG has no element for: fills and strokes of opaque stops are written
|
||||
//! as four linear gradients, one for each quadrant of the diamond's frame, clipped to the shape
|
||||
//! (or the stroke's outline), others as an image, with a warning; read back, the SVG looks as the
|
||||
//! canvas paints the gradient.
|
||||
|
||||
use vectorcraft_color::{Color, Gradient, GradientGeom, GradientKind, GradientMethod, GradientPaint, GradientStop, Paint};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId};
|
||||
use vectorcraft_geom::{Point, Rect, shapes};
|
||||
use vectorcraft_svg::{ExportOptions, export_full, import};
|
||||
use vectorcraft_testkit::raster::{assert_similar, render_artboard};
|
||||
|
||||
/// `gradient` (red to blue when `None`), from (100, 80) out to a square turned and stretched
|
||||
/// across its vector.
|
||||
fn diamond(gradient: Option<Gradient>) -> Paint {
|
||||
let ramp = Gradient::new(
|
||||
GradientKind::Diamond,
|
||||
vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))],
|
||||
);
|
||||
let mut g = GradientPaint::new(gradient.unwrap_or(ramp));
|
||||
g.geom = Some(GradientGeom { start: Point::new(100.0, 80.0), end: Point::new(150.0, 50.0), aspect: 0.7, focal: None });
|
||||
g.swatch = Some("Gem".into());
|
||||
Paint::Gradient(Box::new(g))
|
||||
}
|
||||
|
||||
/// A 200 × 160 document holding `n`.
|
||||
fn doc_with(mut n: Node) -> Document {
|
||||
let mut d = Document::new(200.0, 160.0);
|
||||
n.id = d.alloc_id();
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_diamond_fill_is_four_linear_gradients_clipped_to_the_shape() {
|
||||
let shape = shapes::ellipse(Rect::new(20.0, 10.0, 180.0, 150.0));
|
||||
let d = doc_with(Node::path(NodeId(0), shape, Appearance::basic(diamond(None), Paint::None, 0.0)));
|
||||
let o = export_full(&d, &ExportOptions::default(), None);
|
||||
assert_eq!(o.svg.matches("<linearGradient").count(), 4, "{}", o.svg);
|
||||
assert!(!o.svg.contains("<radialGradient") && !o.svg.contains("<image "), "no stand-in");
|
||||
assert!(o.svg.contains("<clipPath"), "clipped to the shape");
|
||||
// Not named after the swatch: none of the four is its gradient.
|
||||
assert!(!o.svg.contains("id=\"Gem"), "{}", o.svg);
|
||||
assert!(o.warnings.iter().any(|w| w.contains("diamond gradients are written as four linear gradients")), "{:?}", o.warnings);
|
||||
// Read back, it looks as the canvas paints the gradient, with no hairline between quadrants.
|
||||
assert_similar(&render_artboard(&d), &render_artboard(&import(&o.svg).unwrap()), 4.0, 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_diamond_stroke_is_four_linear_gradients_clipped_to_the_stroke_outline() {
|
||||
let shape = shapes::rectangle(Rect::new(30.0, 30.0, 170.0, 130.0));
|
||||
let d = doc_with(Node::path(NodeId(0), shape, Appearance::basic(Paint::None, diamond(None), 12.0)));
|
||||
let o = export_full(&d, &ExportOptions::default(), None);
|
||||
assert!(o.svg.contains("<clipPath") && o.svg.matches("<linearGradient").count() == 4, "{}", o.svg);
|
||||
assert_similar(&render_artboard(&d), &render_artboard(&import(&o.svg).unwrap()), 8.0, 0.01);
|
||||
}
|
||||
|
||||
/// A stroke's miter reaches past its paint box (the path's box grown by half the weight): the
|
||||
/// quadrants of a diamond, the image of a Perceptual one and the wedges of a conic gradient cover
|
||||
/// it, and read back it is painted as the canvas paints it.
|
||||
#[test]
|
||||
fn a_stroke_past_its_paint_box_is_painted_to_its_tip() {
|
||||
// The center lies below the art, so the miter's tip is farther from it than any corner of the
|
||||
// paint box.
|
||||
let c = Point::new(100.0, 200.0);
|
||||
let geom = GradientGeom { start: c, end: c + vectorcraft_geom::Vec2::new(200.0, 0.0), aspect: 1.0, focal: None };
|
||||
let ramp = |kind, method| {
|
||||
let stops = vec![GradientStop::new(0.0, Color::WHITE), GradientStop::new(1.0, Color::BLACK)];
|
||||
let mut g = GradientPaint::new(Gradient { method, ..Gradient::new(kind, stops) });
|
||||
g.geom = Some(geom);
|
||||
Paint::Gradient(Box::new(g))
|
||||
};
|
||||
let mut wrong = vec![];
|
||||
for (case, paint) in [
|
||||
("diamond", ramp(GradientKind::Diamond, GradientMethod::Classic)),
|
||||
("perceptual diamond", ramp(GradientKind::Diamond, GradientMethod::Perceptual)),
|
||||
("conic", ramp(GradientKind::Conic, GradientMethod::Classic)),
|
||||
] {
|
||||
let mut ap = Appearance::basic(Paint::None, paint, 8.0);
|
||||
for it in &mut ap.items {
|
||||
if let vectorcraft_doc::AppearanceItem::Stroke(s) = it {
|
||||
s.miter_limit = 10.0;
|
||||
}
|
||||
}
|
||||
// The miter's tip runs up to y 23.5; the paint box starts at y 36. Rows 30 and 31 are
|
||||
// covered across pixel column 100.
|
||||
let peak = vectorcraft_geom::SubPath::polyline(&[Point::new(85.0, 100.0), Point::new(100.0, 40.0), Point::new(115.0, 100.0)], false);
|
||||
let d = doc_with(Node::path(NodeId(0), vectorcraft_geom::PathData::single(peak), ap));
|
||||
let o = export_full(&d, &ExportOptions::default(), None);
|
||||
let (want, got) = (render_artboard(&d), render_artboard(&import(&o.svg).unwrap()));
|
||||
for y in [30, 31] {
|
||||
let (a, b) = (want.over_white(100, y), got.over_white(100, y));
|
||||
assert!(a[0] < 200, "{case} at y {y}: the canvas paints the tip: {a:?}");
|
||||
if !a.iter().zip(b).all(|(p, q)| p.abs_diff(q) <= 12) {
|
||||
wrong.push(format!("{case} at y {y}: {a:?} vs {b:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{wrong:?}");
|
||||
}
|
||||
|
||||
/// Translucent stops (the overlapping quadrants would add up their opacity), dither, the
|
||||
/// Perceptual method and a diamond without an extent give an image clipped to the shape.
|
||||
#[test]
|
||||
fn a_diamond_that_quadrants_cannot_draw_is_an_image() {
|
||||
let mut clear = GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0));
|
||||
clear.opacity = 0.0;
|
||||
let ramp = || Gradient::new(GradientKind::Diamond, vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::BLACK)]);
|
||||
let translucent = Gradient::new(GradientKind::Diamond, vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), clear]);
|
||||
let dithered = Gradient { dither: true, ..ramp() };
|
||||
let perceptual = Gradient { method: GradientMethod::Perceptual, ..ramp() };
|
||||
let flat = {
|
||||
let Paint::Gradient(mut g) = diamond(None) else { panic!("a gradient") };
|
||||
g.geom = g.geom.map(|geom| GradientGeom { aspect: 0.0, ..geom });
|
||||
Paint::Gradient(g)
|
||||
};
|
||||
for (case, paint) in [
|
||||
("translucent", diamond(Some(translucent))),
|
||||
("dithered", diamond(Some(dithered))),
|
||||
("perceptual", diamond(Some(perceptual))),
|
||||
("flat", flat),
|
||||
] {
|
||||
let shape = shapes::rectangle(Rect::new(20.0, 20.0, 180.0, 140.0));
|
||||
let d = doc_with(Node::path(NodeId(0), shape, Appearance::basic(paint, Paint::None, 0.0)));
|
||||
let o = export_full(&d, &ExportOptions::default(), None);
|
||||
assert_eq!(o.svg.matches("<image ").count(), 1, "{case}");
|
||||
assert!(o.warnings.iter().any(|w| w.contains("diamond") && w.contains("images")), "{case}: {:?}", o.warnings);
|
||||
assert_similar(&render_artboard(&d), &render_artboard(&import(&o.svg).unwrap()), 12.0, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
/// A diamond on characters, which SVG paints with a gradient element, is the radial gradient
|
||||
/// through its corners, with a warning, centered even when the diamond carries a focal point (a
|
||||
/// plug-in's or a file's, which a diamond doesn't draw).
|
||||
#[test]
|
||||
fn a_diamond_on_characters_is_written_radial() {
|
||||
use vectorcraft_doc::{CharStyle, NodeKind, TextObject};
|
||||
let Paint::Gradient(mut g) = diamond(None) else { panic!("a gradient") };
|
||||
g.geom = g.geom.map(|geom| GradientGeom { focal: Some(Point::new(110.0, 75.0)), ..geom });
|
||||
let st = CharStyle { fill: Paint::Gradient(g), size: 48.0, ..CharStyle::default() };
|
||||
let d = doc_with(Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 80.0), "Gem", st)))));
|
||||
let o = export_full(&d, &ExportOptions::default(), None);
|
||||
assert!(o.svg.contains("<radialGradient"), "{}", o.svg);
|
||||
assert!(!o.svg.contains(" fx=\""), "{}", o.svg);
|
||||
assert!(o.warnings.iter().any(|w| w.contains("diamond gradients on characters")), "{:?}", o.warnings);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ mod container_export;
|
||||
mod css_rules;
|
||||
mod dash_align;
|
||||
mod data_attributes;
|
||||
mod diamond;
|
||||
mod effects_export;
|
||||
mod encoding_profile_fonts;
|
||||
mod focal;
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
//! dot: the bar runs from the center to where the sweep starts, its stops along it by their share
|
||||
//! of the turn.
|
||||
//!
|
||||
//! A diamond gradient shows its square instead of the ellipse, a corner at the end of the bar,
|
||||
//! with the same aspect handle and center ring and no focal dot: drag the dot across the bar to
|
||||
//! stretch the square into a rhombus, and the square elsewhere to rotate it.
|
||||
//!
|
||||
//! Dragging the aspect handle of a radial or diamond gradient whose file gave it a negative
|
||||
//! aspect starts from its size (the aspect's magnitude); only a conic's negative aspect mirrors
|
||||
//! it.
|
||||
//!
|
||||
//! On a freeform gradient the tool edits its points instead (see `freeform`).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
@@ -103,7 +111,7 @@ impl Annotator {
|
||||
fn with(geom: GradientGeom, stops: &[GradientStop]) -> Self {
|
||||
Self { geom, gradient: Gradient::new(GradientKind::Linear, stops.to_vec()) }
|
||||
}
|
||||
/// Placed by a center and an extent ellipse (radial and conic)?
|
||||
/// Placed by a center and an extent ellipse (radial and conic) or square (diamond)?
|
||||
fn radial(&self) -> bool {
|
||||
self.gradient.kind.is_centered()
|
||||
}
|
||||
@@ -116,11 +124,12 @@ impl Annotator {
|
||||
pub fn aspect_point(&self) -> Point {
|
||||
self.geom.start - self.normal() * (self.vector().hypot() * self.geom.aspect.abs())
|
||||
}
|
||||
/// How far `p` is from a radial's extent ellipse (approximately, along the ray from the
|
||||
/// centre).
|
||||
/// How far `p` is from a radial's extent ellipse, or a diamond's square (approximately, along
|
||||
/// the ray from the center).
|
||||
fn off_ellipse(&self, p: Point) -> Option<f64> {
|
||||
let m = self.geom.unit_frame()?;
|
||||
let rho = (m.inverse() * p).to_vec2().hypot();
|
||||
let q = (m.inverse() * p).to_vec2();
|
||||
let rho = if self.gradient.kind == GradientKind::Diamond { q.x.abs() + q.y.abs() } else { q.hypot() };
|
||||
(rho > 1e-12).then(|| p.distance(self.geom.start) * (1.0 - 1.0 / rho).abs())
|
||||
}
|
||||
fn vector(&self) -> Vec2 {
|
||||
@@ -249,14 +258,19 @@ pub struct GradientTool {
|
||||
}
|
||||
|
||||
/// Overlays of an annotator: the bar, the handles, the midpoint diamonds and a chip per stop; a
|
||||
/// radial's extent ellipse with its aspect handle, centre ring and focal dot.
|
||||
/// radial's extent ellipse (a diamond's square) with its aspect handle, center ring and focal dot.
|
||||
fn annotator_overlays(cx: &ToolContext, a: &Annotator) -> Vec<Overlay> {
|
||||
let (s, e) = (a.geom.start, a.geom.end);
|
||||
let shade = Vec2::new(0.0, cx.tol(1.0));
|
||||
let mut o = vec![];
|
||||
if let Some(m) = a.geom.unit_frame().filter(|_| a.radial()) {
|
||||
// Dark dashes over a light line: readable on any art.
|
||||
let path = vectorcraft_geom::kurbo::Ellipse::from_affine(m).to_path(cx.tol(0.25));
|
||||
let path = if a.gradient.kind == GradientKind::Diamond {
|
||||
// A diamond's extent is its square, a corner at the end of the vector.
|
||||
super::polygon(&[m * Point::new(1.0, 0.0), m * Point::new(0.0, 1.0), m * Point::new(-1.0, 0.0), m * Point::new(0.0, -1.0)], true)
|
||||
} else {
|
||||
vectorcraft_geom::kurbo::Ellipse::from_affine(m).to_path(cx.tol(0.25))
|
||||
};
|
||||
o.push(Overlay::Path { path: path.clone(), color: LIGHT, width: 1.0, dashed: false });
|
||||
o.push(Overlay::Path { path, color: BAR, width: 1.0, dashed: true });
|
||||
o.push(Overlay::Handle { p: a.aspect_point(), color: BAR });
|
||||
@@ -331,7 +345,9 @@ impl GradientTool {
|
||||
Some((a, Part::End)) => Grab::End(a.geom),
|
||||
Some((a, Part::Rotate)) => Grab::Rotate { geom: a.geom, from: (p - a.geom.start).atan2() - a.vector().atan2() },
|
||||
Some((a, Part::Focal)) => Grab::Focal(a.geom),
|
||||
Some((a, Part::Aspect)) => Grab::Aspect(a.geom),
|
||||
// Only a conic's negative aspect means a mirror (a file's other kinds draw as its size).
|
||||
Some((a, Part::Aspect)) if a.gradient.kind == GradientKind::Conic => Grab::Aspect(a.geom),
|
||||
Some((a, Part::Aspect)) => Grab::Aspect(GradientGeom { aspect: a.geom.aspect.abs(), ..a.geom }),
|
||||
None => {
|
||||
if cx.selection.is_empty() {
|
||||
let Some(h) = hit_test(cx.doc, p, cx.hit_options()) else { return out };
|
||||
|
||||
@@ -397,3 +397,59 @@ fn a_conic_shows_its_extent_ellipse_and_aspect_handle_without_a_focal_dot() {
|
||||
assert!((v["aspect"].as_f64().unwrap() - want).abs() < 1e-9, "{aspect}: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A diamond shows its square (a corner at the end of the vector) with the aspect handle and
|
||||
/// center ring of a radial, without a focal dot. Dragging the aspect handle of one a file gave a
|
||||
/// negative aspect sets a positive one.
|
||||
#[test]
|
||||
fn a_diamond_shows_its_square_and_aspect_handle() {
|
||||
for aspect in [0.5, -0.5] {
|
||||
let (mut d, s) = graded(two());
|
||||
let n = d.node_mut(s.objects[0]).unwrap();
|
||||
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { panic!("a gradient fill") };
|
||||
g.gradient.kind = GradientKind::Diamond;
|
||||
g.geom.as_mut().unwrap().aspect = aspect;
|
||||
let geom = g.geom.unwrap();
|
||||
n.appearance.set_fill(Paint::Gradient(g));
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let o = GradientTool::default().overlays(&cx);
|
||||
let squares: Vec<_> = o.iter().filter_map(|o| if let Overlay::Path { path, dashed: true, .. } = o { Some(path) } else { None }).collect();
|
||||
assert_eq!(squares.len(), 1, "the square");
|
||||
// Four straight sides through the corners: the end of the vector, the aspect handle and
|
||||
// their mirrors about the center.
|
||||
let corners: Vec<Point> = squares[0].elements().iter().filter_map(|e| e.end_point()).collect();
|
||||
let u = geom.end - geom.start;
|
||||
let w = Vec2::new(u.y, -u.x) * 0.5;
|
||||
for want in [geom.end, geom.start - w, geom.start - u, geom.start + w] {
|
||||
assert!(corners.iter().any(|c| c.distance(want) < 1e-9), "{aspect}: {want:?} in {corners:?}");
|
||||
}
|
||||
assert!(!squares[0].elements().iter().any(|e| matches!(e, vectorcraft_geom::kurbo::PathEl::CurveTo(..))), "straight sides");
|
||||
assert!(o.contains(&Overlay::Handle { p: Point::new(100.0, 100.0), color: BAR }), "{aspect}: {o:?}");
|
||||
let a = Annotator::of(&cx).unwrap();
|
||||
assert_eq!(a.hit(&cx, Point::new(100.0, 100.0)), Some(Part::Aspect));
|
||||
let mut t = GradientTool::default();
|
||||
t.pointer(&cx, &ev(PointerKind::Down, Point::new(100.0, 100.0)));
|
||||
let v = preview(&t.pointer(&cx, &ev(PointerKind::Drag, Point::new(100.0, 80.0)))).clone();
|
||||
assert!((v["aspect"].as_f64().unwrap() - 70.0).abs() < 1e-9, "{aspect}: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Dragging the aspect handle of a radial a file gave a negative aspect (which the commands
|
||||
/// refuse) sets a positive one, as for a diamond: only a conic's sign mirrors it.
|
||||
#[test]
|
||||
fn a_radial_with_a_negative_aspect_drags_to_a_positive_one() {
|
||||
let (mut d, s) = graded(two());
|
||||
let n = d.node_mut(s.objects[0]).unwrap();
|
||||
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { panic!("a gradient fill") };
|
||||
g.gradient.kind = GradientKind::Radial;
|
||||
g.geom.as_mut().unwrap().aspect = -0.5;
|
||||
n.appearance.set_fill(Paint::Gradient(g));
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
assert_eq!(Annotator::of(&cx).unwrap().hit(&cx, Point::new(100.0, 100.0)), Some(Part::Aspect));
|
||||
let mut t = GradientTool::default();
|
||||
t.pointer(&cx, &ev(PointerKind::Down, Point::new(100.0, 100.0)));
|
||||
let v = preview(&t.pointer(&cx, &ev(PointerKind::Drag, Point::new(100.0, 80.0)))).clone();
|
||||
assert!((v["aspect"].as_f64().unwrap() - 70.0).abs() < 1e-9, "{v}");
|
||||
}
|
||||
|
||||
@@ -2540,6 +2540,7 @@ digits Malayalam Малаялам
|
||||
Radial Glows Лъчисти сияния
|
||||
Radial Gradient Лъчист градиент
|
||||
Conic Gradient Конусен градиент
|
||||
Diamond Gradient Ромбовиден градиент
|
||||
Radius 1 Радиус 1
|
||||
Radius 2 Радиус 2
|
||||
Radius1 Радиус1
|
||||
@@ -4537,7 +4538,7 @@ axis Both И двете
|
||||
@msg unknown format непознат формат
|
||||
@msg unknown format `{f}` (Save writes {_1}) непознат формат `{f}` (Запазването записва {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) непознат режим на свободна форма `{m}` (точки, линии)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) непознат вид градиент `{k}` (линеен, радиален, конусен, свободна форма)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) непознат вид градиент `{k}` (линеен, радиален, конусен, ромбовиден, свободна форма)
|
||||
@msg unknown highlight `{h}` непознат акцент `{h}`
|
||||
@msg unknown option `{path}.{k}` непозната опция `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) непознати надписи `{o}` (запазване, симулиране или отхвърляне)
|
||||
@@ -5641,7 +5642,7 @@ theme Appearance Външен вид
|
||||
@msg span must be "object" or "selection", got {v} span трябва да е "object" или "selection", а не {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" поставя `gradient`, а не мостра
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" поставя градиент: задайте `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" поставя линеен, радиален или конусен градиент, а не свободен
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" поставя линеен, радиален, конусен или ромбовиден градиент, а не свободен
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" поставя самия градиент: пропуснете `start` и `end` (те вече го поставят в координатите на документа)
|
||||
Make Image Dynamic Динамично изображение
|
||||
Import Data Sets… Импортиране на набори от данни…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1667,6 +1667,7 @@ digits Malayalam Malajalam
|
||||
Points: clicks add free points Tačke: klikovi dodaju slobodne tačke
|
||||
Radial Gradient Radijalni gradijent
|
||||
Conic Gradient Konusni gradijent
|
||||
Diamond Gradient Rombni gradijent
|
||||
Reset to White, Black Vrati na bijelu, crnu
|
||||
Reverse Gradient Obrni gradijent
|
||||
Save to Swatches Sačuvaj u uzorke boja
|
||||
@@ -4936,7 +4937,7 @@ imageTrace Outline Obris
|
||||
@msg unknown format nepoznat format
|
||||
@msg unknown format `{f}` (Save writes {_1}) nepoznat format `{f}` (Sačuvaj zapisuje {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) nepoznat način slobodnog oblika `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) nepoznata vrsta gradijenta `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) nepoznata vrsta gradijenta `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` nepoznato isticanje `{h}`
|
||||
@msg unknown option `{path}.{k}` nepoznata opcija `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) nepoznati pretisci `{o}` (preserve, simulate ili discard)
|
||||
@@ -5662,7 +5663,7 @@ mark-style Japanese Japanski
|
||||
@msg span must be "object" or "selection", got {v} span mora biti "object" ili "selection", dobiveno {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" postavlja `gradient`, a ne uzorak boje
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" postavlja gradijent: navedite `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" postavlja linearni, radijalni ili konusni gradijent, a ne gradijent slobodnog oblika
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" postavlja linearni, radijalni, konusni ili rombni gradijent, a ne gradijent slobodnog oblika
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" postavlja sam gradijent: izostavite `start` i `end` (oni ga već postavljaju u koordinatama dokumenta)
|
||||
Make Image Dynamic Učini sliku dinamičkom
|
||||
Import Data Sets… Uvezi skupove podataka…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3278,6 +3278,7 @@ digits Malayalam Malaiàlam
|
||||
Radial Glows Resplendors radials
|
||||
Radial Gradient Degradat radial
|
||||
Conic Gradient Degradat cònic
|
||||
Diamond Gradient Degradat de rombe
|
||||
Radial Repeat Repetició radial
|
||||
Radius Radi
|
||||
Radius 1 Radi 1
|
||||
@@ -5794,7 +5795,7 @@ digits Malayalam Malaiàlam
|
||||
@msg span must be "object" or "selection", got {v} span ha de ser "object" o "selection", s’ha rebut {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" col·loca un `gradient`, no una mostra
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" col·loca un degradat: dona `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" col·loca un degradat lineal, radial o cònic, no un de lliure
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" col·loca un degradat lineal, radial, cònic o de rombe, no un de lliure
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" col·loca el degradat mateix: omet `start` i `end` (ja el col·loquen en coordenades del document)
|
||||
@msg Split Into Grid: select paths large enough for the grid Divideix en quadrícula: selecciona traçats prou grans per a la quadrícula
|
||||
@msg stop {i} needs `offset` la parada {i} necessita `offset`
|
||||
@@ -5968,7 +5969,7 @@ digits Malayalam Malaiàlam
|
||||
@msg unknown format format desconegut
|
||||
@msg unknown format `{f}` (Save writes {_1}) format desconegut `{f}` (Desa escriu {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) mode de forma lliure desconegut `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) tipus de degradat desconegut `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) tipus de degradat desconegut `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` ressaltat desconegut `{h}`
|
||||
@msg unknown option `{path}.{k}` opció desconeguda `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) sobreimpressions desconegudes `{o}` (preserve, simulate o discard)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1691,6 +1691,7 @@ digits Malayalam Malajálamské
|
||||
Points: clicks add free points Body: kliknutím přidáte volné body
|
||||
Radial Gradient Kruhový přechod
|
||||
Conic Gradient Kuželový přechod
|
||||
Diamond Gradient Kosočtverečný přechod
|
||||
Reset to White, Black Obnovit na bílou a černou
|
||||
Reverse Gradient Obrátit přechod
|
||||
Save to Swatches Uložit do vzorníku
|
||||
@@ -5023,7 +5024,7 @@ imageTrace Outline Obrys
|
||||
@msg unknown format neznámý formát
|
||||
@msg unknown format `{f}` (Save writes {_1}) neznámý formát `{f}` (Uložit zapisuje {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) neznámý režim volného přechodu `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) neznámý druh přechodu `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) neznámý druh přechodu `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` neznámé zvýraznění `{h}`
|
||||
@msg unknown option `{path}.{k}` neznámá možnost `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) neznámé přetisky `{o}` (preserve, simulate nebo discard)
|
||||
@@ -5797,7 +5798,7 @@ mark-style Japanese Japonské
|
||||
@msg span must be "object" or "selection", got {v} span musí být "object" nebo "selection", ne {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" umísťuje `gradient`, ne vzorník
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" umísťuje přechod: zadejte `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" umísťuje lineární, radiální nebo kuželový přechod, ne volný přechod
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" umísťuje lineární, radiální, kuželový nebo kosočtverečný přechod, ne volný přechod
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" umísťuje samotný přechod: vynechte `start` a `end` (ty ho už umísťují v souřadnicích dokumentu)
|
||||
Make Image Dynamic Nastavit obraz jako dynamický
|
||||
Import Data Sets… Importovat sady dat…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1602,6 +1602,7 @@ digits Malayalam Malayalam
|
||||
Points: clicks add free points Punkter: klik tilføjer frie punkter
|
||||
Radial Gradient Radial gradient
|
||||
Conic Gradient Konisk gradient
|
||||
Diamond Gradient Diamantgradient
|
||||
Reset to White, Black Nulstil til hvid, sort
|
||||
Reverse Gradient Omvend gradient
|
||||
Save to Swatches Gem i farveprøver
|
||||
@@ -4633,7 +4634,7 @@ imageTrace Outline Kontur
|
||||
@msg unknown format ukendt format
|
||||
@msg unknown format `{f}` (Save writes {_1}) ukendt format `{f}` (Gem skriver {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) ukendt freeform-tilstand `{m}` (punkter, linjer)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) ukendt gradienttype `{k}` (lineær, radial, konisk, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) ukendt gradienttype `{k}` (lineær, radial, konisk, diamant, freeform)
|
||||
@msg unknown highlight `{h}` ukendt fremhævning `{h}`
|
||||
@msg unknown option `{path}.{k}` ukendt indstilling `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) ukendt overprint `{o}` (bevar, simulér eller forkast)
|
||||
@@ -5397,7 +5398,7 @@ mark-style Japanese Japansk
|
||||
@msg span must be "object" or "selection", got {v} span skal være "object" eller "selection", fik {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" placerer en `gradient`, ikke en farveprøve
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" placerer en overgang: angiv `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" placerer en lineær, radial eller konisk overgang, ikke en fri
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" placerer en lineær, radial, konisk eller diamantformet overgang, ikke en fri
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" placerer overgangen selv: udelad `start` og `end` (de placerer den allerede i dokumentkoordinater)
|
||||
Make Image Dynamic Gør billede dynamisk
|
||||
Import Data Sets… Importér datasæt…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2571,6 +2571,7 @@ digits Malayalam Malayalam
|
||||
Radial Glows Radiale Leuchteffekte
|
||||
Radial Gradient Radialer Verlauf
|
||||
Conic Gradient Konischer Verlauf
|
||||
Diamond Gradient Rautenverlauf
|
||||
Radius 1 Radius 1
|
||||
Radius 2 Radius 2
|
||||
Radius1 Radius1
|
||||
@@ -4632,7 +4633,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg unknown format unbekanntes Format
|
||||
@msg unknown format `{f}` (Save writes {_1}) unbekanntes Format `{f}` („Speichern“ schreibt {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) unbekannter Freiformmodus `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) unbekannte Verlaufsart `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) unbekannte Verlaufsart `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` unbekannte Hervorhebung `{h}`
|
||||
@msg unknown option `{path}.{k}` unbekannte Option `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) unbekannter Wert für overprints `{o}` (preserve, simulate oder discard)
|
||||
@@ -5375,7 +5376,7 @@ effect Soft Weich
|
||||
@msg span must be "object" or "selection", got {v} span muss "object" oder "selection" sein, nicht {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" platziert einen `gradient`, kein Farbfeld
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" platziert einen Verlauf: geben Sie `gradient` an
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" platziert einen linearen, radialen oder konischen Verlauf, keinen Freiform-Verlauf
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" platziert einen linearen, radialen, konischen oder Rautenverlauf, keinen Freiform-Verlauf
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" platziert den Verlauf selbst: lassen Sie `start` und `end` weg (sie platzieren ihn bereits in Dokumentkoordinaten)
|
||||
Make Image Dynamic Bild dynamisch machen
|
||||
Import Data Sets… Datensätze importieren…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1899,6 +1899,7 @@ font-technology Variable Μεταβλητές
|
||||
Radial Ακτινικό
|
||||
Radial Gradient Ακτινωτό ντεγκραντέ
|
||||
Conic Gradient Κωνικό ντεγκραντέ
|
||||
Diamond Gradient Ρομβοειδές ντεγκραντέ
|
||||
Random Τυχαίο
|
||||
Range: Εύρος:
|
||||
Rasterize Μετατροπή σε εικονοστοιχεία
|
||||
@@ -4664,7 +4665,7 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg unknown format άγνωστη μορφή
|
||||
@msg unknown format `{f}` (Save writes {_1}) άγνωστη μορφή `{f}` (η Αποθήκευση υποστηρίζει {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) άγνωστη λειτουργία ελεύθερης μορφής `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) άγνωστος τύπος ντεγκραντέ `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) άγνωστος τύπος ντεγκραντέ `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` άγνωστη επισήμανση `{h}`
|
||||
@msg unknown option `{path}.{k}` άγνωστη επιλογή `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) άγνωστη επιλογή υπερεκτύπωσης `{o}` (preserve, simulate ή discard)
|
||||
@@ -5762,7 +5763,7 @@ effect Points Σημεία
|
||||
@msg span must be "object" or "selection", got {v} το span πρέπει να είναι "object" ή "selection", δόθηκε {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: το "selection" τοποθετεί ένα `gradient`, όχι δείγμα χρώματος
|
||||
@msg span: "selection" places a gradient: give `gradient` span: το "selection" τοποθετεί ντεγκραντέ: δώστε `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: το "selection" τοποθετεί γραμμικό, ακτινικό ή κωνικό ντεγκραντέ, όχι ελεύθερης μορφής
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: το "selection" τοποθετεί γραμμικό, ακτινικό, κωνικό ή ρομβοειδές ντεγκραντέ, όχι ελεύθερης μορφής
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: το "selection" τοποθετεί το ίδιο το ντεγκραντέ: παραλείψτε τα `start` και `end` (το τοποθετούν ήδη σε συντεταγμένες εγγράφου)
|
||||
@msg the Color Picker is not open ο επιλογέας χρώματος δεν είναι ανοιχτός
|
||||
@msg the Simplify slider is not open ο ρυθμιστής Απλοποίησης δεν είναι ανοιχτός
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2613,6 +2613,7 @@ digits Malayalam Malayálam
|
||||
Radial Glows Resplandores radiales
|
||||
Radial Gradient Degradado radial
|
||||
Conic Gradient Degradado cónico
|
||||
Diamond Gradient Degradado de rombo
|
||||
Radius 1 Radio 1
|
||||
Radius 2 Radio 2
|
||||
Radius1 Radio1
|
||||
@@ -4715,7 +4716,7 @@ axis Both Ambos
|
||||
@msg unknown format formato desconocido
|
||||
@msg unknown format `{f}` (Save writes {_1}) formato desconocido `{f}` (Guardar escribe {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) modo de forma libre desconocido `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) tipo de degradado desconocido `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) tipo de degradado desconocido `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` resaltado desconocido `{h}`
|
||||
@msg unknown option `{path}.{k}` opción desconocida `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) sobreimpresiones desconocidas `{o}` (preserve, simulate o discard)
|
||||
@@ -5739,7 +5740,7 @@ mark-style Japanese Japonesas
|
||||
@msg span must be "object" or "selection", got {v} span debe ser "object" o "selection", no {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" coloca un `gradient`, no una muestra
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" coloca un degradado: indique `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" coloca un degradado lineal, radial o cónico, no uno de forma libre
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" coloca un degradado lineal, radial, cónico o de rombo, no uno de forma libre
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" coloca el propio degradado: omita `start` y `end` (ya lo colocan en coordenadas del documento)
|
||||
Make Image Dynamic Hacer imagen dinámica
|
||||
Import Data Sets… Importar conjuntos de datos…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1664,6 +1664,7 @@ digits Malayalam Malajalam
|
||||
Points: clicks add free points Pisteet: napsautukset lisäävät vapaita pisteitä
|
||||
Radial Gradient Säteittäinen liukuväri
|
||||
Conic Gradient Kartiomainen liukuväri
|
||||
Diamond Gradient Vinoneliöliukuväri
|
||||
Reset to White, Black Palauta valkoinen, musta
|
||||
Reverse Gradient Käännä liukuväri
|
||||
Save to Swatches Tallenna värimalleihin
|
||||
@@ -4925,7 +4926,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg unknown format tuntematon muoto
|
||||
@msg unknown format `{f}` (Save writes {_1}) tuntematon muoto `{f}` (Tallenna kirjoittaa muotoja {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) tuntematon vapaamuototila `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) tuntematon liukuvärin laji `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) tuntematon liukuvärin laji `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` tuntematon korostus `{h}`
|
||||
@msg unknown option `{path}.{k}` tuntematon asetus `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) tuntematon päällepainatusten käsittely `{o}` (preserve, simulate tai discard)
|
||||
@@ -5774,7 +5775,7 @@ mark-style Japanese Japanilainen
|
||||
@msg span must be "object" or "selection", got {v} span-arvon on oltava "object" tai "selection", saatiin {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" sijoittaa `gradient`-liukuvärin, ei värimallia
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" sijoittaa liukuvärin: anna `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" sijoittaa lineaarisen, säteittäisen tai kartiomaisen liukuvärin, ei vapaamuotoista
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" sijoittaa lineaarisen, säteittäisen, kartiomaisen tai vinoneliöliukuvärin, ei vapaamuotoista
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" sijoittaa itse liukuvärin: jätä pois `start` ja `end` (ne sijoittavat sen jo asiakirjan koordinaatteihin)
|
||||
@msg the Color Picker is not open värinvalitsin ei ole auki
|
||||
@msg the Simplify slider is not open Yksinkertaista-liukusäädin ei ole auki
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2605,6 +2605,7 @@ digits Malayalam Malayalam
|
||||
Radial Glows Halos radiaux
|
||||
Radial Gradient Dégradé radial
|
||||
Conic Gradient Dégradé conique
|
||||
Diamond Gradient Dégradé en losange
|
||||
Radius 1 Rayon 1
|
||||
Radius 2 Rayon 2
|
||||
Radius1 Rayon1
|
||||
@@ -4704,7 +4705,7 @@ axis Both Les deux
|
||||
@msg unknown format format inconnu
|
||||
@msg unknown format `{f}` (Save writes {_1}) format inconnu `{f}` (Enregistrer écrit {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) mode de forme libre inconnu `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) type de dégradé inconnu `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) type de dégradé inconnu `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` mise en évidence inconnue `{h}`
|
||||
@msg unknown option `{path}.{k}` option inconnue `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) surimpressions inconnues `{o}` (preserve, simulate ou discard)
|
||||
@@ -5792,7 +5793,7 @@ theme Appearance Apparence
|
||||
@msg span must be "object" or "selection", got {v} span doit être "object" ou "selection", pas {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" place un `gradient`, pas une nuance
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" place un dégradé : indiquez `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" place un dégradé linéaire, radial ou conique, pas un dégradé de forme libre
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" place un dégradé linéaire, radial, conique ou en losange, pas un dégradé de forme libre
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" place le dégradé lui-même : omettez `start` et `end` (ils le placent déjà en coordonnées du document)
|
||||
Make Image Dynamic Rendre l'image dynamique
|
||||
Import Data Sets… Importer des jeux de données…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3571,6 +3571,7 @@ antiAlias Sharp शार्प
|
||||
Radial Glows रेडियल चमक
|
||||
Radial Gradient रेडियल ग्रेडिएंट
|
||||
Conic Gradient शंक्वाकार ग्रेडिएंट
|
||||
Diamond Gradient डायमंड ग्रेडिएंट
|
||||
Radial Repeat रेडियल दोहराव
|
||||
Radius त्रिज्या
|
||||
Radius 1 त्रिज्या 1
|
||||
@@ -7483,7 +7484,7 @@ antiAlias Sharp शार्प
|
||||
@msg span must be "object" or "selection", got {v} span: स्पैन को "object" या "selection" होना चाहिए, {v} मिला
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" एक `gradient` रखता है, स्वॉच नहीं
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" ग्रेडिएंट रखता है: `gradient` दें
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" एक रैखिक, रेडियल या शंक्वाकार ग्रेडिएंट रखता है, फ्रीफॉर्म ग्रेडिएंट नहीं
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" एक रैखिक, रेडियल, शंक्वाकार या डायमंड ग्रेडिएंट रखता है, फ्रीफॉर्म ग्रेडिएंट नहीं
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" खुद ग्रेडिएंट रखता है: `start` और `end` न दें (वे उसे डॉक्यूमेंट निर्देशांकों पर रख देते हैं)
|
||||
@msg speed must be accelerated, step or pause speed: गति को तेज, कदम या रोकें होना चाहिए
|
||||
@msg Split Into Grid: select paths large enough for the grid स्प्लिट इनो ग्रिड: ग्रिड के लिए काफी बड़े पाथ सिलेक्ट करें
|
||||
@@ -7902,7 +7903,7 @@ antiAlias Sharp शार्प
|
||||
@msg unknown format `{f}` (vcswatches, gpl, ase or css) अज्ञात फ़ॉर्मैट `{f}` (vcswatches, gpl, ase या css)
|
||||
@msg unknown freeform mode `{m}` (points, lines) अज्ञात फ्रीफॉर्म मोड `{m}` (बिंदुओं, लाइनों)
|
||||
@msg unknown from {f} {f} से अज्ञात
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) अज्ञात ग्रेडिएंट `{k}` (रैखिक, रेडियल, शंक्वाकार, फ्रीफॉर्म)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) अज्ञात ग्रेडिएंट `{k}` (रैखिक, रेडियल, शंक्वाकार, डायमंड, फ्रीफॉर्म)
|
||||
@msg unknown graph type `{t}` अज्ञात ग्राफ प्रकार `{t}`
|
||||
@msg unknown grid colours `{name}` (one of {_1}) अज्ञात ग्रिड रंग `{name}` ({_1} में से एक)
|
||||
@msg unknown gridType `{t}` अज्ञात gridType `{t}`
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3162,6 +3162,7 @@ mark-style Japanese Japán
|
||||
Radial Glows Körkörös ragyogások
|
||||
Radial Gradient Körkörös színátmenet
|
||||
Conic Gradient Kúpos színátmenet
|
||||
Diamond Gradient Rombusz színátmenet
|
||||
Radial Repeat Körkörös ismétlés
|
||||
Radius Sugár
|
||||
Radius 1 Sugár 1
|
||||
@@ -5528,7 +5529,7 @@ imageTrace Outline Körvonal
|
||||
@msg unknown format ismeretlen formátum
|
||||
@msg unknown format `{f}` (Save writes {_1}) ismeretlen formátum: `{f}` (a Mentés ezeket írja: {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) ismeretlen szabad formájú mód: `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) ismeretlen színátmenet-fajta: `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) ismeretlen színátmenet-fajta: `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` ismeretlen kiemelés: `{h}`
|
||||
@msg unknown option `{path}.{k}` ismeretlen beállítás: `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) ismeretlen felülnyomás-kezelés: `{o}` (preserve, simulate vagy discard)
|
||||
@@ -5780,7 +5781,7 @@ imageTrace Outline Körvonal
|
||||
@msg span must be "object" or "selection", got {v} a span értéke "object" vagy "selection" lehet, nem {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" egy `gradient` értéket helyez el, nem színmintát
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" színátmenetet helyez el: adja meg a `gradient` értéket
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" lineáris, sugaras vagy kúpos színátmenetet helyez el, nem szabad formájút
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" lineáris, sugaras, kúpos vagy rombusz színátmenetet helyez el, nem szabad formájút
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" magát a színátmenetet helyezi el: hagyja el a `start` és `end` értéket (ezek már a dokumentum koordinátáiban helyezik el)
|
||||
Make Image Dynamic Kép dinamikussá tétele
|
||||
Import Data Sets… Adatkészletek importálása…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1602,6 +1602,7 @@ digits Malayalam Malayalam
|
||||
Points: clicks add free points Titik: klik menambahkan titik bebas
|
||||
Radial Gradient Gradien radial
|
||||
Conic Gradient Gradien kerucut
|
||||
Diamond Gradient Gradien wajik
|
||||
Reset to White, Black Setel ulang ke putih, hitam
|
||||
Reverse Gradient Balikkan gradien
|
||||
Save to Swatches Simpan ke swatch
|
||||
@@ -4633,7 +4634,7 @@ imageTrace Outline Kontur
|
||||
@msg unknown format format tidak dikenal
|
||||
@msg unknown format `{f}` (Save writes {_1}) format `{f}` tidak dikenal (Simpan menulis {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) mode bentuk bebas `{m}` tidak dikenal (titik, garis)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) jenis gradien `{k}` tidak dikenal (linear, radial, kerucut, bentuk bebas)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) jenis gradien `{k}` tidak dikenal (linear, radial, kerucut, wajik, bentuk bebas)
|
||||
@msg unknown highlight `{h}` sorotan `{h}` tidak dikenal
|
||||
@msg unknown option `{path}.{k}` opsi `{path}.{k}` tidak dikenal
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) overprint `{o}` tidak dikenal (pertahankan, simulasikan, atau buang)
|
||||
@@ -5397,7 +5398,7 @@ mark-style Japanese Jepang
|
||||
@msg span must be "object" or "selection", got {v} span harus "object" atau "selection", diterima {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" menempatkan `gradient`, bukan swatch
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" menempatkan gradien: berikan `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" menempatkan gradien linear, radial, atau kerucut, bukan gradien bebas
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" menempatkan gradien linear, radial, kerucut, atau wajik, bukan gradien bebas
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" menempatkan gradien itu sendiri: jangan sertakan `start` dan `end` (keduanya sudah menempatkannya dalam koordinat dokumen)
|
||||
Make Image Dynamic Jadikan gambar dinamis
|
||||
Import Data Sets… Impor himpunan data…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2340 and column 5.
|
@@ -2647,6 +2647,7 @@ digits Malayalam Malayalam
|
||||
Radial Glows Bagliori radiali
|
||||
Radial Gradient Sfumatura radiale
|
||||
Conic Gradient Sfumatura conica
|
||||
Diamond Gradient Sfumatura a rombo
|
||||
Radius 1 Raggio 1
|
||||
Radius 2 Raggio 2
|
||||
Radius1 Raggio1
|
||||
@@ -4758,7 +4759,7 @@ axis Both Entrambi
|
||||
@msg unknown format formato sconosciuto
|
||||
@msg unknown format `{f}` (Save writes {_1}) formato sconosciuto `{f}` (Salva scrive {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) modalità a forma libera sconosciuta `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) tipo di sfumatura sconosciuto `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) tipo di sfumatura sconosciuto `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` evidenziazione sconosciuta `{h}`
|
||||
@msg unknown option `{path}.{k}` opzione sconosciuta `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) valore di sovrastampa sconosciuto `{o}` (preserve, simulate o discard)
|
||||
@@ -5410,7 +5411,7 @@ lighting Light Luce
|
||||
@msg span must be "object" or "selection", got {v} span deve essere "object" o "selection", non {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" posiziona un `gradient`, non un campione
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" posiziona una sfumatura: indica `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" posiziona una sfumatura lineare, radiale o conica, non una sfumatura a forma libera
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" posiziona una sfumatura lineare, radiale, conica o a rombo, non una sfumatura a forma libera
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" posiziona la sfumatura stessa: ometti `start` e `end` (la posizionano già in coordinate del documento)
|
||||
Make Image Dynamic Rendi immagine dinamica
|
||||
Import Data Sets… Importa set di dati…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2688,6 +2688,7 @@ mark-style Japanese 日本式
|
||||
Radar Graph Tool レーダーチャートツール
|
||||
Radial Gradient 円形グラデーション
|
||||
Conic Gradient 円錐グラデーション
|
||||
Diamond Gradient ひし形グラデーション
|
||||
Random ランダム
|
||||
Randomize Colors カラーをランダム化
|
||||
Randomly change color order カラーの順序をランダムに変更
|
||||
@@ -4364,7 +4365,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg stop {i} needs `offset` 分岐点 {i} には `offset` が必要です
|
||||
@msg stop {i} needs a valid `color` 分岐点 {i} には有効な `color` が必要です
|
||||
@msg stop {i}: `{k}` must be a number 分岐点 {i}: `{k}` には数値を指定してください
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) `{k}` は不明なグラデーションの種類です (linear、radial、conic、freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) `{k}` は不明なグラデーションの種類です (linear、radial、conic、diamond、freeform)
|
||||
@msg the art could not be sampled アートをサンプリングできませんでした
|
||||
@msg the art is too light for any dots アートが明るすぎて網点を生成できません
|
||||
@msg the document has no symbols ドキュメントにシンボルがありません
|
||||
@@ -5453,7 +5454,7 @@ lighting Light ライト
|
||||
@msg span must be "object" or "selection", got {v} span は "object" または "selection" で指定してください ({v} は指定できません)
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" は `gradient` を配置します。スウォッチは配置できません
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" はグラデーションを配置します: `gradient` を指定してください
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" は線形、円形または円錐グラデーションを配置します。フリーグラデーションは配置できません
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" は線形、円形、円錐またはひし形グラデーションを配置します。フリーグラデーションは配置できません
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" はグラデーション自体を配置します: `start` と `end` は省略してください (これらはすでにドキュメント座標で配置を指定します)
|
||||
Make Image Dynamic 画像を動的にする
|
||||
Import Data Sets… データセットを読み込み…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1536,6 +1536,7 @@ digits Malayalam მალაიალამური
|
||||
Points: clicks add free points წერტილები: დაწკაპუნებები ამატებს თავისუფალ წერტილებს
|
||||
Radial Gradient რადიალური გრადიენტი
|
||||
Conic Gradient კონუსური გრადიენტი
|
||||
Diamond Gradient რომბული გრადიენტი
|
||||
Reset to White, Black თეთრზე და შავზე დაბრუნება
|
||||
Reverse Gradient გრადიენტის შებრუნება
|
||||
Gradient Presets გრადიენტის პრესეტები
|
||||
@@ -4460,7 +4461,7 @@ font-technology Variable ცვლადი
|
||||
@msg unknown format უცნობი ფორმატი
|
||||
@msg unknown format `{f}` (Save writes {_1}) უცნობი ფორმატი `{f}` (შენახვა წერს: {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) უცნობი თავისუფალი რეჟიმი `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) უცნობი გრადიენტის სახე `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) უცნობი გრადიენტის სახე `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` უცნობი გამოკვეთა `{h}`
|
||||
@msg unknown option `{path}.{k}` უცნობი პარამეტრი `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) უცნობი ზედდაბეჭდვა `{o}` (preserve, simulate ან discard)
|
||||
@@ -5461,7 +5462,7 @@ imageTrace Outline კონტური
|
||||
@msg span must be "object" or "selection", got {v} span უნდა იყოს "object" ან "selection", მიღებულია {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" ათავსებს `gradient`-ს და არა ფერის ნიმუშს
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" ათავსებს გრადიენტს: მიუთითეთ `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" ათავსებს წრფივ, რადიალურ ან კონუსურ გრადიენტს და არა თავისუფალი ფორმისას
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" ათავსებს წრფივ, რადიალურ, კონუსურ ან რომბულ გრადიენტს და არა თავისუფალი ფორმისას
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" თავად ათავსებს გრადიენტს: გამოტოვეთ `start` და `end` (ისინი მას უკვე დოკუმენტის კოორდინატებში ათავსებენ)
|
||||
Make Image Dynamic გამოსახულების დინამიკურად ქცევა
|
||||
Import Data Sets… მონაცემთა ნაკრებების იმპორტი…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2678,6 +2678,7 @@ mark-style Japanese 일본식
|
||||
Radar Graph Tool 레이더 그래프 도구
|
||||
Radial Gradient 방사형 그레이디언트
|
||||
Conic Gradient 원뿔형 그레이디언트
|
||||
Diamond Gradient 다이아몬드 그레이디언트
|
||||
Random 무작위
|
||||
Randomize Colors 색상 무작위 변경
|
||||
Randomly change color order 색상 순서를 무작위로 변경
|
||||
@@ -4291,7 +4292,7 @@ theme Appearance 외관
|
||||
@msg stop {i} needs `offset` 정지점 {i}에는 `offset`이 필요합니다
|
||||
@msg stop {i} needs a valid `color` 정지점 {i}에는 올바른 `color`가 필요합니다
|
||||
@msg stop {i}: `{k}` must be a number 정지점 {i}: `{k}`은(는) 숫자여야 합니다
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) 알 수 없는 그레이디언트 종류 `{k}`(linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) 알 수 없는 그레이디언트 종류 `{k}`(linear, radial, conic, diamond, freeform)
|
||||
@msg the art could not be sampled 아트웍을 샘플링할 수 없습니다
|
||||
@msg the art is too light for any dots 아트웍이 너무 밝아 점을 만들 수 없습니다
|
||||
@msg the document has no symbols 문서에 심볼이 없습니다
|
||||
@@ -5371,7 +5372,7 @@ lighting Light 광원
|
||||
@msg span must be "object" or "selection", got {v} span은 "object" 또는 "selection"이어야 합니다. {v}은(는) 사용할 수 없습니다
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection"은 `gradient`를 배치합니다. 견본은 배치할 수 없습니다
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection"은 그레이디언트를 배치합니다: `gradient`를 지정하세요
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection"은 선형, 방사형 또는 원뿔형 그레이디언트를 배치합니다. 자유형 그레이디언트는 배치할 수 없습니다
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection"은 선형, 방사형, 원뿔형 또는 다이아몬드 그레이디언트를 배치합니다. 자유형 그레이디언트는 배치할 수 없습니다
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection"은 그레이디언트 자체를 배치합니다: `start`와 `end`를 빼세요 (이미 문서 좌표로 배치합니다)
|
||||
Make Image Dynamic 이미지를 동적으로 만들기
|
||||
Import Data Sets… 데이터 세트 가져오기…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1616,6 +1616,7 @@ digits Malayalam Malayalam
|
||||
Points: clicks add free points Punten: klikken voegen vrije punten toe
|
||||
Radial Gradient Radiaal verloop
|
||||
Conic Gradient Conisch verloop
|
||||
Diamond Gradient Ruitverloop
|
||||
Reset to White, Black Herstellen naar wit, zwart
|
||||
Reverse Gradient Verloop omkeren
|
||||
Save to Swatches Opslaan in stalen
|
||||
@@ -4645,7 +4646,7 @@ imageTrace Outline Contour
|
||||
@msg unknown format onbekende indeling
|
||||
@msg unknown format `{f}` (Save writes {_1}) onbekende indeling `{f}` (Opslaan schrijft {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) onbekende vrijvormmodus `{m}` (punten, lijnen)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) onbekend verlooptype `{k}` (lineair, radiaal, conisch, vrijvorm)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) onbekend verlooptype `{k}` (lineair, radiaal, conisch, ruit, vrijvorm)
|
||||
@msg unknown highlight `{h}` onbekende markering `{h}`
|
||||
@msg unknown option `{path}.{k}` onbekende optie `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) onbekende overdruk `{o}` (behouden, simuleren of verwerpen)
|
||||
@@ -5409,7 +5410,7 @@ mark-style Japanese Japans
|
||||
@msg span must be "object" or "selection", got {v} span moet "object" of "selection" zijn, maar is {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" plaatst een `gradient`, geen staal
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" plaatst een verloop: geef `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" plaatst een lineair, radiaal of conisch verloop, geen vrij verloop
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" plaatst een lineair, radiaal, conisch of ruitvormig verloop, geen vrij verloop
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" plaatst het verloop zelf: laat `start` en `end` weg (die plaatsen het al in documentcoördinaten)
|
||||
Make Image Dynamic Afbeelding dynamisch maken
|
||||
Import Data Sets… Gegevenssets importeren…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1603,6 +1603,7 @@ digits Malayalam Malajalam
|
||||
Points: clicks add free points Punkty: kliknięcia dodają wolne punkty
|
||||
Radial Gradient Gradient promienisty
|
||||
Conic Gradient Gradient stożkowy
|
||||
Diamond Gradient Gradient rombowy
|
||||
Reset to White, Black Resetuj do bieli i czerni
|
||||
Reverse Gradient Odwróć gradient
|
||||
Save to Swatches Zapisz w próbkach
|
||||
@@ -4849,7 +4850,7 @@ imageTrace Outline Kontur
|
||||
@msg unknown format nieznany format
|
||||
@msg unknown format `{f}` (Save writes {_1}) nieznany format `{f}` (zapis tworzy {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) nieznany tryb dowolny `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) nieznany rodzaj gradientu `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) nieznany rodzaj gradientu `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` nieznane wyróżnienie `{h}`
|
||||
@msg unknown option `{path}.{k}` nieznana opcja `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) nieznana wartość nadruków `{o}` (preserve, simulate lub discard)
|
||||
@@ -5650,7 +5651,7 @@ mark-style Japanese Japońskie
|
||||
@msg span must be "object" or "selection", got {v} span musi mieć wartość "object" lub "selection", a nie {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" umieszcza `gradient`, a nie próbkę
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" umieszcza gradient: podaj `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" umieszcza gradient liniowy, radialny lub stożkowy, a nie dowolny
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" umieszcza gradient liniowy, radialny, stożkowy lub rombowy, a nie dowolny
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" umieszcza sam gradient: pomiń `start` i `end` (one już umieszczają go we współrzędnych dokumentu)
|
||||
Make Image Dynamic Uczyń obraz dynamicznym
|
||||
Import Data Sets… Importuj zestawy danych…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2178,6 +2178,7 @@ digits Malayalam Malaiala
|
||||
Radial Glows Brilhos Radiais
|
||||
Radial Gradient Gradiente Radial
|
||||
Conic Gradient Gradiente Cônico
|
||||
Diamond Gradient Gradiente em Losango
|
||||
Radius: Raio:
|
||||
Rainbow Arco-íris
|
||||
Random Aleatório
|
||||
@@ -4255,7 +4256,7 @@ axis Both Ambos
|
||||
@msg unknown format formato desconhecido
|
||||
@msg unknown format `{f}` (Save writes {_1}) formato desconhecido `{f}` (Salvar grava {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) modo de forma livre desconhecido `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) tipo de gradiente desconhecido `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) tipo de gradiente desconhecido `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` destaque desconhecido `{h}`
|
||||
@msg unknown option `{path}.{k}` opção desconhecida `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) sobreimpressões desconhecidas `{o}` (preserve, simulate ou discard)
|
||||
@@ -5680,7 +5681,7 @@ mark-style Japanese Japonesas
|
||||
@msg span must be "object" or "selection", got {v} span deve ser "object" ou "selection", não {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" posiciona um `gradient`, não uma amostra
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" posiciona um degradê: informe `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" posiciona um degradê linear, radial ou cônico, não um de forma livre
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" posiciona um degradê linear, radial, cônico ou em losango, não um de forma livre
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" posiciona o próprio degradê: omita `start` e `end` (eles já o posicionam em coordenadas do documento)
|
||||
Make Image Dynamic Tornar imagem dinâmica
|
||||
@msg Batch: {count} processed Lote: {count} processados
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2642,6 +2642,7 @@ digits Malayalam Малаялам
|
||||
Radial Glows Радиальные свечения
|
||||
Radial Gradient Радиальный градиент
|
||||
Conic Gradient Конический градиент
|
||||
Diamond Gradient Ромбовидный градиент
|
||||
Radius 1 Радиус 1
|
||||
Radius 2 Радиус 2
|
||||
Radius1 Радиус1
|
||||
@@ -4752,7 +4753,7 @@ axis Both Оба
|
||||
@msg unknown format неизвестный формат
|
||||
@msg unknown format `{f}` (Save writes {_1}) неизвестный формат `{f}` (сохранение записывает {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) неизвестный режим произвольной формы `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) неизвестный тип градиента `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) неизвестный тип градиента `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` неизвестное выделение `{h}`
|
||||
@msg unknown option `{path}.{k}` неизвестный параметр `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) неизвестные наложения `{o}` (preserve, simulate или discard)
|
||||
@@ -5404,7 +5405,7 @@ lighting Light Свет
|
||||
@msg span must be "object" or "selection", got {v} span должен быть "object" или "selection", а не {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" размещает `gradient`, а не образец
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" размещает градиент: укажите `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" размещает линейный, радиальный или конический градиент, а не произвольный
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" размещает линейный, радиальный, конический или ромбовидный градиент, а не произвольный
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" сам размещает градиент: уберите `start` и `end` (они уже задают его в координатах документа)
|
||||
Make Image Dynamic Сделать изображение динамическим
|
||||
Import Data Sets… Импортировать наборы данных…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2550,6 +2550,7 @@ digits Malayalam Malayalam
|
||||
Radial Glows Radiellt sken
|
||||
Radial Gradient Radiell gradient
|
||||
Conic Gradient Konisk gradient
|
||||
Diamond Gradient Rombgradient
|
||||
Radius 1 Radie 1
|
||||
Radius 2 Radie 2
|
||||
Radius1 Radie1
|
||||
@@ -4613,7 +4614,7 @@ axis Both Båda
|
||||
@msg unknown format okänt format
|
||||
@msg unknown format `{f}` (Save writes {_1}) okänt format `{f}` (Spara skriver {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) okänt fritt läge `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) okänd gradienttyp `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) okänd gradienttyp `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` okänd högdager `{h}`
|
||||
@msg unknown option `{path}.{k}` okänt alternativ `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) okänt värde för övertryck `{o}` (preserve, simulate eller discard)
|
||||
@@ -5766,7 +5767,7 @@ theme Appearance Utseende
|
||||
@msg span must be "object" or "selection", got {v} span måste vara "object" eller "selection", fick {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" placerar en `gradient`, inte en färgruta
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" placerar en gradient: ange `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" placerar en linjär, radiell eller konisk gradient, inte en fri
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" placerar en linjär, radiell, konisk eller rombformad gradient, inte en fri
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" placerar själva gradienten: utelämna `start` och `end` (de placerar den redan i dokumentkoordinater)
|
||||
@msg the Color Picker is not open färgväljaren är inte öppen
|
||||
@msg the Simplify slider is not open Förenkla-reglaget är inte öppet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2577,7 +2577,7 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[
|
||||
("scale must be a number from {_1} to {_2}, not {_3}", &["scale"]),
|
||||
("scale must be a number from {_1} to {_2}, not {s}", &["scale"]),
|
||||
("show must be true or false", &["show"]),
|
||||
("span: \"selection\" places a linear, radial or conic gradient, not a freeform one", &["span"]),
|
||||
("span: \"selection\" places a linear, radial, conic or diamond gradient, not a freeform one", &["span"]),
|
||||
("thickness must be from 0.01 to 5000 pt", &["thickness"]),
|
||||
("title can't be empty", &["title"]),
|
||||
("view is required", &["view"]),
|
||||
|
||||
@@ -2422,6 +2422,7 @@ digits Malayalam Malayalam
|
||||
Radial Glows Radyal Parıltılar
|
||||
Radial Gradient Radyal Gradyan
|
||||
Conic Gradient Konik Gradyan
|
||||
Diamond Gradient Elmas Gradyan
|
||||
Radius 1 Yarıçap 1
|
||||
Radius 2 Yarıçap 2
|
||||
Radius1 Yarıçap1
|
||||
@@ -4462,7 +4463,7 @@ axis Both Her İkisi
|
||||
@msg unknown format bilinmeyen format
|
||||
@msg unknown format `{f}` (Save writes {_1}) bilinmeyen format `{f}` (Kaydet, {_1} yazar)
|
||||
@msg unknown freeform mode `{m}` (points, lines) bilinmeyen serbest biçimli mod `{m}` (noktalar, çizgiler)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) bilinmeyen gradyan türü `{k}` (doğrusal, radyal, konik, serbest biçimli)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) bilinmeyen gradyan türü `{k}` (doğrusal, radyal, konik, elmas, serbest biçimli)
|
||||
@msg unknown highlight `{h}` bilinmeyen vurgu `{h}`
|
||||
@msg unknown option `{path}.{k}` bilinmeyen seçenek `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) bilinmeyen üst baskılar `{o}` (koru, benzet veya at)
|
||||
@@ -5473,7 +5474,7 @@ theme Appearance Görünüm
|
||||
@msg span must be "object" or "selection", got {v} span "object" veya "selection" olmalıdır, {v} değil
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" bir renk örneği değil, bir `gradient` yerleştirir
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" bir degrade yerleştirir: `gradient` verin
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" serbest biçimli bir degrade değil, doğrusal, radyal veya konik bir degrade yerleştirir
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" serbest biçimli bir degrade değil, doğrusal, radyal, konik veya elmas bir degrade yerleştirir
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" degradenin kendisini yerleştirir: `start` ve `end` değerlerini vermeyin (bunlar degradeyi zaten belge koordinatlarına yerleştirir)
|
||||
Make Image Dynamic Görüntüyü Dinamik Hale Getirin
|
||||
Import Data Sets… Veri Kümelerini İçe Aktar…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2639,6 +2639,7 @@ digits Malayalam Малаялам
|
||||
Radial Glows Радіальні світіння
|
||||
Radial Gradient Радіальний градієнт
|
||||
Conic Gradient Конічний градієнт
|
||||
Diamond Gradient Ромбоподібний градієнт
|
||||
Radius 1 Радіус 1
|
||||
Radius 2 Радіус 2
|
||||
Radius1 Радіус1
|
||||
@@ -4742,7 +4743,7 @@ axis Both Обидві
|
||||
@msg unknown format невідомий формат
|
||||
@msg unknown format `{f}` (Save writes {_1}) невідомий формат `{f}` (збереження записує {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) невідомий режим довільної форми `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) невідомий тип градієнта `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) невідомий тип градієнта `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` невідоме виділення `{h}`
|
||||
@msg unknown option `{path}.{k}` невідомий параметр `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) невідомі накладення `{o}` (preserve, simulate або discard)
|
||||
@@ -5390,7 +5391,7 @@ lighting Light Світло
|
||||
@msg span must be "object" or "selection", got {v} span має бути "object" або "selection", а не {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" розміщує `gradient`, а не зразок
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" розміщує градієнт: вкажіть `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" розміщує лінійний, радіальний або конічний градієнт, а не довільний
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" розміщує лінійний, радіальний, конічний або ромбоподібний градієнт, а не довільний
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" сам розміщує градієнт: приберіть `start` і `end` (вони вже задають його в координатах документа)
|
||||
Make Image Dynamic Зробити зображення динамічним
|
||||
Import Data Sets… Імпортувати набори даних…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -2943,6 +2943,7 @@ mark-style Japanese Kiểu Nhật
|
||||
Radial Glows Ánh sáng hướng tâm
|
||||
Radial Gradient Chuyển sắc hướng tâm
|
||||
Conic Gradient Chuyển sắc hình nón
|
||||
Diamond Gradient Chuyển sắc hình thoi
|
||||
Radius Bán kính
|
||||
Radius 1 Bán kính 1
|
||||
Radius 2 Bán kính 2
|
||||
@@ -5188,7 +5189,7 @@ theme Appearance Giao diện
|
||||
@msg unknown format định dạng không xác định
|
||||
@msg unknown format `{f}` (Save writes {_1}) định dạng không xác định `{f}` (Lưu ghi {_1})
|
||||
@msg unknown freeform mode `{m}` (points, lines) chế độ tự do không xác định `{m}` (points, lines)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) loại chuyển sắc không xác định `{k}` (linear, radial, conic, freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) loại chuyển sắc không xác định `{k}` (linear, radial, conic, diamond, freeform)
|
||||
@msg unknown highlight `{h}` tô sáng không xác định `{h}`
|
||||
@msg unknown option `{path}.{k}` tùy chọn không xác định `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) chế độ in đè không xác định `{o}` (preserve, simulate hoặc discard)
|
||||
@@ -5391,7 +5392,7 @@ theme Appearance Giao diện
|
||||
@msg span must be "object" or "selection", got {v} span phải là "object" hoặc "selection", không phải {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" đặt một `gradient`, không phải mẫu màu
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" đặt một dải chuyển màu: hãy cung cấp `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" đặt dải chuyển màu tuyến tính, xuyên tâm hoặc hình nón, không phải dạng tự do
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" đặt dải chuyển màu tuyến tính, xuyên tâm, hình nón hoặc hình thoi, không phải dạng tự do
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" tự đặt dải chuyển màu: hãy bỏ `start` và `end` (chúng đã đặt nó theo tọa độ tài liệu)
|
||||
Make Image Dynamic Tạo hình ảnh động
|
||||
Import Data Sets… Nhập tập dữ liệu…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -2515,6 +2515,7 @@ digits Malayalam 马拉雅拉姆数字
|
||||
Radial Glows 径向光晕
|
||||
Radial Gradient 径向渐变
|
||||
Conic Gradient 圆锥渐变
|
||||
Diamond Gradient 菱形渐变
|
||||
Radius 1 半径 1
|
||||
Radius 2 半径 2
|
||||
Radius1 半径1
|
||||
@@ -4080,7 +4081,7 @@ imageTrace Outline 轮廓
|
||||
@msg stop {i} needs `offset` 色标 {i} 需要 `offset`
|
||||
@msg stop {i} needs a valid `color` 色标 {i} 需要有效的 `color`
|
||||
@msg stop {i}: `{k}` must be a number 色标 {i}:`{k}` 必须是数字
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) 未知的渐变类型 `{k}` (linear、radial、conic、freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) 未知的渐变类型 `{k}` (linear、radial、conic、diamond、freeform)
|
||||
@msg the art could not be sampled 无法对图稿取样
|
||||
@msg the art is too light for any dots 图稿太浅,无法生成任何网点
|
||||
@msg the document has no symbols 文档中没有符号
|
||||
@@ -4824,7 +4825,7 @@ imageTrace Outline 轮廓
|
||||
@msg span must be "object" or "selection", got {v} span 必须是 "object" 或 "selection",实际为 {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" 放置的是 `gradient`,而不是色板
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" 放置渐变:请给出 `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" 放置线性、径向或圆锥渐变,而不是任意形状渐变
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" 放置线性、径向、圆锥或菱形渐变,而不是任意形状渐变
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" 自行放置渐变:请省略 `start` 和 `end` (它们已按文档坐标放置渐变)
|
||||
@msg Batch: {count} processed 批处理:已处理 {count} 个
|
||||
@msg Batch: {count} processed, {failed} failed 批处理:已处理 {count} 个,失败 {failed} 个
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1890,7 +1890,7 @@ file-filter Templates 範本
|
||||
@msg stop {i} needs `offset` 色標 {i} 需要 `offset`
|
||||
@msg stop {i} needs a valid `color` 色標 {i} 需要有效的 `color`
|
||||
@msg stop {i}: `{k}` must be a number 色標 {i}:`{k}` 必須是數字
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, freeform) 未知的漸層類型 `{k}` (linear、radial、conic、freeform)
|
||||
@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) 未知的漸層類型 `{k}` (linear、radial、conic、diamond、freeform)
|
||||
@msg the art could not be sampled 無法對圖稿取樣
|
||||
@msg the art is too light for any dots 圖稿太淡,無法產生任何網點
|
||||
@msg the document has no symbols 文件中沒有符號
|
||||
@@ -2633,7 +2633,7 @@ file-filter Templates 範本
|
||||
@msg span must be "object" or "selection", got {v} span 必須是 "object" 或 "selection",收到 {v}
|
||||
@msg span: "selection" places a `gradient`, not a swatch span: "selection" 放置的是 `gradient`,而非色票
|
||||
@msg span: "selection" places a gradient: give `gradient` span: "selection" 會放置漸層:請提供 `gradient`
|
||||
@msg span: "selection" places a linear, radial or conic gradient, not a freeform one span: "selection" 放置線性、放射狀或圓錐漸層,而非任意形狀漸層
|
||||
@msg span: "selection" places a linear, radial, conic or diamond gradient, not a freeform one span: "selection" 放置線性、放射狀、圓錐或菱形漸層,而非任意形狀漸層
|
||||
@msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" 會自行放置漸層:請不要提供 `start` 和 `end` (它們已依文件座標放置漸層)
|
||||
@msg Batch: {count} processed 批次:已處理 {count} 個
|
||||
@msg Batch: {count} processed, {failed} failed 批次:已處理 {count} 個,失敗 {failed} 個
|
||||
@@ -3599,6 +3599,7 @@ digits Malayalam 馬來亞拉姆數字
|
||||
Radial Glows 放射狀光暈
|
||||
Radial Gradient 放射狀漸層
|
||||
Conic Gradient 圓錐漸層
|
||||
Diamond Gradient 菱形漸層
|
||||
Radius 1 半徑 1
|
||||
Radius 2 半徑 2
|
||||
Radius1 半徑1
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -90,6 +90,7 @@ pub static ICONS: &[(&str, &[u8])] = &[
|
||||
("dc-fx", include_bytes!("../../../assets/icons/dc-fx.svg")),
|
||||
("dc-gamut", include_bytes!("../../../assets/icons/dc-gamut.svg")),
|
||||
("dc-grad-conic", include_bytes!("../../../assets/icons/dc-grad-conic.svg")),
|
||||
("dc-grad-diamond", include_bytes!("../../../assets/icons/dc-grad-diamond.svg")),
|
||||
("dc-grad-freeform", include_bytes!("../../../assets/icons/dc-grad-freeform.svg")),
|
||||
("dc-grad-linear", include_bytes!("../../../assets/icons/dc-grad-linear.svg")),
|
||||
("dc-grad-radial", include_bytes!("../../../assets/icons/dc-grad-radial.svg")),
|
||||
|
||||
@@ -124,8 +124,8 @@ fn dragged_stops(origin: &[GradientStop], i: usize, offset: f32, copy: bool, ove
|
||||
|
||||
// ---------- shared edits ----------
|
||||
|
||||
/// The Type row: Linear, Radial, Conic and Freeform for the paint behind the active proxy (one of
|
||||
/// them lit when it is a gradient). The Gradient panel's and the Properties panel's.
|
||||
/// The Type row: Linear, Radial, Conic, Diamond and Freeform for the paint behind the active
|
||||
/// proxy (one of them lit when it is a gradient). The Gradient panel's and the Properties panel's.
|
||||
pub(crate) fn type_row(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let kind = current(app).map(|g| g.gradient.kind);
|
||||
ui.horizontal(|ui| {
|
||||
@@ -134,6 +134,7 @@ pub(crate) fn type_row(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
(GradientKind::Linear, "dc-grad-linear", tl!("Linear Gradient")),
|
||||
(GradientKind::Radial, "dc-grad-radial", tl!("Radial Gradient")),
|
||||
(GradientKind::Conic, "dc-grad-conic", tl!("Conic Gradient")),
|
||||
(GradientKind::Diamond, "dc-grad-diamond", tl!("Diamond Gradient")),
|
||||
(GradientKind::Freeform, "dc-grad-freeform", tl!("Freeform Gradient")),
|
||||
] {
|
||||
if widgets::icon_button(ui, icon, tip, kind == Some(k), 24.0).clicked() {
|
||||
|
||||
@@ -370,6 +370,35 @@ fn the_aspect_field_keeps_a_mirrored_conic_mirrored() {
|
||||
assert!((geom.aspect + 0.8).abs() < 1e-9 && geom.is_mirrored(), "{geom:?}");
|
||||
}
|
||||
|
||||
/// The Type row's Diamond button (the fourth of five) makes the gradient a diamond, and the
|
||||
/// Aspect Ratio field then edits its aspect.
|
||||
#[test]
|
||||
fn the_diamond_button_makes_a_diamond_whose_aspect_field_edits_it() {
|
||||
let mut app = app(json!({"gradient": {"kind": "linear", "start": [150, 150], "end": [200, 150]}}));
|
||||
let mut p = Panel::new();
|
||||
let texts = p.frame(&mut app, vec![], Modifiers::NONE);
|
||||
let label = texts.iter().find(|(t, _)| t == "Type:").unwrap_or_else(|| panic!("the Type row: {texts:?}")).1;
|
||||
let buttons = p.row_after(label, 24.0);
|
||||
assert_eq!(buttons.len(), 5, "Linear, Radial, Conic, Diamond and Freeform");
|
||||
p.click(&mut app, buttons[3].center());
|
||||
assert_eq!(fill(&app).gradient.kind, GradientKind::Diamond);
|
||||
let texts = p.frame(&mut app, vec![], Modifiers::NONE);
|
||||
// The aspect field is the first 100% (above Opacity's).
|
||||
let field = texts
|
||||
.iter()
|
||||
.filter(|(t, _)| t == "100%")
|
||||
.map(|(_, at)| *at)
|
||||
.min_by(|a, b| a.y.total_cmp(&b.y))
|
||||
.unwrap_or_else(|| panic!("the aspect field: {texts:?}"));
|
||||
p.click(&mut app, field);
|
||||
let key = |k, modifiers| Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
p.frame(&mut app, vec![key(egui::Key::A, Modifiers::COMMAND), Event::Text("80".into())], Modifiers::NONE);
|
||||
p.frame(&mut app, vec![key(egui::Key::Enter, Modifiers::NONE)], Modifiers::NONE);
|
||||
let g = fill(&app);
|
||||
assert_eq!(g.gradient.kind, GradientKind::Diamond);
|
||||
assert!((g.geom.unwrap().aspect - 0.8).abs() < 1e-9, "{:?}", g.geom);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_thumbnail_menu_applies_gradient_swatches_and_saves_one() {
|
||||
let mut app = app(json!({"color": "#336699"}));
|
||||
|
||||
@@ -142,6 +142,7 @@ pub(crate) fn list_icons(ui: &Ui, r: Rect, paint: &Paint, global: bool, spot: bo
|
||||
GradientKind::Linear => "dc-grad-linear",
|
||||
GradientKind::Radial => "dc-grad-radial",
|
||||
GradientKind::Conic => "dc-grad-conic",
|
||||
GradientKind::Diamond => "dc-grad-diamond",
|
||||
GradientKind::Freeform => "dc-grad-freeform",
|
||||
};
|
||||
icons::paint(ui, icon, kind.expand(1.0), t.icon);
|
||||
|
||||
@@ -747,6 +747,18 @@ mod gradient_chip_tests {
|
||||
assert!(at(&mirrored, 2, 4).abs_diff(64) <= 2 && at(&mirrored, 2, 0).abs_diff(191) <= 2, "{mirrored:?}");
|
||||
}
|
||||
|
||||
/// A diamond chip runs from the center out to a square whose edges pass through the chip's
|
||||
/// corners: halfway along an edge and halfway to a corner are the same color.
|
||||
#[test]
|
||||
fn diamond_chip_runs_out_to_the_corners() {
|
||||
let at = |colors: &[Color32], i: usize, j: usize| colors[j * 5 + i].r();
|
||||
let chip = gradient_chip_colors(&paint(GradientKind::Diamond, 0.0), 20.0, 20.0, 4, 4);
|
||||
assert_eq!((at(&chip, 2, 2), at(&chip, 0, 0), at(&chip, 4, 4)), (255, 0, 0), "{chip:?}");
|
||||
for (i, j) in [(2, 0), (0, 2), (1, 1), (3, 3)] {
|
||||
assert!(at(&chip, i, j).abs_diff(128) <= 2, "({i}, {j}): {chip:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_chip_runs_from_the_centre_out() {
|
||||
let colors = gradient_chip_colors(&paint(GradientKind::Radial, 0.0), 20.0, 20.0, 4, 4);
|
||||
|
||||
+33
-9
@@ -462,7 +462,7 @@ so a bad one draws nothing. The object and its styling are one `command.batch`,
|
||||
it can't read, a swatch the document lacks) fails the call and leaves the document as it was. The reply adds `name`, `opacity` and `effects` (each `effect.apply` result)
|
||||
when given; `paint` lists the stroke options applied.
|
||||
A gradient given to several objects fits each one's own bounds; `paint.setFill` / `paint.setStroke` with
|
||||
`span: "selection"` place a linear, radial or conic gradient once on the union of the targets' bounds instead, so one
|
||||
`span: "selection"` place a linear, radial, conic or diamond gradient once on the union of the targets' bounds instead, so one
|
||||
ramp runs across them.
|
||||
`paint.setFill` without `item` on a grayscale or 1-bit image sets the color the image is drawn in: each pixel takes
|
||||
the color as far as the pixel is dark, and white pixels are transparent. `none` removes the color. An image with an
|
||||
@@ -1085,20 +1085,44 @@ another form gives a warning:
|
||||
{"name":"run_command","arguments":{"command":"paint.setFill","params":{"gradient":{"kind":"conic","start":[150,150],"end":[200,150],"aspect":-100}}}}
|
||||
```
|
||||
|
||||
Dither (the Gradient panel's box) draws a linear, radial or conic gradient fill with fine noise that hides banding;
|
||||
SVG and PDF exports write such a fill as an image at the Document Raster Effects resolution. It applies to fills
|
||||
(`stroke: true` is refused):
|
||||
A diamond gradient (`kind: "diamond"`) runs its stops from the center (the start) out to a square with a corner at
|
||||
the end of the vector, a diamond on the page while the vector is level; `aspect` stretches the square across the
|
||||
vector into a rhombus. Fitted to an object, it is centered on the object's box, and the edge of its square runs
|
||||
through the box's farthest corner; at an angle of 0 or 90 degrees the edges run through all four corners. Its
|
||||
annotator draws the square with the aspect dot and center ring of a radial gradient; a `focal` or a negative
|
||||
`aspect` given for a diamond gradient is an error. Dither and Method apply as they do to a linear gradient, and the
|
||||
canvas draws it as pixels, out to the miters, caps and arrowheads of a stroke. PDF, SVG, EPS and legacy `.ai` files
|
||||
have no diamond gradient: one of opaque stops is written as four linear gradients, one for each quarter of the
|
||||
square, clipped to the shape (PDF as axial shadings). In PDF and SVG, one with translucent stops, a dithered fill or
|
||||
a square with no area is an image, and so is a Perceptual one, except in PDF when its stops are spot colors that the
|
||||
Method paragraph below describes. SVG writes one on type characters as a radial gradient through the square's
|
||||
corners. PDF import reads a function-based shading (type 1) whose color is the same along each square about the
|
||||
origin of its space, with corners on its axes, as a diamond gradient. CSS writes a radial gradient through the
|
||||
square's corners. A swatch library saved as CSS gets four `linear-gradient` layers, one for each quarter of the box,
|
||||
which make a value for the `background` property but not for `background-image`; a swatch whose angle is not a
|
||||
multiple of 90 degrees gets a circular `radial-gradient` instead, and, as for a radial swatch, the aspect is not
|
||||
written. The `.ai` editing data holds a radial gradient through the corners with the same stops. Object › Expand
|
||||
turns it into nested squares (`steps`) or a mesh of rings around the center. A document export that writes it in
|
||||
another form gives a warning:
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"paint.setFill","params":{"gradient":{"kind":"diamond","start":[150,150],"end":[200,150],"aspect":60}}}}
|
||||
```
|
||||
|
||||
Dither (the Gradient panel's box) draws a linear, radial, conic or diamond gradient fill with fine noise that hides
|
||||
banding; SVG and PDF exports write such a fill as an image at the Document Raster Effects resolution. It applies to
|
||||
fills (`stroke: true` is refused):
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"paint.editGradient","params":{"dither":true}}}
|
||||
```
|
||||
|
||||
Method (the Gradient panel's menu): `classic` (the default) mixes the colours between stops in display RGB,
|
||||
`perceptual` mixes a linear, radial or conic gradient's colors in OKLab so the steps look even. SVG exports write a
|
||||
Perceptual gradient as an image at the Document Raster Effects resolution (a dithered Perceptual fill with its noise).
|
||||
PDF exports write it the same way, except for a linear, radial or conic gradient with spot color stops whose other stops
|
||||
are all paper white or written in CMYK or gray, that is not a dithered fill and whose curve does not have too many
|
||||
stops. An export that converts colors to a CMYK destination profile writes RGB and Lab stops in CMYK, and so does a
|
||||
`perceptual` mixes a linear, radial, conic or diamond gradient's colors in OKLab so the steps look even. SVG exports
|
||||
write a Perceptual gradient as an image at the Document Raster Effects resolution (a dithered Perceptual fill with
|
||||
its noise). PDF exports write it the same way, except for a linear, radial, conic or diamond gradient with spot color
|
||||
stops whose other stops are all paper white or written in CMYK or gray, that is not a dithered fill and whose curve
|
||||
does not have too many stops (a diamond as four such shadings). An export that converts colors to a CMYK destination profile writes RGB and Lab stops in CMYK, and so does a
|
||||
CMYK document unless the export converts colors to an RGB destination profile. PDF writes tints of one spot ink as a Separation shading in that ink, and spot inks mixed with other spot, CMYK
|
||||
or gray inks as a DeviceN shading.
|
||||
`paint.setFill`/`paint.setStroke` take it in `gradient` too:
|
||||
|
||||
@@ -1311,6 +1311,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles snap and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh.
|
||||
- The Gradient panel's Delete Stop sits at the right end of the ramp, level with it, so a panel too short to show the stop fields still shows it; it deletes the selected stop (never below two), as dragging a stop down off the ramp does (#5403).
|
||||
- Conic gradients, VectorCraft's own gradient type: the stops sweep once around the center from the vector, clockwise on the page (a negative aspect, or a reflection, turns the sweep the other way), with a Conic button in the Gradient panel's Type row and the Gradient tool's extent ellipse and aspect handle; they follow transforms, Expand into wedges or a mesh, open from `.af` files, and export to PDF as a function-based shading that PDF import reads back as a conic gradient; SVG, EPS and legacy `.ai` files get solid wedges, and CSS a `conic-gradient`.
|
||||
- Diamond gradients, VectorCraft's own gradient type: the stops run from the center out to a square with a corner at the end of the vector (the aspect stretches it into a rhombus), with a Diamond button in the Gradient panel's Type row and the Gradient tool's square and aspect handle; they follow transforms, Expand into nested squares or a mesh, and export to PDF, SVG, EPS and legacy `.ai` as four linear gradients, one for each quarter of the square (an image in PDF and SVG for translucent stops, a dithered fill, the Perceptual method other than spot color shadings in PDF, or a square with no area), and on type characters in SVG as a radial gradient; PDF import reads a function-based shading whose color is the same along each square about its origin as a diamond gradient.
|
||||
- The stop popover a double-click on a stop opens (the Gradient tool's annotator or the Gradient panel) has an eyedropper beside its Color and Swatches tabs: it closes the popover and samples the next colour clicked on the art into the stop, as the Gradient panel's eyedropper does (#4681).
|
||||
Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)).
|
||||
With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke.
|
||||
|
||||
Reference in New Issue
Block a user