Formulas#

Formulas: the examples, and Plasma playing in its details

A formula is a few lines of maths that draw on the panel, every pixel at once. Change a number and the picture changes as you type. Formulas are written in Psi (Ψ), OpenDMD's small language for the panel's pixels, but you don't need to know it to start: open an example, drag a number, and watch.

Open Formulas (Ψ) in the app's menu. Formulas work in the online demo too, with no player at all.

Formulas opens on the examples (Big clock, plasma, fire, rain, a clock face, and more) and the formulas you've saved. Pick one to see it playing in the panel look, with its sliders if it has any.

Big clock, the first example, shows what a GIF can't do: it draws the real time in big rainbow digits, with a bar along the bottom that fills once a minute, a step a second. Check it against your watch. A GIF can't know the time; one that only showed each minute of a day would already be 2.7 MB, and with the seconds and the moving colours it would be gigabytes. The formula is under 1 KB, most of it comments.

From there:

If you leave the editor without saving, the gallery offers Continue editing at the top, and the app asks before anything replaces it.

On your player, Show on panel plays a formula there until the playlist moves on; save it as a GIF to keep it. Formulas that draw over a GIF have their own section, Over a GIF.

The editor#

The preview is on top (on a phone it stays in view while you scroll) and the formula underneath; the preview follows as you type. Under it, a line says how long the loop is (or that the formula follows the clock) and how hard it is for the panel. If something is wrong, the editor says what in plain words just above the formula, marks the place, and keeps playing the last version that worked, dimmed.

A plasma in two minutes#

Press + New formula, clear the text, and build it up line by line.

  1. Start with a gradient. x goes from 0 at the left edge to 1 at the right, and gray turns a number from 0 to 1 into black to white:

gray(x)

  1. Make waves. sin goes up and down between −1 and 1 once for every 1 it is given, so x*2 gives two waves across the panel. Halve it and add a half to keep it between 0 and 1:

gray(sin(x*2)*0.5 + 0.5)

  1. Set it moving. t is the time in seconds; loop 8 makes the loop eight seconds long, and t/8 moves the wave once along in that time, so it loops without a jump:

loop 8 gray(sin(x*2 + t/8)*0.5 + 0.5)

  1. Add a second wave going up and down the panel (y is 0 at the bottom, 1 at the top), moving the other way and twice as fast. Give the sum a name:

loop 8 wave = sin(x*2 + t/8) + sin(y*1.5 - t/4) gray(wave/4 + 0.5)

  1. Colour it. hsv takes a hue (once round the rainbow for every 1), a saturation and a brightness. Let the wave pick the hue, and turn the rainbow once a loop:

loop 8 wave = sin(x*2 + t/8) + sin(y*1.5 - t/4) hsv(wave/4 + t/8, 1, 1)

  1. One more wave, on a slant, and it's the Plasma example:

loop 8 wave = sin(x*2 + t/8) + sin(y*1.5 - t/4) + sin(x*1.5 + y + t/8) hsv(wave/6 + t/8, 0.9, 1)

Now drag the 2 in x*2, or the 0.9, and see what happens.

The language#

A formula is a few lines: names you set (wave = …, each name once), and a colour on the last line. It is worked out for every pixel, every frame. -- starts a comment. Everything is a number; there are no loops, lists or words (a single character in quotes, 'A', is a number too: its code).

Values

name what
x, y where the pixel is: 0 to 1 across (left to right) and up (bottom to top)
px, py the pixel's column, 0–127, and row from the bottom, 0–31
i the pixel's number, 0–4095, from the top left
t seconds into the loop
hour, minute, second the time of day
a, b, c the sliders, 0 to 1
w, h 128 and 32
cx, cy where the pixel is from the middle, in panel heights: cx from −2 at the left edge to 2 at the right, cy from −0.5 at the bottom to 0.5 at the top
r, angle how far the pixel is from the middle (round, though the panel is 4 times wider than tall: r < 0.4 is a disc), and its angle round the middle, in turns
ty the pixel's row counted from the top, 0–31

Functions. Waves and angles are in turns, not degrees or radians: sin(0.25) is 1, and sin repeats every 1.

function what
sin(v), cos(v) a smooth wave between −1 and 1
tri(v), saw(v), sqr(v) triangle, sawtooth and square waves, −1 to 1
abs(v), floor(v), frac(v), sqrt(v) size, whole part, fraction, square root
min(a, b), max(a, b), clamp(v, lo, hi) limits
mix(a, b, k) from a to b as k goes from 0 to 1
step(edge, v) 1 when v is past edge, else 0
smoothstep(e0, e1, v) a soft step from 0 to 1
len(dx, dy) the distance, √(dx² + dy²)
atan2(dy, dx) an angle, in turns (−½ to ½)
noise(x, y, z) smooth random hills, 0 to 1
hash(a, b) a random number, 0 to 1
c ? a : b, select(c, a, b) a when c is true (not 0), else b
digit(n, col, row) 1 where digit n (0 to 9) has a pixel in the panel's font, else 0
glyph(ch, col, row) the same for a character: glyph('A', col, row)
bit(n, i) bit i of the whole number n, 1 or 0, for patterns of your own
pick(k, a, b, …) a when k is 0 (up to 1), b when it is 1, and so on; 0 below 0 or past the last

Digits and letters. digit and glyph read the panel's own font, the one the clock and the title cards use: a character is 5 columns by 7 rows (col 0 to 4 from the left, row 0 to 6 from the top; a digit uses columns 0 to 3 and rows 0 to 5, the rest is the gap to the next one). Anything outside that, or a character the font hasn't, is 0, so a character drawn big is just its column and row worked out from the pixel: digit(7, floor(px/4), floor(ty/4)) draws a 7 four times the size in the top-left corner (rows count down from the top here, as ty does; py counts up). pick chooses which character goes where: glyph(pick(floor(px/5), 'H', 'I'), px % 5, ty) writes HI in the top left, and nothing after it. Add small for the smaller font, 4 by 6 (digits 3 by 5): digit(n, col, row, small), or bold for chunky 3 by 5 digits (digits only): digit(n, col, row, bold). Big clock is made this way, in bold.

Operators: + - * / %, comparisons < <= > >= == != (1 for true, 0 for false), and and, or, not. There is no power operator: write x * x.

Colour (the last line):

colour what
hsv(hue, sat, val) hue in turns; saturation and brightness 0 to 1
rgb(r, g, b) red, green and blue, 0 to 1 each
pal(name, v) a palette, v from 0 to 1: fire, ice (or ocean), chrono, amber, rainbow, gray
gray(v) black to white
#ff8800 a colour by its red, green and blue, two hex digits each (#f80 for short)

A last line that is just a number is gray of it.

Colours by name. #ff8800 is a colour, the way web pages write them. It can be the whole last line, and you can do sums with colours there: #ff8800 * k is that orange at brightness k, mix(#102040, #ff8800, y) goes from dark blue at the bottom to orange at the top, and pick(k, #ff0000, #00ff00) picks one. rgb(#ff8800, k) covers only k of the pixel. A colour can't be given a name of its own (c = #ff8800 is an error): write it in the last line, where it is used. In the editor a colour is underlined in its own colour.

The loop and the frame. loop N on a line of its own sets how long the loop is, in seconds (8 if you leave it out). For a seamless loop, everything that moves should come back to where it started: use t/N, t*2/N and so on with the loop's own N. Values inside frame { … } are worked out once a frame rather than for every pixel; they can't use the pixel's position.

Layers. A formula can be a stack of layers, bottom to top, each with its own names and its own colour on its last line:

loop 4
layer {
  wave = sin(x*2 + t/4) + sin(y*1.5 - t/2)
  hsv(wave/4 + t/4, 0.9, 1)
}
layer multiply { gray(py % 2 ? 1 : 0.6) }

The word after layer says how it goes onto what is below it: over (the default: it covers it), add (lighter), multiply (darker: white leaves it as it was), screen (lighter, softly), max or min. Names set outside the layers can be used in all of them; names set inside a layer are its own, so two layers can each have a v, and you can paste a whole formula into a layer { … } to combine it with another. loop goes on its own line outside the layers.

How much it covers. Every colour takes one more value, if you like: how much of the pixel it covers, from 0 to 1 — rgb(r, g, b, k), hsv(h, s, v, k), gray(v, k), pal(fire, v, k). Where it covers less than all, what is below shows through.

Two formulas in one. A formula can hold another one as a part:

loop 16
part bars {
  loop 4
  bar = max(0, 1 - abs(cy - 0.35*sin(t/4))*5)
  bar * #ff9933
}
part rings {
  loop 4
  d = r
  pal(rainbow, frac(d*1.5 - t/2))
}
k = smoothstep(0.25, 0.75, sin(t/16)*0.5 + 0.5)
layer { bars }
layer { rings(x, y, k) }

You don't have to type a part out: in the editor, write use Rings as ring on a line of its own, with the name of one of your formulas or an example, and when you move to another line the editor puts that formula in as part ring { … }, ready to change. Everything you save or share then has the whole of it, so it works anywhere: if a use line names a formula there isn't (or the formula you're editing), the editor says so and won't save or share until it's fixed.

The examples Bars to stars, Rings through stripes and Kaleidoscope are made this way.

Limits. A formula can be 4,096 characters and 256 steps (the editor shows the count). Numbers go from −32,768 to 32,767; past that they stop at the end instead of wrapping round, and dividing by zero gives the biggest number of the right sign rather than an error.

Over a GIF#

A formula can draw over a GIF instead of replacing it: scanlines, a colour grade, a vignette, sparkles, a clock in the corner. Four values say what is under it at the pixel:

name what
ur, ug, ub the red, green and blue under the formula, 0 to 1
ul how bright it is there, 0 to 1

The bottom layer, and any colour that covers less than all of a pixel, let it show through. For example, a sepia tone:

rgb(ul*1.1 + 0.08, ul*0.92 + 0.04, ul*0.65)

or scanlines, which keep the GIF and darken every other row:

layer multiply { gray(py % 2 ? 1 : 0.55) }

The examples' Over a GIF section has more. In the editor and in a formula's details, such a formula is drawn over a sample picture; Pick a GIF… draws it over one of your GIFs (from the playlist) or a GIF file from your computer instead, moving as the GIF does. Save as GIF… then saves the formula over that GIF. A formula played on its own has nothing under it: ur, ug, ub and ul are 0 (black).

On your player:

Any formula can be the look, not only the ones that draw over a GIF: Use as the look and Settings › Look both have a Blend and an Amount. The blend says how the formula goes onto the GIF — Over (it covers it), Screen (it lightens: the GIF's bright parts stay and the formula shows in its dark ones), Add, Multiply (it darkens and tints), Lighten or Darken (the brighter or darker of the two) — and the amount how much of it shows, 0 to 100 %. A formula that draws over a GIF already starts at Over, 100 %: exactly as before. One that covers the whole panel, like Plasma, starts at Screen, 40 % (or Over, 30 % when that is lighter for the panel), and the preview shows the choice as you move it. A formula with layers of its own goes Over only (its layers keep their own blends).

Settings lists all your formulas and all the examples. Over at 100 % of a formula that covers the whole panel would hide the GIF, so it is refused with a sentence saying so (choose a lower amount or another blend), and a formula too heavy to draw over every GIF (more than about 12 ms a frame on the panel, the blend included) is listed as too heavy. Under the controls, Settings says how fast the look moves: about 50 times a second, or 25 for a heavy one (over 8 ms); the GIFs play at their own speed either way. Shared looks stand alone: the player is sent the formula with the blend written around it, so a look keeps working even after the formula is edited or deleted here. When the player is short of memory (several downloads at once, say) it shows the GIFs without the formula for a while.

Keeping them fast#

Formulas are made to run on the panel's own small processor too, which draws every pixel 50 times a second. The editor rates each one light, medium or heavy for the panel (hover for an estimate in milliseconds); all the examples are light or medium.

Sharing#

Copy share link gives a link to the formula's own page on the OpenDMD site, with the whole formula in it, its name and its sliders. Anyone can open it, with or without a player: the page plays it in the panel look, shows its code and its sliders, and has

On your player, the share box also has Copy a link to this display: that one opens the formula in your own player's app, on your own network. Keep formulas short to share them: past about 1,500 characters the link gets long enough that some apps cut it, and the share box says so.

The formula gallery on the OpenDMD site has formulas people have made, and the examples. Open shows one on its own page; Remix opens it in the demo's editor.

To add yours, open its share link and press Submit to the gallery: give it a description, your name or GitHub handle and a licence (CC0, anyone may use it; or CC BY 4.0, with credit), then Open on GitHub. GitHub opens with the file ready and makes the pull request for you (you need a free GitHub account). A check says if anything needs fixing, a maintainer looks at it, and once it's merged it appears in the gallery within a few minutes. The gallery takes formulas that are light enough for the panel (at most 10 ms a frame as the editor estimates it) and at most 4 KB. The details are in the gallery's README.

Saving as a GIF#

Save as GIF… draws one loop of the formula, at 50 frames a second (25 for loops longer than about 5 seconds, and at most 240 frames), and opens it in Create Art, titled with its name. Add text, trim it or change its speed there, then save it to your card: it plays on the panel like any other GIF and goes in playlists. A formula with a long loop, or one that follows the clock, keeps only those few seconds, as they look when you save.

Formulas you save are kept in this browser. Another browser, or a cleared browser, won't have them: share a link to yourself to move one.

Coming next#

Formulas in playlists, next to the effects, are coming: a formula then runs on the player itself, follows the clock and loops forever without the size of a GIF.