A picture read from an SVG with Curves.from_svg/2: its shapes as curves, in the same groups and
order, with everything else kept as markup, so Curves.Svg can draw it again as it was.
drawing = Curves.from_svg(File.read!("logo.svg"))
# Every shape, wherever it is in the picture
for shape <- Curves.Drawing.shapes(drawing), subpath <- shape.subpaths do
Curves.solve!(subpath.curve, 0.5)
end
# Draw it again, as designed
Curves.Svg.save!(drawing, "logo_again.svg")Changing it
The Curves.Transform functions move, scale and rotate a whole drawing, or a group or shape in
it, just like a curve. To change one part, find it by its id: update/3 changes it with a
function, get/2 and put/3 take it out and put it back, and update_in/3 and get_in/2 follow
a path of ids, one inside the next. by_id/1 does the same for Elixir's own Kernel.get_in/2,
Kernel.update_in/3 and Kernel.pop_in/2. update_shapes/2 and update_curves/2 change every
shape or every curve.
drawing
|> Curves.Transform.inc_position({10, 0})
|> Curves.Drawing.update_in(["car", "front_wheel"], &Curves.Transform.inc_rotation(&1, 90, around: :center))
|> Curves.Drawing.update_shapes(&put_in(&1.attributes["stroke"], "red"))| Field | What it holds |
|---|---|
:width, :height | the <svg> element's width and height, as written (they may have units), or nil |
:view_box | its viewBox as {min_x, min_y, width, height}, or nil |
:read_with_flip_y | whether Curves.from_svg/2 was given flip_y: true. Curves.Svg uses it to flip the view box of an SVG that has a size but no viewBox. It says how the SVG was read, so Curves.Transform.flip_y/2 doesn't change it. |
:attributes | all of the <svg> element's own attributes, as strings |
:children | what is in it, in order: a Curves.Drawing.Shape for each shape, a Curves.Drawing.Group for each group, and a Curves.Drawing.Raw for everything else that is kept as markup, like text |
:skipped | everything that couldn't be read as curves, and why, so nothing goes missing silently |
:origin, :scale, :rotation | how Curves.Transform has moved, scaled and rotated the whole drawing. They start at {0.0, 0.0}, 1.0 and 0.0. |
Each entry in :skipped is a map saying what the element was, why it wasn't read as curves
(:reason), and what was kept of it (:kept):
%{element: "text", id: "title", reason: :not_curves, kept: :as_markup}
%{element: "script", id: nil, reason: :unsafe, kept: :no}:kept | Meaning |
|---|---|
:as_markup | kept as a Curves.Drawing.Raw, so it is drawn again just as it was, but has no curves |
:partly | some of it is kept as curves (path data with an error: the subpaths before the error) |
:no | nothing is kept: it won't be drawn again (though for :empty and :invalid, SVG doesn't draw it either) |
So everything that won't appear when the drawing is drawn again is
Enum.filter(drawing.skipped, &(&1.kept == :no)).
:reason | Meaning | :kept |
|---|---|---|
:not_curves | an element that isn't made of curves (<text>, <image>, <use>), or a nested <svg>, which Curves can't follow | :as_markup |
:unsafe | <script> or <foreignObject>, which are never kept, so a drawing drawn again can't run anything | :no |
{:unsupported_unit, value} | a size in %, em, mm and so on, which needs a screen to measure; only plain numbers and px are read | :as_markup |
{:invalid_transform, message} | a transform that couldn't be read; the element (for a group, everything in it) isn't read as curves | :as_markup (:no for the <svg> itself) |
{:invalid_path, message} | path data with an error in it; like a browser, the shape still has the subpaths before the error | :partly |
{:invalid, message} | a shape SVG treats as an error, like a negative radius, or (:partly) a polyline with an odd number of coordinates | :no or :partly |
:empty | a shape SVG doesn't draw, like a circle with radius 0 | :no |
Summary
Types
Something inside a drawing or a group.
A drawing, or a group in one: something with :children.
A transform, {a, b, c, d, e, f}, like SVG's matrix(a, b, c, d, e, f).
Something that can be found by its id.
Something that couldn't be read as curves. See the tables above.
Functions
An Access function for the shape or group with this id, to use with Elixir's own
Kernel.get_in/2, Kernel.update_in/3 and Kernel.pop_in/2 on a drawing.
Every curve in the drawing (or group), shape by shape and subpath by subpath, in order, where it
is drawn (see shapes/1).
Takes the shape or group with this id out of the drawing. Raises an ArgumentError if there
is none.
The shape or group with this id, anywhere in the drawing (or group), as {:ok, node}, or
:error if there is none. The first one wins if several share an id.
The shape or group with this id, or nil. See fetch/2.
Like get/2, following a path of ids: each id is looked for inside the one before it. Returns
nil if any of them isn't there.
Puts node (a shape, group or raw markup) in place of the shape or group with this id. Raises
an ArgumentError if there is none. Handy to take a part out, change it, and put it back.
Every shape in the drawing (or group), in order, including those inside groups, where it is
drawn: any moves, scaling and rotation of the drawing, its groups and the shape itself (see
Curves.Transform) are applied to its curves, and included in its :transform.
Changes the shape or group with this id (the first one, anywhere in the drawing) to what fun
returns for it. Raises an ArgumentError if there is none, or if fun doesn't return a shape,
group or raw markup.
Changes every curve in the drawing (or group, or shape) to what fun returns for it, e.g. to
apply a Curves.Transform to each curve on its own.
Like update/3, following a path of ids: each id is looked for inside the one before it, and the
last one is changed by fun. Raises an ArgumentError if any of them isn't there, or if a shape
is asked for something inside it.
Types
@type child() :: Curves.Drawing.Shape.t() | Curves.Drawing.Group.t() | Curves.Drawing.Raw.t()
Something inside a drawing or a group.
@type container() :: t() | Curves.Drawing.Group.t()
A drawing, or a group in one: something with :children.
A transform, {a, b, c, d, e, f}, like SVG's matrix(a, b, c, d, e, f).
@type node_with_id() :: Curves.Drawing.Shape.t() | Curves.Drawing.Group.t()
Something that can be found by its id.
@type skipped() :: %{ element: String.t(), id: String.t() | nil, reason: term(), kept: :as_markup | :partly | :no }
Something that couldn't be read as curves. See the tables above.
@type t() :: %Curves.Drawing{ attributes: %{required(String.t()) => String.t()}, children: [child()], height: String.t() | nil, origin: Curves.Types.point_tuple(), read_with_flip_y: boolean(), rotation: float(), scale: number() | {number(), number()}, skipped: [skipped()], view_box: {float(), float(), float(), float()} | nil, width: String.t() | nil }
Functions
@spec by_id(String.t()) :: Access.access_fun(container(), child())
An Access function for the shape or group with this id, to use with Elixir's own
Kernel.get_in/2, Kernel.update_in/3 and Kernel.pop_in/2 on a drawing.
get_in/2 gives nil if there is no such id, while update_in/3 and pop_in/2 raise an
ArgumentError, like update/3 and delete/2.
Examples
iex> drawing = Curves.from_svg(~s(<svg><g id="car"><circle id="wheel" r="1"/></g></svg>))
iex> wheel = Curves.Drawing.by_id("wheel")
iex> get_in(drawing, [Curves.Drawing.by_id("car"), wheel]).element
:circle
iex> drawing = update_in(drawing, [wheel], &Curves.Transform.set_scale(&1, 2))
iex> Curves.Drawing.get(drawing, "wheel").scale
2
iex> {_wheel, drawing} = pop_in(drawing, [wheel])
iex> Curves.Drawing.get(drawing, "wheel")
nil
@spec curves(container()) :: [Curves.Spline.Curve.t() | Curves.Polyline.Curve.t()]
Every curve in the drawing (or group), shape by shape and subpath by subpath, in order, where it
is drawn (see shapes/1).
Examples
iex> drawing = Curves.from_svg(~s(<svg><circle r="1"/><path d="M0 0 L1 1 M5 5 L6 6"/></svg>))
iex> drawing |> Curves.Drawing.curves() |> length()
3
Takes the shape or group with this id out of the drawing. Raises an ArgumentError if there
is none.
Examples
iex> drawing = Curves.from_svg(~s(<svg><circle id="sun" r="3"/><rect width="1" height="1"/></svg>))
iex> drawing |> Curves.Drawing.delete("sun") |> Curves.Drawing.shapes() |> Enum.map(& &1.element)
[:rect]
@spec fetch(container(), String.t()) :: {:ok, node_with_id()} | :error
The shape or group with this id, anywhere in the drawing (or group), as {:ok, node}, or
:error if there is none. The first one wins if several share an id.
Examples
iex> drawing = Curves.from_svg(~s(<svg><g id="house"><rect id="door" width="1" height="2"/></g></svg>))
iex> {:ok, door} = Curves.Drawing.fetch(drawing, "door")
iex> door.element
:rect
iex> Curves.Drawing.fetch(drawing, "garden")
:error
@spec get(container(), String.t()) :: node_with_id() | nil
The shape or group with this id, or nil. See fetch/2.
Examples
iex> drawing = Curves.from_svg(~s(<svg><circle id="sun" r="3"/></svg>))
iex> Curves.Drawing.get(drawing, "sun").element
:circle
iex> Curves.Drawing.get(drawing, "moon")
nil
Like get/2, following a path of ids: each id is looked for inside the one before it. Returns
nil if any of them isn't there.
Examples
iex> drawing = Curves.from_svg(~s(<svg><g id="car"><circle id="wheel" r="1"/></g><circle id="wheel" r="9"/></svg>))
iex> Curves.Drawing.get_in(drawing, ["car", "wheel"]).attributes["r"]
"1"
Puts node (a shape, group or raw markup) in place of the shape or group with this id. Raises
an ArgumentError if there is none. Handy to take a part out, change it, and put it back.
Examples
iex> drawing = Curves.from_svg(~s(<svg><circle id="sun" r="3"/></svg>))
iex> sun = Curves.Drawing.get(drawing, "sun")
iex> drawing = Curves.Drawing.put(drawing, "sun", Curves.Transform.flip_x(sun))
iex> Curves.Drawing.get(drawing, "sun").scale
{-1.0, 1.0}
@spec shapes(container()) :: [Curves.Drawing.Shape.t()]
Every shape in the drawing (or group), in order, including those inside groups, where it is
drawn: any moves, scaling and rotation of the drawing, its groups and the shape itself (see
Curves.Transform) are applied to its curves, and included in its :transform.
Examples
iex> drawing = Curves.from_svg(~s(<svg><circle r="1"/><g><rect width="2" height="1"/></g></svg>))
iex> drawing |> Curves.Drawing.shapes() |> Enum.map(& &1.element)
[:circle, :rect]
iex> moved = Curves.Transform.inc_x(drawing, 10)
iex> [circle | _] = Curves.Drawing.shapes(moved)
iex> Curves.solve!(hd(circle.subpaths).curve, 0.0)
{11.0, 0.0}
@spec update(container(), String.t(), (node_with_id() -> child())) :: container()
Changes the shape or group with this id (the first one, anywhere in the drawing) to what fun
returns for it. Raises an ArgumentError if there is none, or if fun doesn't return a shape,
group or raw markup.
Examples
iex> drawing = Curves.from_svg(~s(<svg><g id="wheel"><circle r="1"/></g></svg>))
iex> drawing = Curves.Drawing.update(drawing, "wheel", &Curves.Transform.set_scale(&1, 3))
iex> Curves.Drawing.get(drawing, "wheel").scale
3
@spec update_curves( container() | Curves.Drawing.Shape.t(), (Curves.Types.curve_struct() -> Curves.Types.curve_struct()) ) :: container() | Curves.Drawing.Shape.t()
Changes every curve in the drawing (or group, or shape) to what fun returns for it, e.g. to
apply a Curves.Transform to each curve on its own.
Examples
iex> drawing = Curves.from_svg(~s(<svg><path d="M0 0 L4 0"/></svg>))
iex> drawing = Curves.Drawing.update_curves(drawing, &Curves.Transform.reverse/1)
iex> drawing |> Curves.Drawing.curves() |> hd() |> Curves.solve!(0.0)
{4.0, 0.0}
Like update/3, following a path of ids: each id is looked for inside the one before it, and the
last one is changed by fun. Raises an ArgumentError if any of them isn't there, or if a shape
is asked for something inside it.
Examples
iex> drawing = Curves.from_svg(~s(<svg><g id="car"><g id="front"><circle id="wheel" r="1"/></g></g></svg>))
iex> drawing = Curves.Drawing.update_in(drawing, ["car", "wheel"], &Curves.Transform.inc_rotation(&1, 90))
iex> Curves.Drawing.get_in(drawing, ["car", "front", "wheel"]).rotation
1.5707963267948966
@spec update_shapes(container(), (Curves.Drawing.Shape.t() -> Curves.Drawing.Shape.t())) :: container()
Changes every shape in the drawing (or group), wherever it is, to what fun returns for it.
The shapes are as stored: a group's transform isn't applied to them (see shapes/1). fun must
return a shape, group or raw markup; to take shapes out, use delete/2.
Examples
iex> drawing = Curves.from_svg(~s(<svg><circle r="1"/><g><rect width="2" height="1"/></g></svg>))
iex> drawing = Curves.Drawing.update_shapes(drawing, &put_in(&1.attributes["fill"], "red"))
iex> drawing |> Curves.Drawing.shapes() |> Enum.map(& &1.attributes["fill"])
["red", "red"]