Curves.Drawing (curves v1.0.1)

Copy Markdown View Source

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"))
FieldWhat it holds
:width, :heightthe <svg> element's width and height, as written (they may have units), or nil
:view_boxits viewBox as {min_x, min_y, width, height}, or nil
:read_with_flip_ywhether 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.
:attributesall of the <svg> element's own attributes, as strings
:childrenwhat 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
:skippedeverything that couldn't be read as curves, and why, so nothing goes missing silently
:origin, :scale, :rotationhow 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}
:keptMeaning
:as_markupkept as a Curves.Drawing.Raw, so it is drawn again just as it was, but has no curves
:partlysome of it is kept as curves (path data with an error: the subpaths before the error)
:nonothing 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)).

:reasonMeaning:kept
:not_curvesan 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
:emptya 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.

t()

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.

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.

Types

child()

Something inside a drawing or a group.

container()

@type container() :: t() | Curves.Drawing.Group.t()

A drawing, or a group in one: something with :children.

matrix()

@type matrix() :: {float(), float(), float(), float(), float(), float()}

A transform, {a, b, c, d, e, f}, like SVG's matrix(a, b, c, d, e, f).

node_with_id()

@type node_with_id() :: Curves.Drawing.Shape.t() | Curves.Drawing.Group.t()

Something that can be found by its id.

skipped()

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

t()

@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

by_id(id)

@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

curves(container)

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

delete(container, id)

@spec delete(container(), String.t()) :: container()

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]

fetch(map, id)

@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

get(container, id)

@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

get_in(node, arg2)

@spec get_in(container(), [String.t()]) :: child() | 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"

put(container, id, node)

@spec put(container(), String.t(), child()) :: container()

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}

shapes(container)

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

update(container, id, fun)

@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

update_curves(shape, fun)

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}

update_in(container, list, fun)

@spec update_in(container(), [String.t(), ...], (child() -> child())) :: container()

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

update_shapes(container, fun)

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