{:curves, "~> 1.0"}Why upgrade
- 40–200× faster, with no dependencies: Nx is gone, and results are 64-bit floats.
- Correct: Hermite splines pass through their knots, B-splines run in order,
:force_percentworks everywhere, and bad input raises a clearArgumentError. - Stable: after 1.0.0, breaking changes wait for 2.0.
- More: polylines, nine kinds of spline, exact arcs and ellipses, more transforms, drawing curves as SVG and reading them from SVG, and a tutorial with interactive Livebooks.
See the changelog for everything.
Breaking changes
1. No more Nx
Results are plain 64-bit floats. The :float_dtype option is gone.
# 0.2.x
Curves.define_bezier(:linear, float_dtype: 64)
# 1.0
Curves.define_bezier(:linear)2. Curves.Utils.Plotting is replaced by Curves.Svg
# 0.2.x
curve
|> Curves.Utils.Plotting.curve_to_scatterplot(count: 300)
|> Tucan.scatter("x", "y")
# 1.0
Curves.Svg.save!(curve, "curve.svg")In Livebook: curve |> Curves.Svg.render() |> Kino.Image.new(:svg).
3. Spline out-of-bounds errors include u
# 0.2.x
{:error, :out_of_bounds} = Curves.solve(spline, 99.0)
# 1.0
{:error, {:out_of_bounds, 99.0}} = Curves.solve(spline, 99.0)4. Curve fields are private
# 0.2.x
spline.max_u
# 1.0
Curves.max_u(spline)5. Curves.Utils.Types is now Curves.Types
# 0.2.x
@spec move(Curves.Utils.Types.point_tuple()) :: Curves.Utils.Types.point_tuple()
# 1.0
@spec move(Curves.Types.point_tuple()) :: Curves.Types.point_tuple()The tensor types point/0, points/0 and point_names/0 are gone: use point_tuple/0 and point_list/0.
6. :force_percent changes
It works for every curve, whether passed to define_*, solve or take, or set in config. It measures
the curve after its transforms, so results stay between 0.0 and 1.0, and a flat axis gives 0.5.
curve = Curves.define_bezier(:linear_horizontal)
Curves.solve!(curve, 0.5, force_percent: true)
# 0.2.x: {0.5, 1.0}
# 1.0: {0.5, 0.5}7. B-splines run in order
0.2.x solved B-spline segments backwards, so the curve jumped about as u grew. The shape is the same,
but solve gives different points for the same u.
8. Beziers need 2 to 4 points
curve = Curves.define_bezier([{0, 0}, {1, 1}, {2, 0}, {3, 1}, {4, 0}])
Curves.solve(curve, 0.5)
# 0.2.x: raw tensors
# 1.0: {:error, {:too_many_points, 5, 4}}For longer paths, use a spline.
9. Hermite splines take tangents
Each point is [knot, tangent], and the curve passes through every knot.
# 0.2.x
Curves.define_hermite([{0, 0}, {2, 2}, {4, 0}])
# 1.0
Curves.define_hermite([[{0, 0}, {4, 0}], [{2, 2}, {0, 4}], [{4, 0}, {4, 0}]])For automatic tangents, use Curves.define_catmull_rom/2.
10. Bezier spline points are SVG-style
Each point is a knot, then the control points of the curve to the next knot, like an SVG path:
[knot] is a line (L), [knot, c] a quadratic (Q), [knot, c0, c1] a cubic (C). The last point is
just [knot].
# 0.2.x: each knot with the handles it arrives from and leaves towards
Curves.define_bezier_spline([
[{5, 10}, {10, 10}],
[{10, 5}, {5, 5}, {15, 5}],
[{30, 10}, {32, 5}]
])
# 1.0: the same curve
Curves.define_bezier_spline([
[{5, 10}, {10, 10}, {5, 5}],
[{10, 5}, {15, 5}, {32, 5}],
[{30, 10}]
])To convert old points:
def to_svg_style(points) do
points
|> Enum.chunk_every(2, 1)
|> Enum.map(fn
[[knot | _] = point, next] -> [knot, leaving(point), arriving(next)]
[[knot | _]] -> [knot]
end)
end
defp leaving([knot]), do: knot
defp leaving([_knot, c]), do: c
defp leaving([_knot, _arrive, leave]), do: leave
defp arriving([knot]), do: knot
defp arriving([_knot, c]), do: c
defp arriving([_knot, arrive, _leave]), do: arrive11. Scaling no longer moves a curve
Curves are scaled and rotated around their own {0, 0}, then moved to their position.
curve =
Curves.define_bezier([{0, 0}, {4, 0}])
|> Curves.Transform.set_position({10, 10})
|> Curves.Transform.set_scale(2)
Curves.solve!(curve, 0.0)
# 0.2.x: {20.0, 20.0}
# 1.0: {10.0, 10.0}Only curves that are both moved and scaled change.
12. Predefined.get_coords/1 raises for an unknown name
Curves.Bezier.Predefined.get_coords(:nope)
# 0.2.x: nil
# 1.0: raises ArgumentError