Skip to content

Commit 7525f2d

Browse files
committed
Implement ParamCurveDeriv for Arc
This solves a small part of linebender#556. Namely: being able to get at the tangents of an `Arc`. Similar to how e.g. `CubicBez::deriv` returns a `QuadraticBez` as a curve or how `Line::deriv` returns `ConstPoint` as a degenerate curve, even though the derivative actually is a tangent mapping, this implementation returns `ArcDeriv` as a curve. Numerically this implementation and the previous implementations are sound, but `ParamCurveDeriv` itself is muddying semantics a bit. We do document that already. `ArcDeriv` is closed under repeated differentation. Implementing `ParamCurveDeriv` gives the ability to use the result as a real object. Like the existing implementations, this makes it possible to find the second, third, etc., derivatives. You can compute the second derivative of an `Arc` as `let arc_deriv2 = arc.deriv().deriv()`, and use it to get at, e.g., the arc's curvature. There's more detail in the discussion of the [PR that triggered this](linebender#556), thinking of cleaner traits/types, and perhaps ways to get at tangents for curves that can't cleanly implement `ParamCurveDeriv`. This current PR intentionally does the simplest thing to make `Arc` differentiable.
1 parent 5610501 commit 7525f2d

1 file changed

Lines changed: 137 additions & 1 deletion

File tree

kurbo/src/arc.rs

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
//! An ellipse arc.
55
6-
use crate::{Affine, Ellipse, ParamCurve, PathEl, Point, Rect, Shape, Vec2};
6+
use crate::{Affine, Ellipse, ParamCurve, ParamCurveDeriv, PathEl, Point, Rect, Shape, Vec2};
77
use core::{
88
f64::consts::{FRAC_PI_2, PI},
99
iter,
@@ -193,6 +193,121 @@ impl ParamCurve for Arc {
193193
}
194194
}
195195

196+
/// The "derivative curve" of an [`Arc`].
197+
///
198+
/// Note that, like the documentation of [`ParamCurveDeriv::deriv`] states, implementations on this
199+
/// type are semantically somewhat inaccurate: the derivative of a curve is not, strictly speaking,
200+
/// a curve itself.
201+
///
202+
/// The difference with [`Arc`] is that this object has no `center`: that's a constant that
203+
/// differentiates to zero.
204+
pub struct ArcDeriv {
205+
/// The arc's radii, where the vector's x-component is the radius in the
206+
/// positive x direction after applying `x_rotation`.
207+
pub radii: Vec2,
208+
/// The start angle in radians.
209+
pub start_angle: f64,
210+
/// The angle between the start and end of the arc, in radians.
211+
pub sweep_angle: f64,
212+
/// How much the arc is rotated, in radians.
213+
pub x_rotation: f64,
214+
}
215+
216+
impl ArcDeriv {
217+
#[inline(always)]
218+
const fn as_arc_with_center(&self, center: Point) -> Arc {
219+
Arc {
220+
center,
221+
radii: self.radii,
222+
start_angle: self.start_angle,
223+
sweep_angle: self.sweep_angle,
224+
x_rotation: self.x_rotation,
225+
}
226+
}
227+
}
228+
229+
impl ParamCurveDeriv for Arc {
230+
type DerivResult = ArcDeriv;
231+
232+
fn deriv(&self) -> ArcDeriv {
233+
// The center is constant over `t` and differentiates away, and as the arc's derivative is
234+
// represented by another arc (but without center), we can just delegate to `ArcDeriv`
235+
// directly.
236+
ArcDeriv {
237+
radii: self.radii,
238+
start_angle: self.start_angle,
239+
sweep_angle: self.sweep_angle,
240+
x_rotation: self.x_rotation,
241+
}
242+
.deriv()
243+
}
244+
}
245+
246+
impl ParamCurveDeriv for ArcDeriv {
247+
type DerivResult = ArcDeriv;
248+
249+
fn deriv(&self) -> ArcDeriv {
250+
// Given the axis-aligned parametric elliptical arc
251+
//
252+
// ```
253+
// p(a) = center + (radii.x * cos(a), radii.y * sin(a))
254+
// ```
255+
//
256+
// we have tangents
257+
//
258+
// ```
259+
// p'(a) = (-radii.x sin(a), radii.y * cos(a)).
260+
// ```
261+
//
262+
// Our parameterization is over `t` with `a = angle_at(t) = start_angle + sweep_angle * t`.
263+
// Hence,
264+
//
265+
// ```
266+
// p'(t) = sweep_angle * (-radii.x sin(angle_at(t)), radii.y * cos(angle_at(t)))
267+
// ```
268+
//
269+
// or equivalently
270+
//
271+
// ```
272+
// p'(t) = sweep_angle * (radii.x cos(angle_at(t) + pi/2), radii.y * sin(angle_at(t) + pi/2)).
273+
// ```
274+
//
275+
// Instead of being axis-aligned, our ellipse is rotated by the 2x2 rotation matrix
276+
// `R(x_rotation)`. This has the effect of rotating the tangents by the same amount. Hence,
277+
// the derivative is given by
278+
//
279+
// ```
280+
// sweep_angle * R(x_rotation) * (radii.x cos(angle_at(t) + pi/2), radii.y * sin(angle_at(t) + pi/2)).
281+
// ```
282+
//
283+
// This is exactly another `Arc`, but with zero center, precisely what `ArcDeriv` models.
284+
// As this has the same form again, `ArcDeriv` is closed under repeated differentiation.
285+
ArcDeriv {
286+
radii: self.radii * self.sweep_angle,
287+
start_angle: self.start_angle + FRAC_PI_2,
288+
sweep_angle: self.sweep_angle,
289+
x_rotation: self.x_rotation,
290+
}
291+
}
292+
}
293+
294+
impl ParamCurve for ArcDeriv {
295+
fn eval(&self, t: f64) -> Point {
296+
self.as_arc_with_center(Point::ZERO).eval(t)
297+
}
298+
299+
fn subsegment(&self, range: Range<f64>) -> Self {
300+
let arc = self.as_arc_with_center(Point::ZERO).subsegment(range);
301+
302+
Self {
303+
radii: arc.radii,
304+
start_angle: arc.start_angle,
305+
sweep_angle: arc.sweep_angle,
306+
x_rotation: arc.x_rotation,
307+
}
308+
}
309+
}
310+
196311
impl Shape for Arc {
197312
type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
198313

@@ -338,4 +453,25 @@ mod tests {
338453
let arc = Arc::new((0.0, 0.0), (2.0, 1.0), 1.0, 1e-9, FRAC_PI_4);
339454
assert_subsegment_matches(arc, 0.25..0.75);
340455
}
456+
457+
#[test]
458+
fn arc_first_and_second_derivatives() {
459+
let arc = Arc::new((1.0, -2.0), (2.0, 1.0), 0.0, PI, FRAC_PI_2);
460+
let d1 = arc.deriv();
461+
let d2 = d1.deriv();
462+
463+
// Unrotated: (2 cos(pi t), sin(pi t))
464+
// Rotated by pi/2: (-sin(pi t), 2 cos(pi t))
465+
// So:
466+
// p'(t) = (-pi cos(pi t), -2 pi sin(pi t))
467+
// p''(t) = (pi^2 sin(pi t), -2 pi^2 cos(pi t))
468+
469+
assert_point_near(d1.eval(0.0), Point::new(-PI, 0.0));
470+
assert_point_near(d1.eval(0.5), Point::new(0.0, -2.0 * PI));
471+
assert_point_near(d1.eval(1.0), Point::new(PI, 0.0));
472+
473+
assert_point_near(d2.eval(0.0), Point::new(0.0, -2.0 * PI * PI));
474+
assert_point_near(d2.eval(0.5), Point::new(PI * PI, 0.0));
475+
assert_point_near(d2.eval(1.0), Point::new(0.0, 2.0 * PI * PI));
476+
}
341477
}

0 commit comments

Comments
 (0)