Draws a curve (a Bezier, a spline or a polyline) as an SVG image, using only Elixir.
The curve is solved at :samples evenly spaced points from start to end, and those points are joined with a line.
The curve's own points are drawn as dots, and the image is scaled to fit while keeping the curve's shape.
curve = Curves.define_b_spline([{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 2}, {1, 2}])
Curves.Svg.save!(curve, "b_spline.svg", title: "B-Spline")path_data/1 gives a curve as exact SVG path data instead, for your own <path> elements.
Drawings
A Curves.Drawing (from Curves.from_svg/2) is drawn as designed by default (mode: :design):
in its own size and view box, with its groups, each shape's own styles (fill, stroke, class,
...) and its kept markup (text, <defs>, ...), so it looks like the SVG it was read from. Each
shape is one <path>, its curves written exactly with path_data/1, and its transform kept on
it so strokes are as wide as they were. Each group keeps its own transform too, so a
clip-path, mask or filter on it still lines up with what is inside it.
Since every shape becomes a <path>, CSS that picks shapes by their element, like
rect { fill: red } in a <style>, no longer matches them. Classes and ids still work.
drawing = Curves.from_svg(File.read!("logo.svg"))
Curves.Svg.save!(drawing, "logo_again.svg")As designed, the options that apply are :width, :height and :view (to frame it differently),
:background (none by default), :title, :markers, and :show_points and :show_polygon
(off by default) with their colours and sizes, which are in pixels as usual. The rest are for
curves.
mode: :inspect draws a drawing like a curve instead: every curve in the curve colour, fitted
together, with y going up, dots, dashed lines and the grid. Pass flip_y: true to
Curves.from_svg/2 to have it the right way up then.
Options
key | default | description |
|---|---|---|
:samples | 100 | How many points to solve along the curve, not counting the start. More gives a smoother line. |
:width | 600 | Image width in pixels. |
:height | 600 | Image height in pixels. |
:padding | 40 | Empty space around the curve, in pixels. |
:background | "#ffffff" | Background colour. nil leaves it transparent. |
:curve_color | "#1c7ed6" | Colour of the curve. |
:curve_width | 3 | Thickness of the curve. |
:show_points | true | Draw the curve's own points as dots. |
:point_color | "#d9480f" | Colour of the dots. |
:point_radius | 5 | Size of the dots. |
:show_polygon | true | Draw dashed lines showing how the curve's points shape it. For beziers and bezier splines they go from each control point to the knot it pulls on; for other splines one line joins the points in order. |
:polygon_color | "#adb5bd" | Colour of that dashed line. |
:view | nil | The area to draw, as two opposite corners: {{x1, y1}, {x2, y2}}. nil fits the picture to the curve. A fixed view keeps several pictures (or the frames of an animation) at the same scale, so you can see a curve move and grow. Anything outside it is cut off. |
:markers | [] | Extra {x, y} points to highlight, e.g. [Curves.solve!(curve, 0.5)] to show where t = 0.5 lands. They are in the same coordinates Curves.solve/3 returns. |
:marker_color | "#7048e8" | Colour of the markers. |
:marker_radius | 7 | Size of the markers. |
:title | nil | Text drawn in the top-left corner. |
:show_axes | true | Draw a grid with the x and y values along the bottom and left edges, like a ruler. The lines at x = 0 and y = 0 are darker. The image is always scaled to fit, so without the grid a curve looks the same at any size. The numbers sit in the padding, so a small :padding hides them. false turns the grid off. |
:axis_color | "#868e96" | Colour of the grid lines and their numbers. |
:solve_opts | [] | Options passed to Curves.take!/3 and Curves.solve!/3. |
:mode | :inspect (curves), :design (drawings) | How to draw a Curves.Drawing: as designed, or like a curve. See "Drawings" above. A curve is always drawn like this, in :inspect mode. |
The show_* options treat nil like false.
The dots are the points the curve stores. For most curves these are the points you defined it with,
but B-splines store the control points they generate from yours, and ellipses and arcs store the
corners of the shape they stay inside (see Curves.Spline.Type.Ellipse).
If :solve_opts turns on :force_percent, the curve is drawn in 0..1 but its points are not,
so set show_points: false and show_polygon: false.
For example, the grid shows that a scaled curve is bigger, even though the image fits it to the same size:
curve = Curves.define_bezier(:ease_in_out) |> Curves.Transform.set_scale(10)
Curves.Svg.render(curve)
Summary
Functions
A curve as SVG path data, the d of a <path> element: exact, not sampled. A Bezier curve is one
L, Q or C command; a spline is a C command for each segment (whatever kind of spline it
is, each segment is exactly a cubic Bezier curve) and A commands for its arcs; a polyline is L
commands. Every transform is included. Numbers are written as briefly as they can be while
reading back as exactly the same float.
Builds the SVG for a curve, or for every curve in a Curves.Drawing, and returns it as a string.
Builds the SVG for a curve (or a Curves.Drawing) with render/2 and writes it to path.
Returns path, and raises if the file can't be written.
Functions
@spec path_data(Curves.Types.curve_struct()) :: String.t()
A curve as SVG path data, the d of a <path> element: exact, not sampled. A Bezier curve is one
L, Q or C command; a spline is a C command for each segment (whatever kind of spline it
is, each segment is exactly a cubic Bezier curve) and A commands for its arcs; a polyline is L
commands. Every transform is included. Numbers are written as briefly as they can be while
reading back as exactly the same float.
It doesn't include :force_percent, which changes the numbers solve returns, not the curve.
Examples
iex> Curves.Svg.path_data(Curves.define_bezier([{0, 0}, {1, 2}, {3, 2}, {4, 0}]))
"M0.0,0.0 C1.0,2.0 3.0,2.0 4.0,0.0"
iex> Curves.Svg.path_data(Curves.define_polyline([{0, 0}, {2, 1}]) |> Curves.Transform.set_scale(2))
"M0.0,0.0 L4.0,2.0"
iex> Curves.Svg.path_data(Curves.define_circle({0, 0}, 1, to: 90))
"M1.0,0.0 A1.0 1.0 0.0 0 1 6.123233995736766e-17,1.0"
@spec render( Curves.Types.curve_struct() | Curves.Drawing.t(), keyword() ) :: String.t()
Builds the SVG for a curve, or for every curve in a Curves.Drawing, and returns it as a string.
See the module docs for opts, including :samples for how smooth the line is.
Examples
iex> curve = Curves.define_bezier(:linear)
iex> svg = Curves.Svg.render(curve, width: 100, height: 100)
iex> String.starts_with?(svg, "<svg")
true
iex> svg =~ ~s(<circle cx="40.00" cy="60.00")
trueA drawing is drawn as designed: each shape is a <path>, with its own style.
iex> drawing = Curves.from_svg(~s(<svg><circle r="2" fill="red"/><path d="M0 0 L4 4 M0 4 L4 0"/></svg>))
iex> svg = Curves.Svg.render(drawing)
iex> svg =~ ~s(<path d="M0.0,0.0 L4.0,4.0 M0.0,4.0 L4.0,0.0"/>)
true
iex> svg =~ ~s( fill="red"/>)
true
@spec save!(Curves.Types.curve_struct() | Curves.Drawing.t(), Path.t(), keyword()) :: Path.t()
Builds the SVG for a curve (or a Curves.Drawing) with render/2 and writes it to path.
Returns path, and raises if the file can't be written.
Examples
iex> path = Path.join(System.tmp_dir!(), "curves_svg_doctest.svg")
iex> curve = Curves.define_bezier(:ease_in_out_cubic)
iex> Curves.Svg.save!(curve, path, samples: 50)
iex> File.read!(path) |> String.starts_with?("<svg")
true