Fragtime language

Patches

A patch is a set of nodes wired into one Output. Every node is a picture: a formula for each pixel in terms of its position (x, y) and time (t). The whole patch compiles to a single fragment shader that plays on the GPU, looping every duration seconds. Patches are saved as .fragtime files (JSON, one line per node); the editor's Source tab shows the text.

Nodes

Sourcea formula with no inputs: noise, gradients, fractals
Imagea formula over an image file, usually sample(x,y) (add your own files with Images…)
Expressiona formula that reads its inputs as $name. Warp, Mask and Displace in the bar are Expressions filled in for those jobs: $in[x+…, y+…], terp($a,$b,$m), $in[x+($by-.5)*.1, …]
Blendb over a with a blend mode: normal, multiply, screen, overlay, lighten, darken, difference, exclusion, softlight, hardlight
Sequencepaints its inputs in order, each while it is alive, each with its own blend mode onto what is below it: layers, and scenes one after another
Timeplays its input at another time: time is where in its lifetime, in seconds (t*dur*2 plays it twice as fast), and loop wraps it
Outputthe frame: width, height, fps, duration, oversample

The Source, Image and Expression nodes are formula nodes, and have these settings, each a formula (evaluated per frame) unless it says otherwise:

pix / rgb / hsv / monothe colour: a formula giving a colour (pix: a sample, rgb(), hsv(), or a grey value), three formulas (red, green, blue or hue, saturation, value), or one grey formula
left, top, width, heightthe field of x,y the picture covers (default 0,0 to 1, with height keeping pixels square: w*fh/fw)
center(on/off) left,top are the centre instead of the corner
min, maxthe range of the formula's result that maps to black..white (default 0..1)
wrap, cycle(on/off) values outside min..max wrap around instead of clipping; rotate the values by 1/256 a frame
image, tilethe picture for sample(), r[x,y], iw, ih; (on/off) coordinates wrap when read
zoom, zoomfunc, endwidthzoom factor per frame and a curve of t for it (default t), or the width to zoom to by the end
recurse, p0, q0iteration limit for the fractals (default 64) and the Julia constant (jx, jy)
oversample(on/off) override the Output's 2×2 supersampling for this node

Lifetime

Any node may have a debut (seconds) and a duration, relative to whatever pulls it: the Sequence that paints it, or the node that reads it. Without them a node lives as long as its caller, and t runs 0..1 over that. In a Sequence, debut may be last: when the previous input ends. That is how a piece in scenes is made: a Sequence of nodes with debut=last and a duration each.

Formulas

Ordinary arithmetic with + - * / ^ (or **), %/mod, comparisons < <= = == != <> >= > (giving 1 or 0), &&/and, ||/or, bitwise & | xor ~, and cond ? a : b. Case does not matter. pi, pi2 (2π) and e are constants.

Variables

x, ythe point being plotted, in the node's field (0..1 across a default field)
a, dpolar coordinates about the field's centre: angle (0..2π) and distance
ttime within the node's lifetime, 0..1
gttime within the whole movie, 0..1
n, gnframe number within the node's lifetime / the movie
w, h, left, top, cx, cythe field's size, corner and centre
px, py, pw, phpixel coordinates and the picture's size in pixels
fw, fhthe frame size
sx, sy, szthe pixel mapped onto a sphere
iw, ihthe image's size
r, g, bthe image's red, green (0–255) and blue (0–1) at this pixel; r[x,y] etc. at a point
z, zoom, recurse, dur, duration, minval, maxval, p0, q0the node's settings (dur: its lifetime in seconds)
?a random number 0..1

Reading inputs

A node reads the nodes wired into it by the names of its inputs:

$ainput a at this point and time
$a[x, y]at the point x,y (0..1 across a's field; wraps if a is tiled). $a[(x-left)/w,(y-top)/h] is the same place in a's field as here
$a{d, a}at polar coordinates about a's centre
$a[t]at time t (0..1 of a's lifetime)
$a[x, y, t]both
sample(), sample(x,y)the image at this point / at x,y (0..1); samptile is a synonym
samplep(), samplep(x,y)the image by pixel

Functions

sin cos tan asin acos atan sinh cosh tanhand sin2, cos2, sin3, cos3 (squared, cubed)
sqrt log log10 exp pow(x,y) pow2(x)
int abs sign
min(a,b) max(a,b) clamp(x,lo,hi) limit(x,lo,hi)
angle(x,y) dist(x,y)polar coordinates of a vector
terp(a,b) terp(a,b,x)interpolate a→b over the node's time, or by x (0..1); works on colours
ease(x) ease()smoothstep of x / of t
bias(x,p) gain(x,p)Perlin's bias and gain curves; bias(p) applies to t
pinch(x) pinch(x,p)1-(1-x)^p
noise() noise(x,y) noise(x,y,z)gradient noise, −1..1
fnoise(...) 1/f noise: 8 octaves of noise
turb(...)turbulence: 8 octaves of |noise|
fbm(x,y,z,H,lacunarity,octaves) fbmt(...)fractional Brownian motion with full control (noise/fnoise/turb with 6 arguments do the same)
noise2(x,y) noise2(x,y,p)noise with a gain() interpolation curve
wnoise1() wnoise2()Worley (cell) noise: distance to the nearest and second-nearest feature point
mand() mand(x,y)Mandelbrot escape count at this point (0 inside the set); mandel too
mandc()continuous-potential Mandelbrot
mand3()the cubic Mandelbrot
mandq() mandq(y,z)the quaternion Mandelbrot
julia() julia(p,q) julia(x,y,p,q)Julia set escape count, with the node's p0,q0 or the given constant
dragon()the dragon iteration
zebra(n,width,step,offset)stripes
fib(n) prime(n)Fibonacci number; 1 if prime
linen(t,attack,dur,decay) linenr(t,attack,decay,ratio)envelopes (from Syd)
rgb(r,g,b) hsv(h,s,v) triple(a,b,c)make a colour

The shader

The editor recompiles the patch as you type and plays it live, at the Output's size. Save frame saves the picture; Render movie… draws every frame on the GPU into an MP4 (H.264) or a zip of PNGs, at any size. Save shader writes a page that plays the loop by itself, with the shader and any images inside it (space pauses, arrows step, s saves a frame, o toggles oversampling). GLSL writes the fragment shader alone, in Shadertoy's dialect (mainImage, iTime, iResolution, iChannel0…), so it can be pasted there (give its channels the patch's images, nearest filtering, no vertical flip). The 2×2 box turns oversampling off in the live view, for four times the speed.

The shader is 32-bit float, so very deep fractal zooms (widths of a billionth) dissolve into noise; shallow ones are fine. ? is a random stream per pixel and frame. Where a formula leaves its domain (bias, gain and two-argument ease outside 0..1, for instance) the result is undefined. A shader can't have wires that loop, more than four images, or iteration counts (recurse) above 4000.

QTS scripts

Fragtime began as the browser port of QTS ("QuickTime Synthesizer", 2001–2003), whose pictures were scripts of nested <seq> tags. Open… still reads them and imports each as the patch it amounts to: a leaf <seq> becomes a formula node with the settings it inherited, a <seq> with children a Sequence, $name a wire, chop= the blend mode, fw, fh, framerate the Output. The examples are imported scripts, and draw exactly what the scripts did.

// a two-second movie, 320x240 at 24 fps, oversampled
<seq fw=320 fh=240 framerate=24 oversample>
  <seq id=a debut=0 duration=1 center width=4 min=-1 mono="fnoise(x,y,t)" />
  <seq debut=last duration=1 hsv=".6, $a, 1-$a" />
</seq>

The original renderer (C++, as a native tool and WebAssembly) is in the source repository; the QTS site, qts.jbum.com, keeps it in the browser.

Notes