All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[1.0.0] - 2026-10-21
The first stable release. From here on, breaking changes wait for 2.0. Since 1.0.0-rc.2 there are bug fixes and clearer docs, listed below, and no breaking changes.
Coming from 0.2.x? The changes are listed under 1.0.0-rc.2 and 1.0.0-rc.1 below, and the upgrade guide shows how to update for each breaking change. In short:
- Faster, with no dependencies: Nx is gone, everything runs on plain 64-bit floats, and solving a point is 40–130× faster.
- More correct: Hermite splines pass through their knots, B-splines run in order,
:force_percentworks everywhere, and bad input raises a clearArgumentError. - More curves: polylines, nine kinds of spline, and exact elliptical arcs, ellipses and circles.
- More transforms: separate x and y scales, flips,
reverse/1, and turning around any point witharound:. - SVG in and out:
Curves.Svgdraws any curve or drawing, andCurves.from_svg/2reads an SVG's shapes as curves. - New docs: a four-chapter tutorial with interactive Livebooks, and a guide to choosing a curve.
Added
Curves.Drawing.Grouphas a:transformfield: every SVGtransformthat applied to the group, like a shape's:transform.Curves.Svgdraws the group with it.Curves.Drawinghas a:read_with_flip_yfield: whetherCurves.from_svg/2was givenflip_y: true.
Changed
- The upgrade guide is much shorter: why to upgrade, then one before-and-after example for each breaking change.
- The introduction's first example solves the curve at
t = 0.5, matching its picture. - Catmull-Rom splines are built in time proportional to the number of knots, not its square: 8,000 knots take about a fifth of the time.
Curves.Svgwrites a closed shape's path data without the extra line back to its start beforeZ, so markers aren't drawn twice at the start.Curves.Svgwrites numbers in a transform that are within rounding of a whole number as that number, e.g.0.0instead of6.123233995736766e-17.- Clearer docs:
- The options table in
Curvessays thatsolveandtakeignore:origin, and that unknown options are ignored. Curves.Bezier.Predefinedsays which names share a curve (:easeis:ease_in, for example), and gives CSS's own points; the guide to choosing a curve no longer says the presets are CSS's.Curves.Transformsays thataround: :centeron a drawing counts only its shapes (text can't be measured), that the move made byaround:stays, and whatinc_scale/3does to a flipped curve.Curves.Svgsays that CSS picking shapes by element (likerect { ... }) no longer matches them once drawn, since every shape is a<path>.Curves.from_svg/2says that an empty<g/>is left out, andCurves.Drawing.by_id/1says when it raises.
- The options table in
Fixed
- A drawing drawn with
Curves.Svg:- A group's
clip-path,maskorfilterlines up with its shapes again: the group is drawn with its owntransform, as in the SVG, not with that transform moved onto each shape inside it. - A
<use>of a shape inside a transformed group no longer gets the group's transform as well. - An SVG with a
widthandheightin pixels but noviewBoxshows0..widthby0..height, as SVG does, instead of being zoomed in to its curves (flipped when read withflip_y: true). - An SVG with a
viewBoxbut nowidthorheightis drawn with none, so it fills the space it is shown in, as before, instead of being 1 pixel per unit. - The view box fitted to a drawing without one is exact: it no longer cuts off the tips of arcs, like a rounded rectangle's corners.
- Flat content, like a single horizontal line, gets a view box 1 unit across, not
1.0e-9. - Line breaks and tabs in an attribute are written as
and	, so they read back the same.
- A group's
- Reading SVGs:
- Path data may have a comma between repeated sets of arguments, as SVG allows, e.g.
M0,0,10,10orL1 2,3 4. - A character reference to a character XML doesn't allow, like
�, is kept as written, so a drawing drawn again is still valid XML. - A
<rect>or<ellipse>with a negative, unreadable orautorxorryis read as browsers draw it: that radius is the same as the other one (SVG 2'sauto), instead of the shape being skipped.
- Path data may have a comma between repeated sets of arguments, as SVG allows, e.g.
Curves.Drawing.shapes/1resets a shape's own move, scale and rotation even when they cancel out its groups', so a shape put back into a drawing isn't moved twice.- Solving a spline or polyline at a
uup to0.001pastmax_u(a rounding error, like adding0.1thirty times) gives its end, instead of an out-of-bounds error. It already allowed the same below0. - Clear
ArgumentErrors, as for Bezier curves and polylines:- every spline's
define_*with a bad:origin(was aMatchError), or with points that aren't a list, like a single{x, y}(was aProtocol.UndefinedError); Curves.max_u/1with something that isn't a curve (was aFunctionClauseError);Curves.Drawing.update_shapes/2with a function that returns:delete.
- every spline's
[1.0.0-rc.2] - 2026-10-11
The second release candidate. It has three breaking changes, listed under Changed; sections 10 to 12 of the upgrade guide show how to update.
Added
New kinds of curve
Curves.define_polyline/2makes aCurves.Polyline.Curve: knots joined by straight lines, like SVG's<polyline>. It is solved withulike a spline (each line adds one tou), at a steady speed along each line, and works withtake, the transforms,:force_percentandCurves.Svg.- Five new kinds of spline. Like Catmull-Rom, you only give each one the knots, and it passes through every one of them. Each has its own module under
Curves.Spline.Type, and works withtake, the transforms,:force_percentandCurves.Svg:Curves.define_cardinal/2: Catmull-Rom with a:tensionoption that sets how tightly it bends at each knot (d3'scurveCardinal).Curves.define_centripetal_catmull_rom/2: Catmull-Rom that takes the distances between knots into account, so it never makes loops or cusps. Its:alphaoption (default0.5) also gives plain (0.0) and chordal (1.0) Catmull-Rom.Curves.define_kochanek_bartels/2: a TCB spline, Catmull-Rom with:tension,:continuityand:biasdials, as used for keyframes in animation tools.Curves.define_monotone/2: a monotone cubic spline (Fritsch–Carlson) for chart data. It never overshoots: between two knots it only goes up, down or stays flat. Each knot'sxmust always increase (or always decrease).Curves.define_natural_cubic/2: the smoothest curve through every knot, with a continuous bend everywhere.
- Exact elliptical arcs, which aren't made of Bezier curves:
Curves.define_bezier_spline/2takes[knot, {:arc, rx, ry, rotation, large_arc, sweep}], an arc to the next knot exactly like SVG'sAcommand, with SVG's rules for radii that are too small, zero or negative.Curves.define_ellipse/3andCurves.define_circle/3make an ellipse or circle, or an arc of one with the:fromand:tooptions (in degrees), like a canvas'sarc()andellipse(). It is one segment, sougoes from0.0to1.0, and a whole one ends exactly where it started.- Arcs work with every transform (even different x and y scales),
reverse/1,:force_percentandCurves.Svg. They start and end exactly on their knots. The curve stores extra points that the arc stays inside, whichCurves.Svgdraws as dots.
Transforms
- More transforms in
Curves.Transform:set_scale/3andinc_scale/3take an{x, y}pair to scale the two directions separately, e.g.set_scale(curve, {2, 0.5}).flip_x/2andflip_y/2mirror a curve.- An
around:option on the scale, flip and rotation functions turns the curve around:center(the middle of its points) or any{x, y}point in its own coordinates, which then stays where it is. reverse/1gives the same curve travelled the other way.
Reading and drawing SVGs
Curves.from_svg/2reads an SVG into aCurves.Drawing:- Its shapes (
<path>,<rect>,<circle>,<ellipse>,<line>,<polyline>and<polygon>) become curves, with aCurves.Drawing.Subpathper continuous piece, holding a curve and whether it is closed. Every path command works, with exact arcs. - Every
transform(on shapes and groups, evenskewX,skewYandmatrix) is applied to the curves exactly. - It keeps the SVG's structure (
Curves.Drawing.Group), each shape's styles and transform, and everything that isn't curves (text,<defs>,<style>, ...) as markup (Curves.Drawing.Raw).Curves.Drawing.shapes/1andcurves/1list everything in it. - Anything that can't be read as curves (text, images,
<use>, sizes in%orem, bad path data, ...) is listed in the drawing's:skippedfield with the reason. flip_y: trueturns SVG's downward y around.- It has no XML library and never creates atoms from its input.
<script>,<foreignObject>, animations of links and event handlers, event attributes andjavascript:links are never kept, and kept markup is always well formed. - Every
Curves.Transformfunction also takes a whole drawing, or a group, shape or subpath in it, and changes it as a whole, text included;around: :centeris its middle. The change is kept on the drawing, group or shape (:origin,:scale,:rotation), like a curve's. Curves.Drawing.get/2,fetch/2,put/3,update/3anddelete/2find and change a part by itsid;get_in/2andupdate_in/3follow a path of ids;by_id/1does the same forKernel.get_in/2,update_in/3andpop_in/2;update_shapes/2andupdate_curves/2change every shape or curve.- Each entry in
:skippedsays what was kept of it:kept: :as_markup,:partlyor:no. The reason:not_curves(text, images, ...) is told apart from:unsafe(<script>,<foreignObject>).
- Its shapes (
Curves.from_svg_path/2reads just the path data of one<path>.Curves.Svg.render/2andsave!/3draw aCurves.Drawingas designed: in its own size and view box, with its groups, colours, text and gradients, so it looks like the SVG it was read from.mode: :inspectdraws it like a curve instead.Curves.Svg.path_data/1writes any curve as exact SVG path data:Ccommands for each spline segment (every kind of spline),Afor arcs (even skewed ones), andLfor straight lines.Curves.Svghas a:viewoption,{{x1, y1}, {x2, y2}}, that draws a fixed area instead of fitting the picture to the curve, so several pictures or animation frames share the same scale.Curves.Svghas a:markersoption that highlights extra{x, y}points, e.g.markers: [Curves.solve!(curve, 0.5)]to show wheret = 0.5lands.:marker_colorand:marker_radiusstyle them.Curves.Svghas a:show_axesoption that draws a grid with the x and y values along the bottom and left edges, like a ruler, so you can see a curve's real size and position. The x = 0 and y = 0 lines are darker.:axis_colorsets its colour. It is on by default;show_axes: falsegives the old look.
Docs
- A new tutorial for new users, in four chapters: your first curve, Bezier curves, splines, and transforms and drawing. The hexdocs version is full of pictures drawn with
Curves.Svg, and its simplified first page is now the docs' landing page. Each chapter is also an interactive Livebook with sliders, animations and an easing race. They replace the two old livebooks. - A new guide, Choosing a curve, compares every kind of curve, with real-world examples of where each is used.
Curves.Bezier.Predefined's docs show a picture of every predefined curve.
Changed
- Breaking: Bezier spline points are SVG-style. Each point is a knot, then the control points of the curve to the next knot:
[knot]is a straight line (SVGL),[knot, c]a quadratic curve (Q) and[knot, c0, c1]a cubic curve (C). They used to be the two handles of the knot itself (the control point the curve arrives from, and the one it leaves towards). The last point only ends the curve, so control points there (including on a single knot) raise anArgumentError. Old points without them still work, but draw a different curve. The upgrade guide has a function that converts old points to new ones exactly. - Breaking: Transforms apply in the usual scale, rotate, move order: a curve is scaled and rotated around its own
{0, 0}, then moved to its position. The position used to be scaled too, so scaling a moved curve also moved it (e.g. from{10, 10}to{20, 20}withset_scale(2)), and a rotation then turned it around a point that was no longer on the curve. Only curves that are both moved and scaled give different results. See the upgrade guide. - Breaking:
Curves.Bezier.Predefined.get_coords/1raises anArgumentErrorfor a name that isn't a predefined curve, likedetails/1does. It used to returnnil. Curves.Svgdraws the dashed lines of beziers and bezier splines from each control point to the knot it pulls on, instead of one line through every point. A straight line has none.Curves.Svgimages now include the axes grid by default (see:show_axesabove).- Passing something that isn't a curve to
Curves.solve/3,solve!/3,take/3,take!/3, theCurves.Transformfunctions, orCurves.Svg.render/2andsave!/3now raises anArgumentErrorsuch asexpected a Curves.Bezier.Curve, Curves.Spline.Curve or Curves.Polyline.Curve, got: :nope. It used to raise aCaseClauseError,FunctionClauseError,KeyErrororUndefinedFunctionError, depending on the function and the input. - Other invalid input now raises an
ArgumentErrorthat says what was expected, instead of an error from deep inside the library:- an unknown predefined curve name (
define_bezier(:nope),Predefined.details/1,Predefined.get_coords/1) orPredefined.list/1order. define_bezier/2given something that isn't a list or a name, e.g. a single{x, y}.- any point,
:origin, orCurves.Transformposition that isn't an{x, y}tuple of numbers, in everydefine_*function. - a
t/uthat isn't a number insolve/3andsolve!/3. - a
Curves.Transformamount (inc_x/2,set_scale/2,set_rotation/2, ...) that isn't a number. - an unknown spline type (it was a
RuntimeError).
- an unknown predefined curve name (
Curves.Svg'sshow_points,show_polygonandshow_axestreatnillikefalse.show_points: nilused to crash, andshow_polygon: nildrew the polygon.
Fixed
- Dialyzer no longer warns about breaking the opaqueness of
Curves.Bezier.Curve.t()andCurves.Spline.Curve.t()inside the library. Code outside the two curve modules no longer reads or matches their fields. Curves.Svgno longer misplaces its grid when a curve's points differ only by rounding (e.g. a rotated straight line far from{0, 0}). Such tiny differences were blown up to fill the picture, which lost all precision; they now count as flat.- Bezier
take/3andtake!/3end exactly at the end of the curve. For somen(e.g. 49) the last sample used to be att = 0.9999999999999999, so it was very slightly short of the last point.
[1.0.0-rc.1] - 2026-10-10
The first release candidate for 1.0. Version 0.3.0 was never released; its changes ship here. Coming from 0.2.x? See the upgrade guide.
Added
Curves.max_u/1returns the highestt/ua curve accepts:1.0for beziers, the number of segments for splines.Curves.Svgdraws any Bezier or Spline curve as an SVG image, with no extra dependencies.render/2returns the SVG as a string andsave!/3writes it to a file. Options control the number of samples (default 100), the size, colours, title, and whether the curve's points and the dashed line joining them are drawn.
Changed
- Breaking:
define_hermite/2now takes each point as[knot, tangent](both{x, y}) and builds a true cubic Hermite spline: it passes exactly through every knot and leaves each one along its tangent. It used to take bare knots and compute its own slopes. For automatic tangents, usedefine_catmull_rom/2. - Breaking: The curve structs (
Curves.Bezier.Curve,Curves.Spline.Curve) are now opaque: their fields are internal and may change in any 1.x release. UseCurves.max_u/1instead of readingcurve.max_u. - Breaking: Splines now read the app-wide
:force_percentsetting on everysolve/take, like beziers do. Previously they read it only when defined or transformed. - Breaking: With
:force_percent, an axis where every point has the same value (a flat curve) now returns0.5instead of that raw value, so results are always numbers in0.0..1.0. - Breaking: Curves no longer uses Nx, and has no runtime dependencies. Points are plain
{x, y}float tuples and results are 64-bit floats (0.2.x used 16-bit by default), so values differ slightly in their last digits. The:float_dtypeoption is gone; passing it has no effect. - Breaking: Removed
Curves.Utils.Plotting, which the 0.2.x livebooks used with VegaLite and Tucan. UseCurves.Svg.render/2instead (in Livebook:Kino.Image.new(svg, :svg)). - Breaking:
Curves.Utils.Typesis nowCurves.Types. Its Nx tensor types (point/0,points/0andpoint_names/0) are gone along with Nx; useCurves.Types.point_tuple/0andpoint_list/0. - Breaking: Spline out-of-bounds errors now include the offending
u, matching the bezier error shape.{:error, :out_of_bounds}is now{:error, {:out_of_bounds, u}}. - Everything is much faster now that it runs on plain floats instead of Nx (measured against 0.2.x's code on the same machine): solving a point takes 0.15–0.26 µs instead of 9–34 µs (40–130×),
take!/3of 100 points takes 14–17 µs instead of 2.8–3.5 ms (about 200×), defining a curve is 6–9× faster (Hermite: 9 µs instead of 778 µs), andCurves.Svg.render/2of a spline takes about 65 µs. take/3andtake!/3raise anArgumentErrorwhennisn't a positive integer.n = 0used to crash with anArithmeticError, and a negativenreturned a misleading{:error, {:out_of_bounds, _}}.define_b_spline/2with fewer than 4 points, anddefine_catmull_rom/2ordefine_bezier_spline/2with an empty list, raise anArgumentErrorsaying how many points are needed, instead of anEnum.EmptyErrororFunctionClauseErrorfrom deep inside the library.define_bezier_spline/2raises anArgumentErrorexplaining the expected point format when a point isn't a list of 1 to 3{x, y}tuples (e.g. a plain{x, y}), instead of aFunctionClauseErrorfrom deep inside the library.- Bezier curves with fewer than 2 points can now be defined and transformed. Solving them returns
{:error, {:too_few_points, n, 2}}instead of crashing indefine_bezier/2.
Fixed
Curves.Svg.render/2no longer crashes (0.0 * :infinity) for a curve whose points are all the same; it draws it at the centre of the image.- Hermite splines missed their own knots (e.g.
{10.0017, 5.0033}instead of{10, 5}), moved by the wrong amount when transformed, and could produce huge values near vertical slopes. The slope's axes were swapped, vertical slopes became{1_000_000, 1}, and Bezier-style control points were fed into the Hermite formula. Replaced by a correct cubic Hermite spline (see Changed). - The exceptions (
OutOfBoundT,OutOfBoundU,TooFewPoints,TooManyPoints) now fill in their fields (t,u,max,n,min) instead of leaving themnil, and are documented. - Spline
solve/3andsolve!/3accept integeruvalues instead of raisingFunctionClauseError. - Spline
take!/3raisesCurves.Exceptions.OutOfBoundUinstead of aMatchError. - Spline
take/3no longer fails when float rounding pushes the final sample past the last segment. - Splines solved at a
uwithin 0.0001 of the start now return the right point instead of the start point. A small negativeu(allowed down to -0.001) now extends slightly before the start, instead of being mirrored to just after it. :force_percentpassed tosolve/3ortake/3is now honoured for splines.:force_percentpassed todefine_bezier/2, or set in application config, is now honoured for bezier curves.:force_percentnow rescales using the curve's position after moves, scaling, rotation and the:originoption, so transformed curves return values between0.0and1.0. Previously it used the points as defined, so transformed curves could return values far outside that range. Untransformed curves are unchanged. Rotating a flat curve can make that axis return values from0.0to1.0, because rotation adds tiny rounding errors.- B-spline segments were solved in reverse order, so as
uincreased the curve jumped between segments instead of running smoothly from start to end. They now run in order.solve/3andtake/3return different points for the sameuon B-splines, but the overall shape is unchanged. - Bezier curves with more than 4 points now return
{:error, {:too_many_points, n, 4}}(or raiseCurves.Exceptions.TooManyPoints) instead of returning raw tensors.
[0.2.7] - 2026-09-21
Changed
- Simplified the Livebook plotting. Removed labels and dots. Using Tucan's builtin functions for scaling.
[0.2.6] - 2026-09-21
Added
- Transform functions (position, rotate, scale)
[0.2.5] - 2026-09-19
Added
- Splines! 🥳🥳🥳
This is the first release with splines. There should be no breaking changes, so it is still a patch version. Four Spline types have been defined: Hermite, CatmullRom, BezierSpline, and BSpline. All are using uniform cubic implementations.
Changed
- Documentation has been overhauled quite a lot. Better typespecs. Options are finally documented.
[0.2.4] - 2026-08-29
Fixed
- Predefined curve linear uses Y=1.0 instead of Y=0.0, du
[0.2.3] - 2026-08-18
Added
- Lots of documentation and typespecs
- more helper functions in
Curves.Utils.Predefined
Changed
Exposed some modules to documentation that were previously set to @moduledoc false.
[0.2.2] - 2026-08-17
Changed
- Support forcing curves to use percents
[0.2.1] - 2026-08-17
Added
[0.2.0] - 2026-08-16
A lot of preparatory work before implementing splines. This release tightens up and polishes the API for bezier curves.
BREAKING CHANGES
Curves.define_curve/2 renamed Curves.define_bezier.
originally I was hoping the API could intelligently switch between bezier and spline, but upon closer investigation, it won't work.
For one thing, a curve with 4 points would be ambiguous. Which is it, bezier or spline?
Added
- Predefined bezier curves
Curves.Utils.Plottingwhich simplifies and beautifies the livebook experience. Maybe a user can also get some other use out of it.- More docs, tests, typespecs.
[0.1.0] - 2026-08-09
Added
The initial release.