# `Curves.Svg`
[🔗](https://github.com/greetingsfellowhumans/curves/blob/main/lib/curves/svg.ex#L1)

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.

```elixir
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.

```elixir
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:

```elixir
curve = Curves.define_bezier(:ease_in_out) |> Curves.Transform.set_scale(10)
Curves.Svg.render(curve)
```

# `path_data`

```elixir
@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`

```elixir
@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")
    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!`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
