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
| Source | a formula with no inputs: noise, gradients, fractals |
| Image | a formula over an image file, usually sample(x,y) (add your own files with Images…) |
| Expression | a 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, …] |
| Blend | b over a with a blend mode: normal, multiply, screen, overlay, lighten, darken,
difference, exclusion, softlight, hardlight |
| Sequence | paints 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 |
| Time | plays 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 |
| Output | the 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 / mono | the 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, height | the 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, max | the 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, tile | the picture for sample(), r[x,y], iw, ih; (on/off) coordinates wrap when read |
| zoom, zoomfunc, endwidth | zoom factor per frame and a curve of t for it (default t), or the width to zoom to by the end |
| recurse, p0, q0 | iteration 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, y | the point being plotted, in the node's field (0..1 across a default field) |
| a, d | polar coordinates about the field's centre: angle (0..2π) and distance |
| t | time within the node's lifetime, 0..1 |
| gt | time within the whole movie, 0..1 |
| n, gn | frame number within the node's lifetime / the movie |
| w, h, left, top, cx, cy | the field's size, corner and centre |
| px, py, pw, ph | pixel coordinates and the picture's size in pixels |
| fw, fh | the frame size |
| sx, sy, sz | the pixel mapped onto a sphere |
| iw, ih | the image's size |
| r, g, b | the 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, q0 | the 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:
| $a | input 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 tanh | and 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
- The expression language is Pixel Magic's (1995), as QTS had it; the examples are QTS's sample scripts from 2001–2003.
- Thumbnails on the desk show each node at the slider's time, on its own, at the Output's shape.