Tutorial

This tutorial assumes that you have Flitter installed and that you are comfortable running it from the command-line. We’ll be building scripts as we go along and, as Flitter will live reload code, you can follow along in the editor of your choice making changes and seeing the result instantly.

Declarative Visuals

Flitter is first-and-foremost a declarative language for describing visuals. What is to be drawn is declared, or described, with the engine then being responsible for doing the work necessary to show those visuals.

Let’s start with the simplest example:

!window size=1280;720

Save this to a file, say tutorial.fl, and execute it with:

$ flitter tutorial.fl
09:09:23.036 11632:.engine.control  | SUCCESS: Loaded page 0: tutorial.fl

A 16:9 black window should open. The size=1280;720 declares that we want to draw 1280 x 720 graphics inside this window. The window won’t necessarily actually be 1280 x 720, Flitter will resize it as necessary to fit on screen neatly and the OS is responsible for the pixel dimensions of the framebuffer that backs the window. However, the size here is an important hint for things we will put inside this window.

At this point we should probably put something inside the window. Change the code to read:

!window size=1280;720
  !canvas

You can leave Flitter running in the terminal, changes to the file will be immediately reloaded. Nothing will visually change about the window at the moment, but this new line instructs Flitter to place a drawing canvas inside the window.

The drawing canvas that we have placed inside the window will be exactly 1280 x 720 pixels in size – the canvas inherits its size from the enclosing window size attribute. We could override this by putting a size= attribute after !canvas.

We should start actually drawing something. Change the file to read:

!window size=1280;720
  !canvas
    !text text="Hello world!" font_size=100 point=640;360 color=1

You should see the words “Hello world!” appear in white in the middle of the window.

Simple "Hello World!" image

Lets unpick what is happening here: Each line beginning ! and followed by a name creates a node of that kind. These can be followed by any number of name=value pairs, each of which adds an attribute to the preceding node. Values are vectors of any combination of numbers or Unicode strings surrounded by single or double quote characters. Semicolons ; are used to separate multiple items in a vector. Here the size attribute of !window and the point attribute of !text are both two-item numeric vectors. All of the other values are single-item vectors. There are no non-vector values in Flitter.

Nodes form a tree, with each allowed to have multiple child nodes. The structure of this tree is created through indentation. Any number of indented nodes given below another node will become children of that node. All children of a specific node must be indented to the same level. Indenting in further, as has been done with the !text node, causes a new parent/child relationship. So the !canvas node is a child of !window and the !text node is a child of !canvas. There is no specific rule to how many spaces you use for indenting as long as it is consistent within a block. We’re using 2 spaces in this tutorial to save space, 4 is probably more common. You can use hard tabs, but it is not recommended unless your editor enforces this and never mixes spaces into the indentation.

Names, including node kinds and attributes, may be any Unicode alphabetic character or an underscore, followed by any number of Unicode alphanumeric characters or underscores. The Flitter language itself only constructs trees of nodes and has no notion of which kinds or attributes are correct at any point in those trees. However, after a tree has been created, it is passed off to the rendering framework which then interprets the kinds, attributes and values to render some output.

In this case the renderer is doing the following:

  • A window with a 16:9 aspect ratio has been created. This will be kept open as long as Flitter is running and the program continues to contain the !window node. The window can be resized and moved around, but will retain this 16:9 aspect and cannot be manually closed. The window has a black background.

  • Inside this window, a Skia 1280 x 720 pixel !canvas has been created. It is initially filled with transparent pixels.

  • The canvas renderer draws some text (!text text="Hello world!"), in Helvetica (the default typeface family) at 100px (font_size=100), centred within the window (point=640;360), and in white (color=1).

The rendering roughly proceeds down through the tree drawing everything that needs to be drawn, and then works back up compositing the elements together. So the text is drawn inside the canvas, and then this canvas is drawn into the window.

To a degree, the Flitter tree of nodes is similar to an HTML DOM tree of elements. However, there are no text nodes as it is not a markup language. In many ways, Flitter is more similar to Scalable Vector Graphics (SVG).

Add the following line to the code:

!window size=1280;720
  !canvas
    !text text="Hello world!" font_size=100 point=630;370 color=1;0;0
    !text text="Hello world!" font_size=100 point=640;360 color=1

You should see the window now displays the original message in white, overlaid on top of the same text in red drawn with an offset. The result is a red “shadow” to the original text. There are some immediately useful things to learn from this:

  • Drawing generally proceeds downwards through the file, with later elements drawn on top of earlier ones.

  • The drawing canvas follows the common document convention of the origin being at the top left and the y axis pointing down. So 370 is lower down in the window than 360 and 630 is further left than 640.

  • Colors can be specified as either 1- or 3-item vectors. If given as a single value then the number represents a gray level from 0 to 1, if given as three values then the number represents an RGB triplet, also in the range 0 to 1.

"Hello World!" written in white with a redshadow

We can pull out some of the duplicated values in these two text nodes so that the intention is more clear. Try changing the code to the following (be careful of the new indentation):

!window size=1280;720
  !canvas
    !group font_size=100 translate=640;360 color=1
      !text text="Hello world!" point=-10;10 color=1;0;0
      !text text="Hello world!"

Here we place the two !text nodes inside a !group node that abstracts out the common font_size, changes the drawing origin with translate and sets a default color. The first !text node overrides this default color and, specifies a drawing point offset from this origin, 10px to the left and 10px down. The second !text node doesn’t specify a point at all, which causes it to be drawn at the group origin and, without a color attribute, it will be drawn with the group color.

!group nodes alter the drawing context for the nodes that they contain. They are able to change the local transformation matrix that establishes the drawing coordinate system, including rotating and scaling; change the default drawing color, and various other paint properties like line width; and set default font properties, including font size, the typeface family and weight. We still have to specify the actual text to be drawn at both nodes as this is individual to each !text node.

Try adding a final line to this program (again, note the indentation level):

!window size=1280;720
  !canvas
    !group font_size=100 translate=640;360 color=1
      !text text="Hello world!" point=-10;10 color=1;0;0
      !text text="Hello world!"
    !text text="Figure 1:" point=100;100 color=1

This new piece of text is drawn much smaller in the top left of the window. This is because it has reverted to the default font size, which is just 20px, and the default drawing origin of the top left. In fact, if we had left off the color=1 attribute then it wouldn’t have appeared at all, as the default drawing color is black. None of the drawing context introduced by the !group node is retained outside of it.

"Hello World!" written in white with a red shadow and "Figure 1" small in thetop-left corner

An important lesson to learn from this tiny example is that both block structure (this is in that) and context (like origin and paint color) are managed through indentation in Flitter. There are no braces or close tags, and no need to explicitly save and restore context.

Named Values

So far, the code we’ve written looks more like configuration data than a program. Let’s try using some of the features we would normally associate with a programming language.

The concept of named values is common across programming, usually in the form of variables. Flitter has no variables as it is a pure functional programming language. However, we can still give names to values. This has benefits in readability and in sharing common calculations.

We introduce names into programs with let expressions:

let SIZE=1280;720

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      !text text="Hello world!" point=-10;10 color=1;0;0
      !text text="Hello world!"
    !text text="Figure 1:" point=100;100 color=1

A let expression declares a new name, bound to a value, that is available for the remainder of the current block scope. In this case, we have introduced the let at the top of the outermost scope, so this name will be available anywhere inside the program. We use this new SIZE name to set the size of the window, and also to calculate the middle of the canvas when we move the origin in the group.

The calculation of SIZE/2 divides both items of the SIZE vector by 2 at the same time, giving us 640;360 as the resulting value for the translate attribute. All mathematical operators and functions in Flitter operate on entire vectors at once. Generally, when one vector is smaller than another the items of the smaller vector are repeated as necessary to complete the calculation. So, here, SIZE/2 is equivalent to (1280;720)/(2;2) and the division proceeds piecewise.

Functions

We could abstract out the text "Hello world!" into a name to avoid using it twice, but let’s introduce a function to abstract out the repeated !text nodes as well.

let SIZE=1280;720

func text3d(text, offset, shadow_color)
  !text text=text point=-offset;offset color=shadow_color
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      text3d("Hello world!", 10, 1;0;0)
    !text text="Figure 1:" point=100;100 color=1

We introduce a new function with func. This is followed by the name of the new function and its parameters. The body of the function is simply a sequence of indented expressions below it. We call the function in the familiar way.

But what is actually happening here? The function call evaluates to two !text nodes. These are composed into a 2-item vector and this is the return value of the function. At the point that we call it, that 2-element vector becomes the value being appended to the enclosing !group node.

In fact, wherever multiple expressions are given on sequential lines, they represent a sequence that is implicitly composed together into a single vector, and whenever we indent one expression from another we introduce an implicit append operator. Even our original simple program consisted of a series of node expressions, vector compositions and node append operations. In fact, each attribute set – such as size= – is another implicit binary operator, taking a node on the left side and an attribute value on the right. So there was a great deal more execution happening in our original code than it appeared.

For Loops

It would be great if instead of this simple shadow we could fill in the gap between the shadow and the text to create a solid 3D effect. We could achieve this by drawing more pieces of text in the shadow_color at different offsets.

We’ll use a for loop to do this:

let SIZE=1280;720

func text3d(text, offset, shadow_color)
  for i in offset..0|-1
    !text text=text point=-i;i color=shadow_color
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      text3d("Hello world!", 10, 1;0;0)
    !text text="Figure 1:" point=100;100 color=1

A for loop iterates over a source vector given after the in keyword, binding the value of each item to the name given between for and in, and then evaluating the body of the loop. As with function definitions, the body is indented.

We have made use of a range expression to provide the source vector for this loop. Ranges specify a start value, a stop value, and a step size. The stop value is not included in the vector. The | step value is optional and taken as 1 if not given. The start value is also optional and will be taken as 0 if not given. Thus, ..n is a quick way to get the values 0 to n-1. This range runs from offset towards 0, stepping by -1 each time. With offset equal to 10, this gives the vector 10;9;8;7;6;5;4;3;2;1.

The for loop iterates across this vector, binding i to the next value on each iteration. Loops evaluate to a vector composed from the result of each evaluation of the loop body. The result of this loop is a 10-item vector of !text nodes and these are then composed together with the following !text node so that the function returns a vector of 11 nodes. These 11 nodes are then appended to the group.

"Hello World!" written in white with a 3D solid effect and "Figure 1" smallin the top-left corner

Note

Flitter is a strict functional programming language, and so ranges are fully evaluated before use. For this reason, ranges must include a stop value and the range ..1000000 will create a 1 million item long numeric vector.

The only exception to strict evaluation is the usual short-circuit logical operations (see Operators in the language documentation).

Animation

While this code is starting to look more like a program, it still results in unchanging graphical output. In fact, Flitter is able to recognise that this program is entirely static and it will be compiled down to a single literal node tree – the function call is inlined and the loop unrolled.

Let’s introduce some animation. Animation in Flitter is achieved by writing a program that introduces some visual change linked to time. The main way that we incorporate time is with the use of the beat global name:

let SIZE=1280;720

func text3d(text, offset, shadow_color)
  for i in offset..0|-1
    !text text=text point=-i;i color=shadow_color
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      text3d("Hello world!", 10, hsv(beat/10;1;1))
    !text text="Figure 1:" point=100;100 color=1

beat provides a monotonically increasing value linked to the current program tempo. The default tempo is 120bpm, or 2 beats per second. Here we are calling the hsv() function with a 3-item vector of hue, saturation and value. Hue is calculated by dividing the current beat counter by 10. At 120bpm it will take 5 seconds to cycle around the hue wheel.

"Hello World!" written in white with a 3D effect that animates changingcolor through a spectrum and "Figure 1" small in the top-leftcorner

Let’s make our example a bit more trippy by animating the individual pieces of text that make up the 3D shadow:

let SIZE=1280;720

func text3d(text, offset, start_hue)
  for i in offset..0|-1
    !text text=text point=-i;i color=hsv(start_hue-i/offset;1;1)
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      text3d("Hello world!", 100, beat/10)
    !text text="Figure 1:" point=100;100 color=1

Now we are calling the hsv() function inside the loop for each text node, using a starting hue passed in as an argument to the function and offsetting this slightly for each iteration. The starting hue changes with the beat counter so that we get a constantly moving spectrum shadow. We’ve increased the offset to 100 so that the effect is more apparent.

As a further flourish, let’s make the 3D shadow fade into the distance:

let SIZE=1280;720

func text3d(text, offset, start_hue)
  for i in offset..0|-1
    let k=i/offset
    !text text=text point=-i;i color=hsv(start_hue-k;1;1-k)
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      text3d("Hello world!", 100, beat/10)
    !text text="Figure 1:" point=100;100 color=1

Note that we’ve introduced a new name, k, inside the body of the for loop. Names can be introduced with let in any expression sequence. The name will only be bound within that sequence (and only for expressions following the let). Therefore, outside of the loop the name k is not defined.

"Hello World!" written in white with a long animated rainbow 3D shadow and"Figure 1" small in the top-left corner

Template Functions

We are effectively using the text3d() function to define a new kind of compound node. This is such a common pattern, that Flitter provides some syntactic sugar to make the intention of the code easier to read:

let SIZE=1280;720

func text3d(_, text, offset, start_hue)
  for i in offset..0|-1
    let k=i/offset
    !text text=text point=-i;i color=hsv(start_hue-k;1;1-k)
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      @text3d text="Hello world!" offset=100 start_hue=beat/10
    !text text="Figure 1:" point=100;100 color=1

The @ character introduces a template function call. It is followed by the name of the function and then a number of named arguments, using the same convention as node attributes.

Note that we’ve added a new initial parameter to the text3d() function definition. If a template function call has an indented sequence of expressions, then the result vector from evaluating those expressions is passed in as the first argument. This allows a template function call to be provided with “child” nodes that it can manipulate and return – often they are appended as children to whatever node or nodes the function creates. In this case, we do not expect any such nodes and will ignore any that exist so, by convention, we name this parameter _ to indicate that it is unused.

The result is a normal function call, but the idea that we mean this to be a new kind of user-defined node is made clearer. In fact, we can also provide default values to the parameters in the function definition and this allows us to skip arguments in the call:

let SIZE=1280;720

func text3d(_, text, offset=100, start_hue=0)
  for i in offset..0|-1
    let k=i/offset
    !text text=text point=-i;i color=hsv(start_hue-k;1;1-k)
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      @text3d text="Hello world!" start_hue=beat/10
    !text text="Figure 1:" point=100;100 color=1

The named arguments can be given in any order. Any function parameters that lack a default value and that are not specified as arguments will be given the special value null, which is the empty vector.

If Expressions

This new version of text3d() is great, but we can no longer specify a simple colored shadow. We can make the code switch between different pieces of behaviour with an if expression:

let SIZE=1280;720

func text3d(_, text, offset=100, start_hue, shadow_color)
  for i in offset..0|-1
    let k=i/offset
    if start_hue != null
      !text text=text point=-i;i color=hsv(start_hue-k;1;1-k)
    else
      !text text=text point=-i;i color=shadow_color*(1-k)
  !text text=text

!window size=SIZE
  !canvas
    !group font_size=100 translate=SIZE/2 color=1
      @text3d text="Hello world!" shadow_color=1;0;0 -- start_hue=beat/10
    !text text="Figure 1:" point=100;100 color=1

Here we’ve removed the default value from start_hue and added back in a shadow_color parameter again. Inside the loop we test whether start_hue is the null vector, which would indicate that it has not been supplied as an argument, and draw the shadow text using shadow_color instead. We retain the fading behaviour by multiplying this color by 1-k so that it fades to black. As colors are just numerical vectors, we can use normal mathematical operations on them.

We’ve added a shadow_color argument in the template function call and also put -- in front of the previous start_hue argument. This begins a comment block that extends to the end of the line. Thus, putting -- in front of any code will cause the parser to ignore it.

Summary

This tutorial has introduced most of the important features of programming with Flitter. The remaining language features are documented in the language documentation. It is important to remember that the language itself does not render any output and that expressions like !text are not instructions to draw text, but node expressions to be gathered up into a tree that is then passed to the rendering framework. Individual renderers will treat these nodes in different ways.