From 7b29e8f9ae3aba9ce3f80f8f07cdd18995104d15 Mon Sep 17 00:00:00 2001 From: James Harton Date: Thu, 5 Jan 2017 14:06:19 +1300 Subject: [PATCH] Improve test and documentation coverage. --- lib/collectable/vivid/frame.ex | 16 ++++++ lib/string/chars/vivid/shape.ex | 2 + lib/vivid.ex | 3 ++ lib/vivid/arc.ex | 4 +- lib/vivid/bounds.ex | 12 ++++- lib/vivid/bounds/of.ex | 12 +++++ lib/vivid/bounds/of/group.ex | 1 + lib/vivid/box.ex | 93 ++++++++++++++++++++++++++++++++- lib/vivid/buffer.ex | 39 +++++++++++++- lib/vivid/circle.ex | 73 ++++++++++++++++++++++++-- lib/vivid/font.ex | 49 ++++++++++++++++- lib/vivid/frame.ex | 41 +++++++++++++-- lib/vivid/group.ex | 29 +++++++--- lib/vivid/line.ex | 13 ++++- lib/vivid/math.ex | 15 ++++++ lib/vivid/path.ex | 50 ++++++++++++------ lib/vivid/point.ex | 26 ++++++++- lib/vivid/polygon.ex | 51 ++++++++++++------ lib/vivid/rasterize.ex | 13 +++++ lib/vivid/rgba.ex | 38 ++++++++++++-- lib/vivid/shape.ex | 8 +++ lib/vivid/transform.ex | 45 ++++++++++++---- lib/vivid/transform/point.ex | 19 +++++-- lib/vivid/transformable.ex | 11 +++- test/vivid/box_test.exs | 4 ++ test/vivid/buffer_test.exs | 4 ++ test/vivid/font_test.exs | 4 ++ test/vivid/math_test.exs | 4 ++ 28 files changed, 605 insertions(+), 74 deletions(-) create mode 100644 lib/collectable/vivid/frame.ex create mode 100644 lib/vivid/shape.ex create mode 100644 test/vivid/box_test.exs create mode 100644 test/vivid/buffer_test.exs create mode 100644 test/vivid/font_test.exs create mode 100644 test/vivid/math_test.exs diff --git a/lib/collectable/vivid/frame.ex b/lib/collectable/vivid/frame.ex new file mode 100644 index 0000000..3880363 --- /dev/null +++ b/lib/collectable/vivid/frame.ex @@ -0,0 +1,16 @@ +defimpl Collectable, for: Vivid.Frame do + alias Vivid.Frame + + @doc """ + Collect an enumerable into a Frame. + """ + + def into(%Frame{shapes: shapes}=frame) do + {shapes, fn + [], {:cont, {_shape,_colour}=shape} -> [shape] + new_shapes, {:cont, {_shape,_colour}=shape} -> [shape | new_shapes] + new_shapes, :done -> %{frame | shapes: shapes ++ Enum.reverse(new_shapes)} + _, :halt -> :ok + end} + end +end \ No newline at end of file diff --git a/lib/string/chars/vivid/shape.ex b/lib/string/chars/vivid/shape.ex index a51dc54..1bb801f 100644 --- a/lib/string/chars/vivid/shape.ex +++ b/lib/string/chars/vivid/shape.ex @@ -1,6 +1,8 @@ defmodule Vivid.ShapeToString do alias Vivid.{Bounds, Frame, Transform, RGBA} + @moduledoc false + def to_string(shape) do bounds = Bounds.bounds(shape) width = Bounds.width(bounds) + 3 |> round diff --git a/lib/vivid.ex b/lib/vivid.ex index 2e081b7..41cfbfb 100644 --- a/lib/vivid.ex +++ b/lib/vivid.ex @@ -10,6 +10,9 @@ defmodule Vivid do @moduledoc ~S""" Vivid is a 2D rendering engine implemented purely in Elixir. + If you add `use Vivid` to your module then aliases for all the common Vivid + modules will automatically be defined for you. + ## Examples Drawing a box on the frame diff --git a/lib/vivid/arc.ex b/lib/vivid/arc.ex index 7fc89ca..ecb6c90 100644 --- a/lib/vivid/arc.ex +++ b/lib/vivid/arc.ex @@ -52,7 +52,7 @@ defmodule Vivid.Arc do ...> |> Vivid.Arc.center #Vivid.Point<{10, 10}> """ - @spec center(Art.t) :: Point.t + @spec center(Arc.t) :: Point.t def center(%Arc{center: p}), do: p @doc """ @@ -65,7 +65,7 @@ defmodule Vivid.Arc do ...> |> Vivid.Arc.center #Vivid.Point<{15, 15}> """ - @spec center(Art.t, Point.t) :: Arc.t + @spec center(Arc.t, Point.t) :: Arc.t def center(%Arc{}=a, %Point{}=p), do: %{a | center: p} @doc """ diff --git a/lib/vivid/bounds.ex b/lib/vivid/bounds.ex index bb75a07..cdfbce2 100644 --- a/lib/vivid/bounds.ex +++ b/lib/vivid/bounds.ex @@ -1,11 +1,13 @@ defmodule Vivid.Bounds do - alias Vivid.{Bounds, Point} + alias Vivid.{Bounds, Point, Shape} defstruct ~w(min max)a @moduledoc """ Provides information about the bounds of a box and pixel positions within it. """ + @opaque t :: %Bounds{min: Point.t, max: Point.t} + @doc """ Initialise arbitrary bounds. @@ -14,6 +16,7 @@ defmodule Vivid.Bounds do iex> Vivid.Bounds.init(0, 0, 5, 5) #Vivid.Bounds<[min: #Vivid.Point<{0, 0}>, max: #Vivid.Point<{5, 5}>]> """ + @spec init(number, number, number, number) :: Bounds.t def init(x0, y0, x1, y1), do: %Bounds{min: Point.init(x0, y0), max: Point.init(x1, y1)} @doc """ @@ -25,6 +28,7 @@ defmodule Vivid.Bounds do ...> |> Vivid.Bounds.bounds #Vivid.Bounds<[min: #Vivid.Point<{0.0, 0.0}>, max: #Vivid.Point<{20.0, 20.0}>]> """ + @spec bounds(Shape.t) :: Bounds.t def bounds(%Bounds{}=bounds), do: bounds def bounds(shape) do {min, max} = Vivid.Bounds.Of.bounds(shape) @@ -40,6 +44,7 @@ defmodule Vivid.Bounds do ...> |> Vivid.Bounds.width 20.0 """ + @spec width(Shape.t) :: number def width(%Bounds{min: %Point{x: x0}, max: %Point{x: x1}}), do: abs(x1 - x0) def width(shape), do: shape |> bounds |> width @@ -52,6 +57,7 @@ defmodule Vivid.Bounds do ...> |> Vivid.Bounds.height 20.0 """ + @spec height(Shape.t) :: number def height(%Bounds{min: %Point{y: y0}, max: %Point{y: y1}}), do: abs(y1 - y0) def height(shape), do: shape |> bounds |> height @@ -64,6 +70,7 @@ defmodule Vivid.Bounds do ...> |> Vivid.Bounds.min #Vivid.Point<{0.0, 0.0}> """ + @spec min(Shape.t) :: Point.t def min(%Bounds{min: min}), do: min def min(shape), do: shape |> bounds |> min @@ -76,6 +83,7 @@ defmodule Vivid.Bounds do ...> |> Vivid.Bounds.max #Vivid.Point<{20.0, 20.0}> """ + @spec max(Shape.t) :: Point.t def max(%Bounds{max: max}), do: max def max(shape), do: shape |> bounds |> max @@ -89,6 +97,7 @@ defmodule Vivid.Bounds do ...> |> Vivid.Bounds.center_of #Vivid.Point<{10.0, 10.0}> """ + @spec center_of(Shape.t) :: Point.t def center_of(%Bounds{min: %Point{x: x0, y: y0}, max: %Point{x: x1, y: y1}}) do x = x0 + (x1 - x0) / 2 y = y0 + (y1 - y0) / 2 @@ -125,6 +134,7 @@ defmodule Vivid.Bounds do ...> |> Vivid.Bounds.contains?(Vivid.Point.init(11, 11)) false """ + @spec contains?(Shape.t, Point.t) :: boolean def contains?(%Bounds{min: %Point{x: x0, y: y0}, max: %Point{x: x1, y: y1}}, %Point{x: x, y: y}) when x0 <= x and x <= x1 and y0 <= y and y <= y1, do: true def contains?(_, _), do: false end diff --git a/lib/vivid/bounds/of.ex b/lib/vivid/bounds/of.ex index 49d25b9..ab75fa2 100644 --- a/lib/vivid/bounds/of.ex +++ b/lib/vivid/bounds/of.ex @@ -1,3 +1,15 @@ defprotocol Vivid.Bounds.Of do + alias Vivid.{Shape, Point} + @moduledoc """ + This protocol is used to calculate the bounds of a given shape. + + Implement this protocol if you are defining any new shape types. + """ + + @doc """ + Return the bounds of a Shape as a two element tuple of bottom-left and + top-right points. + """ + @spec bounds(Shape.t) :: {Point.t, Point.t} def bounds(shape) end \ No newline at end of file diff --git a/lib/vivid/bounds/of/group.ex b/lib/vivid/bounds/of/group.ex index a4b56a8..d402183 100644 --- a/lib/vivid/bounds/of/group.ex +++ b/lib/vivid/bounds/of/group.ex @@ -4,6 +4,7 @@ defimpl Vivid.Bounds.Of, for: Vivid.Group do shapes |> Enum.map(&Vivid.Bounds.Of.bounds(&1)) |> Enum.reduce(fn + {min, max}, nil -> {min, max} {pmin, pmax}, {min, max} -> min = if pmin.x < min.x, do: Point.init(pmin.x, min.y), else: min min = if pmin.y < min.y, do: Point.init(min.x, pmin.y), else: min diff --git a/lib/vivid/box.ex b/lib/vivid/box.ex index 41fd32c..1774c1c 100644 --- a/lib/vivid/box.ex +++ b/lib/vivid/box.ex @@ -1,10 +1,43 @@ defmodule Vivid.Box do - alias Vivid.{Box, Point, Polygon, Bounds} + alias Vivid.{Box, Point, Polygon, Bounds, Shape} defstruct ~w(bottom_left top_right fill)a + @moduledoc """ + Short-hand for creating rectangle polygons. + + This module doesn't have very much logic other than knowing how to + turn itself into a Polygon. + """ + + @opaque t :: Box.t + + @doc """ + Initialize a Box from it's bottom left and top right points. + + ## Examples + + iex> use Vivid + ...> Box.init(Point.init(1,1), Point.init(4,4)) + #Vivid.Box<[bottom_left: #Vivid.Point<{1, 1}>, top_right: #Vivid.Point<{4, 4}>]> + """ + @spec init(Point.t, Point.t) :: Box.t def init(%Point{}=bl, %Point{}=tr), do: init(bl, tr, false) + + @doc false + @spec init(Point.t, Point.t, boolean) :: Box.t def init(%Point{}=bl, %Point{}=tr, fill) when is_boolean(fill), do: %Box{bottom_left: bl, top_right: tr, fill: fill} + @doc """ + Initialize a box from the bounds of an arbitrary shape. + + ## Examples + + iex> use Vivid + ...> Circle.init(Point.init(5,5), 5) + ...> |> Box.init_from_bounds + #Vivid.Box<[bottom_left: #Vivid.Point<{0.0, 0.2447174185242318}>, top_right: #Vivid.Point<{10.0, 9.755282581475768}>]> + """ + @spec init_from_bounds(Shape.t) :: Box.t def init_from_bounds(shape, fill \\ false) do bounds = shape |> Bounds.bounds min = bounds |> Bounds.min @@ -12,11 +45,69 @@ defmodule Vivid.Box do init(min, max, fill) end + @doc """ + Return the bottom left corner of the box. + + ## Example + + iex> use Vivid + ...> Box.init(Point.init(1,1), Point.init(4,4)) + ...> |> Box.bottom_left + #Vivid.Point<{1, 1}> + """ + @spec bottom_left(Box.t) :: Point.t def bottom_left(%Box{bottom_left: bl}), do: bl + + @doc """ + Return the top left corner of the box. + + ## Example + + iex> use Vivid + ...> Box.init(Point.init(1,1), Point.init(4,4)) + ...> |> Box.top_left + #Vivid.Point<{1, 4}> + """ + @spec top_left(Box.t) :: Point.t def top_left(%Box{bottom_left: bl, top_right: tr}), do: Point.init(bl.x, tr.y) + + @doc """ + Return the top right corner of the box. + + ## Example + + iex> use Vivid + ...> Box.init(Point.init(1,1), Point.init(4,4)) + ...> |> Box.top_right + #Vivid.Point<{4, 4}> + """ + @spec top_right(Box.t) :: Point.t def top_right(%Box{top_right: tr}), do: tr + + @doc """ + Return the top right corner of the box. + + ## Example + + iex> use Vivid + ...> Box.init(Point.init(1,1), Point.init(4,4)) + ...> |> Box.bottom_right + #Vivid.Point<{4, 1}> + """ + @spec bottom_right(Box.t) :: Point.t def bottom_right(%Box{bottom_left: bl, top_right: tr}), do: Point.init(tr.x, bl.y) + @doc """ + Convert a Box into a Polygon. + + ## Example + + iex> use Vivid + ...> Box.init(Point.init(1,1), Point.init(4,4)) + ...> |> Box.to_polygon + #Vivid.Polygon<[#Vivid.Point<{1, 1}>, #Vivid.Point<{1, 4}>, #Vivid.Point<{4, 4}>, #Vivid.Point<{4, 1}>]> + """ + @spec to_polygon(Box.t) :: Polygon.t def to_polygon(box) do Polygon.init([ bottom_left(box), diff --git a/lib/vivid/buffer.ex b/lib/vivid/buffer.ex index 5f83003..0279b8b 100644 --- a/lib/vivid/buffer.ex +++ b/lib/vivid/buffer.ex @@ -4,11 +4,32 @@ defmodule Vivid.Buffer do @moduledoc """ Used to convert a Frame into a buffer for display. + + You're unlikely to need to use this module directly, instead you will + likely want to use `Frame.buffer/2` instead. + + Buffer implements the `Enumerable` protocol. """ - @doc """ + @opaque t :: %Buffer{buffer: [RGBA.t], rows: integer, columns: integer} + + @doc ~S""" Render the buffer horizontally, ie across rows then up columns. + + ## Example + + iex> use Vivid + ...> Frame.init(5, 5, RGBA.white) + ...> |> Frame.push(Line.init(Point.init(0, 2), Point.init(5, 2)), RGBA.black) + ...> |> Buffer.horizontal + ...> |> to_string + "@@@@@\n" <> + "@@@@@\n" <> + " \n" <> + "@@@@@\n" <> + "@@@@@\n" """ + @spec horizontal(Frame.t) :: [RGBA.t] def horizontal(%Frame{shapes: shapes, width: w, height: h}=frame) do buffer = allocate(frame) bounds = Bounds.bounds(frame) @@ -16,9 +37,23 @@ defmodule Vivid.Buffer do %Buffer{buffer: buffer, rows: h, columns: w} end - @doc """ + @doc ~S""" Render the buffer vertically, ie up columns then across rows. + + ## Example + + iex> use Vivid + ...> Frame.init(5, 5, RGBA.white) + ...> |> Frame.push(Line.init(Point.init(0, 2), Point.init(5, 2)), RGBA.black) + ...> |> Buffer.vertical + ...> |> to_string + "@@ @@\n" <> + "@@ @@\n" <> + "@@ @@\n" <> + "@@ @@\n" <> + "@@ @@\n" """ + @spec vertical(Frame.t) :: [RGBA.t] def vertical(%Frame{shapes: shapes, width: w, height: h}=frame) do bounds = Bounds.bounds(frame) buffer = allocate(frame) diff --git a/lib/vivid/circle.ex b/lib/vivid/circle.ex index 43c86af..4d2b8c9 100644 --- a/lib/vivid/circle.ex +++ b/lib/vivid/circle.ex @@ -7,6 +7,8 @@ defmodule Vivid.Circle do Represents a circle based on it's center point and radius. """ + @opaque t :: %Circle{center: Point.t, radius: number, fill: boolean} + @doc """ Creates a circle from a point in 2D space and a radius. @@ -15,9 +17,15 @@ defmodule Vivid.Circle do iex> Vivid.Circle.init(Vivid.Point.init(5,5), 4) #Vivid.Circle<[center: #Vivid.Point<{5, 5}>, radius: 4]> """ - def init(point, radius), do: init(point, radius, false) + @spec init(Point.t, number) :: Circle.t + def init(%Point{}=point, radius) + when is_number(radius) and radius > 0, + do: init(point, radius, false) + + @doc false + @spec init(Point.t, number, boolean) :: Circle.t def init(%Point{}=point, radius, fill) - when is_number(radius) and is_boolean(fill) + when is_number(radius) and is_boolean(fill) and radius > 0 do %Circle{ center: point, @@ -31,18 +39,22 @@ defmodule Vivid.Circle do ## Example - iex> Vivid.Circle.init(Vivid.Point.init(5,5), 4) |> Vivid.Circle.radius + iex> Vivid.Circle.init(Vivid.Point.init(5,5), 4) + ...> |> Vivid.Circle.radius 4 """ + @spec radius(Cricle.t) :: number def radius(%Circle{radius: r}), do: r @doc """ Returns the center point of a circle. ## Example - iex> Vivid.Circle.init(Vivid.Point.init(5,5), 4) |> Vivid.Circle.center + iex> Vivid.Circle.init(Vivid.Point.init(5,5), 4) + ...> |> Vivid.Circle.center %Vivid.Point{x: 5, y: 5} """ + @spec center(Circle.t) :: Point.t def center(%Circle{center: point}), do: point @doc """ @@ -50,12 +62,63 @@ defmodule Vivid.Circle do ## Example - iex> Vivid.Circle.init(Vivid.Point.init(5,5), 4) |> Vivid.Circle.circumference + iex> Vivid.Circle.init(Vivid.Point.init(5,5), 4) + ...> |> Vivid.Circle.circumference 25.132741228718345 """ + @spec circumference(Circle.t) :: number def circumference(%Circle{radius: radius}), do: 2 * :math.pi * radius + @doc ~S""" + Convert the circle into a Polygon. + + We convert a circle into a Polygon whenever we Transform or render it, so + sometimes it might be worth doing it yourself and specifying how many vertices + the polygon should have. + + If unspecified then `steps` is set to the diameter of the circle rounded to + the nearest integer. + + ## Examples + + iex> use Vivid + ...> Circle.init(Point.init(5,5), 5) + ...> |> Circle.to_polygon + ...> |> to_string + "@@@@@@@@@@@@@\n" <> + "@@@@ @@@@\n" <> + "@@@ @@@@@ @@@\n" <> + "@@ @@@@@@@ @@\n" <> + "@@ @@@@@@@ @@\n" <> + "@ @@@@@@@@@ @\n" <> + "@ @@@@@@@@@ @\n" <> + "@ @@@@@@@@@ @\n" <> + "@@ @@@@@@@ @@\n" <> + "@@ @@@@@@@ @@\n" <> + "@@@ @@@@@ @@@\n" <> + "@@@@ @@@@\n" <> + "@@@@@@@@@@@@@\n" + + iex> use Vivid + ...> Circle.init(Point.init(5,5), 5) + ...> |> Circle.to_polygon(3) + ...> |> to_string + "@@@@@@@@@@@\n" <> + "@ @@@@@@@@\n" <> + "@ @ @@@@@@\n" <> + "@ @@@ @@@@\n" <> + "@ @@@@@ @@\n" <> + "@ @@@@@@@ @\n" <> + "@ @@@@@ @@\n" <> + "@ @@@ @@@@\n" <> + "@ @@ @@@@@@\n" <> + "@ @@@@@@@\n" <> + "@ @@@@@@@@@\n" <> + "@@@@@@@@@@@\n" + """ + @spec to_polygon(Circle.t) :: Polygon.t def to_polygon(%Circle{radius: radius}=circle), do: to_polygon(circle, round(radius * 2)) + @spec to_polygon(Circle.t, number) :: Polygon.t def to_polygon(%Circle{center: center, radius: radius, fill: fill}, steps) do h = center |> Point.x k = center |> Point.y diff --git a/lib/vivid/font.ex b/lib/vivid/font.ex index edc9dfa..ae7d66a 100644 --- a/lib/vivid/font.ex +++ b/lib/vivid/font.ex @@ -1,9 +1,51 @@ defmodule Vivid.Font do - alias Vivid.{Point, Group} + alias Vivid.{Point, Group, Shape} alias Vivid.Font.Char @font_vertical_offset 10 + @moduledoc """ + This module takes characters generated by the Hershey module and converts them + into groups of shapes using the character's specified left and right padding. + + Specifically this module only knows about the `rowmans` Hershey font, because + it's all I needed. We need a real font layout system. PR's gratefully accepted. + """ + + @doc ~S""" + Convert a String containing one or more characters into a shape. + + Can only handle characters defined in the `rowmans` Hershey font. Carriage + returns and line feeds are also not supported. + + The second argument is a scale factor. Defaults to `1.0`. + + ## Example + + iex> use Vivid + ...> Font.line("hello world", 0.75) + ...> |> to_string + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n" <> + "@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @\n" <> + "@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @\n" <> + "@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @\n" <> + "@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @\n" <> + "@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @\n" <> + "@ @@@ @@@@@@@@@@ @@@@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@ @@@@@@@ @@@@@@@@ @@ @@@ @@@@@@@@ @@ @\n" <> + "@ @@ @@@@ @@@@@@@ @@@@ @@@@@@ @@@@@ @@@@@@ @@@@ @@@@@@@@@@@@@@@@@ @@@@@ @@@@@ @@@@@ @@@@ @@@@@@ @ @@@@@@@ @@@@@@ @@@@ @\n" <> + "@ @@@@@@ @@@@@ @@@@@@@ @@@@@ @@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@ @@@ @ @@@ @@@@@ @@@@@@@@ @@@@@ @@@@@@@@ @@@@@ @@@@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@ @@@@@@@@ @@@@ @@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@ @@@ @ @@@ @@@@@ @@@@@@@@ @@@@@ @@@@@@@@ @@@@@ @@@@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@ @@@@@@@@ @@@@ @@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@ @@@ @ @@@ @@@@@ @@@@@@@@ @@@@@ @@@@@@@@@ @@@@@ @@@@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@ @@@@ @@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@ @@@ @ @@@ @@@@@ @@@@@@@@ @@@@@ @@@@@@@@@ @@@@@ @@@@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@ @@@@@@@@@@@@@ @@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@ @ @@@ @ @@@@@@ @@@@@@@@ @@@@@ @@@@@@@@@ @@@@@ @@@@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@ @@@@@@@@@@@@@ @@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@ @ @@@ @ @@@@@@ @@@@@@@@ @@@@@ @@@@@@@@@ @@@@@ @@@@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@ @@@@@@@@ @@@@ @@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@ @ @@@ @ @@@@@@ @@@@@@@@ @@@@@ @@@@@@@@@ @@@@@ @@@@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@@ @@@@@@ @@@@@ @@@@@ @@@@@@ @@@@@@ @@@@@@@@@@@@@@@@@@@ @ @@@ @ @@@@@@@ @@@@@@ @@@@@@ @@@@@@@@@ @@@@@@ @@@@@@ @\n" <> + "@ @@@@@@@@ @@@@@@@ @@@@ @@@@@@ @@@@@ @@@@@@@ @@@@ @@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@@@@ @@@@ @@@@@@@ @@@@@@@@@ @@@@@@@ @@@@ @ @\n" <> + "@ @@@@@@@@ @@@@@@@@ @@@@@@@ @@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ @@@@@ @@@@@@@@@@ @@@@@@@@ @@@@@@@@@ @@@@@@@@ @@ @\n" <> + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n" + """ + @spec line(String.t, number) :: Shape.t def line(str, scale \\ 1.0) do font = rowmans str @@ -26,6 +68,11 @@ defmodule Vivid.Font do |> Enum.into(Group.init) end + @doc """ + Convert the `rowmans` font into a map with the codepoints (characters) as the + index, and the font character as the value. + """ + @spec rowmans() :: map def rowmans do [ " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "↑", ".", diff --git a/lib/vivid/frame.ex b/lib/vivid/frame.ex index 77744a9..00f156f 100644 --- a/lib/vivid/frame.ex +++ b/lib/vivid/frame.ex @@ -1,9 +1,42 @@ defmodule Vivid.Frame do - alias Vivid.{Frame, RGBA, Buffer} + alias Vivid.{Frame, RGBA, Buffer, Shape} defstruct ~w(width height background_colour shapes)a - @moduledoc """ - A frame buffer or something. + @moduledoc ~S""" + Frame represents a collection of colours and shapes. + + Frame implements both the `Enumerable` and `Collectable` protocols. + + ## Examples + + iex> use Vivid + ...> Enum.map(1..5, fn i -> + ...> line = Line.init(Point.init(1,1), Point.init(20, i * 4)) + ...> {line, RGBA.black} + ...> end) + ...> |> Enum.into(Frame.init(24, 21, RGBA.white)) + ...> |> to_string + "@@@@@@@@@@@@@@@@@@@@ @@@\n" <> + "@@@@@@@@@@@@@@@@@@@ @@@@\n" <> + "@@@@@@@@@@@@@@@@@@ @@@@@\n" <> + "@@@@@@@@@@@@@@@@@ @@@@@@\n" <> + "@@@@@@@@@@@@@@@@ @@@ @@@\n" <> + "@@@@@@@@@@@@@@@ @@@ @@@@\n" <> + "@@@@@@@@@@@@@@ @@ @@@@@\n" <> + "@@@@@@@@@@@@@ @@ @@@@@@@\n" <> + "@@@@@@@@@@@@ @@ @@@@ @@@\n" <> + "@@@@@@@@@@@ @@ @@@ @@@@\n" <> + "@@@@@@@@@@ @ @@ @@@@@@\n" <> + "@@@@@@@@@ @ @@ @@@@@@@@\n" <> + "@@@@@@@@ @ @@ @@@@@ @@@\n" <> + "@@@@@@@ @ @@@ @@@@@\n" <> + "@@@@@@ @ @@@ @@@@@@@@\n" <> + "@@@@@ @ @@ @@@@@@@@@@\n" <> + "@@@@ @@@@@@ @@@\n" <> + "@@@ @@@ @@@@@@@\n" <> + "@@ @@@@@@@@@@@@@\n" <> + "@ @@@@@@@@@@@@@@@@@@@\n" <> + "@@@@@@@@@@@@@@@@@@@@@@@@\n" """ @opaque t :: %Frame{width: integer, height: integer, background_colour: RGBA.t, shapes: []} @@ -166,7 +199,7 @@ defmodule Vivid.Frame do and in `:vertical` mode the buffer is rendered column-by-column then row-by-row. - Returns a one-dimensional List of %RGBA{} colours with alpha-compositing + Returns a one-dimensional List of `RGBA` colours with alpha-compositing completed. """ @spec buffer(Frame.t) :: [RGBA.t] diff --git a/lib/vivid/group.ex b/lib/vivid/group.ex index 9f8e2f2..32169a9 100644 --- a/lib/vivid/group.ex +++ b/lib/vivid/group.ex @@ -1,28 +1,39 @@ defmodule Vivid.Group do - alias Vivid.Group + alias Vivid.{Group, Shape} defstruct ~w(shapes)a @moduledoc """ Represents a collection of shapes which can be Rasterized in a single pass. + + Group implements both the `Enumerable` and `Collectable` protocols. """ + @opaque t :: %Group{shapes: [Shape.t]} + @doc """ - Initialize a group either empty or from a list of shapes. + Initialize an empty group. ## Examples + iex> Vivid.Group.init + #Vivid.Group<[]> + """ + @spec init() :: Group.t + def init, do: %Group{shapes: MapSet.new()} + + @doc """ + Initialize a group from a list of shapes. + + ## Example + iex> circle = Vivid.Circle.init(Vivid.Point.init(5,5), 5) ...> line = Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(10,10)) ...> Vivid.Group.init([circle, line]) #Vivid.Group<[#Vivid.Line<[origin: #Vivid.Point<{1, 1}>, termination: #Vivid.Point<{10, 10}>]>, #Vivid.Circle<[center: #Vivid.Point<{5, 5}>, radius: 5]>]> - - iex> Vivid.Group.init - %Vivid.Group{shapes: MapSet.new()} """ - - def init, do: %Group{shapes: MapSet.new()} + @spec init([Shape.t]) :: Group.t def init(shapes) do - %Group{shapes: MapSet.new(shapes)} + %Group{shapes: Enum.into(shapes, MapSet.new)} end @doc """ @@ -35,6 +46,7 @@ defmodule Vivid.Group do ...> |> Vivid.Group.delete(line) %Vivid.Group{shapes: MapSet.new()} """ + @spec delete(Group.t, Shape.t) :: Group.t def delete(%Group{shapes: shapes}, shape), do: shapes |> MapSet.delete(shape) |> init @doc """ @@ -49,5 +61,6 @@ defmodule Vivid.Group do %Vivid.Line{origin: %Vivid.Point{x: 1, y: 1}, termination: %Vivid.Point{x: 10, y: 10}} ])} """ + @spec put(Group.t, Shape.t) :: Group.t def put(%Group{shapes: shapes}, shape), do: shapes |> MapSet.put(shape) |> init end \ No newline at end of file diff --git a/lib/vivid/line.ex b/lib/vivid/line.ex index e24a722..293aee7 100644 --- a/lib/vivid/line.ex +++ b/lib/vivid/line.ex @@ -7,6 +7,8 @@ defmodule Vivid.Line do Represents a line segment between two Points in 2D space. """ + @opaque t :: %Line{origin: Point.t, termination: Point.t} + @doc ~S""" Creates a Line. @@ -16,9 +18,12 @@ defmodule Vivid.Line do %Vivid.Line{origin: %Vivid.Point{x: 1, y: 1}, termination: %Vivid.Point{x: 4, y: 4}} """ + @spec init(Point.t, Point.t) :: Line.t def init(%Point{}=o, %Point{}=t) do %Line{origin: o, termination: t} end + + @spec init([Point.t]) :: Line.t def init([o,t]) do init(o,t) end @@ -31,6 +36,7 @@ defmodule Vivid.Line do iex> Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(4,4)) |> Vivid.Line.origin %Vivid.Point{x: 1, y: 1} """ + @spec origin(Line.t) :: Point.t def origin(%Line{origin: o}), do: o @doc ~S""" @@ -41,6 +47,7 @@ defmodule Vivid.Line do iex> Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(4,4)) |> Vivid.Line.termination %Vivid.Point{x: 4, y: 4} """ + @spec termination(Line.t) :: Point.t def termination(%Line{termination: t}), do: t @doc ~S""" @@ -51,6 +58,7 @@ defmodule Vivid.Line do iex> Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(14,4)) |> Vivid.Line.width 13 """ + @spec width(Line.t) :: number def width(%Line{}=line), do: abs(x_distance(line)) @doc ~S""" @@ -61,6 +69,7 @@ defmodule Vivid.Line do iex> Vivid.Line.init(Vivid.Point.init(14,1), Vivid.Point.init(1,4)) |> Vivid.Line.x_distance -13 """ + @spec x_distance(Line.t) :: number def x_distance(%Line{origin: %Point{x: x0}, termination: %Point{x: x1}}), do: x1 - x0 @doc ~S""" @@ -71,7 +80,7 @@ defmodule Vivid.Line do iex> Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(4,14)) |> Vivid.Line.height 13 """ - + @spec height(Line.t) :: number def height(%Line{}=line), do: abs(y_distance(line)) @doc ~S""" @@ -82,6 +91,7 @@ defmodule Vivid.Line do iex> Vivid.Line.init(Vivid.Point.init(1,14), Vivid.Point.init(4,1)) |> Vivid.Line.y_distance -13 """ + @spec y_distance(Line.t) :: number def y_distance(%Line{origin: %Point{y: y0}, termination: %Point{y: y1}}), do: y1 - y0 @doc ~S""" @@ -93,6 +103,7 @@ defmodule Vivid.Line do iex> Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(4,5)) |> Vivid.Line.length 5.0 """ + @spec length(Line.t) :: number def length(%Line{}=line) do dx2 = line |> width |> pow(2) dy2 = line |> height |> pow(2) diff --git a/lib/vivid/math.ex b/lib/vivid/math.ex index 6c51187..9aeeba2 100644 --- a/lib/vivid/math.ex +++ b/lib/vivid/math.ex @@ -1,8 +1,23 @@ defmodule Vivid.Math do + @moduledoc """ + I made this because I was constantly importing a small selection of + Erlang's `:math` module, and then manually implementing + `degrees_to_radians/1` which got pretty annoying after a while. + """ + defdelegate pi(), to: :math defdelegate cos(x), to: :math defdelegate sin(x), to: :math defdelegate pow(x,y), to: :math defdelegate sqrt(x), to: :math + + @doc """ + Convert degrees into radians. + + ## Examples: + + iex> 180 |> Vivid.Math.degrees_to_radians + :math.pi + """ def degrees_to_radians(degrees), do: degrees / 360.0 * 2.0 * pi end \ No newline at end of file diff --git a/lib/vivid/path.ex b/lib/vivid/path.ex index 75f3ca6..7c3554c 100644 --- a/lib/vivid/path.ex +++ b/lib/vivid/path.ex @@ -1,15 +1,30 @@ defmodule Vivid.Path do - alias Vivid.{Path, Point, Line} + alias Vivid.{Path, Point, Line, Shape} defstruct vertices: [] @moduledoc """ Describes a path as a series of vertices. + + Path implements both the `Enumerable` and `Collectable` protocols. """ - @doc """ - Initialize a path either empty or from a list of points. + @opaque t :: %Path{vertices: [Shape.t]} - ## Examples + @doc """ + Initialize an empty path. + + ## Example + + iex> Vivid.Path.init + %Vivid.Path{vertices: []} + """ + @spec init() :: Path.t + def init, do: %Path{vertices: []} + + @doc """ + Initialize a path from a list of points. + + ## Example iex> Vivid.Path.init([Vivid.Point.init(1,1), Vivid.Point.init(1,2), Vivid.Point.init(2,2), Vivid.Point.init(2,1)]) %Vivid.Path{vertices: [ @@ -18,13 +33,9 @@ defmodule Vivid.Path do %Vivid.Point{x: 2, y: 2}, %Vivid.Point{x: 2, y: 1} ]} - - iex> Vivid.Path.init - %Vivid.Path{vertices: []} """ - + @spec init([Point.t]) :: Path.t def init(points) when is_list(points), do: %Path{vertices: points} - def init, do: %Path{vertices: []} @doc """ Convert a path into a list of lines joined by the vertices. @@ -39,6 +50,7 @@ defmodule Vivid.Path do %Vivid.Line{origin: %Vivid.Point{x: 2, y: 2}, termination: %Vivid.Point{x: 2, y: 1}}] """ + @spec to_lines(Path.t) :: [Line.t] def to_lines(%Path{vertices: points}) do points_to_lines([], points) end @@ -51,6 +63,7 @@ defmodule Vivid.Path do iex> Vivid.Path.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Path.delete(Vivid.Point.init(2,2)) %Vivid.Path{vertices: [%Vivid.Point{x: 1, y: 1}]} """ + @spec delete(Path.t, Point.t) :: Path.t def delete(%Path{vertices: points}, %Point{}=point) do points |> List.delete(point) @@ -65,6 +78,7 @@ defmodule Vivid.Path do iex> Vivid.Path.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Path.delete_at(1) %Vivid.Path{vertices: [%Vivid.Point{x: 1, y: 1}]} """ + @spec delete_at(Path.t, integer) :: Path.t def delete_at(%Path{vertices: points}, index) do points |> List.delete_at(index) @@ -72,20 +86,21 @@ defmodule Vivid.Path do end @doc """ - Remove a vertex at a specific index in the Path. + Return the first vertex in the Path. ## Example iex> Vivid.Path.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Path.first %Vivid.Point{x: 1, y: 1} """ + @spec first(Path.t) :: Point.t def first(%Path{vertices: points}) do points |> List.first end @doc """ - Remove a vertex at a specific index in the Path. + Insert a vertex at a specific index in the Path. ## Example @@ -96,6 +111,7 @@ defmodule Vivid.Path do %Vivid.Point{x: 2, y: 2} ]} """ + @spec insert_at(Path.t, integer, Point.t) :: Path.t def insert_at(%Path{vertices: points}, index, %Point{}=point) do points |> List.insert_at(index, point) @@ -103,20 +119,21 @@ defmodule Vivid.Path do end @doc """ - Remove a vertex at a specific index in the Path. + Return the last vertex in the Path. ## Example iex> Vivid.Path.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Path.last %Vivid.Point{x: 2, y: 2} """ + @spec last(Path.t) :: Point.t def last(%Path{vertices: points}) do points |> List.last end @doc """ - Remove a vertex at a specific index in the Path. + Replace a vertex at a specific index in the Path. ## Example @@ -127,20 +144,21 @@ defmodule Vivid.Path do %Vivid.Point{x: 3, y: 3} ]} """ + @spec replace_at(Path.t, integer, Point.t) :: Path.t def replace_at(%Path{vertices: points}, index, %Point{}=point) do points |> List.replace_at(index, point) |> init end - def points_to_lines(lines, []), do: lines + defp points_to_lines(lines, []), do: lines - def points_to_lines([], [origin | [term | points]]) do + defp points_to_lines([], [origin | [term | points]]) do line = Line.init(origin, term) points_to_lines([line], points) end - def points_to_lines(lines, [point | rest]) do + defp points_to_lines(lines, [point | rest]) do origin = lines |> List.last |> Line.termination term = point lines = lines ++ [Line.init(origin, term)] diff --git a/lib/vivid/point.ex b/lib/vivid/point.ex index 30ba617..b9e0044 100644 --- a/lib/vivid/point.ex +++ b/lib/vivid/point.ex @@ -6,6 +6,8 @@ defmodule Vivid.Point do Represents an individual point in (2D) space. """ + @opaque t :: %Point{x: number, y: number} + @doc ~S""" Creates a Point. @@ -14,7 +16,10 @@ defmodule Vivid.Point do iex> Vivid.Point.init(13, 27) %Vivid.Point{x: 13, y: 27} """ - def init(x, y) do + @spec init(number, number) :: Point.t + def init(x, y) + when is_number(x) and is_number(y) + do %Point{x: x, y: y} end @@ -26,6 +31,7 @@ defmodule Vivid.Point do iex> Vivid.Point.init(13, 27) |> Vivid.Point.x 13 """ + @spec x(Point.t) :: number def x(%Point{x: x}), do: x @doc ~S""" @@ -36,17 +42,34 @@ defmodule Vivid.Point do iex> Vivid.Point.init(13, 27) |> Vivid.Point.y 27 """ + @spec y(Point.t) :: number def y(%Point{y: y}), do: y @doc """ Simple helper to swap X and Y coordinates - used when translating the frame buffer to vertical. + + ## Example + + iex> Vivid.Point.init(13, 27) + ...> |> Vivid.Point.swap_xy + #Vivid.Point<{27, 13}> """ + @spec swap_xy(Point.t) :: Point.t def swap_xy(%Point{x: x, y: y}), do: Point.init(y, x) @doc """ Return the vector in `x` and `y` between point `a` and point `b`. + + ## Example + + iex> use Vivid + ...> a = Point.init(10, 10) + ...> b = Point.init(20, 20) + ...> Point.vector(a, b) + {10, 10} """ + @spec vector(Point.t, Point.t) :: {number, number} def vector(%Point{x: x0, y: y0}, %Point{x: x1, y: y1}) do {x1 - x0, y1 - y0} end @@ -60,5 +83,6 @@ defmodule Vivid.Point do ...> |> Vivid.Point.round #Vivid.Point<{1, 5}> """ + @spec round(Point.t) :: Point.t def round(%Point{x: x, y: y}), do: Point.init(Kernel.round(x), Kernel.round(y)) end \ No newline at end of file diff --git a/lib/vivid/polygon.ex b/lib/vivid/polygon.ex index 676ee81..2344aba 100644 --- a/lib/vivid/polygon.ex +++ b/lib/vivid/polygon.ex @@ -4,12 +4,27 @@ defmodule Vivid.Polygon do @moduledoc """ Describes a Polygon as a series of vertices. + + Polygon implements both the `Enumerable` and `Collectable` protocols. """ - @doc """ - Initialize a Polygon either empty or from a list of points. + @opaque t :: %Polygon{vertices: [Point.t], fill: boolean} - ## Examples + @doc """ + Initialize an empty Polygon. + + ## Example + + iex> Vivid.Polygon.init + %Vivid.Polygon{vertices: []} + """ + @spec init() :: Polygon.t + def init, do: %Polygon{vertices: [], fill: false} + + @doc """ + Initialize a Polygon from a list of points. + + ## Example iex> Vivid.Polygon.init([Vivid.Point.init(1,1), Vivid.Point.init(1,2), Vivid.Point.init(2,2), Vivid.Point.init(2,1)]) %Vivid.Polygon{vertices: [ @@ -18,13 +33,12 @@ defmodule Vivid.Polygon do %Vivid.Point{x: 2, y: 2}, %Vivid.Point{x: 2, y: 1} ]} - - iex> Vivid.Polygon.init - %Vivid.Polygon{vertices: []} """ - - def init, do: %Polygon{vertices: [], fill: false} + @spec init([Point.t]) :: Polygon.t def init(points) when is_list(points), do: %Polygon{vertices: points, fill: false} + + @doc false + @spec init([Point.t], boolean) :: Polygon.t def init(points, fill) when is_list(points) and is_boolean(fill), do: %Polygon{vertices: points, fill: fill} @doc """ @@ -42,6 +56,7 @@ defmodule Vivid.Polygon do %Vivid.Line{origin: %Vivid.Point{x: 2, y: 1}, termination: %Vivid.Point{x: 1, y: 1}}] """ + @spec to_lines(Polygon.t) :: [Line.t] def to_lines(%Polygon{vertices: points}) do points_to_lines([], points) end @@ -54,6 +69,7 @@ defmodule Vivid.Polygon do iex> Vivid.Polygon.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Polygon.delete(Vivid.Point.init(2,2)) %Vivid.Polygon{vertices: [%Vivid.Point{x: 1, y: 1}]} """ + @spec delete(Polygon.t, Point.t) :: Polygon.t def delete(%Polygon{vertices: points}, %Point{}=point) do points |> List.delete(point) @@ -68,6 +84,7 @@ defmodule Vivid.Polygon do iex> Vivid.Polygon.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Polygon.delete_at(1) %Vivid.Polygon{vertices: [%Vivid.Point{x: 1, y: 1}]} """ + @spec delete_at(Polygon.t, integer) :: Polygon.t def delete_at(%Polygon{vertices: points}, index) do points |> List.delete_at(index) @@ -75,20 +92,21 @@ defmodule Vivid.Polygon do end @doc """ - Remove a vertex at a specific index in the Polygon. + Return the first vertex in the Polygon. ## Example iex> Vivid.Polygon.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Polygon.first %Vivid.Point{x: 1, y: 1} """ + @spec first(Polygon.t) :: Point.t def first(%Polygon{vertices: points}) do points |> List.first end @doc """ - Remove a vertex at a specific index in the Polygon. + Insert a vertex at a specific index in the Polygon. ## Example @@ -99,6 +117,7 @@ defmodule Vivid.Polygon do %Vivid.Point{x: 2, y: 2} ]} """ + @spec insert_at(Polygon.t, integer, Point.t) :: Polygon.t def insert_at(%Polygon{vertices: points}, index, %Point{}=point) do points |> List.insert_at(index, point) @@ -106,20 +125,21 @@ defmodule Vivid.Polygon do end @doc """ - Remove a vertex at a specific index in the Polygon. + Return the last vertext in the Polygon. ## Example iex> Vivid.Polygon.init([Vivid.Point.init(1,1), Vivid.Point.init(2,2)]) |> Vivid.Polygon.last %Vivid.Point{x: 2, y: 2} """ + @spec last(Polygon.t) :: Point.t def last(%Polygon{vertices: points}) do points |> List.last end @doc """ - Remove a vertex at a specific index in the Polygon. + Replace a vertex at a specific index in the Polygon. ## Example @@ -130,24 +150,25 @@ defmodule Vivid.Polygon do %Vivid.Point{x: 3, y: 3} ]} """ + @spec replace_at(Polygon.t, integer, Point.t) :: Polygon.t def replace_at(%Polygon{vertices: points}, index, %Point{}=point) do points |> List.replace_at(index, point) |> init end - def points_to_lines(lines, []) do + defp points_to_lines(lines, []) do origin = lines |> List.last |> Line.termination term = lines |> List.first |> Line.origin lines ++ [Line.init(origin, term)] end - def points_to_lines([], [origin | [term | points]]) do + defp points_to_lines([], [origin | [term | points]]) do line = Line.init(origin, term) points_to_lines([line], points) end - def points_to_lines(lines, [point | rest]) do + defp points_to_lines(lines, [point | rest]) do origin = lines |> List.last |> Line.termination term = point lines = lines ++ [Line.init(origin, term)] diff --git a/lib/vivid/rasterize.ex b/lib/vivid/rasterize.ex index 4e175d7..65a2cb3 100644 --- a/lib/vivid/rasterize.ex +++ b/lib/vivid/rasterize.ex @@ -1,3 +1,16 @@ defprotocol Vivid.Rasterize do + alias Vivid.Shape + @moduledoc """ + The Rasterize protocol is responsible for converting shapes into bitmaps. + + If you're defining your own shape then you need to implement this protocol. + """ + + @doc """ + Convert a shape into a bitmap. + + Takes a `shape` and returns a `MapSet` of points within `bounds`. + """ + @spec rasterize(Shape.t, Bounds.t) :: MapSet def rasterize(shape, bounds) end \ No newline at end of file diff --git a/lib/vivid/rgba.ex b/lib/vivid/rgba.ex index 50eab97..16fae1a 100644 --- a/lib/vivid/rgba.ex +++ b/lib/vivid/rgba.ex @@ -7,8 +7,19 @@ defmodule Vivid.RGBA do @moduledoc """ Defines a colour in RGBA colour space. + + Colour and alpha values are defined as `0 >= n >= 1`. """ + @type zero_to_one :: number + @opaque t :: %RGBA{red: zero_to_one, + green: zero_to_one, + blue: zero_to_one, + alpha: zero_to_one, + a_red: zero_to_one, + a_green: zero_to_one, + a_blue: zero_to_one} + @doc """ Create a colour. Like magic. @@ -17,9 +28,10 @@ defmodule Vivid.RGBA do iex> Vivid.RGBA.init(0.1, 0.2, 0.3, 0.4) #Vivid.RGBA<{0.1, 0.2, 0.3, 0.4}> """ - + @spec init(zero_to_one, zero_to_one, zero_to_one) :: RGBA.t def init(red, green, blue), do: init(red, green, blue, 1) + @spec init(zero_to_one, zero_to_one, zero_to_one, zero_to_one) :: RGBA.t def init(red, green, blue, 1) when is_number(red) and is_number(green) and is_number(blue) and red >= 0 and red <= 1 @@ -80,6 +92,7 @@ defmodule Vivid.RGBA do iex> Vivid.RGBA.white #Vivid.RGBA<{1, 1, 1, 1}> """ + @spec white() :: RGBA.t def white, do: RGBA.init(1,1,1) @doc """ @@ -90,6 +103,7 @@ defmodule Vivid.RGBA do iex> Vivid.RGBA.black #Vivid.RGBA<{0, 0, 0, 1}> """ + @spec black() :: RGBA.t def black, do: RGBA.init(0,0,0) @doc """ @@ -101,7 +115,8 @@ defmodule Vivid.RGBA do ...> |> Vivid.RGBA.red 0.7 """ - def red(%RGBA{red: r}), do: r + @spec red(RGBA.t) :: zero_to_one + def red(%RGBA{red: r}), do: r @doc """ Return the green component of the colour. @@ -112,6 +127,7 @@ defmodule Vivid.RGBA do ...> |> Vivid.RGBA.green 0.6 """ + @spec green(RGBA.t) :: zero_to_one def green(%RGBA{green: g}), do: g @doc """ @@ -123,7 +139,8 @@ defmodule Vivid.RGBA do ...> |> Vivid.RGBA.blue 0.5 """ - def blue(%RGBA{blue: b}), do: b + @spec blue(RGBA.t) :: zero_to_one + def blue(%RGBA{blue: b}), do: b @doc """ Return the alpha component of the colour. @@ -134,6 +151,7 @@ defmodule Vivid.RGBA do ...> |> Vivid.RGBA.alpha 0.4 """ + @spec alpha(RGBA.t) :: zero_to_one def alpha(%RGBA{alpha: a}), do: a @doc """ @@ -145,6 +163,7 @@ defmodule Vivid.RGBA do ...> |> Vivid.RGBA.to_hex "#B39980" """ + @spec to_hex(RGBA.t) :: String.t def to_hex(%RGBA{red: r, green: g, blue: b, alpha: 1}) do r = r |> f2h g = g |> f2h @@ -168,7 +187,7 @@ defmodule Vivid.RGBA do iex> Vivid.RGBA.over(Vivid.RGBA.black, Vivid.RGBA.init(1,1,1, 0.5)) #Vivid.RGBA<{0.5, 0.5, 0.5, 1.0}> """ - + @spec over(RGBA.t, RGBA.t) :: RGBA.t def over(nil, %RGBA{}=colour), do: colour def over(%RGBA{}, %RGBA{alpha: 1}=visible), do: visible def over(%RGBA{}=visible, %RGBA{alpha: 0}), do: visible @@ -196,11 +215,22 @@ defmodule Vivid.RGBA do iex> Vivid.RGBA.black |> Vivid.RGBA.luminance 0.0 """ + @spec luminance(RGBA.t) :: zero_to_one def luminance(%RGBA{a_red: r, a_green: g, a_blue: b}) do [rl, gl, bl] = [r, g, b ] |> Enum.map(&pow(&1, 2.2)) 0.2128 * rl + 0.7150 * gl + 0.0722 * bl end + @doc """ + Convert a colour to an ASCII character. + + This isn't very scientific, but helps with debugging and is used in the + implementations of `String.Chars` for Vivid types. + + The chacaters used (from black to white) are `" .:-=+*#%@"`. These are + chosen based on the `luminance/1` value of the colour. + """ + @spec to_ascii(RGBA.t) :: String.t def to_ascii(%RGBA{}=colour) do l = luminance(colour) c = l * (@ascii_luminance_map_length - 1) |> round diff --git a/lib/vivid/shape.ex b/lib/vivid/shape.ex new file mode 100644 index 0000000..f52721b --- /dev/null +++ b/lib/vivid/shape.ex @@ -0,0 +1,8 @@ +defmodule Vivid.Shape do + alias Vivid.{Arc, Bounds, Box, Circle, Group, Line, Path, Point, Polygon} + @moduledoc """ + Doesn't do anything - is merely a type to represent an arbitrary shape. + """ + + @type t :: Arc.t | Bounds.t | Box.t | Circle.t | Group.t | Line.t | Path.t | Point.t | Polygon.t +end \ No newline at end of file diff --git a/lib/vivid/transform.ex b/lib/vivid/transform.ex index 8caf3af..f25d847 100644 --- a/lib/vivid/transform.ex +++ b/lib/vivid/transform.ex @@ -1,11 +1,16 @@ defmodule Vivid.Transform do - alias Vivid.{Point, Transform, Bounds} + alias Vivid.{Point, Transform, Bounds, Shape} alias Vivid.Transformable import Vivid.Math defstruct [operations: [], shape: nil] defmodule Operation do + alias __MODULE__ defstruct ~w(function name)a + + @moduledoc false + + @opaque t :: %Operation{function: function, name: String.t} end @moduledoc """ @@ -25,6 +30,10 @@ defmodule Vivid.Transform do #Vivid.Polygon<[#Vivid.Point<{30.106601717798213, 21.696699141100893}>, #Vivid.Point<{19.5, 24.803300858899107}>, #Vivid.Point<{8.893398282201787, 17.303300858899107}>, #Vivid.Point<{19.5, 14.196699141100893}>]> """ + @opaque t :: %Transform{shape: Shape.t, operations: [Operation.t]} + @type shape_or_transform :: Transform.t | Shape.t + @type degrees :: number + @doc """ Translate (ie move) a shape by adding `x` and `y` to each Point. @@ -35,6 +44,7 @@ defmodule Vivid.Transform do ...> |> Vivid.Transform.apply #Vivid.Polygon<[#Vivid.Point<{15, 10}>, #Vivid.Point<{15, 15}>, #Vivid.Point<{10, 15}>, #Vivid.Point<{10, 10}>]> """ + @spec translate(shape_or_transform, number, number) :: Transform.t def translate(shape, x, y) do fun = fn _shape -> &Transform.Point.translate(&1, x, y) @@ -59,6 +69,7 @@ defmodule Vivid.Transform do #Vivid.Polygon<[#Vivid.Point<{12.5, -2.5}>, #Vivid.Point<{12.5, 17.5}>, #Vivid.Point<{2.5, 17.5}>, #Vivid.Point<{2.5, -2.5}>]> """ + @spec scale(shape_or_transform, number) :: Transform.t def scale(shape, uniform) do fun = fn shape -> origin = Bounds.center_of(shape) @@ -68,6 +79,7 @@ defmodule Vivid.Transform do apply_transform(shape, fun, "scale-#{uniform}x") end + @spec scale(shape_or_transform, number, number) :: Transform.t def scale(shape, x, y) do fun = fn shape -> origin = Bounds.center_of(shape) @@ -78,7 +90,7 @@ defmodule Vivid.Transform do end @doc """ - Rotate a shape around an origin point. The default point the shape's center. + Rotate a shape around it's center point. ## Example @@ -86,12 +98,8 @@ defmodule Vivid.Transform do ...> |> Vivid.Transform.rotate(45) ...> |> Vivid.Transform.apply #Vivid.Polygon<[#Vivid.Point<{22.071067811865476, 16.464466094067262}>, #Vivid.Point<{15.0, 18.535533905932738}>, #Vivid.Point<{7.9289321881345245, 13.535533905932738}>, #Vivid.Point<{15.0, 11.464466094067262}>]> - - iex> Vivid.Box.init(Vivid.Point.init(10,10), Vivid.Point.init(20,20)) - ...> |> Vivid.Transform.rotate(45, Vivid.Point.init(5,5)) - ...> |> Vivid.Transform.apply - #Vivid.Polygon<[#Vivid.Point<{12.071067811865476, 13.535533905932738}>, #Vivid.Point<{5.000000000000002, 15.606601717798215}>, #Vivid.Point<{-2.0710678118654737, 10.606601717798215}>, #Vivid.Point<{5.0, 8.535533905932738}>]> """ + @spec rotate(shape_or_transform, degrees) :: Transform.t def rotate(shape, degrees) do radians = degrees_to_radians(degrees) fun = fn shape -> @@ -101,6 +109,18 @@ defmodule Vivid.Transform do apply_transform(shape, fun, "rotate-#{degrees}-around-center") end + + @doc """ + Rotate a shape around an origin point. + + ## Example + + iex> Vivid.Box.init(Vivid.Point.init(10,10), Vivid.Point.init(20,20)) + ...> |> Vivid.Transform.rotate(45, Vivid.Point.init(5,5)) + ...> |> Vivid.Transform.apply + #Vivid.Polygon<[#Vivid.Point<{12.071067811865476, 13.535533905932738}>, #Vivid.Point<{5.000000000000002, 15.606601717798215}>, #Vivid.Point<{-2.0710678118654737, 10.606601717798215}>, #Vivid.Point<{5.0, 8.535533905932738}>]> + """ + @spec rotate(shape_or_transform, degrees, Point.t) :: Transform.t def rotate(shape, degrees, %Point{x: x, y: y}=origin) do radians = degrees_to_radians(degrees) fun = fn _shape -> @@ -120,6 +140,7 @@ defmodule Vivid.Transform do ...> |> Vivid.Transform.apply #Vivid.Polygon<[#Vivid.Point<{11.0, 1.0}>, #Vivid.Point<{11.0, 11.0}>, #Vivid.Point<{1.0, 11.0}>, #Vivid.Point<{1.0, 1.0}>]> """ + @spec center(shape_or_transform, Shape.t) :: Transform.t def center(shape, bounds) do bounds = Bounds.bounds(bounds) bounds_width = Bounds.width(bounds) @@ -144,6 +165,7 @@ defmodule Vivid.Transform do ...> |> Vivid.Transform.apply #Vivid.Polygon<[#Vivid.Point<{40.0, 0.0}>, #Vivid.Point<{40.0, 80.0}>, #Vivid.Point<{0.0, 80.0}>, #Vivid.Point<{0.0, 0.0}>]> """ + @spec stretch(shape_or_transform, Shape.t) :: Transform.t def stretch(shape, bounds) do bounds = Bounds.bounds(bounds) bounds_min = Bounds.min(bounds) @@ -179,6 +201,7 @@ defmodule Vivid.Transform do ...> |> Vivid.Transform.apply #Vivid.Polygon<[#Vivid.Point<{40.0, 0.0}>, #Vivid.Point<{40.0, 40.0}>, #Vivid.Point<{0.0, 40.0}>, #Vivid.Point<{0.0, 0.0}>]> """ + @spec fill(shape_or_transform, Shape.t) :: Transform.t def fill(shape, bounds) do bounds = Bounds.bounds(bounds) bounds_min = Bounds.min(bounds) @@ -216,6 +239,7 @@ defmodule Vivid.Transform do ...> |> Vivid.Transform.apply #Vivid.Polygon<[#Vivid.Point<{80.0, 0.0}>, #Vivid.Point<{80.0, 80.0}>, #Vivid.Point<{0.0, 80.0}>, #Vivid.Point<{0.0, 0.0}>]> """ + @spec overflow(shape_or_transform, Shape.t) :: Transform.t def overflow(shape, bounds) do bounds = Bounds.bounds(bounds) bounds_min = Bounds.min(bounds) @@ -246,15 +270,16 @@ defmodule Vivid.Transform do Create an arbitrary transformation. Takes a shape and a function which is called with a shape argument (not necessarily the shape - passed-in, depending on where this transformation is in the transformation pipeline. + passed-in, depending on where this transformation is in the transformation pipeline). The function must return another function which takes and manipulates a point. ## Example + The example below translates a point right by half it's width. + iex> Vivid.Box.init(Vivid.Point.init(10,10), Vivid.Point.init(20,20)) ...> |> Vivid.Transform.transform(fn shape -> - ...> # Translate a point right by half it's width ...> width = Vivid.Bounds.width(shape) ...> fn point -> ...> x = point |> Vivid.Point.x @@ -266,11 +291,13 @@ defmodule Vivid.Transform do ...> |> Vivid.Transform.apply #Vivid.Polygon<[#Vivid.Point<{25, 10}>, #Vivid.Point<{25, 20}>, #Vivid.Point<{15, 20}>, #Vivid.Point<{15, 10}>]> """ + @spec transform(shape_or_transform, function) :: Transform.t def transform(shape, fun), do: apply_transform(shape, fun, inspect(fun)) @doc """ Apply a transformation pipeline returning the modified shape. """ + @spec apply(Transform.t) :: Shape.t def apply(%Transform{operations: operations, shape: shape}) do operations |> Enum.reverse diff --git a/lib/vivid/transform/point.ex b/lib/vivid/transform/point.ex index d8f3866..fe263d5 100644 --- a/lib/vivid/transform/point.ex +++ b/lib/vivid/transform/point.ex @@ -5,19 +5,30 @@ defmodule Vivid.Transform.Point do @moduledoc """ Standard transformations which can be applied to points without knowing the details of the geometry. + + Used extensively by `Transform`, however you can use these functions + as input to the `Transformable` protocol, should you require. """ + @type degrees :: number + @type radians :: number + @doc """ Translate a point (ie move it) by adding `x` and `y` to it's coordinates. """ + @spec translate(Point.t, number, number) :: Point.t def translate(%Point{x: x0, y: y0}, x, y), do: Point.init(x0 + x, y0 + y) @doc """ - Scale a point (ie move it) by multiplying it's distance from the origin point by `x_factor` and `y_factor`. - The default origin point is `{0, 0}` + Scale a point (ie move it) by multiplying it's distance from the `0`, `0` point by `x_factor` and `y_factor`. """ + @spec scale(Point, number, number) :: Point.t def scale(%Point{}=point, x_factor, y_factor), do: scale(point, x_factor, y_factor, Point.init(0,0)) - def scale(%Point{x: x, y: y}, x_factor, y_factor, %Point{x: xo, y: yo}) do + + @doc """ + Scale a point (ie move it) by multiplying it's distance from the origin point by `x_factor` and `y_factor`. + """ + def scale(%Point{x: x, y: y}=_point, x_factor, y_factor, %Point{x: xo, y: yo}=_origin) do x = (x - xo) * x_factor + xo y = (y - yo) * y_factor + yo Point.init(x, y) @@ -26,11 +37,13 @@ defmodule Vivid.Transform.Point do @doc """ Rotate a point `degrees` around an origin point. """ + @spec rotate(Point.t, Point.t, degrees) :: Point.t def rotate(point, origin, degrees), do: rotate_radians(point, origin, degrees_to_radians(degrees)) @doc """ Rotate a point `radians` around an origin point. """ + @spec rotate_radians(Point.t, Point.t, radians) :: Point.t def rotate_radians(%Point{x: x0, y: y0}, %Point{x: x1, y: y1}, radians) do x = x0 - x1 y = y0 - y1 diff --git a/lib/vivid/transformable.ex b/lib/vivid/transformable.ex index 63cefd4..baf24c1 100644 --- a/lib/vivid/transformable.ex +++ b/lib/vivid/transformable.ex @@ -1,3 +1,12 @@ defprotocol Vivid.Transformable do - def transform(shape, fun) + alias Vivid.Shape + @moduledoc """ + This protocol is used to apply *point* transformations to a shape. + """ + + @doc """ + Transform all of a shape's points using `fun`. + """ + @spec transform(Shape.t, function) :: Shape.t + def transform(shape, fun) end \ No newline at end of file diff --git a/test/vivid/box_test.exs b/test/vivid/box_test.exs new file mode 100644 index 0000000..2180a82 --- /dev/null +++ b/test/vivid/box_test.exs @@ -0,0 +1,4 @@ +defmodule Vivid.BoxTest do + use ExUnit.Case + doctest Vivid.Box +end \ No newline at end of file diff --git a/test/vivid/buffer_test.exs b/test/vivid/buffer_test.exs new file mode 100644 index 0000000..8af27d9 --- /dev/null +++ b/test/vivid/buffer_test.exs @@ -0,0 +1,4 @@ +defmodule Vivid.BufferTest do + use ExUnit.Case + doctest Vivid.Buffer +end \ No newline at end of file diff --git a/test/vivid/font_test.exs b/test/vivid/font_test.exs new file mode 100644 index 0000000..f5cd12f --- /dev/null +++ b/test/vivid/font_test.exs @@ -0,0 +1,4 @@ +defmodule Vivid.FontTest do + use ExUnit.Case + doctest Vivid.Font +end \ No newline at end of file diff --git a/test/vivid/math_test.exs b/test/vivid/math_test.exs new file mode 100644 index 0000000..a8fae3f --- /dev/null +++ b/test/vivid/math_test.exs @@ -0,0 +1,4 @@ +defmodule Vivid.MathTest do + use ExUnit.Case + doctest Vivid.Math +end \ No newline at end of file