Improve test and documentation coverage.

This commit is contained in:
James Harton 2017-01-05 14:06:19 +13:00
parent f8822fca40
commit 7b29e8f9ae
28 changed files with 605 additions and 74 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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),

View file

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

View file

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

View file

@ -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
[
" ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "", ".",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

8
lib/vivid/shape.ex Normal file
View file

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

View file

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

View file

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

View file

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

4
test/vivid/box_test.exs Normal file
View file

@ -0,0 +1,4 @@
defmodule Vivid.BoxTest do
use ExUnit.Case
doctest Vivid.Box
end

View file

@ -0,0 +1,4 @@
defmodule Vivid.BufferTest do
use ExUnit.Case
doctest Vivid.Buffer
end

4
test/vivid/font_test.exs Normal file
View file

@ -0,0 +1,4 @@
defmodule Vivid.FontTest do
use ExUnit.Case
doctest Vivid.Font
end

4
test/vivid/math_test.exs Normal file
View file

@ -0,0 +1,4 @@
defmodule Vivid.MathTest do
use ExUnit.Case
doctest Vivid.Math
end