Amulet Logo

Amulet Manual

Introduction

Amulet is a Lua-based toolkit for experimenting with interactive graphics and audio. It provides a cross-platform API for drawing graphics, playing audio and responding to user input, and a command-line interpreter for running Amulet scripts.

It tries to be simple to use, but without making too many assumptions about how you want to use it.

Amulet currently runs on the following platforms:

Support for Android will be added at some point as well.

How to use this document

If you have some programming experience, but are new to Lua then the Lua primer should bring you up to speed.

The quickstart tutorial introduces basic concepts and walks you through drawing things on screen, playing audio and responding to user input.

The online editor contains numerous examples you can experiment with from your browser.

If you see a function argument with square brackets around it in a function signature, then that argument is optional.

Lua primer

Amulet code is written in Lua. The version of Lua used by default is LuaJIT-2 on Windows, Mac and Linux and Lua-5.1 on all other platforms.

What follows is a quick introduction to Lua. For more detail please see the Lua manual.

Comments:

-- This is a single line comment

--[[ This is
a multi-line
comment ]]

Local variables (block scope):

local x = 3.14 -- a number
local str = "a string"
local str2 = [[
a
multi-line
string]]
local bool = true -- a bool, can also be false
local y = nil -- nil value
local v1, v2 = 1, 2 -- create two local variables at once

Global variables:

score = 0
title = "My Game"

If-then-else:

local x = 2
if x > 1 then
    print("x > 1")
elseif x > 0 then
    print("0 < x <= 0")
else
    print("x <= 0")
end

The else part of an if-then-else executes only if the condition evaluates to false or nil.

While loop:

local n = 5
while n > 0 do
    print(n)
    n = n - 1
end
-- prints 5 4 3 2 1

Repeat-until loop:

local n = 0
repeat
    n = n + 1
    print(n)
until n == 5
-- prints 1 2 3 4 5

For loop:

for i = 1, 5 do
    print(i)
end
-- prints 1 2 3 4 5
for j = 10, 1, -2 do
    print(j)
end
-- prints 10 8 6 4 2

You can break out of a loop using the break statement:

for j = 1, 10 do
    print(j)
    if j == 5 then
        break
    end
end
-- prints 1 2 3 4 5

Operators:

The arithmetic operators are: +, -, *, /, ^ (exponent) and % (modulo).

The relational operators are: ==, ~= (not equal), <, >, <= and >=

The logical operators are: and, or and not.

The string concatenation operator is two dots (e.g. "abc".."def").

Tables:

Tables are the only data structure in Lua. They can be used as key-value maps or arrays.

Keys and values can be of any type except nil.

local t = {} -- create empty table
t["score"] = 10
t[1] = "foo"
t[true] = "x"

-- this creates a table with 2 string keys:
local t2 = {foo = "bar", baz = 123}

Special syntax is provided for string keys:

local t = {}
t.score = 10
print(t.score)

The # operator returns the length of an array and array indices start at 1 by default.

local arr = {3, 4, 5}
for i = 1, #arr do
    print(arr[i])
end
-- prints 3, 4, 5
table.insert(arr, 6) -- appends 6 to end of arr
table.remove(arr, 1) -- removes the first element of arr
-- arr is now {4, 5, 6}

Setting a key's value to nil removes the key and indexing a missing key returns nil.

You can iterate over all key/value pairs using the pairs function. Note that the order is not preserved.

local t = {a = 1, b = 2, c = 3}
for k, v in pairs(t) do
    print(k..":"..v)
end
-- prints a:1 c:3 b:2

To iterate over an array table, keeping the order, use ipairs:

local arr = {"a", "b", "c"}
for k, v in ipairs(arr) do
    print(k..":"..v)
end
-- prints 1:a 2:b 3:c

Functions:

function factorial(n)
    if n <= 1 then
        return 1
    else
        return n * factorial(n-1)
    end
end
print(factorial(3)) -- prints 6

Functions are values in Lua so they can be assigned to variables:

local add = function(a, b)
    return a + b
end
print(add(1, 2)) -- prints 3

Special syntax is provided for adding functions to tables:

local t = {}
function t.say_hello()
    print("hello")
end

This is equivalent to:

local t = {}
t.say_hello = function()
    print("hello")
end

Special syntactic sugar allows you to use function fields like methods in object oriented languages:

The code:

function t:f()
    ...
end

is equivalent to:

function t.f(self)
    ...
end

and the code:

t:f()

is equivalent to:

t.f(t)

For example:

local t = {x = 3}
function t:print_x()
    print(self.x)
end
t:print_x()   -- prints 3
t.x = 4
t:print_x()   -- prints 4

If a function call has only a single string or table argument, the parentheses can be omitted:

local
function append_z(str)
    return str.."z"
end
print(append_z"xy") -- prints xyz

local
function sum(values)
    local total = 0
    for _, value in ipairs(values) do
        total = total + value
    end
    return total
end
print(sum{3, 1, 6}) -- prints 10

Functions may also return multiple values:

function f()
    return 1, 2
end
local x, y = f()
print(x + y) -- prints 3

Quickstart tutorial

Installing Amulet

Windows:

Download the Windows installer and run it. Or, if you prefer, download the Windows zip archive, extract it to a directory of your choice and add that directory to your PATH.

Mac:

Download the OSX pkg file and run it. Or, if you prefer, download the OSX zip archive, extract it to a directory of your choice and add that directory to your PATH.

Linux:

Download and extract the Linux zip archive to a directory of your choice. Then add the directory to your PATH. The amulet executable is for x86_64. If you're running a 32 bit system, rename the file amulet.i686 to amulet.

Online editor: There is also an online editor which you can use without having to download or install anything. However be aware that there are some limitations when using the online editor: you can't load image or audio files and only one lua module is allowed per project. These restrictions do not apply when exporting to HTML from the desktop version.

Running a script

Create a text file called main.lua containing the following:

log("It works!")

Open a terminal ("command prompt" on Windows) and change to the directory containing the file. Then type "amulet main.lua":

> amulet main.lua
main.lua:1: It works!

If you see the text "main.lua:1: It works!", Amulet is installed and working.

If you are using the online editor, just type this into the main text area and click "Run".

Creating a window

Type the following into main.lua:

local win = am.window{
    title = "Hi",
    width = 400,
    height = 300,
    clear_color = vec4(1, 0, 0.5, 1)
}

and run it as before. This time a bright pink window should appear.

Rendering text

Add the following line to main.lua after the line that creates the window:

win.scene = am.text("Hello!")

This assigns a scene graph to the window. The scene has a single text node. The window will render its scene graph each frame.

Transformation nodes

Change the previous line to:

win.scene = 
    am.translate(150, 100)
    ^ am.scale(2)
    ^ am.rotate(math.rad(90))
    ^ am.text("Hello!")

This adds translate, scale and rotate nodes as parents of the text node. These nodes transform the position, size and rotation of all their children. The resulting scene graph looks like this:

The translate node moves its descendents to the right 150 and up 100 (by convention the y axis increases in upward direction). The scale node doubles its descendent's size and the rotate node rotates its descendents by 90 degrees (math.rad converts degrees to radians). Finally the text node renders some text to the screen.

When you run the program you should see something like this:

Actions

Add the following to the end of main.lua:

win.scene:action(function(scene)
    scene"rotate".angle = am.frame_time * 4
end)

When you run it the text will spin.

This code adds an action to the scene, which is a function that's run once per frame. Actions can be added to any scene node. In this case we've added it to the node win.scene, which is the top translate node of our scene graph. The node to which an action is attached is passed as an argument to the action function.

The line:

scene"rotate".angle = am.frame_time * 4

first finds a node with the tag "rotate" in the scene graph. By default nodes have tags that correspond to their names, so this returns the rotate node. You can also add your own tags to nodes using the tag method which we'll discuss in more detail in the next section.

Then we set the angle property of the rotate node to the current frame time (the time at the beginning of the frame, in seconds) times 4.

Note that scene"rotate" could also be written as scene("rotate"). The first form takes advantage of some Lua syntactic sugar that allows function parenthesis to be omitted if the argument is a single literal string.

Since this code is run each frame, it causes the text to spin.

Here is the complete code listing:

local win = am.window{
    title = "Hi",
    width = 400,
    height = 300,
    clear_color = vec4(1, 0, 0.5, 1)
}
win.scene = 
    am.translate(150, 100)
    ^ am.scale(2)
    ^ am.rotate(math.rad(90))
    ^ am.text("Hello!")
win.scene:action(function(scene)
    scene"rotate".angle = am.frame_time * 4
end)

Drawing shapes

Here is a simple program that draws 2 red circles on either side of a yellow square on a blue background.

local red = vec4(1, 0, 0, 1)
local blue = vec4(0, 0, 1, 1)
local yellow = vec4(1, 1, 0, 1)

local win = am.window{
    title = "Hi",
    width = 400,
    height = 300,
    clear_color = blue,
}

win.scene =
    am.group()
    ^ {
        am.translate(-150, 0)
        ^ am.circle(vec2(0, 0), 50, red)
        ,
        am.translate(150, 0)
        ^ am.circle(vec2(0, 0), 50, red)
        ,
        am.translate(0, -25)
        ^ am.rect(-50, -50, 50, 50, yellow)
    }

This time, we've created variables for the different colours we'll need. In Amulet colours are 4-dimensional vectors. Each component of the vector represents the red, green, blue and alpha intensity of the colour and ranges from 0 to 1.

The scene graph has a group node at the top. group nodes don't have any effect on the rendering and are only used to group other nodes together. The group node has 3 children, each of which is a translate node with a shape child. The scene graph looks like this:

Instead of building the scene graph using the ^ operator as we've done above, we can also do it step-by-step using the append method, which adds a node to the child list of another node:

local circle1_node = am.translate(-150, 0)
circle1_node:append(am.circle(vec2(0, 0), 50, red))

local circle2_node = am.translate(150, 0)
circle2_node:append(am.circle(vec2(0, 0), 50, red))

local rect_node = am.translate(0, -25)
rect_node:append(am.rect(-50, -50, 50, 50, yellow))

local group_node = am.group()
group_node:append(circle1_node)
group_node:append(circle2_node)
group_node:append(rect_node)

win.scene = group_node

This results in the exact same scene graph.

Responding to key presses

Let's change the above program so that the left circle only appears while the A key is down and the right circle only appears while the B key is down.

In order to distinguish the two circles in the scene graph we'll give them different tags.

Change the scene setup code to look like this:

win.scene =
    am.group()
    ^ {
        am.translate(-150, 0):tag"left_eye"
        ^ am.circle(vec2(0, 0), 50, red)
        ,
        am.translate(150, 0):tag"right_eye"
        ^ am.circle(vec2(0, 0), 50, red)
        ,
        am.translate(0, -25)
        ^ am.rect(-50, -50, 50, 50, yellow)
    }

The only difference is the addition of :tag"left_eye" and :tag"right_eye". These add the tag "left_eye" to the translate node which is the parent of the left circle node and "right_eye" to the right translate node which is the parent of the right circle node.

Now add the following action:

win.scene:action(function(scene)
    scene"left_eye".hidden = not win:key_down"a"
    scene"right_eye".hidden = not win:key_down"b"
end)

The hidden field of a node determines whether it is drawn or not. Each frame we set the hidden field of the left and right translate nodes to whether the A or B keys are being pressed. win:key_down(X) returns true if key X was being held down at the start of the frame.

You may notice that the three shapes appear briefly when the window is first shown and then disappear immediately. This is because actions only start running in the next frame, so the shapes are only hidden on the second frame. To fix this we can add the following lines either before or after we add the action (it doesn't matter where):

win.scene"left_eye".hidden = true
win.scene"right_eye".hidden = true

Here is the complete code listing:

local red = vec4(1, 0, 0, 1)
local blue = vec4(0, 0, 1, 1)
local yellow = vec4(1, 1, 0, 1)

local win = am.window{
    title = "Hi",
    width = 400,
    height = 300,
    clear_color = blue,
}

win.scene =
    am.group()
    ^ {
        am.translate(-150, 0):tag"left_eye"
        ^ am.circle(vec2(0, 0), 50, red)
        ,
        am.translate(150, 0):tag"right_eye"
        ^ am.circle(vec2(0, 0), 50, red)
        ,
        am.translate(0, -25)
        ^ am.rect(-50, -50, 50, 50, yellow)
    }

win.scene:action(function(scene)
    scene"left_eye".hidden = not win:key_down"a"
    scene"right_eye".hidden = not win:key_down"b"
end)

win.scene"left_eye".hidden = true
win.scene"right_eye".hidden = true

Drawing sprites

Sprites can be drawn using sprite nodes. To create a sprite node, pass the name of a .png or .jpg file to the am.sprite() function and add it to your scene graph.

Let's create a beach scene using the following two images:

beach.jpg

beach.jpg

ball.png

ball.png

Download these images and copy them to the same directory as your main.lua file. Then copy the following into main.lua:

local win = am.window{
    title = "Beach",
    width = 400,
    height = 300,
}

win.scene = 
    am.group()
    ^ {
        am.sprite"beach.jpg"
        ,
        am.translate(0, -60)
        ^ am.sprite"ball.png"
    }

Run the program and you should get something like this:

The children of any scene node are always drawn in order, so first the beach.jpg sprite node is drawn and then the ball.png sprite, with it's corresponding translation, is drawn. This ensures the ball is visible on the beach. If we draw the beach second it would obscure the ball.

If you are using the online editor then you won't be able to load image files. Instead you'll need to draw the sprites using text. See the "Beach ball" example in the online editor for an equivalent example that doesn't load any image files. See also the documentation for am.sprite for how to create sprites with text.

Responding to mouse clicks

Let's make the ball bounce when we click it. We'll add a rotate node so we can make the ball spin when it's in the air. We'll also tag the ball's translate and rotate nodes so we can easily access them:

win.scene = 
    am.group()
    ^ {
        am.sprite"beach.jpg"
        ,
        am.translate(0, -60):tag"ballt"
        ^ am.rotate(0):tag"ballr"
        ^ am.sprite"ball.png"
    }

Now add an action to animate the ball when it's clicked:

-- ball state variables:
local ball_pos = vec2(0, -60)
local ball_angle = 0
local velocity = vec2(0)
local spin = 0

-- constants:
local min_pos = vec2(-180, -60)
local max_pos = vec2(180, 500)
-- min and max impulse velocity:
local min_v = vec2(-50, 150)
local max_v = vec2(50, 300)
local gravity = vec2(0, -500)

win.scene:action(function(scene)
    -- check if the left mouse button was pressed
    if win:mouse_pressed"left" then
        local mouse_pos = win:mouse_position()
        -- check if the mouse click is on the ball
        if math.distance(mouse_pos, ball_pos) < 50 then
            -- compute a velocity based on click position
            local dir = math.normalize(ball_pos - mouse_pos)
            velocity = dir * 300
            velocity = math.clamp(velocity, min_v, max_v)
            -- set a random spin
            spin = math.random() * 4 - 2
        end
    end

    -- update the ball position
    ball_pos = ball_pos + velocity * am.delta_time

    -- if the ball is on the ground, set the
    -- velocity and spin to zero.
    if ball_pos.y <= -60 then
        velocity = vec2(0)
        spin = 0
    end

    -- clamp the ball position so it doesn't disappear
    -- off the edge of the screen
    ball_pos = math.clamp(ball_pos, min_pos, max_pos)

    -- update the ball angle
    ball_angle = ball_angle + spin * am.delta_time

    -- update the ball translate and rotate nodes
    scene"ballt".position2d = ball_pos
    scene"ballr".angle = ball_angle

    -- apply gravity to the velocity
    velocity = velocity + gravity * am.delta_time
end)

First we create some variables to keep track of the ball's state. We need to track its position, angle, velocity and spin (angular velocity). The ball_pos and velocity variables are 2 dimensional vectors, since we want to track position and velocity along both the x and y axes. We could have made separate variables for the x and y components, but using a vec2 is more concise. Note that if the values of the x and y components of the vector are the same, we only need to give the value once, so we just need to write vec2(0) when initialising the velocity instead of vec2(0, 0).

We also create some constants that we'll need. We define the minimum and maximum positions of the ball (min_pos, max_pos). We also define the minimum and maximum impulse velocity to apply to the ball when it's clicked (min_v, max_v). And finally we create a constant for gravity.

Next comes the action itself. The comments in the body of the action should help you work out what's going on, but here are some things to note:

Playing audio

To play a sound, attach a play action to a scene node in the current scene. For example to play a sound file bounce.ogg we could do the following:

scene:action(am.play("bounce.ogg"))

The file bounce.ogg must exist in the same directory as the script.

To have a sound loop, pass in a second argument of true, like so:

win.scene:action(am.play("ocean.ogg", true))

Note that Amulet only reads Ogg Vorbis audio files.

Here are some audio files you can try out with the beach ball game we made previously:

And here is the complete code listing for the beach ball game with sounds included. Note that we need an extra variable on_ground to keep track of whether the ball was on the ground, so we don't play the landing sound every frame.

local win = am.window{
    title = "Beach",
    width = 400,
    height = 300,
}

win.scene = 
    am.group()
    ^ {
        am.sprite"beach.jpg"
        ,
        am.translate(0, -60):tag"ballt"
        ^ am.rotate(0):tag"ballr"
        ^ am.sprite"ball.png"
    }

local ball_pos = vec2(0, -60)
local ball_angle = 0
local velocity = vec2(0)
local spin = 0
local min_pos = vec2(-180, -60)
local max_pos = vec2(180, 500)
local min_v = vec2(-50, 150)
local max_v = vec2(50, 300)
local gravity = vec2(0, -500)
local on_ground = true

win.scene:action(function(scene)
    -- check if the left mouse button was pressed
    if win:mouse_pressed"left" then
        local mouse_pos = win:mouse_position()
        -- check if the mouse click is on the ball
        if math.distance(mouse_pos, ball_pos) < 50 then
            -- compute a velocity based on click position
            local dir = math.normalize(ball_pos - mouse_pos)
            velocity = dir * 300
            velocity = math.clamp(velocity, min_v, max_v)
            -- set a random spin
            spin = math.random() * 4 - 2
            -- play bounce sound
            scene:action(am.play("bounce.ogg"))
            on_ground = false
        end
    end

    -- update the ball position
    ball_pos = ball_pos + velocity * am.delta_time

    -- if the ball lands on the ground, set the
    -- velocity and spin to zero.
    if ball_pos.y <= -60 and not on_ground then
        velocity = vec2(0)
        spin = 0
        -- play land sound
        scene:action(am.play("land.ogg"))
        on_ground = true
    end

    -- clamp the ball position so it doesn't disappear
    -- off the edge of the screen
    ball_pos = math.clamp(ball_pos, min_pos, max_pos)

    -- update the ball angle
    ball_angle = ball_angle + spin * am.delta_time

    -- update the ball translate and rotate nodes
    scene"ballt".position2d = ball_pos
    scene"ballr".angle = ball_angle

    -- apply gravity to the velocity
    velocity = velocity + gravity * am.delta_time
end)

-- play ocean sound in a loop
win.scene:action(am.play("ocean.ogg", true))

Math

Vectors

Amulet has built-in support for 2, 3 or 4 dimensional vectors. Vectors are typically used to represent things like position, direction or velocity in 2 or 3 dimensional space. Representing RGBA colors is another common use of 4 dimensional vectors.

In Amulet vectors are immutable. This means that once you create a vector, its value cannot be changed. Instead you need to construct a new vector.

Constructing vectors

To construct a vector use one of the functions vec2, vec3 or vec4. A vector may be constructed by passing its components as separate arguments to one of these functions, for example:

local velocity = vec3(1, 2, 3)

Passing a single number to a vector constructor will set all components of the vector to that value. For example:

local origin = vec2(0)

sets origin to the value vec2(0, 0).

It's also possible to construct a vector from a combination of other vectors and numbers. The new vector's components will be taken from the other vectors in the order they are passed in. For example:

local bottom_left = vec2(0)
local top_right = vec2(10, 100)
local rect = vec4(bottom_left, top_right)

sets rect to vec4(0, 0, 10, 100)

Accessing vector components

There are multiple ways to access the components of a vector. The first component can be accessed using any of the fields x, r or s; the second using any of the fields y, g or t; the third using any of the fields z, b or p; and the fourth using any of the fields w, a or q. Here are some examples:

local color = vec4(0.1, 0.3, 0.7, 0.8)
print(color.r..", "..color.g..", "..color.b..", "..color.a)
local point = vec2(5, 2)
print("x="..point.x..", y="..point.y)

A vector's components can also be accessed with 1-based integer indices.

Vectors support the Lua length operator (#), which returns the number of components of the vector (not its magnitude). This allows for iterating through the components of a vector of unknown size, for example:

local v = vec3(10, 20, 30)
for i = 1, #v do
    print(position[i])
end

Swizzle fields

Another way to construct vectors is to recombine the components of an existing vector using swizzle fields, which are special fields whose names consist of a combination of any of the component field characters. A new vector containing the named components will be returned. For example:

local dir = vec3(1, 2, 3)
print(dir.xy)
print(dir.rggb)
print(dir.zzys)

Running the above code results in the following output:

vec2(1, 2)
vec4(1, 2, 2, 3)
vec4(3, 3, 2, 1)

Note: You can pass vectors, matrices and quaternions directly to print or other functions that expect strings and they will be formatted appropriately.

Vector update syntax

Although you can't directly set the components of a vector, Amulet provides some syntactic sugar to make it easier to create a new vector from an existing vector that has only some fields modified. Say, for example, you had a 3 dimensional vector, v1, and you wanted to create a new vector, v2, that had the same components as v1, except for the y component, which you'd like to be 10. One way to do this would be to write:

v2 = vec3(v1.x, 10, v1.z)

but Amulet also allows you to write:

v2 = v1{y = 10}

You can use this syntax to "update" multiple components and it also supports swizzle fields. For example:

local v = vec4(1, 2, 3, 4)
v = v{x = 5, ba = vec2(6)}

This would set v to vec4(5, 2, 6, 6).

If the values of a swizzle field are going to be updated to the same value (as with ba above), you can just set the field to the value instead of constructing a vector. So the above could also have been written as:

v = v{x = 5, ba = 6}

Vector arithmetic

You can do arithmetic with vectors using the standard operators +, -, * and /. If both operands are vectors then they should have the same size and the operation is applied in a component-wise fashion, yielding a new vector of the same size. If one operand is a number then the operation is applied to each component of the vector, yielding a new vector of the same size as the vector operand. For example:

print(vec2(3, 4) + 1)
print(vec3(30) / vec3(3, 10, 5))
print(2 * vec4(1, 2, 3, 4))

produces the following output:

vec2(4, 5)
vec3(10, 3, 6)
vec4(2, 4, 6, 8)

Matrices

Amulet has built-in support for 2x2, 3x3 and 4x4 matrices. Matrices are typically used to represent transformations in 2 or 3 dimensional space such as rotation, scaling, translation or perspective projection.

Matrices, like vectors, are immutable.

Constructing matrices

Use one of the functions mat2, mat3 or mat4 to construct a 2x2, 3x3 or 4x4 matrix.

Passing a single number argument to one of the matrix constructors generates a matrix with all diagonal elements equal to the number and all other elements equal to zero. For example mat3(1) constructs the 3x3 identity matrix:

\[\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\]

You can also pass the individual elements of the matrix as arguments to one of the constructors. These can either be numbers or vectors or a mix of the two. As the constructor arguments are consumed from left to right, the matrix is filled in column by column. For example:

local m = mat3(1, 2, 3,
               4, 5, 6,
               7, 8, 9)

sets m to the matrix:

\[\begin{bmatrix} 1 & 4 & 7 \\ 2 & 5 & 8 \\ 3 & 6 & 9 \end{bmatrix}\]

Here's another example:

local m = mat4(vec3(1, 2, 3), 4,
               vec4(5, 6, 7, 8),
               vec2(9, 10), vec2(11, 12),
               13, 14, 15, 16)

This sets m to the matrix:

\[\begin{bmatrix} 1 & 5 & 9 & 13 \\ 2 & 6 & 10 & 14 \\ 3 & 7 & 11 & 15 \\ 4 & 8 & 12 & 16 \end{bmatrix}\]

Note: Matrix constructors are admittedly somewhat confusing, because when you write the matrix constructor in code the columns are layed out horizontally. This is however the convention used in the OpenGL Shader Language (GLSL).

A matrix can also be constructed by passing an existing matrix to one of the matrix construction functions. If the existing matrix is larger than the new one, the new matrix's elements come from the top-left corner of the existing matrix. Otherwise the top-left corner of the new matrix is filled with the contents of the existing matrix and the rest from the identity matrix. For example:

local m = mat4(mat2(1, 2, 3, 4))

will set m to the matrix:

\[\begin{bmatrix} 1 & 3 & 0 & 0 \\ 2 & 4 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}\]

Finally a 3x3 or 4x4 rotation matrix can be constructed from a quaternion by passing the quaternion as the single argument to mat3 or mat4 (see quaternions).

Accessing matrix components

The columns of a matrix can be accessed as vectors using 1-based integer indices. The Lua length operator can be used to determine the number of columns. For example:

local matrix = mat2(1, 0, 0, 2)
for i = 1, #matrix do
    print(matrix[i])
end

This would produce the following output:

vec2(1, 0)
vec2(0, 2)

Matrix arithmetic

As with vectors the +, -, * and / operators work with matrices too. When one operand is a number, the result is a new matrix of the same size with the operator applied to each element of the matrix. For example:

local m1 = 2 * mat2(1, 2, 3, 4)

sets m1 to the matrix:

\[\begin{bmatrix} 2 & 6 \\ 4 & 8 \end{bmatrix}\]

and:

local m2 = mat3(3) - 1

sets m2 to the matrix:

\[\begin{bmatrix} 2 & -1 & -1 \\ -1 & 2 & -1 \\ -1 & -1 & 2 \end{bmatrix}\]

When both operands are matrices, the + and - operators work in a similar way to vectors, with the operations applied component-wise. For example:

local m3 = mat2(1, 2, 3, 4) + mat2(0.1, 0.2, 0.3, 0.4)

sets m3 to the matrix:

\[\begin{bmatrix} 1.1 & 3.3 \\ 2.2 & 4.4 \end{bmatrix}\]

However, when both operands are matrices, the * operator computes the matrix product.

If the first operand is a vector and the second is a matrix, then the first operand is taken to be a row vector (a matrix with one row) and should have the same number of columns as the matrix. The result is the matrix product of the row vector and the matrix, which is another row vector.

Similarly if the first argument is a matrix and the second a vector, the vector is taken to be a column vector (a matrix with one column) and the result is the matrix product of the matrix and column vector, which is another column vector.

The / operator also works when both arguments are matrices and is equivalent to multiplying the first matrix by the inverse of the second.

Quaternions

Quaternions are useful for representing 3D rotations.

Like vectors and matrices they are immutable.

Constructing quaternions

The quat function is used to construct quaternions. The simplest way to construct a quaternion is to pass an angle (in radians) and a unit 3D vector representing the axis about which the rotation should occur. For example:

local q = quat(math.rad(45), vec3(0, 0, 1))

constructs a quaternion that represents a 45 degree rotation around the z axis. (math.rad converts degrees to radians).

If the axis argument is omitted then it is taken to be vec3(0, 0, 1), so the above is equivalent to:

local q = quat(math.rad(45))

This is a useful shortcut for 2D rotations in the xy plane.

A quaternion can also be constructed from Euler angles. Euler angles are rotations around the x, y and z axes, also known as pitch, yaw and roll. For example:

local q = quat(math.rad(30), math.rad(60), math.rad(20))

constructs a quaternion that represents the rotation you'd end up with if you first rotated 30 degrees around the x axis, then 60 degrees around the y axis and finally 20 degrees around the z axis.

If two unit vector arguments are given, then the quaternion represents the rotation that would be needed to rotate the one vector into into the other. For example:

local q = quat(vec3(1, 0, 0), vec3(0, 1, 0))

The above quaternion represents a rotation of 90 degrees in the xy plane, since it rotates a vector pointing along the x axis to one pointing along the y axis.

A quaternion can be constructed from a 3x3 or 4x4 matrix by passing the matrix as the single argument to quat.

A quaternion can also be converted to a 3x3 or 4x4 matrix by passing it as the single argument to the mat3 or mat4 functions (see mat-cons).

Finally a quaternion can be constructed from the coefficients of its real and imaginary parts:

local q = quat(w, x, y, z)

w is the real part and x, y and z are the coeffients of the imaginary numbers \(i\), \(j\) and \(k\).

Quaternion fields

The angle, axis, pitch, roll, yaw, w, x, y and z fields can be used to read the corresponding attributes of a quaternion.

Note: Quaternions use a normalized internal representation, so the value returned by a field might be different from the value used to construct the quaternion. Though the quaternion as a whole represents the equivalent rotation.

Quaternion operations

Quaternions can be multiplied together using the * operator. The result of multiplying 2 quaternions is the rotation that results from applying the first quaternion's rotation followed by the second quaternion's rotation.

Multiplying a quaternion by a vector rotates the vector. For example:

local v1 = vec3(1, 0, 0)
local q = quat(math.rad(90), vec3(0, 0, 1))
local v2 = q * v1

would set v2 to the vector vec3(0, 1, 0), which is v1 rotated 90 degrees in the xy plain.

A vec2 can be rotated in a similar way (the z component is assumed to be zero and the z component of the result is dropped, yielding another vec2).

Math functions

The following functions supplement the standard Lua math functions.

math.sign(n)

Returns +1 if n > 0, -1 if n < 0 and 0 if n == 0.

math.fract(v)

Returns the fractional part of v. If v is a vector it returns a vector of the same size with each component being the fractional part of the corresponding component in the original vector.

math.clamp(v, min, max)

Clamps a value v between min and max. v, min and max may be vectors. In this case each component is clamped based on the corresponding components in min and max.

math.randvec2()

Returns a vec2 with all components set to a random number between 0 and 1.

math.randvec3()

Returns a vec3 with all components set to a random number between 0 and 1.

math.randvec4()

Returns a vec4 with all components set to a random number between 0 and 1.

math.dot(vector1, vector2)

Returns the dot product of two vectors. The vectors must have the same size.

math.cross(vector1, vector2)

Returns the cross product of two 3 dimensional vectors.

math.normalize(vector)

Returns the normalized form of a vector (i.e. the vector that points in the same direction, but whose length is 1). If the given vector has zero length, then a vector of the same size is returned whose first component is 1 and whose remaining components are 0.

math.length(vector)

Returns the length of a vector.

math.distance(vector1, vector2)

Returns the distance between two vectors.

math.inverse(matrix)

Returns the inverse of a matrix.

math.lookat(eye, center, up)

Creates a 4x4 view matrix at eye, looking in the direction of center with the y axis of the camera pointing in the same direction as up.

math.perspective(fovy, aspect, near, far)

Creates a 4x4 matrix for a symmetric perspective-view frustum.

math.ortho(left, right, bottom, top [, near, far])

Creates a 4x4 orthographic projection matrix. near and far are the distance from the viewer of the near and far clipping plains (negative means behind the viewer). Their default values are -1 and 1.

math.oblique(angle, zscale, left, right, bottom, top [, near, far])

Creates a 4x4 oblique projection matrix. near and far are the distance from the viewer of the near and far clipping plains (negative means behind the viewer). Their default values are -1 and 1.

math.translate4(pos)

Creates a 4x4 translation matrix. pos should be a vec3.

math.scale4(scl)

Creates a 4x4 scale matrix. scl should be a vec3.

math.perlin(pos [, period])

Generate perlin noise. pos can be a 2, 3, or 4 dimensional vector, or a number. If the second argument is supplied then the noise will be periodic with the given period. period should be of the same type as pos and its components should be integers greater than 1.

The returned value is between -1 and 1.

math.simplex(pos)

Generate simplex noise. pos can be a 2, 3, or 4 dimensional vector, or a number.

The returned value is between -1 and 1.

math.mix(from, top, t)

Returns the linear interpolation between from and to determined by t. from and to can be numbers or vectors, and must be the same type. t should be a number between 0 and 1. from and to can also be quaternions. In this case math.mix returns the spherical linear interpolation of the two quaternions.

math.slerp(from, top, t)

Returns the spherical linear interpolation of the two quaternions from and to. t should be a number between 0 and 1. Unlike (math.mix)[#math.mix] this interpolation takes the shortest path.


Buffers and views

Buffers are contiguous blocks of memory. They are used for storing images, audio and vertex data, or anything else you like.

You can't access a buffer's memory directly. Instead you access a buffer through a view. Views provide a typed array-like interface to the buffer.

Buffers

am.buffer(size)

Returns a new buffer of the given size in bytes.

The buffer's memory will be zeroed.

The # operator can be used to retrieve the size of a buffer in bytes.

Fields:

Methods:

am.load_buffer(filename)

Loads the given file and returns a buffer containing the file's data, or nil if the file wasn't found.

am.base64_encode(buffer)

Returns a base64 encoding of a buffer as a string.

am.base64_decode(string)

Converts a base64 string to a buffer.

Views

buffer:view(type [, offset [, stride [, count]]])

Returns a view into buffer.

type can be one of the following:

type size (bytes) Lua value range internal range endianess
"float" 4 approx -3.4e38 to 3.4e38 same native
"vec2" 8 any vec2 same native
"vec3" 12 any vec3 same native
"vec4" 16 any vec4 same native
"byte" 1 -128 to 127 same N/A
"ubyte" 1 0 to 255 same N/A
"byte_norm" 1 -1.0 to 1.0 -127 to 127 N/A
"ubyte_norm" 1 0.0 to 1.0 0 to 255 N/A
"short" 2 -32768 to 32767 same native
"ushort" 2 0 to 65535 same native
"short_norm" 2 -1.0 to 1.0 -32767 to 32767 native
"ushort_norm" 2 0.0 to 1.0 0 to 65535 native
"ushort_elem" 2 1 to 65536 0 to 65535 native
"int" 4 -2147483648 to 2147483647 same native
"uint" 4 0 to 4294967295 same native
"uint_elem" 4 1 to 4294967296 0 to 4294967295 native

The _norm types map Lua numbers in the range -1 to 1 (or 0 to 1 for unsigned types) to integer values in the buffer.

The _elem types are specifically for element array buffers and offset the Lua numbers by 1 to conform to the Lua convention of array indices starting at 1.

All view types currently use the native platform endianess, which happens to be little-endian on all currently supported platforms.

The offset argument is the byte offset of the first element of the view. The default is 0.

The stride argument is the distance between consecutive values in the view, in bytes. The default is the size of the view type.

The count argument determines the number of elements in the view. The underlying buffer must be large enough to accommodate the elements with the given stride. The default is the maximum supported by the buffer with the given stride.

You can read and write to views as if they were Lua arrays (as with Lua arrays, indices start at 1). For example:

local buf = am.buffer(12)
local view = buf:view("float")
view[1] = 1.5 
view[2] = view[1] + 2

Attempting to read an index less than 1 or larger than the number of elements will return nil.

You can retrieve the number of elements in a view using the # operator.

View fields

view.buffer

The buffer associated with the view.

Readonly.

View methods

view:slice(n [, count])

Returns a new view of the same type as view that references the same buffer, but which starts at the nth element of view and continues for count elements. If count is omitted it is #view - n + 1 (i.e. it covers all the elements of view after and including the nth).

view:set(val [, start [, count]])

Bulk sets values in a view. This is faster than setting them one at a time.

If val is a number or vector then this sets all elements of view to val.

If val is a table then the elements are set to their corresponding values from the table.

As a special case, if val is a table of numbers and the view's type is a vector, then the elements of the table will be used to set the components of the vectors in the view. For example:

local verts = am.buffer(24):view("vec3")
verts:set{1, 2, 3, 4, 5, 6}
print(verts[1]) -- vec3(1, 2, 3)
print(verts[2]) -- vec3(4, 5, 6)

Finally if val is another view then the elements are set to the corresponding values from that view. The views may be of different types as long as they are "compatible". The types are converted as if each element were set using the Lua code view1[i] = view2[i]. This means you can't set a number view to a vector view or vice versa.

If start is given then only elements at that index and beyond will be set. The default value for start is 1.

If count is given then at most that many elements will be set.

am.float_array(table)

Returns a float view to a newly created buffer and fills it with the values in the given table.

am.byte_array(table)

Returns a byte view to a newly created buffer and fills it with the values in the given table.

am.ubyte_array(table)

Returns a ubyte view to a newly created buffer and fills it with the values in the given table.

am.byte_norm_array(table)

Returns a byte_norm view to a newly created buffer and fills it with the values in the given table.

am.ubyte_norm_array(table)

Returns a ubyte_norm view to a newly created buffer and fills it with the values in the given table.

am.short_array(table)

Returns a short view to a newly created buffer and fills it with the values in the given table.

am.ushort_array(table)

Returns a ushort view to a newly created buffer and fills it with the values in the given table.

am.short_norm_array(table)

Returns a short_norm view to a newly created buffer and fills it with the values in the given table.

am.ushort_norm_array(table)

Returns a ushort_norm view to a newly created buffer and fills it with the values in the given table.

am.int_array(table)

Returns an int view to a newly created buffer and fills it with the values in the given table.

am.uint_array(table)

Returns a uint view to a newly created buffer and fills it with the values in the given table.

am.int_norm_array(table)

Returns an int_norm view to a newly created buffer and fills it with the values in the given table.

am.uint_norm_array(table)

Returns a uint_norm view to a newly created buffer and fills it with the values in the given table.

am.ushort_elem_array(table)

Returns a ushort_elem view to a newly created buffer and fills it with the values in the given table.

am.uint_elem_array(table)

Returns a uint_elem view to a newly created buffer and fills it with the values in the given table.

am.vec2_array(table)

Returns a vec2 view to a newly created buffer and fills it with the values in the given table.

The table may contain either vec2s or numbers (though not a mix). If the table contains numbers they are used for the vector components and the resulting view will have half the number of elements as there are numbers in the table.

am.vec3_array(table)

Returns a vec3 view to a newly created buffer and fills it with the values in the given table.

The table may contain either vec3s or numbers (though not a mix). If the table contains numbers they are used for the vector components and the resulting view will have a third the number of elements as there are numbers in the table.

am.vec4_array(table)

Returns a vec4 view to a newly created buffer containing the values in the given table.

The table may contain either vec4s or numbers (though not a mix). If the table contains numbers they are used for the vector components and the resulting view will have a quarter the number of elements as there are numbers in the table.

am.struct_array(size, spec)

Returns a table of views of the given size as defined by spec. spec is a sequence of view name (a string) and view type (also a string) pairs. The returned table can be passed directly to the am.bind function. The views all use the same underlying buffer.

For example:

local arr = am.struct_array(3, {"vert", "vec2", "color", "vec4"})
arr.vert:set{vec2(-1, 0), vec2(1, 0), vec2(0, 1)} 
arr.color:set(vec4(1, 0, 0.5, 1))

Windows and input

Creating a window

am.window(settings)

Creates a new window and returns a handle to it. settings is a table with any of the following fields:

Window fields

window.left

The x coordinate of the left edge of the window in the window's default coordinate system.

Readonly.

window.right

The x coordinate of the right edge of the window, in the window's default coordinate system.

Readonly.

window.bottom

The y coordinate of the bottom edge of the window, in the window's default coordinate system.

Readonly.

window.top

The y coordinate of the top edge of the window, in the window's default coordinate system.

Readonly.

window.width

The width of the window in the window's default coordinate system. This will always be equal to the width setting supplied when the window was created if the letterbox setting is enabled. Otherwise it may be larger, but it will never be smaller than the width setting.

Readonly.

window.height

The height of the window in the window's default coordinate space. This will always be equal to the height setting supplied when the window was created if the letterbox setting is enabled. Otherwise it may be larger, but it will never be smaller than the height setting.

Readonly.

window.pixel_width

The real width of the window in pixels.

Readonly.

window.pixel_height

The real height of the window in pixels

Readonly.

window.mode

See window settings.

Updatable.

window.clear_color

See window settings.

Updatable.

window.letterbox

See window settings.

Updatable.

window.lock_pointer

See window settings.

Updatable.

window.show_cursor

See window settings.

Updatable.

window.scene

The scene node currently attached to the window. This scene will be rendered to the window each frame.

Updatable.

window.projection

See window settings.

Updatable.

Closing a window

window:close()

Closes the window and quits the application if this was the only window.

Detecting when a window resizes

window:resized()

Returns true if the window's size changed since the last frame.

Detecting key presses

The following functions detect physical key states and are not affected by the keyboard layout preferences selected in the OS (except when targetting HTML).

Keys are represented by one of the following strings:

Keys not listed above are represented as a hash followed by the scancode, for example "#101".

window:key_down(key)

Returns true if the given key was down at the start of the current frame.

window:keys_down()

Returns an array of the keys that were down at the start of the current frame.

window:key_pressed(key)

Returns true if the given key's state changed from up to down since the last frame.

Note that if key_pressed returns true for a particular key, then key_down will also return true. Also if key_pressed returns true for a particular key then key_released will return false for the same key. (If necessary, Amulet will postpone key release events to the next frame to ensure this.)

window:keys_pressed(key)

Returns an array of all the keys whose state changed from up to down since the last frame.

window:key_released(key)

Returns true if the given key's state changed from down to up since the last frame.

Note that if key_released returns true for a particular key, then key_down will return false. Also if key_released returns true for a particular key then key_pressed will return false. (If necessary, Amulet will postpone key press events to the next frame to ensure this.)

window:keys_released(key)

Returns an array of all the keys whose state changed from down to up since the last frame.

Detecting mouse events

window:mouse_position()

Returns the position of the mouse cursor, as a vec2, in the window's coordinate system.

window:mouse_norm_position()

Returns the position of the mouse cursor in normalized device coordinates, as a vec2.

window:mouse_pixel_position()

Returns the position of the mouse cursor in pixels where the bottom left corner of the window has coordinate (0, 0), as a vec2.

window:mouse_delta()

Returns the change in mouse position since the last frame, in the window's coordinate system (a vec2).

window:mouse_norm_delta()

Returns the change in mouse position since the last frame, in normalized device coordinates (a vec2).

window:mouse_pixel_delta()

Returns the change in mouse position since the last frame, in pixels (a vec2).

window:mouse_down(button)

Returns true if the given button was down at the start of the frame. button may be "left", "right" or "middle".

window:mouse_pressed(button)

Returns true if the given mouse button's state changed from up to down since the last frame. button may be "left", "right" or "middle".

Note that if mouse_pressed returns true for a particular button then mouse_down will also return true. Also if mouse_pressed returns true for a particular button then mouse_released will return false. (If necessary, Amulet will postpone button release events to the next frame to ensure this.)

window:mouse_released(button)

Returns true if the given mouse button's state changed from down to up since the last frame. button may be "left", "right" or "middle".

Note that if mouse_released returns true for a particular button then mouse_down will return false. Also if mouse_released returns true for a particular button then mouse_pressed will return false. (If necessary, Amulet will postpone button press events to the next frame to ensure this.)

window:mouse_wheel()

Returns the mouse scroll wheel position (a vec2).

window:mouse_wheel_delta()

Returns the change in mouse scroll wheel position since the last frame (a vec2).

Detecting touch events

window:touches_began()

Returns an array of the touches that began since the last frame.

Each touch is an integer and each time a new touch occurs the lowest available integer greater than or equal to 1 is assigned to the touch.

If there are no other active touches then the next touch will always be 1, so if your interface only expects a single touch at a time, you can just use 1 for all touch functions that take a touch argument and any additional touches will be ignored.

window:touches_ended()

Returns an array of the touches that ended since the last frame.

See window:touches_began for more info about the returned touches.

window:active_touches()

Returns an array of the currently active touches.

See window:touches_began for more info about the returned touches.

window:touch_began([touch])

Returns true if the specific touch began since the last frame.

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_ended([touch])

Returns true if the specific touch ended since the last frame.

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_active([touch])

Returns true if the specific touch is active.

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_position([touch])

Returns the last touch position in the window's coordinate system (a vec2).

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_norm_position([touch])

Returns the last touch position in normalized device coordinates (a vec2).

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_pixel_position([touch])

Returns the last touch position in pixels, where the bottom left corner of the window is (0, 0) (a vec2).

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_delta([touch])

Returns the change in touch position since the last frame in the window's coordinate system (a vec2).

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_norm_delta([touch])

Returns the change in touch position since the last frame in normalized device coordinates (a vec2).

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_pixel_delta([touch])

Returns the change in touch position since the last frame in pixels (a vec2).

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_force([touch])

Returns the force of the touch where 0 means no force and 1 is "average" force. Harder presses will result in values larger than 1.

The default value for touch is 1.

See window:touches_began for additional notes on the touch argument.

window:touch_force_available()

Returns true if touch force is supported on the device.

Scenes

Scene graphs are how you draw graphics in Amulet. Scene nodes are connected together to form a graph which is attached to a window (via the window's scene field). The window will then render the scene graph each frame.

Scene nodes correspond to graphics commands. They either change the rendering state in some way, for example by applying a transformation or changing the blend mode, or execute a draw command, which renders something to the screen.

Each scene node has a list of children, which are rendered in order.

A scene node may be the child of multiple other scene nodes, so in general a scene node does not have a unique parent. Cycles are also allowed and are handled by imposing a limit to the number of times a node can be recursively rendered.

Scene graph construction syntax

Special syntax is provided for constructing scene graphs from nodes. The expression:

node1 ^ node2

adds node2 as a child of node1 and returns node1. The resulting scene graph looks like this:

The expression:

node1 ^ { node2, node3 }

adds both node2 and node3 as children of node1 and returns node1:

The expression:

node1 ^ { node2, node3 } ^ node4

does the same as the previous expression, except node4 is added as a child of both node2 and node3:

If node2 or node3 were graphs with multiple nodes, then node4 would be added to the leaf nodes of those graphs.

Here is a more complex example:

node1 
    ^ node2
    ^ {
        node3
        ^ node4
        ,
        node5
        ^ {node6, node7, node8}
    }
    ^ node9
    ^ node10

The above expression results in the following graph:

Scene node common fields and methods

The following fields and methods are common to all scene nodes.

node.hidden

Determines whether the node and its children are rendered. The default is false, meaning that the node is rendered.

Updatable.

node.paused

Determines whether the node and its children's actions are executed. The default is false, meaning the actions are executed.

Note that a descendent node's actions might still be executed if it is has another, non-paused, parent.

Updatable.

node.num_children

Returns the node's child count.

Readonly.

node.recursion_limit

This determines the number of times the node will be rendered recursively when part of a cycle in the scene graph. The default is 8.

Updatable.

node:tag(tagname)

Adds a tag to a node and returns the node. tagname should be a string.

Note that most scene nodes receive a default tag name when they are created. See the documentation of the different nodes below for what these default tags are.

No more than 65535 unique tag names may be created in a single application.

node:untag(tagname)

Removes a tag from a node and returns the node.

node:all(tagname)

Searches node and all its descendents for any nodes with the tag tagname and returns them as a table.

node(tagname)

Searches node and its descendents for tagname and returns the first matching node found, or nil if no matching nodes were found. The search is depth-first left-to-right.

The found node's parent is also returned as a second value, unless the found node was the root of the given subgraph.

node:action([id,] action)

Attaches an action to a node and returns the node.

action may be a function or a coroutine.

The action function will be called exactly once per frame as long as the node is part of a scene graph that is attached to a window. If a coroutine is used, it will be run until it yields or finishes. The action will be run for the first time on the frame after it was attached to the node.

The action function may accept a single argument which is the node to which it is attached. If a coroutine is used, then the node is returned by coroutine.yield().

If the action function returns true then the action will be removed from the node and not run again. Similarly if a coroutine yields true or finishes.

Each action has an ID. If the id argument is omitted, then its ID is the value of the action argument. If present, id may be a value of any type besides nil, a function or coroutine (typically it's a string).

Multiple actions may be attached to a scene node, but they must all have unique ids. If you attempt to attach an action with an ID that is already used by another action on the same node, then the other action will be removed before the new one is attached.

The order that actions are run is determined by the node's position in the scene graph. Each node is visited in depth-first, left-to-right order and the actions on each node are run in the order they were added to the node. Each action is never run more than once per frame, even if the node occurs multiple times in the graph or is part of a cycle. For example, given the following scene graph:

The nodes will be visited in this order:

Note that the action execution order is determined before the first action runs each frame and is not affected by any modifications to the scene graph made by actions running during the frame. Any modifications to the scene graph will only affect the order of actions in subsequent frames.

node:late_action([id,] action)

Attach a late action to a scene node. Late actions are the same as normal actions, except they are run after all normal actions are finished.

See also node:action.

node:cancel(id)

Cancels an action.

node:append(child)

Appends child to the end of node's child list and returns node.

node:prepend(child)

Adds child to the start of node's child list and returns node.

node:remove(child)

Removes the first occurrence of child from node's child list and returns node.

node:remove(tagname)

Searches for a node with tag tagname in the descendents of node and removes the first one it finds. Then returns node.

node:remove_all()

Removes all of node's children and returns node.

node:replace(child, replacement)

Replaces the first occurrence of child with replacement in node's child list and returns node.

node:replace(tagname, replacement)

Searches for a node with tag tagname in the descendents of node and replaces the first one it finds with replacement. Then returns node.

node:child(n)

Returns the nth child of node (or nil if there is no such child).

node:child_pairs()

Returns an iterator over all node's children. For example:

for i, child in node:child_pairs() do
    -- do something with child
end

Basic nodes

am.group(children)

Group nodes are only for grouping child nodes under a common parent. They have no other effect. The children can be passed in as a table.

Default tag: "group".

Example:

local group_node = am.group{node1, node2, node3}

am.text([font, ] string [, color] [, halign [, valign]])

Renders some text.

font is an object generated using the sprite packing tool. If omitted, the default font will be used, which is a monospace font of size 16px.

color should be a vec4. The default color is white.

halign and valign specify horizontal and vertical alignment. The allowed values for halign are "left", "right" and "center". The allowed values for valign are "bottom", "top" and "center". The default in both cases is "center".

Fields:

Default tag: "text".

am.sprite(source [, color] [, halign [, valign]])

Renders a sprite (an image).

source can be either a filename, an ASCII art string or a sprite spec.

When source is a filename, that file is loaded and displayed as the sprite. Currently only .png and .jpg files are supported. Note that loaded files are cached, so each file will only be loaded once.

source may also be an ASCII art string. This is a string with at least one newline character. Each row in the string represents a row of pixels. Here's an example:

local face = [[
..YYYYY..
.Y.....Y.
Y..B.B..Y
Y.......Y
Y.R...R.Y
Y..RRR..Y
.Y.....Y.
..YYYYY..
]]
am.window{}.scene = am.scale(20) ^ am.sprite(face)

The resulting image looks like this:

face

face

The mapping from characters to colors is determined by the am.ascii_color_map table. By default this is defined as:

am.ascii_color_map = {
    W = vec4(1, 1, 1, 1),          -- full white
    w = vec4(0.75, 0.75, 0.75, 1), -- silver
    K = vec4(0, 0, 0, 1),          -- full black
    k = vec4(0.5, 0.5, 0.5, 1),    -- dark grey
    R = vec4(1, 0, 0, 1),          -- full red
    r = vec4(0.5, 0, 0, 1),        -- half red (maroon)
    Y = vec4(1, 1, 0, 1),          -- full yellow
    y = vec4(0.5, 0.5, 0, 1),      -- half yellow (olive)
    G = vec4(0, 1, 0, 1),          -- full green
    g = vec4(0, 0.5, 0, 1),        -- half green
    C = vec4(0, 1, 1, 1),          -- full cyan
    c = vec4(0, 0.5, 0.5, 1),      -- half cyan (teal)
    B = vec4(0, 0, 1, 1),          -- full blue
    b = vec4(0, 0, 0.5, 1),        -- half blue (navy)
    M = vec4(1, 0, 1, 1),          -- full magenta
    m = vec4(0.5, 0, 0.5, 1),      -- half magenta
    O = vec4(1, 0.5, 0, 1),        -- full orange
    o = vec4(0.5, 0.25, 0, 1),     -- half orange (brown)
}

but you can modify it as you please (though this must be done before creating a sprite).

Any characters not in the color map will come out as transparent pixels, except for space characters which are ignored.

The third kind of source is a sprite spec. Sprite specs are usually generated using the sprite packing tool, though you can create them manually as well if you like.

You can define your own sprite spec by supplying a table with all of the following fields:

Typically x1 and y1 would both be zero and x2 and y2 would be equal to width and height, though they may be different when transparents pixels are removed from the edges of sprites when packing them. The width and height fields are used for adjusting sprite position based on the requested alignment.

The color argument is a vec4 that applies a tinting color to the sprite. The default is white (no tinting).

The halign and valign arguments determine the alignment of the sprite. The allowed values for halign are "left", "right" and "center". The allowed values for valign are "bottom", "top" and "center". The default in both cases is "center".

Fields:

Default tag: "sprite".

am.rect(x1, y1, x2, y2 [, color])

Draws a rectangle from (x1, y1) to (x2, y2).

color should be a vec4 and defaults to white.

Fields:

Default tag: "rect".

am.circle(center, radius [, color [, sides]])

Draws a circle or regular polygon.

center should be a vec2.

color should be a vec4. The default is white.

sides is the number of sides to use when rendering the circle. The default is 255. You can change this to make other regular polygons. For example change it to 6 to draw a hexagon.

Fields:

Default tag: "circle".

am.line(point1, point2 [, thickness [, color]])

Draws a line from point1 to point2.

point1 and point1 should be vec2s.

thickness should be a number. The default is 1.

color should be a vec4. The default is white.

Fields:

Default tag: "line".

am.particles2d(settings)

Renders a simple 2D particle system.

settings should be a table with any of the following fields:

In the table the _var fields are an amount that is added to and subtracted from the corresponding field (the one without the _var suffix) to get the range of values from which one is randomly chosen. For example if source_pos is vec2(1, 0) and source_pos_var is vec2(3, 2), then source positions will be chosen in the range vec2(-2, -2) to vec2(4, 2).

All of the sprite settings are exposed as updatable fields on the particles node.

Note that no blending is applied to the particles, so if you want alpha on your particles, then you need to add a am.blend node. For example:

local node = am.blend("add_alpha")
    ^ am.particles2d{
        source_pos = win:mouse_position(),
        source_pos_var = vec2(20),
        max_particles = 1000,
        emission_rate = 500,
        start_particles = 0,
        life = 0.4,
        life_var = 0.1,
        angle = math.rad(90),
        angle_var = math.rad(180),
        speed = 200,
        start_color = vec4(1, 0.3, 0.01, 0.5),
        start_color_var = vec4(0.1, 0.05, 0.0, 0.1),
        end_color = vec4(0.5, 0.8, 1, 1),
        end_color_var = vec4(0.1),
        start_size = 30,
        start_size_var = 10,
        end_size = 2,
        end_size_var = 2,
        gravity = vec2(0, 2000),
    }

Methods:

Default tag: "particles2d".

Transformation nodes

The following nodes apply transformations to all their descendents.

Note: These nodes have an optional uniform argument in the first position of their construction functions. This argument is only relevant if you're writing your own shader programs. Otherwise you can ignore it.

am.translate([uniform,] position)

Apply a translation to a 4x4 matrix uniform. uniform is the uniform name as a string. It is "MV" by default.

position may be either 2 or 3 numbers (the x, y and z components) or a vec2 or vec3.

If the z component is omitted it is assumed to be 0.

Fields:

Default tag: "translate".

Examples:

local node1 = am.translate(10, 20)
local node2 = am.translate(vec2(10, 20))
local node3 = am.translate("MyModelViewMatrix", 1, 2, -3.5)
local node4 = am.translate(vec3(1, 2, 3))
node1.position2d = vec2(30, 40)
node2.x = 40
node2.y = 50
node3.position = vec3(1, 2, -3)

am.scale([uniform,] scaling)

Apply a scale transform to a 4x4 matrix uniform. uniform is the uniform name as a string. It is "MV" by default.

scaling may be 1, 2 or 3 numbers or a vec2 or vec3. If 1 number is provided it is assume to be the x and y components of the scaling and the z scaling is assumed to be 1. If 2 numbers or a vec2 is provided, they are the scaling for the x and y components and z is assumed to be 1.

Fields:

Default tag: "scale".

Examples:

local node1 = am.scale(2)
local node2 = am.scale(2, 1)
local node3 = am.scale(vec2(1, 2))
local node4 = am.scale("MyModelViewMatrix", vec3(0.5, 2, 3))
node1.scale2d = vec2(1)
node2.x = 3
node4.scale = vec3(1, 3, 2)

am.rotate([uniform,] rotation)

Apply a rotation to a 4x4 matrix uniform. uniform is the uniform name as a string. It is "MV" by default.

rotation can be either a quaternion, or an angle (in radians) followed by an optional vec3 axis. If the axis is omitted it is assumed to be vec3(0, 0, 1) so the rotation becomes a 2D rotation in the xy plane.

Fields:

Default tag: "rotate".

Examples:

local node1 = am.rotate(math.rad(45))
local node2 = am.rotate(math.pi/4, vec3(0, 1, 0))
local node3 = am.rotate("MyModelViewMatrix", 
    quat(math.pi/6, vec3(1, 0, 0)))
node1.angle = math.rad(60)
node2.axis = vec3(0, 0, 1)
node3.rotation = quat(math.rad(60), vec3(0, 0, 1))

Advanced nodes

am.use_program(program)

Sets the shader program to use when rendering descendents. A program object can be created using the am.program function.

Fields:

Default tag: "use_program".

am.bind(bindings)

Binds shader program parameters (uniforms and attributes) to values. bindings is a table mapping shader parameter names to values.

The named parameters are matched with the uniforms and attributes in the shader program just before a am.draw node is executed.

Program parameter types are mapped to the following Lua types:

Program parameter type Lua type
float uniform number
vec2 uniform vec2
vec3 uniform vec3
vec4 uniform vec4
mat2 uniform mat2
mat3 uniform mat3
mat4 uniform mat4
sampler2D uniform texture2d
float attribute view("float")
vec2 attribute view("vec2")
vec3 attribute view("vec3")
vec4 attribute view("vec4")

Any bound parameters not in the program are ignored, but all program parameters must have been bound before a draw node is executed.

Note: The parameter P is initially bound to a 4x4 projection matrix defined by the window's coordinate system, while the parameter MV (the default model view matrix) is initially bound to the 4x4 identity matrix.

The bound parameters are available as updatable fields on the bind node. The fields have the same names as their corresponding parameters.

Default tag: "bind".

Example:

local bind_node = am.bind{
    P = mat4(1),
    MV = mat4(1),
    color = vec4(1, 0, 0, 1),
    vert = am.vec2_array{
        vec2(-1, -1),
        vec2(0, 1),
        vec2(1, -1)
    }
}
-- update a parameter
bind_node.color = vec4(0, 1, 1, 1)

am.draw(primitive [, elements] [, first [, count]])

Draws the currently bound vertices using the current shader program with the currently bound parameter values.

primitive can be one of the following:

Note that "line_loop" and "triangle_fan" may be slow on some systems.

elements, if supplied, should be a ushort_elem or uint_elem view containing 1-based attribute indices. If omitted the attributes are rendered in order as if elements were 1, 2, 3, 4, 5, ... etc. See also buffers and views.

first specifies where in the list of vertices to start drawing (starting from 1). The default is 1.

count specifies how many vertices to draw. The default is as many as are supplied through bound vertex attributes and the elements view if present.

Fields:

Default tag: "draw".

Here is a complete example that draws a triangle with red, green and blue corners using am.use_program, am.bind and am.draw nodes:

local win = am.window{}
local prog = am.program([[
    precision highp float;
    attribute vec2 vert;
    attribute vec4 color;
    uniform mat4 MV;
    uniform mat4 P;
    varying vec4 v_color;
    void main() {
        v_color = color;
        gl_Position = P * MV * vec4(vert, 0.0, 1.0);
    }
]], [[
    precision mediump float;
    varying vec4 v_color;
    void main() {
        gl_FragColor = v_color;
    }
]])
win.scene =
    am.use_program(prog)
    ^ am.bind{
        P = mat4(1),
        MV = mat4(1),
        color = am.vec4_array{
            vec4(1, 0, 0, 1),
            vec4(0, 1, 0, 1),
            vec4(0, 0, 1, 1)
        },
        vert = am.vec2_array{
            vec2(-1, -1),
            vec2(0, 1),
            vec2(1, -1)
        }
    }
    ^ am.draw"triangles"

The resulting image looks like this:

am.blend(mode)

Set the blending mode.

The possible values for mode are:

Fields:

am.color_mask(red, green, blue, alpha)

Apply a color mask. The four arguments can be true or false and determine whether the corresponding color channel is updated in the rendering target (either the current window or framebuffer being rendered to).

For example using a mask of am.color_mask(false, true, false, true) will cause only the green and alpha channels to be updated.

am.cull_face(face)

Culls triangles with a specific winding.

The possible values for face are:

Fields:

am.depth_test(test [, mask])

Sets the depth test and mask. The window or framebuffer being rendered to needs to have a depth buffer for this to have any effect.

test is used to determine whether a fragment is rendered by comparing the depth value of the fragment to the value in the depth buffer. The possible values for test are:

Mask determines whether the fragment depth is written to the depth buffer. The possible values are true and false. The default is true.

am.viewport(x, y, width, height)

Set the viewport, which is the rectangular area of the window into which rendering will occur.

x and y is the bottom-left corner of the viewport in pixels, where the bottom-left corner of the window is (0, 0). width and height are also in pixels.

Fields:

Default tag: "viewport".

am.lookat([uniform,] eye, center, up)

Sets uniform to the "lookat matrix" which looks from eye (a vec3) to center (a vec3), with up (a unit vec3) as the up direction.

This node can be thought of a camera positioned at eye and facing the point center.

The default value for uniform is "MV".

Fields:

Default tag: "lookat".

am.cull_sphere([uniforms...,] radius [, center])

This first takes the matrix product of the given uniforms (which should be mat4s). Then it determines whether the sphere with the given center and radius would be visible using the previously computed matrix product as the model-view-projection matrix. If it wouldn't be visible then none of this node's children are rendered (i.e. they are culled).

The default value for uniforms is "P" and "MV" and the default value for center is vec3(0).

Fields:

am.billboard([uniform,] [preserve_scaling])

Removes rotation from uniform, which should be a mat4. By default uniform is "MV".

If preserve_scaling is false or omitted then any scaling will also be removed from the matrix. If it is true, then scaling will be preserved, as long as it's the same across all three axes.

Default tag: "billboard"

am.read_uniform(uniform)

This node has no effect on rendering. Instead it records the value of the named uniform when rendering occurs.

This is useful for finding the value of the model-view matrix (MV) at a specific node without having to keep track of all the ancestor transforms. This could then be used to, for example, determine the position of a mouse click in a node's coordinate space, by taking the inverse of the model-view matrix.

Fields:

am.quads(n, spec)

Returns a node that renders a set of quads. The returned node is actually an am.bind node with an am.draw node child. i.e. no program or blending is defined -- these must be created separately as parent nodes.

n is the initial capacity. Set this to the number of quads you think you'll want to render. It doesn't matter if it's too small as the capacity will be increased as required, though it's slightly faster if no capacity increases are required.

spec is a table of attribute name and type pairs (the same as used for am.struct_array ).

Fields:

Methods:

Example:

local quads = am.quads(2, {"vert", "vec2", "color", "vec3"})
quads:add_quad{vert = {vec2(-100, 0), vec2(-100, -100),
                       vec2(0, -100), vec2(0, 0)},
               color = {vec3(1, 0, 0), vec3(0, 1, 0),
                        vec3(0, 0, 1), vec3(1, 1, 1)}}
quads:add_quad{vert = {vec2(0, 100), vec2(0, 0),
                       vec2(100, 0), vec2(100, 100)},
               color = {vec3(1, 0, 0), vec3(0, 1, 0),
                        vec3(0, 0, 1), vec3(1, 1, 1)}}
local win = am.window{}
local prog = am.program([[
    precision highp float;
    attribute vec2 vert;
    attribute vec3 color;
    uniform mat4 MV;
    uniform mat4 P;
    varying vec3 v_color;
    void main() {
        v_color = color;
        gl_Position = P * MV * vec4(vert, 0.0, 1.0);
    }
]], [[
    precision mediump float;
    varying vec3 v_color;
    void main() {
        gl_FragColor = vec4(v_color, 1.0);
    }
]])
win.scene = am.use_program(prog) ^ quads

The above program produces the following output:

am.postprocess(settings)

Allows for post-processing of a scene. First the children of the postprocess node are rendered into a texture, then the texture is rendered to the entire window using a user-supplied shader program.

settings is a table containing any number of the following fields:

The shader program should expect the following uniforms and attributes:

uniform sampler2D tex;
attribute vec2 vert;
attribute vec2 uv;

Note that if either width or height are set then they must both be set.

Fields:

Methods:

Creating custom scene nodes

Creating a custom leaf node is relatively simple: just create a function that constructs the graph you want and return it.

You can add custom methods by setting the appropriate fields on the root node of the returned graph (any node can have any number of custom fields set on it, as long as they don't clash with pre-defined fields).

If you define methods of the form:

function node:get_FIELD()
    ...
end

function node:set_FIELD(val)
    ...
end

Then you will be able to access FIELD as if it's a regular field and the appropriate access method will be called.

For example:

node.FIELD = val

will be equivalent to:

node:set_FIELD(val)

If you want a readonly field, just define the get_FIELD method and not the set_FIELD method.

The am.text, am.sprite and am.particles2d nodes are all implemented this way. Their source is here and here.

If you want to create a non-leaf node, then you need to use the am.wrap function:

am.wrap(node)

This "wraps" node inside a special type of node called a wrap node.

When a wrap node is rendered it renders the inner node. However any nodes added as children of a wrap node are also added to the leaf node(s) of the inner node.

For example suppose we want to create a transformation node called move_and_rotate that does both a translation and a rotation:

function move_and_rotate(x, y, degrees)
    return am.translate(x, y) ^ am.rotate(math.rad(degrees))
end

We would like to be able to create such a node and add children to it. Like so:

local mvrot = move_and_rotate(10, 20, 60)
mvrot:append(am.rect(-10, -10, 10, 10))

However what this will do is add the rect node as a child of the translate node returned by move_and_rotate.

Instead we need to do:

mvrot"rotate":append(am.rect(-10, -10, 10, 10))

which is a bit clunky.

A wrap node solves this problem:

function move_and_rotate(x, y, degrees)
    return am.wrap(am.translate(x, y) ^ am.rotate(math.rad(degrees)))
end
local mvrot = move_and_rotate(10, 20, 60)
mvrot:append(am.rect(-10, -10, 10, 10))

For completeness here we add some fields to set the x, y and degrees properties of our new node:

function move_and_rotate(x, y, degrees)
    local inner = am.translate(x, y) ^ am.rotate(math.rad(degrees))
    local wrapped = am.wrap(inner)
    function wrapped:get_x()
        return x
    end
    function wrapped:set_x(v)
        x = v
        inner.position2d = vec2(x, y)
    end
    function wrapped:get_y()
        return y
    end
    function wrapped:set_y(v)
        y = v
        inner.position2d = vec2(x, y)
    end
    function wrapped:get_degrees()
        return degrees
    end
    function wrapped:set_degrees(v)
        degrees = v
        inner"rotate".angle = math.rad(degrees)
    end
    return wrapped
end
local mvrot = move_and_rotate(-100, -100, 0)
mvrot:append(am.rect(-50, -50, 50, 50))
mvrot.x = 100
mvrot.y = 100
mvrot.degrees = 45

There are some caveats when using wrap nodes:

The am.postprocess node is implemented using a wrap node. You can view it's implementation here.

Shader programs

Compiling a shader program

am.program(vertex_shader, fragment_shader)

Compiles and returns a shader program for use with am.use_program nodes.

vertex_shader and fragment_shader should be the sources for the vertex and fragment shaders in the OpenGL ES shader language version 1. This is the same shader language supported by WebGL 1.

Example:

local vert_shader = [[
    precision highp float;
    attribute vec3 vert;
    uniform mat4 MV;
    uniform mat4 P;
    void main() {
        gl_Position = P * MV * vec4(vert, 1);
    }
]]
local frag_shader = [[
    precision mediump float;
    void main() {
        gl_FragColor = vec4(1.0, 0, 0.5, 1.0);
    }
]]
local prog = am.program(vert_shader, frag_shader)

Image buffers

An image buffer represents a 2D image in memory. Each pixel occupies 4 bytes with 1 byte per channel (RGBA). The data for the image buffer is stored in an am.buffer.

Creating image buffers

am.image_buffer([buffer, ] width [, height])

Creates an image buffer of the given width and height. If height is omitted it is the same as width (the image is square).

If a buffer (created using am.buffer) is given, it is used as the image buffer. It must have the correct size, which is 4 * width * height. If buffer is omitted a new one is created.

Fields:

am.load_image(filename)

Loads the given image file and returns a new image buffer. Only .png and .jpg files are supported. Returns nil if the file was not found.

Saving images

image_buffer:save_png(filename)

Saves the given image as a png in filename.

Pasting images

image_buffer:paste(src, x, y)

Pastes one image into another such that the bottom-left corner of the source image is at the given pixel coordinate in the target image. The bottom-left pixel of the target image has coordinate (1, 1).

Decoding/encoding PNGs

am.encode_png(image_buffer)

Returns a raw buffer containing the png encoding of the given image.

am.decode_png(buffer)

Converts the raw buffer, which should be a png encoding of an image, into an image buffer.

Textures

Textures are 2D images that can be used as input to shader programs when bound to a sampler2D uniform.

They can also be rendered to via framebuffers.

A texture may optionally have a backing image buffer. If a texture has a backing image buffer, then any changes to the image buffer will be automatically transferred to the texture.

Textures always have 4 channels (RGBA) with 1 byte per channel.

Creating a texture

am.texture2d(width [, height])

Creates a texture of the given width and height without a backing image buffer.

am.texture2d(image_buffer)

Creates a texture using the given image buffer as the backing image buffer.

am.texture2d(filename)

This is shorthand for am.texture2d(am.load_image(filename)).

Texture fields

texture.width

Readonly.

texture.height

Readonly.

texture.image_buffer

The backing image buffer or nil if there isn't one.

Readonly.

texture.minfilter

Defines the minification filter which is applied when the texture's pixels are smaller that the screen's pixels.

The allowed values are:

If one of the mipmap filters is used a mipmap will be automatically generated.

Updatable.

texture.magfilter

Defines the magnification filter which is applied when the texture's pixels are larger that the screen's pixels.

The allowed values are:

Updatable.

texture.filter

This field can be used to set both the minification and magnification fields. The allowed values are:

Updatable.

texture.swrap

Sets the wrapping mode in the x direction. The allowed values are:

Note that the texture width must be a power of 2 if either "repeat" or "mirrored_repeat" is used.

Updatable.

texture.twrap

Sets the wrapping mode in the y direction. The allowed values are:

Note that the texture height must be a power of 2 if either "repeat" or "mirrored_repeat" is used.

Updatable.

texture.wrap

This field can be used to set the wrap mode in both the x and y directions at the same time.

The allowed values are:

Updatable.

Framebuffers

A framebuffer is like an off-screen window you can draw to. It has a texture associated with it that gets updated when you draw to the framebuffer. The texture can then be used as input to further rendering.

Creating a framebuffer

am.framebuffer(texture [, depth_buf [, stencil_buf]])

Creates framebuffer with the given texture attached.

depth_buf and stencil_buf determine whether the framebuffer has a depth and/or stencil buffer. These should be true or false.

Framebuffer fields

framebuffer.clear_color

The color to use when clearing the framebuffer (a vec4).

Updatable.

framebuffer.projection

A mat4 projection matrix to use when rendering nodes into this framebuffer. The "P" uniform will be set to this. If this is not specified then the projection math.ortho(-width/2, width/2, -height/2, height/2) is used.

Updatable.

framebuffer.pixel_width

The width of the framebuffer, in pixels.

Readonly.

framebuffer.pixel_height

The height of the framebuffer, in pixels.

Readonly.

Framebuffer methods

framebuffer:render(node)

Renders node into the framebuffer.

framebuffer:render_children(node)

Renders node's children into the framebuffer (but not node itself).

framebuffer:clear([color [, depth [, stencil]]])

Clears the framebuffer, using the current clear_color.

By default the color, depth and stencil buffers are cleared, but you can selectively clear only some buffers by setting the color, depth and stencil argumnets to true or false.

framebuffer:read_back()

Reads the pixels from the framebuffer into the texture's backing image buffer. This is a relatively slow operation, so use it sparingly.

framebuffer:resize(width, height)

Resize the framebuffer. Only framebuffers with textures that have no backing image buffers can be resized. Also the texture must not use a filter that requires a mipmap.

Actions

This section covers useful action utility functions. For information on the action mechanism see the description of the action method.

Delay action

am.delay(seconds)

Returns an action that does nothing for the given number of seconds.

Combining actions

am.series(actions)

Returns an action that runs the given array of actions one after another.

am.parallel(actions)

Returns an action that runs the given array of actions all at the same time. The returned action finishes once all the actions in the array are finished.

am.loop(func)

func should be a function that returns an action. am.loop returns an action that repeatedly runs the action returned by func.

Tweens

am.tween([target,] time, values [, ease])

Returns an action that changes the values of one or more fields of target to new values over a given time period.

time is in seconds.

values is a table mapping field names to their new values.

ease is an easing function. This function takes a value between 0 and 1 and returns a value between 0 and 1. This determines the "shape" of the interpolation between the two values. If omitted a linear interpolation is used.

The following easing functions are pre-defined (though of course you can define your own):

Tweening works with fields that are numbers or vectors (vec2, vec3 or vec4). It also works with quaternions. In all cases the math.mix function is used to interpolate between the initial value and the final value.

If target is omitted it is taken to be the node to which the tween action is attached.

Example:

am.window{}.scene =
    am.rect(-100, -100, 100, 0, vec4(1, 0, 0, 1))
    :action(
        am.tween(1, {
            color = vec4(0, 1, 0, 1),
            y2 = 100
        }))

Coroutine actions

An action can also be a coroutine. Inside a coroutine action it can sometimes be useful to wait for another action to finish, such as a tween. That is what the am.wait function is for:

am.wait(action)

Waits for the given action to finish before continuing. This can only be called from within a coroutine.

Example:

am.window{}.scene =
    am.rect(-100, -100, 100, 0, vec4(1, 0, 0, 1))
    :action(coroutine.create(function(node)
        while true do
            am.wait(am.tween(node, 1, {
                color = vec4(0, 1, 0, 1),
                y2 = 100
            }))
            am.wait(am.tween(node, 1, {
                color = vec4(1, 0, 0, 1),
                y2 = 0
            }))
        end
    end))

Time

Frame time

am.frame_time

This contains the time the current frame started, in seconds.

Delta time

am.delta_time

This contains the amount of time that lapsed between the start of the current frame and the start of the previous frame, in seconds.

Real time

am.current_time()

This returns the time since the program started, in seconds. This value can change over the course of a frame.

Saving and loading state

am.save_state(key, state [,format])

Save the table state under key.

format can be json or lua. The default is lua.

Note: The lua format allows users to execute arbitrary lua code by modifying the save file.

am.load_state(key, [,format])

Loads the table saved under key and returns it. If no table has been previously saved under key then nil is returned.

format can be json or lua. The default is lua.

Note: The lua format allows users to execute arbitrary lua code by modifying the save file.

Audio

Audio buffers

An audio buffer is a block of memory that stores an uncompressed audio sample.

An audio buffer may have any number of channels, although Amulet will currently only play up to two channels.

An audio buffer also has a sample rate. Amulet currently plays all audio at a sample rate 44.1kHz and will resample an audio buffer if it has a different sample rate.

The audio data itself is stored in a raw buffer as a series of single precision floats (4 bytes each). The samples for each channel are contiguous. The first channel's data comes first, then the second channel's data, etc (the channels are not interleaved).

If the sample data is stored in the buffer buf, and there are two channels, then the following code will create views that can be used to update or read the samples for each channel:

local c = 2 -- channels
local s = #buf / 4 / c -- samples per channel
local left_channel = buf:view("float", 0, 4, s)
local right_channel = buf:view("float", s * 4, 4, s)

am.audio_buffer(buffer, channels, sample_rate)

Returns a new audio buffer using the given raw buffer (a buffer created with am.buffer). The audio data should be layed out as described above.

channels is the number of channels and sample_rate is the sample rate in Hz.

am.load_audio(filename)

Loads the given audio file and returns a new audio buffer. The file must be a .ogg audio file. Returns nil if the file was not found.

Audio buffer fields

audio_buffer.channels

The number of channels. Readonly.

audio_buffer.sample_rate

The sample rate in Hz. Readonly.

audio_buffer.samples_per_channel

The number of samples per channel. Readonly.

audio_buffer.length

The length of the audio in seconds. Readonly.

audio_buffer.buffer

The underlying raw buffer where the audio data is stored. Readonly.

Playing audio

am.play(source, loop, pitch, gain)

Returns an action that plays audio. Like any other action it needs to be attached to a scene node that's connected to a window to run.

source can either be an audio buffer, the name of a ".ogg" file or a seed generated by the sfxr tool (there's an online version of that tool in the examples list of the online editor).

loop can be true or false.

pitch is a multiplier applied to the playback speed. 1.0 mean play at the original speed. 2.0 means play twice as fast and 0.5 means play at half the original speed.

gain should be between 0 and 1.

Note: When the source is an sfxr seed or a filename, an audio buffer is generated behind the scenes and cached. Therefore there might be a short delay the first time this function is called with a given seed or filename (though for short audio clips this will probably not be noticeable). Also avoid calling this function many times with different seeds or filenames, because that will cause unbounded memory growth.

Generating sound effects

am.sfxr_synth(settings)

Returns an audio buffer containing a generated sound effect.

settings can either be a table containing any number of the following fields:

Field Default value Notes
wave_type "square" Can also be "sawtooth", "sine" or "noise"
base_freq 0.3
freq_limit 0.0
freq_ramp 0.0
freq_dramp 0.0
duty 0.0
duty_ramp 0.0
vib_strength 0.0
vib_speed 0.0
vib_delay 0.0
env_attack 0.0
env_sustain 0.3
env_decay 0.4
env_punch 0.0
filter_on false
lpf_resonance 0.0
lpf_freq 1.0
lpf_ramp 0.0
hpf_freq 0.0
hpf_ramp 0.0
pha_offset 0.0
pha_ramp 0.0
repeat_speed 0.0
arp_speed 0.0
arp_mod 0.0

or a numeric seed. Use the sfxr example in the online editor to generate seeds.

Audio graphs

TODO

(Amulet has support for building graphs of audio effect nodes, however it is currently undocumented because it will probably be changing shortly to use syntax more consistent with the way scene graphs are constructed.)

Audio graph nodes

TODO

Analysing audio

TODO

Controllers

Each controller is assigned an index starting from 1. The first controller attached will always have index 1, so if your game only uses one controller you can just use index 1.

Controllers are supported on Windows, OSX and Linux and up to 8 controllers can be connected at once.

am.controller_present(index)

Returns true if controller index is currently connected.

am.controller_attached(index)

Returns true if controller index was attached since the last frame.

am.controller_detached(index)

Returns true if controller index was removed since the last frame.

am.controllers_present()

Returns a list of currently connected controller indexes.

am.controllers_attached()

Returns a list of controller indexes attached since the last frame.

am.controllers_detached()

Returns a list of controller indexes removed since the last frame.

am.controller_lt_val(index)

Returns the value of the left trigger axis of controller index. The returned value is between 0 and 1.

am.controller_rt_val(index)

Returns the value of the right trigger axis of controller index. The returned value is between 0 and 1.

am.controller_lstick_pos(index)

Returns the position of the left stick of controller index as a vec2. The position values range from -1 to 1 in both the x and y components. Negative x means left and negative y means down.

am.controller_rstick_pos(index)

Returns the position of the left stick of controller index as a vec2. The position values range from -1 to 1 in both the x and y components. Negative x means left and negative y means down.

am.controller_button_pressed(index, button)

Returns true if the given button of controller index was pressed since the last frame.

button can be one of the following strings:

am.controller_button_released(index, button)

Returns true if the given button of controller index was released since the last frame. See am.controller_button_pressed for a list of valid values for button.

am.controller_button_down(index, button)

Returns true if the given button of controller index was down at the start of the current frame. See am.controller_button_pressed for a list of valid values for button.

Packing sprites and generating fonts

Overview

Amulet includes a tool for packing images and font glyphs into a sprite sheet and generating a Lua module for conveniently accessing the images and glyphs therein.

Suppose you have an images directory containing some .png files and a fonts directory containing myfont.ttf and suppose these two directories are subdirectories of the main game directory (where your main.lua file lives). To generate a sprite sheet, run the following command while in the main game directory:

> amulet pack -png mysprites.png -lua mysprites.lua 
              images/*.png fonts/myfont.ttf@32

This will generate mysprites.png and mysprites.lua in the current directory.

The @32 after the font file specifies the size of the glyphs to generate (in this case 32px).

To use the generated sprite sheet in your code, first required the Lua module and then pass the fields in that module to am.sprite or am.text. For example:

local mysprites = require "mysprites"
local run1_node = am.sprite(mysprites.run1)
local text_node = am.text(mysprites.myfont32, "BARF!")

The sprite field names are generated from the image filenames with the extension removed (so the file images/run1.png would result in the field mysprites.run1).

The font field name (myfont32) is the concatenation of the font file name (without the extension) and the font size.

Pack options

In addition to the required -png and -lua options the pack command also supports the following options:

Option Description
-mono Do not anti-alias fonts.
-minfilter The minification filter to apply when loading the sprite sheet texture. linear (the default) or nearest.
-magfilter The magnification filter to apply when loading the sprite sheet texture. linear (the default) or nearest.
-no-premult Do not pre-multiply RGB channels by alpha.
-keep-padding Do not strip transparent pixels around images.

Padding

Unless you specify the -keep-padding option, Amulet will remove excess rows and columns of completely transparent pixels from each image before packing it. It will always however leave a one pixel border of transparent pixels if the image was surrounded by transparent pixels to begin with. This is done to prevent pixels from one image "bleeding" into another image when it's drawn.

When excess rows and columns are removed the vertices of the sprite will be adjusted so the images is still drawn in the correct position, as if the excess pixels were still there.

If an image is not surrounded by a border of transparent pixels, then the border pixels will be duplicated around the image. This helps prevent "cracks" when tiling the image.

In summary, if you don't intend to use an image for tiling, surround it with a border of completely transparent pixels.

Specifying which font glyphs to generate

Optionally each font item may also be followed by a colon and a comma separated list of character ranges to include in the sprite sheet.

The characters in the character range can be written directly if they are ASCII, or given as hexadecimal Unicode codepoints.

Here are some examples of valid font items:

If the character range list is omitted, it defaults to 0x20-0x7E, i.e:

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

Loading bitmap font images

am.load_bitmap_font(filename, key)

Loads the image filename and returns a font using the glyph layout described by key where key is a string containing all the glyphs in the file as they are layed out. All glyphs must have the same width and height which is determined from the width and height of the image and the number of rows and columns in key. For example if the key is:

[[
ABC
DEF
GHI
]]

then the image contains 9 glyphs in 3 rows and 3 columns. If the image has height 30 and width 36 then each glyph has width 10 and height 12.

Exporting

To generate distribution packages for Windows, Mac OS X, iOS, Linux and HTML5, use the amulet export command.

Run the following command from the directory containing your lua, image and audio files:

> amulet export

This will generate package files for each supported platform in the current directory. If you just want to generate a package for one platform you can pass one of the options -windows, -mac, -ios, -linux or -html.

All files in the directory with the following extensions will be included as data in the distribution: .lua, .json, .png, .jpg, .ogg and .obj. All .txt files will also be copied to the generated zip and be visible to the user when they unzip it. This is intended for README.txt or similar files.

If the -r option is given then all subdirectories will also be included (recursively), otherwise only the files in current directory are included.

The generated zip will also contain an amulet_license.txt file containing the Amulet license as well as the licenses of all third party libraries used by Amulet. Some of these licenses require that they be distributed with copies of their libraries, so to comply you should include amulet_license.txt when you distribute your work. Note that these licenses do not apply to your work itself.

If you create a conf.lua file in the same directory as your other Lua files containing the following:

title = "My Game Title"
shortname = "mygame"
author = "Your Name"
appid = "com.some-unique-id.123"
version = "1.0.0"

display_name = "My Game"
dev_region = "en"
supported_languages = "en,fr,nl,de,ja,zh-CN,zh-TW"
icon = "icon.png"
launch_image = "launch.png"
orientation = "any"

then this will be used for various bits of meta-data in the generated packages. In particular shortname will be used in the name of the generated zip files (otherwise they will just be called "Untitled").

If you're generating an iOS package for submission to the App Store you'll need to fill out all the settings.

Note that the generated iOS ipa package is not signed. You will need to sign it using a tool like sigh or this script before submitting it to the app store or deploying on a device.

IMPORTANT: Avoid unzipping and re-zipping the generated packages as you may inadvertently strip the executable bit from some files, which will cause them not to work.

Extra table functions

The following functions suplement the standard Lua table functions.

table.shallow_copy(t)

Returns a shallow copy of t (i.e. a new table with the same keys and values as t).

table.deep_copy(t)

Returns a deep copy of t (all t's keys and values are copied recursively). Cycles are detected and reproduced in the new table.

table.search(arr, elem)

Return the index of elem in arr or nil if it's not found.

table.shuffle(t [,rand])

Randomly rearranges the values of t. The optional rand argument should be a function where rand(n) returns an integer between 1 and n (like math.random). By default math.random is used.

table.clear(t)

Remove all t's pairs.

table.remove_all(arr, elem)

Remove all values equal to elem from arr.

table.append(arr1, arr2)

Inserts all of arr2's values at the end of arr1.

table.merge(t1, t2)

Sets all the key-value pairs from t2 in t1.

table.keys(t)

Returns an array of t's keys.

table.values(t)

Returns an array of t's values.

table.filter(arr, f)

Returns a new array which contains only the values from arr for which f(elem) returns true (or any value besides nil or false).

table.tostring(t)

Converts a table to a string. The returned string is a valid Lua table literal.

table.count(t)

Returns the total number of pairs in the table.

Amulet require function

The require function in Amulet is slightly different from the default one. The default Lua package loaders have been removed and replaced with a custom loader. The loader passes a new empty table into each module it loads. All exported functions can be added to this table, instead of creating a new table. If no other value is returned by the module, the passed in table will be used as the return value for require.

The passed in export table can be accessed via the ... expression. Here's a short example:

local mymodule = ...

mymodule.message = "hello"

function mymodule.print_message()
    print(mymodule.message)
end

If this module is in the file mymodule.lua, then it can be imported like so:

local mymodule = require "mymodule"

mymodule.print_message() -- prints hello

This scheme allows cyclic module imports, e.g. module A requires module B which in turn requires module A. Amulet will detect the recursion and return A's (incomplete) export table in B. Then when A has finished initialising, all its functions will be available in B. This does mean that B can't call any of A's functions while its initialising, but after initialisation all of A's functions will be available.

Of course you can still return your own values from modules and they will be returned by require as with the default Lua require function.

Logging

log(msg, ...)

Log a message to the console. The message will also appear in an overlay on the main window.

msg may contain format specifiers like the standard Lua string.format function.

The logged messages are prefixed with the file name and line number where log was called.

Example:

log("here")
log("num = %g, string = %s", 1, "two")

Preventing accidental global variable usage

noglobals()

Prevents the creation of new global variables. An error will be raised if a new global is created after this call, or if an attempt is made to read a nil global.

Globbing

am.glob(patterns)

Returns an array (table) of file names matching the given glob pattern(s). patterns should be a table of glob pattern strings. A glob pattern is a file path with zero or more wildcard (*) characters.

Any matching files that are directories will have a slash (/) appended to their names, even on Windows.

The slash character (/) can be used as a directory separator on Windows (you don't need to use \). Furthermore returned paths will always have '/' as the directory separator, even on Windows.

Note: This function only searches for files on the file system. It won't search the resource archive in a exported game. Its intended use is for writing file processing utilities and not for use directly in games you wish to distribute.

Example:

local image_files = am.glob{"images/*.png", "images/*.jpg"}

Running JavaScript

am.eval_js(js)

Runs the given JavaScript string and returns the result as a Lua value. JavaScript objects and arrays are converted to Lua tables and other JavaScript types are converted to the corresponding Lua types. undefined is converted to nil.

This function only works when running in a browser. On other platforms it has no effect and always returns nil.

Converting to/from JSON

am.to_json(value)

Converts the given Lua value to a JSON string and returns it.

Tables with string keys are converted to JSON objects and tables with consecutive integer keys starting at 1 are converted to JSON arrays. Empty tables are converted to empty JSON arrays. Other types of tables are not supported anc cycles are not detected.

am.parse_json(json)

Converts the given JSON string to a Lua value and returns it. If there was an error parsing the JSON then nil is returned and the error message is returned as a second return value.

Loading other resources

am.load_script(filename)

Loads the Lua script in filename and returns a function that, when called, will run the script. If the file doesn't exist nil is returned.

am.load_string(filename)

Loads filename and returns its contents as a string or nil if the file wasn't found.

Performance stats

am.perf_stats()

Returns a table with the following fields:

Amulet version

am.version

The current Amulet version, as a string. E.g. "1.0.3".

Platform

am.platform

The platform Amulet is running on. It will be one of the strings "linux" "windows" "osx" "ios" "android" or "html".

Language

am.language()

Returns the user's preferred ISO 639-1 language code in lower case(e.g. "en"), possibly followed by a dash and an ISO 3166-1 coutry code in upper case (e.g. "fr-CA"). The returned value will be one of the languages listed in the conf.lua file (see Exporting). This currently only returns a meaningful value on iOS and Android (on all other platforms it always returns "en").

Game Center (iOS only)

The following functions are only available on iOS.

am.init_gamecenter()

Initialize Game Center. This must be called before any other Game Center functions.

am.gamecenter_available()

Returns true if Game Center was successfully initialized.

am.submit_gamecenter_score(leaderboard_id, score)

Submit a score to a leaderboard. Note that Game Center accepts only integer scores.

am.submit_gamecenter_achievement(achievment_id)

Submit an achievement.

am.show_gamecenter_leaderboard(leaderboard_id)

Display a leaderboard.