M3.98: radial annotator extent ellipse, aspect handle and off-centre focal point

Radial gradients show a dashed extent ellipse: its handle across the bar
sets the aspect ratio, the ellipse elsewhere rotates it, and the dot inside
the centre ring drags the focal point (GradientGeom.focal) for an off-centre
radial. paint.setGradientGeom takes aspect (%) and focal ([x,y] | null), and
start/end may be left out; the focal point keeps its place in the ellipse as
the vector, aspect and object transform. It renders as a two-point radial,
exports and imports as SVG fx/fy and PDF two-point radial shadings, expands
to off-centre rings, and travels in gradient params and the native format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 04:43:43 +07:00
co-authored by Claude Opus 5.5
parent b8793eeaef
commit d8eff17b9f
26 changed files with 701 additions and 106 deletions
+10 -11
View File
@@ -1,7 +1,7 @@
//! Paint conversion (solid, gradients, patterns) and image decoding.
use vectorcraft_color::{GradientKind, Paint};
use vectorcraft_geom::{Affine, Rect, Vec2};
use vectorcraft_geom::{Rect, Vec2};
use vello_cpu::RenderContext;
use vello_cpu::peniko::{self, ColorStop};
@@ -26,16 +26,15 @@ pub(crate) fn set_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Fr
let grad = match g.gradient.kind {
GradientKind::Radial => {
let r = geom.length().max(1e-6) as f32;
let angle = (geom.end - geom.start).atan2();
// Aspect ratio squashes the circle along the perpendicular of the gradient vector.
ctx.set_paint_transform(
Affine::translate(geom.start.to_vec2())
* Affine::rotate(angle)
* Affine::scale_non_uniform(1.0, geom.aspect.max(1e-3))
* Affine::rotate(-angle)
* Affine::translate(-geom.start.to_vec2()),
);
peniko::Gradient::new_radial(geom.start, r).with_stops(stops.as_slice())
// The aspect ratio squashes the circle across the gradient vector.
let squash = geom.radial_squash();
ctx.set_paint_transform(squash);
let g = match geom.focal {
// An off-centre focal point: a two-point radial from it to the extent circle.
Some(f) => peniko::Gradient::new_two_point_radial(squash.inverse() * f, 0.0, geom.start, r),
None => peniko::Gradient::new_radial(geom.start, r),
};
g.with_stops(stops.as_slice())
}
_ => {
ctx.reset_paint_transform();