vivid/guides/getting-started.md
James Harton 264bd2f17c
All checks were successful
continuous-integration/drone/push Build is passing
chore: Update forgejo hostname.
2024-02-05 15:20:22 +13:00

8.8 KiB

Getting Started with Vivid

Vivid is designed to be as straightforward to use as possible whilst still providing enough features to be useful. It was originally concieved for displaying graphics and text on Monochrome 1.3" 128x64 OLED graphic display from Adafruit, which it does.

4 x OLED display clock using Hershey vector fonts

The scope quickly expanded to include arbitrary transforms, basic colour compositing and alpha channels. I even added vivid_png for writing out PNG files.

Adding vivid to your app

Edit your mix.exs and add the current version of vivid to your dependencies list.

If you want to be able to render PNG files then you also want to add vivid_png.

General principals

Vivid tries to consistently reuse a bunch of principals thoughout the code base;

  • All "shape" types, e.g. boxes, circles and polygons have an init function which will generate the shape with as many sane defaults as possible. The corresponding modules also contain functions for manipulating the contents of the shape without needing to know the internal structure of the type. Vivid discourages the use of type structs directly as these are subject to change.

  • Most of the stuff you want to do with shapes, like render, transform or measure are implemented with Elixir protocols, meaning you can define your own shape types as required.

  • The String.Chars protocol has been implemented everywhere it makes sense, so that you can easily debug by just calling to_string/1 or IO.puts/1 on almost any type.

  • If you use Vivid at the top of your module it will automatically add aliases for all the core Vivid types into your local namespace. This can save a lot of typing, but otherwise doesn't do much.

Basic shapes

Vivid implements a number of basic geometric primitives upon which you can compose your own shapes as needed.

Point

The most basic type is the Point, which represents a location in 2 dimensional cartesian space (ie x and y offset).

iex> use Vivid
...> Point.init(13, 27)
#Vivid.Point<{13, 27}>

point example

Line

A Line represents a straight line between to points, called origin and termination in Vivid parlance.

Line implements the Enumerable protocol.

iex> use Vivid
...> Line.init(Point.init(13,27), Point.init(2,3))
#Vivid.Line<[origin: #Vivid.Point<{13, 27}>, termination: #Vivid.Point<{2, 3}>]>

line example

Path

A Path represents an aribitrary number of vertices (points) with lines connecting them. A Path is different to a Polygon in that a Polygon is closed and a Path is open.

A Path must consist of at least two vertices.

Path implements the Enumerable and Collectable protocols so that you can use Enum and Stream to manipulate it.

iex> use Vivid
...> Path.init([Point.init(13,27), Point.init(2,3), Point.init(27,13)])
#Vivid.Path<[#Vivid.Point<{13, 27}>, #Vivid.Point<{2, 3}>, #Vivid.Point<{27, 13}>]>

path example

Polygon

A Polygon also represents an arbitrary number of vertices (points) with lines connecting them, however a Polygon is a closed shape. As such, polygon's must have at least three vertices.

Polygon implements the Enumerable and Collectable protocols so that you can use Enum and Stream to manipulate it.

iex> use Vivid
...> Polygon.init([Point.init(13,27), Point.init(2,3), Point.init(27,13)])
#Vivid.Polygon<[#Vivid.Point<{13, 27}>, #Vivid.Point<{2, 3}>, #Vivid.Point<{27, 13}>]>

polygon example

Box

A Box is a special kind of Polygon where there are exactly four vertices and two lines are horizontal and two lines are vertical, i.e. a rectangle. But you knew that.

Because of the regular nature of rectangles they can be defined with only two points. The first being the lower left corner and the second being the top right corner.

iex> use Vivid
...> Box.init(Point.init(2,3), Point.init(13,27))
#Vivid.Box<[bottom_left: #Vivid.Point<{2, 3}>, top_right: #Vivid.Point<{13, 27}>]>

box example

Circle

I'm sure you know what a Circle is. It is initialised using a center point and a radius.

Circles are converted to polygons when transforming or rendering, so you can't always rely on being able to use the Circle API to manipulate existing shapes.

Often it may be necessary to convert it to a polygon manually before rendering so that you can control the number of vertices in the generated polygon.

iex> use Vivid
...> Circle.init(Point.init(15,15), 10)
#Vivid.Circle<[center: #Vivid.Point<{15, 15}>, radius: 10]>

circle example

Arc

An Arc, also known as a circle segment is a slice of a circle and initialised with a center point and radius much like a circle, however you also provide a start angle and a range (both in degrees) of arc that you wish to render.

Arcs are converted to paths when transforming or rendering, so you can't always rely on being able to use the Arc API to manipulate existing shapes.

You can optionally also specify the number of steps used during path generation in the initialiser.

iex> use Vivid
...> Arc.init(Point.init(15,15), 45, 90)
#Vivid.Arc<[center: #Vivid.Point<{15, 15}>, radius: 10, start_angle: 45, range: 90, steps: 12]>

arc example

Group

A Group allows for arbitrary composition of shapes into a single data structure. It's not so much a shape itself, as a collection of other shapes.

Group also implements the Enumerable and Collectable protocols so that you can use Enum and Stream to create them.

iex> use Vivid
...> box = Box.init(Point.init(2,3), Point.init(13,27))
...> circle = Circle.init(Point.init(15,15), 10)
...> Group.init([box, circle])
#Vivid.Group<[#Vivid.Box<[bottom_left: #Vivid.Point<{2, 3}>, top_right: #Vivid.Point<{13, 27}>]>, #Vivid.Circle<[center: #Vivid.Point<{15, 15}>, radius: 10]>]>

group example

Colours

Vivid (currently) defines all colours in terms of the RGBA colourspace. Create a new colour by passing red, green, blue and opacity values as integers or floats between 0 and 1.

iex> use Vivid
...> RGBA.init(0.75, 0.25, 0.5, 0.8)
#Vivid.RGBA<{0.75, 0.25, 0.5, 0.8}>

rgba example

Compositing

When you place two shapes on a buffer which overlap and where transparency is involved Vivid will use the "over" colour compositing algorithm. See Wikipedia's article on alpha compositing for more information.

Frame

A Frame stores a stack of shapes and corresponding colours and is the only type that can truly be rendered into a buffer.

Shapes and colours are pushed onto the frame in pairs, and composited during the render pass. Frame also implements the Enumerable and Collectable procotols meaning that you can use the Enum and Stream module to build frames.

Frames can be converted to buffers, which are the fully-rendered bitmap output, which is what you'll likely want to use for display.

iex> use Vivid
...> Frame.init(30, 30, RGBA.init(1,0,0,1))
#Vivid.Frame<[width: 30, height: 30, background_colour: #Vivid.RGBA<{1, 0, 0, 1}>]>

Buffer

A Buffer is used to render the contents of a Frame into a bitmap which can be displayed. Buffer implements the Enumerable protocol and emits a list of RGBA colours.

iex> use Vivid
...> Frame.init(30, 30, RGBA.init(1,0,0,1))
...> |> Buffer.horizontal()
#Vivid.Buffer<[rows: 30, columns: 30, size: 900]>