Curves
Introduction
Curves aims to be the best elixir framework for calculating bezier curves and splines.
curve = Curves.define_bezier(:ease_out)
|> Curves.Transform.set_scale(1.5)
|> Curves.Transform.inc_position({10, 10})
|> Curves.Transform.set_rotation(90)
# x and y are floats
{x, y} = Curves.solve!(curve, 0.25)
Installation
Add curves to your dependencies in mix.exs:
def deps do
[
{:curves, "~> 1.0"}
]
end
Directory
- Tutorial, with lots of pictures
- Choosing a curve: every kind of curve compared, with real-world examples
- Interactive tutorial in Livebook (sliders, animations and races):
- Upgrading from 0.2.x to 1.0
- Hex docs
- Hex package
- Github Repo
Hire me
Full stack developer seeking new challenges. You can reach me at hireme@aaronjprice.com
Visual Demo
Ease in-out Bezier
The simplest way to use Curves is with the predefined ones like :ease_in_out
curve = Curves.define_bezier(:ease_in_out)
# Find any point along the curve (t goes from 0.0 to 1.0)...
Curves.solve!(curve, 0.25)
# ...or draw the whole curve as an SVG image
Curves.Svg.save!(curve, "ease_in_out.svg")
Quadratic Bezier
Here we make an asymmetric quadratic curve
points = [
{0, 0},
{20, 50},
{100, 0}
]
curve = Curves.define_bezier(points)
Custom 4-point Bezier
points = [
{0, 0},
{0.8, 0.2},
{0.3, 2.2},
{1, 1}
]
curve = Curves.define_bezier(points)
Polyline
The simplest path: straight lines from knot to knot, like SVG's <polyline>.
curve = Curves.define_polyline([{0, 0}, {2, 3}, {4, 1}, {6, 4}, {8, 2}])
Spline examples
The simplest spline is a chain of bezier curves, each one starting where the last one ended.
Notice we are no longer using define_bezier, but define_bezier_spline. Each point is a list: a knot, then the control points of the curve from that knot to the next one,
just like the commands in an SVG path (or bezierCurveTo on a canvas):
| Point | Curve to the next knot | SVG |
|---|---|---|
[knot] |
a straight line | L |
[knot, c] |
a quadratic curve pulled towards c |
Q |
[knot, c0, c1] |
a cubic curve: leaves knot towards c0, arrives at the next knot from c1 |
C |
[knot, {:arc, rx, ry, rotation, large_arc, sweep}] |
an elliptical arc, exactly like SVG's A |
A |
The last point only ends the curve, so it must be [knot].
Bezier Spline
points = [
# Each point: a knot, then the control points of the curve to the next knot.
[{5.0, 10.0}, {10.0, 10.0}, {5.0, 5.0}], # cubic to {10, 5} (SVG C)
[{10.0, 5.0}, {15.0, 5.0}, {15.0, 6.0}], # cubic to {15, 10}
[{15.0, 10.0}, {15.0, 14.0}, {18.0, 15.0}], # cubic to {20, 15}
[{20.0, 15.0}, {23.0, 15.0}, {32.0, 5.0}], # cubic to {30, 10}
[{30.0, 10.0}] # the last knot only ends the curve
]
curve = Curves.define_bezier_spline(points)
Bezier Spline with an Arc
SVG's M 0 0 L 10 0 A 5 5 0 0 1 10 10 L 0 10: a line, a half circle and another line. Arcs are exact, not made of Bezier curves.
curve = Curves.define_bezier_spline([
[{0, 0}],
[{10, 0}, {:arc, 5, 5, 0, false, true}],
[{10, 10}],
[{0, 10}]
])
Ellipse
An exact ellipse or circle, or an arc of one (with the :from and :to options, in degrees). It is one segment, so u goes from 0.0 to 1.0, once round.
curve = Curves.define_ellipse({0, 0}, {4, 2}, rotation: 30)
circle = Curves.define_circle({0, 0}, 2, from: 90, to: 180)
Cubic Hermite Spline
A smooth curve through every knot. At each knot you give a tangent: the direction (and speed) the curve leaves it with.
curve = Curves.define_hermite([
# [knot, tangent]
[{5, 10}, {5, -5}],
[{10, 5}, {5, 0}],
[{15, 10}, {5, 5}],
[{20, 10}, {5, -3}],
[{25, 5}, {5, 5}],
[{30, 15}, {5, 10}]
])
Catmull-Rom Spline
Similar to the Hermite spline, but now the derivative comes from the slope between the previous and next point.
curve = Curves.define_catmull_rom([
{0, 0},
{1, 0},
{1, 1},
{0, 1},
{0, 2},
{1, 2},
])
B-Spline
The smoothest of the splines featured here, but with the drawback that the line does not actually pass through the control points.
curve = Curves.define_b_spline([
{0, -2},
{3, 4},
{6, 5},
{7, -1},
{10, 3},
{5, 1},
{12, 9}
])
Cardinal Spline
A Catmull-Rom spline with a :tension option: 0.0 is Catmull-Rom, and higher values bend more tightly at each knot.
curve = Curves.define_cardinal([{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 2}, {1, 2}], tension: 0.5)
Centripetal Catmull-Rom Spline
A Catmull-Rom spline that takes the distances between the knots into account, so it never makes loops or sharp points, even when some knots are bunched up.
curve = Curves.define_centripetal_catmull_rom([{0, 0}, {10, 10}, {10.5, 10}, {20, 0}])
Kochanek-Bartels Spline
Also called a TCB spline: a Catmull-Rom spline with :tension, :continuity and :bias dials, as used for keyframes in animation tools.
curve = Curves.define_kochanek_bartels(
[{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 2}, {1, 2}],
tension: 0.2,
continuity: -0.3,
bias: 0.4
)
Monotone Spline
A smooth line through data points that never overshoots them, for charts. Each knot's x must be bigger than the one before.
curve = Curves.define_monotone([{0, 2}, {1, 2}, {2, 2}, {3, 8}, {4, 8.5}, {5, 3}, {6, 3}])
Natural Cubic Spline
The smoothest curve that still passes through every knot.
curve = Curves.define_natural_cubic([
{0, -2},
{3, 4},
{6, 5},
{7, -1},
{10, 3},
{5, 1},
{12, 9}
])
Reading SVGs
Curves.from_svg/2 reads an SVG into a Curves.Drawing: every shape in it (paths, rectangles, circles, ellipses, lines, polylines and polygons), each made of curves you can solve, transform and draw. Paths keep their exact arcs, and every transform is applied, even skews. The drawing also keeps the SVG's groups, styles and everything that isn't curves (like text), so Curves.Svg can draw it again as it was. Curves.Svg.path_data/1 writes any curve as exact SVG path data.
drawing = Curves.from_svg(File.read!("logo.svg"))
for shape <- Curves.Drawing.shapes(drawing), subpath <- shape.subpaths do
Curves.solve!(subpath.curve, 0.5)
end
# Draw it again, as designed: its colours, groups and text are kept
Curves.Svg.save!(drawing, "logo_again.svg")
# Or just the path data of one <path>
[heart] = Curves.from_svg_path("M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z")
It needs no XML library and never creates atoms from the input, so it is safe to use on SVGs from anywhere.
Benchmarks
Benchmarks live in bench/ and use Benchee. Run them all with:
mix bench
To check a change for slowdowns, save a run before the change, then run again afterwards. A run without a tag is compared against every saved run.
mix bench baseline # save this run under the tag "baseline"
mix run bench/solve.exs # later: run one script and compare it to the saved runs
Saved runs go in bench/results/, which is not committed. Only compare runs from the same machine.