Curves.Svg (curves v1.0.1)

Copy Markdown View Source

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

keydefaultdescription
:samples100How many points to solve along the curve, not counting the start. More gives a smoother line.
:width600Image width in pixels.
:height600Image height in pixels.
:padding40Empty space around the curve, in pixels.
:background"#ffffff"Background colour. nil leaves it transparent.
:curve_color"#1c7ed6"Colour of the curve.
:curve_width3Thickness of the curve.
:show_pointstrueDraw the curve's own points as dots.
:point_color"#d9480f"Colour of the dots.
:point_radius5Size of the dots.
:show_polygontrueDraw 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.
:viewnilThe 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_radius7Size of the markers.
:titlenilText drawn in the top-left corner.
:show_axestrueDraw 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

path_data(curve)

@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"

render(curve_or_drawing, opts \\ [])

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")
true

A 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

save!(curve, path, opts \\ [])

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