Upgrading from 0.2.x to 1.0

Copy Markdown View Source
{: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_percent works everywhere, and bad input raises a clear ArgumentError.
  • 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: arrive

11. 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