RectSL cheatsheet
RectSL is Field's language for building scenes from animated rectangles. It creates geometry on the CPU, lets you sample the objects' state, and sends the scene to Field 4 buffers for drawing. Use StackSL alongside it to describe how the materials shade.
Every section below carries a short snippet and a pointer into RectSL examples, where the same words appear as complete, rendered clips. The snippets here are fragments that assume a scene header, so when you want a program to copy and run, follow the pointer.
To write a scene with an assistant, and watch it play as you talk, see Talking a scene into existence: a RectSL box in Field plays in the window above it, and its AI4 chat has tools that draw, trace and compare the scene.
A RectSL string starts with #rectsl when embedded in Kotlin:
import afield.lang.rectsl.*
val source =
"""
#rectsl
->wall 4 4 meters
->duration 4
->fps 20
"left" rect
-1 0 ->xy
1 ->square
'( 0 -1 2 0 4 3 ) ->x
'( 0 0 2 1 4 0 ) ->alpha
;
dup "right" ->rename
->mirror-x
;
"""
val program = RectAnimationOracle.compile(source).program!!
val frame = program.sample(2.0)
Model
RectSL is a small stack language with scene side effects. Numbers, strings, colors, quotes, selections, rects, groups, paints, and scalar tracks can sit on the stack. Most geometry words apply to the active rect/group or to the current selection.
Words beginning with -> are mutators: they consume values and write through to
the active rect, group, selection, or scene. Ordinary words transform stack values,
query the scene, control evaluation, or construct new values/nodes. Thus
f0 tau * computes an angle while f0 tau * ->rot commits that angle to the
active object. The arrow is part of the word, not optional punctuation.
| Syntax | Meaning |
|---|---|
# comment |
Ignores the rest of the line. |
12, -0.25, 1e-3 |
Pushes a scalar number. Signed decimals and scientific notation are accepted. |
0xffaa33 |
Pushes an RGB colour from hexadecimal red, green and blue components, for solid to turn into a fill. |
"name" |
Pushes a string value. Strings name objects, tags, tracks and palettes according to the following word. |
'( ... ) |
Stores a block as a quoted value without running it, for a loop, a track constructor or another word that consumes a quote. |
: name ... ; |
Defines a reusable word from the enclosed program. |
material name '( ... ) |
Defines a named fragment material using StackSL source, to be assigned with ->material once the geometry is arranged. |
Kotlin entry points
| API | Purpose |
|---|---|
RectAnimationOracle.compile(source) |
Parse, type-check, and build a RectProgram. |
RectProgram.sample(time) |
Sample exact rectangle/group state at a time in seconds. |
lowerSampledScene(sample) |
Convert a sampled scene to CPU mesh data. |
RectSLRenderRunner.renderJpgSequence(source, settings) |
Render a JPG frame sequence through a Field renderer. |
RectSLRenderSettings(...) |
Output path, fps, quality, renderer, frame inclusion, and wait policy. |
RectSLEditorExtension.javascript() |
Monaco embedded-language support for #rectsl Kotlin strings. |
Scene words
Every scene opens by saying how big the wall is, in what units, and how long the clip
runs. Below: a wall 1600×900 pixels wide, cropped to fill whatever it is drawn into,
on a near-black background, four seconds at thirty frames a second, then one gold
200-pixel square in the middle of it. Changing pixels to meters leaves the geometry
as it is; the unit tells the renderer and the Field 4 lowering what the numbers mean.
#rectsl
->wall 1600 900 pixels # or `12 8 meters`, or `12 8 abstract`
->fit cover # contain | cover | stretch
->background 0x0a0a12
->duration 4
->fps 30
"mark" rect 0 0 ->xy 200 ->square 0xffcc33 solid ->fill ;
Worked: Kaleidoscope for ->background,
Grid field for pixels with ->fit cover, and
Paint & scene kit for ->fit stretch.
Every word here is prefix: it reads its arguments forward out of the source rather
than off the stack, so there is nothing to push first. ->background accepts both: a
colour literal after it is read forward, and anything else comes off the stack.
| Word | Reads | Notes |
|---|---|---|
->wall meters |
->wall <w> <h> meters |
Physical wall size in meters. meter/m also accepted. |
->wall pixels |
->wall <w> <h> pixels |
Wall size in pixel units. pixel/px also accepted. |
->wall abstract |
->wall <w> <h> abstract |
Wall size in arbitrary units. units/unit also accepted. |
->duration |
->duration <seconds> |
Scene duration in seconds. |
->fps |
->fps <framesPerSecond> |
Sampling/render frame rate. |
->background |
->background <0xrrggbb>, or color ->background |
Sets scene background. |
->fit contain |
->fit contain |
Preserve aspect ratio and fit inside target. |
->fit cover |
->fit cover |
Preserve aspect ratio and cover target. |
->fit stretch |
->fit stretch |
Stretch wall coordinates to target. |
Objects and groups
A scene is a tree of named rects and groups, and the names are how you get back to
anything after you have moved on. Below, a group orbit holds two squares tagged
petal: one authored, and a clone of it shifted along +x. The whole group is then
deep-copied into a twin three units lower, and finally every petal in the scene,
in both copies, is gathered out of its parent into a new group petals.
clone copies the active object; copy-deep copies a group and its descendants, so
the twin gets its own cells rather than sharing them.
"orbit" group
"cell" rect 0 0 ->xy 1.2 ->square "petal" ->tag ;
"cell" ->select clone 2.5 0 ->xy ;
end
"orbit" ->select copy-deep "twin" ->rename 0 -3 ->translate ;
all rects '( "petal" tagged? ) filter "petals" ->group-selected ;
Worked: Groups, selections, and StackSL material,
and Selection & logic, which walks the whole
tree and exercises rect?, group?, id and copy-deep in one scene.
| Word | Stack | Notes |
|---|---|---|
rect |
[id] -- rect |
Creates a rect. If the top stack value is a string, it becomes the id. |
group |
[id] -- group |
Opens a group scope. New objects become group children until end / end-group. |
end, end-group |
-- |
Closes the current group. |
->select |
id -- object |
Selects a rect or group and pushes its reference. |
->rename |
name -- object |
Renames the active object or a reference from the stack. |
->tag |
name -- |
Adds a tag to the active object or current selection. |
id |
-- id |
Pushes the active object id. |
rect? |
-- bool |
1 when the active object is a rect. |
group? |
-- bool |
1 when the active object is a group. |
tagged? |
tag -- bool |
Tests the active object for a tag. |
copy, clone |
-- object |
Shallow copy of the active object. |
copy-deep, clone-deep |
-- object |
Deep copy of a group and its descendants. |
->group-selected |
selection [id] -- group |
Reparents selected objects into a new group. |
Geometry and ordering
Three rects, three different ways of being animated. mark sets each channel by hand:
it slides from x=-5 to x=+5 while turning a full revolution, at eight-tenths scale and
seven-tenths opacity, on draw layer 2 and at the front of it, and it exists only
between t=0.5 and t=3.5. Wherever a channel takes a number it also takes a quote, and
the quote becomes a keyed track. Motion is written this way throughout RectSL.
dart sets no rotation of its own: it travels a curve in x and y, and ->face-travel
turns it to face along that path, so an elongated rect points where it is going.
bullet authors one crossing, and ->live fires 24 copies of it down 24 lanes, each
starting 0.18 s after the last and alive for 2.5 s.
"mark" rect
0 0 ->xy 2 0.6 ->wh
'( 0=-5 4=5 ) ->x # a quote in a channel is a track
'( 0=0 4=6.28318 ) ->rot
0.8 ->scale 0.7 ->alpha
2 ->layer 0.2 ->z ->front
0.5 ->appear 3.5 ->vanish
;
"dart" rect 1.1 0.26 ->wh # drawn along its own +x, so it reads as an arrow
'( 0=-5 4=5 ) ->x '( 0=-3 2=3 4=-3 ) ->y
->face-travel # rotation taken from its own travel
;
"bullet" rect 0.42 ->square '( 0=-8 2.5=8 ) ->x
24 '( -3 f 6 * + ->y i 0.18 * 2.5 ->live ) array
;
Worked: Named tracks and draw layers
for ->layer/->z, Facing the way you travel
for ->face-travel and ->face-travel-at, and
Spawning and despawning
for ->appear/->vanish/->live.
| Word | Stack | Notes |
|---|---|---|
->xy |
x y -- |
Sets the object centre in wall coordinates. The wall’s declared units determine the meaning of x and y. |
->nxy |
nx ny -- |
Sets the centre using normalized wall coordinates in [-1,1]. |
->x, ->y |
scalar -- |
Constant or animated coordinate. A quote becomes a TCB track. |
->wh |
width height -- |
Sets width and height in wall coordinates. |
->nwh |
nw nh -- |
Sets width and height relative to the wall dimensions. |
->w, ->h |
scalar -- |
Sets the width or height channel to a scalar or track. This changes dimensions independently of the object’s scale transform. |
->square |
size -- |
Sets width and height together. |
->rot, ->rotation |
scalar -- |
Sets rotation in radians. A track animates the angle over time. |
->rotate |
scalar -- |
Adds rotation to the active object/selection. |
->face-travel |
-- |
Auto-orient: sets rotation to the heading of the object's own x/y tracks (local +x points along travel). |
->face-travel-at |
offset -- |
Like ->face-travel, but adds a fixed angle offset (correct for how the art is drawn). |
->scale |
scalar -- |
Sets uniform scale on the active object or selection. |
->scale-xy |
sx sy -- |
Sets separate horizontal and vertical scale factors. |
->alpha |
scalar -- |
Sets the opacity channel. Supply a track to fade the object over time. |
->opacity* |
factor -- |
Multiplies the existing opacity by a factor, so relative fades survive dimming a selection. |
->appear |
t -- |
Hard-gate visibility on: invisible before t, unchanged from t. |
->vanish |
t -- |
Hard-gate visibility off: unchanged before t, invisible from t. |
->live |
birth life -- |
Emitter primitive: play the object's animation from birth (local time 0 = birth) and show it only during [birth, birth+life]. |
->z |
scalar -- |
Sets per-rectangle depth metadata. Use draw layers and ordering words when the intended change is paint order. |
->layer |
number|string|track -- |
Draw layer. Higher layers render later. |
->front, ->back |
-- |
Move active object/selection within its layer. |
Paint and material
A rect is filled in one of three ways, and all three are here. panel takes a flat
mid-blue built channel-by-channel, and places itself in normalised wall space rather
than in wall units: dead centre, four-fifths of the wall wide and two-fifths tall.
chip takes a gold-to-magenta ramp baked across its own width on the CPU, and a
StackSL material tint on top of it, fed four numbers of its own through ->shader
that the shader body can read. ghost shows the shorthand: a colour with alpha in it,
no track needed.
The material tint '( … ) body is a StackSL fragment program, compiled with the rest of
Field's shaders.
material tint '(
uv 4.0 * sin abs
)
"panel" rect 0.5 0.5 ->nxy 0.8 0.4 ->nwh
rgb 0.12 0.34 0.62 solid ->fill
;
"chip" rect 0 -2.6 ->xy 1 ->square
0xffcc33 0xff2266 -0.5 0 0.5 0 linear ->fill # two-colour ramp across the rect
0.1 0.2 0.5 0 ->shader # per-rect data its material reads
tint ->material
;
"ghost" rect 3 -2.6 ->xy 1 ->square rgba 0.9 0.4 0.2 0.5 solid ->fill ;
Worked: Paint & scene kit for rgb/rgba/->shader,
Gradient sweep for linear, and
Groups, selections, and StackSL material
for a material on a whole selection.
rgb, rgba and material are prefix: they read their arguments forward out of the
source, not off the stack. So it is rgb 0.9 0.4 0.2, never 0.9 0.4 0.2 rgb, and
those slots take only literals, not expressions. Everything else in the table
is stack-based as written, linear included.
| Word | Stack / reads | Notes |
|---|---|---|
rgb |
rgb <r> <g> <b> → -- color |
Builds an RGB color from three 0..1 literals. |
rgba |
rgba <r> <g> <b> <a> → -- color |
Builds an RGBA color from four 0..1 literals. |
solid |
color -- paint |
Wraps a colour as a solid paint value. Assign it with ->fill to colour a rectangle. |
linear |
color0 color1 x0 y0 x1 y1 -- paint |
CPU-baked two-color fill across the rect. |
->fill |
paint -- |
Assigns the paint on the stack to the active rectangle or selection. The paint can be solid or a two-colour ramp. |
material |
material <name> '( ... ) |
Compiles the quote source as a named StackSL rect material. |
name ->material |
materialName -- |
Assigns a material defined earlier in the program, or a built-in one, to the active rect/selection. |
->shader |
x y z w -- |
Stores four per-rectangle values for the material shader. Their meaning is chosen by the StackSL material that reads them. |
Material StackSL receives rect/material data from the generated shader harness. Prefer RectSL for geometry and StackSL for fragment math; do not duplicate shape layout inside StackSL unless the shape exists only in the shader.
Tracks
Track quotes are flat time value pairs. An optional = may sit between the
time and value of a key to read the pairing aloud: 0=0.2 means "0.2 at time
0". The two forms are interchangeable and may be mixed:
'( 0 -1 2 0 4 1 ) linear
'( 0=-1 2=0 4=1 ) linear
A track can also be named once and referred to by many rects, which is how several
objects come to share one motion rather than four copies of it that drift apart. Below,
rise is a straight ramp from -3 to +3 and pulse is a stepped envelope; bar drives
its y from the first and its opacity from the second, with smooth softening the steps
into an eased curve on the way in. Its scale gets a TCB curve slackened by tcb!.
The last line is the other direction: alpha-track reads the opacity curve back off
the rect, lifts every key by 0.1, clamps the result into [0, 1], and assigns it again.
Every animated channel can be read as well as written.
linear-track "rise" '( 0=-3 2=0 4=3 )
hold-track "pulse" '( 0=0.2 2=1 3=0.45 )
"bar" rect 0 0 ->xy 0.7 1.8 ->wh
"rise" track-ref ->y
"pulse" track-ref smooth ->alpha
'( 0=0.6 2=1.6 4=0.6 ) tcb -0.4 0 0 tcb! ->scale
alpha-track 0.1 value+ 0 1 clamp-values ->alpha # read it back, reshape, reassign
;
Worked: Named tracks and draw layers, and Key lenses, which sets and re-reads every animated channel in turn.
The three definition words are prefix: track, linear-track and hold-track read
a name and a quote forward out of the source. Everything else here is stack-based.
| Word | Stack / reads | Notes |
|---|---|---|
tcb |
quote -- track |
Smooth scalar track using TCB interpolation. |
linear |
quote -- track |
Linear scalar track. |
hold |
quote -- track |
Step/hold scalar track. |
tcb! |
track tension continuity bias -- track |
Sets default TCB parameters. |
track |
track <name> '( ... ) |
Defines a named TCB track. |
linear-track |
linear-track <name> '( ... ) |
Defines a named linear track. |
hold-track |
hold-track <name> '( ... ) |
Defines a named hold track. |
track-ref |
name -- track |
Pushes a named track for reuse. |
x-track, y-track |
-- track |
Pushes the active x/y track. |
w-track, h-track |
-- track |
Pushes width/height tracks. |
rot-track, rotation-track |
-- track |
Pushes rotation track. |
scale-x-track, scale-y-track |
-- track |
Pushes scale tracks. |
alpha-track |
-- track |
Pushes alpha track. |
z-track, layer-track |
-- track |
Pushes ordering tracks. |
Track transforms
A track is a value, so it can be bent before it is assigned. All five rects below ride
the same three-key sweep from x=-8 to x=+8; only the time axis differs. delayed
starts a second and a half late, slower spaces its keys 1.4× farther apart, bounced
plays forward then backward over a two-second period, and eased shapes the
interpolation with a Penner curve and then loops it. Stacked in rows, the one motion
plays as a round, so you can see what each retiming word did.
"plain" rect 0 3 ->xy 1 ->square '( 0=-8 2=0 4=8 ) ->x ;
"delayed" rect 0 1 ->xy 1 ->square '( 0=-8 2=0 4=8 ) 1.5 delay-time ->x ;
"slower" rect 0 -1 ->xy 1 ->square '( 0=-8 2=0 4=8 ) 1.4 time* ->x ;
"bounced" rect 0 -3 ->xy 1 ->square '( 0=-8 1=8 2=-8 ) 2 pingpong-time ->x ;
"eased" rect 0 -5 ->xy 1 ->square '( 0=-8 2=8 ) ease-in-out 2 loop-time ->x ;
Worked: Time canon for the retiming words side by side,
Easing ladder for the easings pulling apart, and
Key lenses for value+, clamp-values and the
keys-before/keys-after/keys-between window filters.
| Word | Stack | Notes |
|---|---|---|
delay-time |
track seconds -- track |
Moves every key later by the supplied number of seconds. The value curve is preserved. |
scale-time |
track factor -- track |
Multiplies key times by a factor. A factor above one makes the same motion occupy more time. |
reverse-time |
track -- track |
Reverses the track’s key times. Use it to traverse an existing motion backwards. |
loop-time |
track period -- track |
Repeats a track over the supplied period in seconds. The original track remains the motion sampled within each period. |
pingpong-time |
track period -- track |
Alternates forward and backward traversal of the track. Use it to avoid an abrupt jump from the end back to the start. |
time+ |
track seconds -- track |
Shifts the track’s key times by a number of seconds. It is the compact form of a time offset. |
time* |
track factor -- track |
Scales the track’s key times by a factor. Use it to retime the same key values. |
snap-time |
track grid -- track |
Snaps key times to the supplied time grid. This quantizes timing rather than key values. |
value+ |
track amount -- track |
Adds the supplied amount to every key value. Timing is preserved while the whole curve moves up or down. |
value* |
track factor -- track |
Multiplies every key value by the supplied factor. Timing is preserved while the amplitude changes. |
clamp-values |
track low high -- track |
Clamps key values between low and high. Use it after a transform when opacity or another bounded channel must stay in range. |
keys-before |
track time -- track |
Keeps the keys at or before the supplied time. Use it to select an early portion of a track. |
keys-after |
track time -- track |
Keeps the keys at or after the supplied time. Use it to discard an earlier part of a track. |
keys-between |
track start end -- track |
Keeps keys in the supplied start/end window. The arguments are times rather than key indices. |
ease-in |
scalar -- track |
Builds quadratic ease-in timing from the scalar input. Motion begins slowly and accelerates. |
ease-out |
scalar -- track |
Builds quadratic ease-out timing. Motion decelerates toward its endpoint. |
ease-in-out |
scalar -- track |
Builds symmetric ease-in and ease-out timing. Motion is slow at both ends and faster in the middle. |
ease-smoothstep, smooth |
scalar -- track |
Uses Hermite smoothstep timing. smooth is the short alias. |
Selection and transforms
Build first, then sort the scene out by logic. pip is one spinning square tagged
spark, reproduced nine times by array with each copy's opacity set from its centred
fraction (so the row is brightest at the ends), and then spread 1.4 units apart by
->stagger. The next four lines never name an individual rect again: a mirrored clone
is folded across the axis x=3 and pushed to the front of its layer; every rect in the
scene is dimmed a further 10%; every object tagged spark is scaled up; and the
original's whole timeline is made to loop every two seconds.
A selection is a value like a track is: all, rects, children and descendants
produce one, filter narrows it, and each/map run a quote against every member,
so an operation can reach copies you never named.
"pip" rect 0 0 ->xy 0.8 ->square "spark" ->tag
'( 0=0 4=6.28318 ) ->rot
9 '( fc abs 0.7 * 0.3 + ->alpha ) array
1.4 0 ->stagger
;
"pip" ->select clone "mirrored" ->rename 3 ->mirror-x-at ->front ;
all rects '( 0.9 ->opacity* ) each ;
all '( "spark" tagged? ) filter '( 1.2 ->scale ) map ;
"pip" ->select 2 ->loop-all-time ;
Worked: Selection & logic for the tree-walking and
filtering words, Whole-timeline transforms
for the *-all-time family and ->stagger,
Stagger cascade for ->stagger-time, and
Kaleidoscope for the mirrors with ->front/->back.
| Word | Stack | Notes |
|---|---|---|
all |
-- selection |
Selects every scene node. Follow with rects, groups or a predicate to narrow the selection. |
roots |
-- selection |
Selects only top-level scene nodes. Children remain reachable through their groups. |
selected |
-- selection |
Pushes the current active object or selection. This makes it available to words that explicitly consume a selection. |
children |
selection -- selection |
Replaces a group selection with its direct children. Use descendants when deeper nested objects should also be included. |
descendants |
selection -- selection |
Selects every descendant of the selected groups. Unlike children, it traverses the nested hierarchy. |
rects, groups |
selection -- selection |
Filters the selection to rectangles or, with groups, to groups. Other node types are removed from the selection. |
tagged |
selection tag -- selection |
Keeps selected objects carrying the supplied tag. Push the tag string after the selection. |
each |
selection quote -- selection |
Runs a quote on each selected object and returns the selection. Use it to apply a shared edit to many objects. |
map |
selection quote -- selection |
Runs a mapping quote over the selected objects. The selection remains the unit passed between scene operations. |
filter, where |
selection quote -- selection |
Keeps objects for which the quote returns true. where is the equivalent spelling. |
->mirror-x, ->mirror-y |
-- |
Mirrors about the zero x or y axis. Use the corresponding -at form for an axis away from the origin. |
->mirror-x-at, ->mirror-y-at |
axis -- |
Mirrors about the supplied axis coordinate. This places the reflection relative to an explicit line in the scene. |
->translate |
dx dy -- |
Translates the active object or selection by dx and dy. Push the horizontal offset before the vertical one. |
->offset |
dx dy -- |
Adds offsets to the object’s position constants or tracks. Animated motion keeps its shape but moves in space. |
->stagger |
dx dy -- |
Adds increasing positional offsets in selection order. Each next object receives another dx and dy step. |
->delay-all-time |
seconds -- |
Shifts all tracks on the object or selection later by the supplied seconds. Their relative timing stays together. |
->scale-all-time |
factor -- |
Multiplies every key time by the supplied factor. Values above one spread keys farther apart in time. |
->reverse-all-time |
-- |
Reverses every track across the scene duration. This keeps the object’s channels synchronized while reversing the motion. |
->loop-all-time |
period -- |
Repeats all tracks with the supplied period. The period is in seconds. |
->pingpong-all-time |
period -- |
Alternates forward and backward playback of all tracks. The supplied period controls the repeated time mapping. |
->stagger-time |
stepSeconds -- |
Adds increasing delays in selection order. The step is measured in seconds per object. |
Loops and blocks
The scene below uses three loops. times runs its body eight times with the index in
i, so the first row is eight cells spread 2.2 units apart. Their opacity comes from a
test rather than a curve: cells 2 through 6 are dimmed and the rest bright, written with
comparisons and not. range counts over an explicit interval.
repeat does the same but reaches for loop-count to normalise its index into [0, 1]
first, which is how a loop places things by fraction rather than by step.
apply runs a quote right there, which is useful when a modifier wants to be grouped.
8 '(
"cell" rect i 3.5 - 2.2 * 3 ->xy 1.2 ->square
i 2 >= i 6 <= and not 0.55 * 0.45 + ->alpha
'( 0.9 ->opacity* ) apply
) times
0 6 '( "dot" rect i 2 * 6 - 0 ->xy 0.6 ->square ) range
4 '( "tick" rect i loop-count 1 - / 8 * 4 - -3 ->xy 0.5 ->square ) repeat
Worked: Selection & logic for times with the
comparison words, Grid field for grid's cell context,
and Paint & scene kit for loop-count, apply
and defer.
| Word | Stack | Notes |
|---|---|---|
apply |
quote -- |
Evaluates the quoted block immediately. This executes the code stored by a quote. |
defer |
quote -- quote |
Leaves a quote on the stack unchanged. Quotes are already deferred values, so this is an explicit marker rather than another execution mode. |
times |
count quote -- |
Runs quote count times and pushes the index before each run. |
repeat |
count quote -- |
Runs quote count times. Use i, n, or loop-count inside. |
range |
start end quote -- |
Runs quote for each integer in [start, end), or descending when end < start. |
i |
-- number |
Pushes the current loop index or range value. Read it inside a loop to vary each iteration. |
loop-count, n |
-- number |
Pushes the current loop’s count. n is the shorter alias. |
Array primitives
Array primitives reproduce a subexpression with modification: the Fischinger idea of one shape multiplied into a rhythm of copies, each shifted in space, time, size, or colour. Inside any repeat/range/array/grid/radial body the per-copy context words below are available; combine them with mix, ->offset, ->rotate, ->polar, and ->delay-all-time to vary each copy.
The first scene below is one tall thin rect reproduced eight times around a circle of
radius 3, each copy turned by its own share of a full turn (f0 tau *) so the whole
thing reads as a flower rather than eight rects in a ring. The second builds a 5×3 grid
and gives each cell an opacity from its distance out from the centre, so the field
fades towards the edges. colc/rowc are the centred column and row, which makes the
middle cell 0.
Neither loop names a copy. The body sees only its position in the array (f0 here,
colc/rowc there), and everything else follows from that.
# eight petals fanned around a centre, each rotated to face outward
"petal" rect 0.4 1.2 ->wh
8 3 '( f0 tau * ->rot ) radial
;
# a centered grid whose cell alpha ripples with distance from the middle
5 3 '(
"c" rect colc 1.4 * rowc 1.4 * ->xy 1 ->square
colc abs rowc abs max 0.4 * 1 swap - ->alpha
) grid
Worked: Reproduction with modification runs the whole family as animated clips — Radial bloom, Grid ripple, Spiral shards, arc/spiral/sunflower, and Splitting with its recursive cascade.
| Word | Stack | Notes |
|---|---|---|
array |
[template] count quote -- selection |
Reproduce a template object count times, running quote on each copy with array context. Element 0 is the template itself; the rest are fresh clones (deep for groups). Template is the active object, or a rect/group taken from the stack. Pushes the selection of all copies. |
radial |
[template] count radius quote -- selection |
Like array, but each copy is first placed on a circle of radius at angle ordinal/count · tau, then quote runs. |
arc |
[template] count radius start sweep quote -- selection |
Aimed spread: places count copies on an arc of radius, from angle start across sweep radians (endpoints inclusive), then runs quote. |
spiral |
[template] count r0 r1 turns quote -- selection |
Places count copies on a spiral: radius ramps r0→r1 while the angle sweeps turns · tau. |
phyllotaxis, sunflower |
[template] count scale quote -- selection |
Golden-angle sunflower: copy i at angle i · 137.5° and radius scale · √i. |
grid |
cols rows quote -- selection |
Runs quote cols·rows times to build fresh cells, with grid context available. Close each cell with ; like repeat. Pushes the selection of created objects. |
->split |
[template] count t quote -- selection |
At time t the template vanishes and count shards are born at its split-point position (sampled from its tracks). Each shard's flight is authored by quote around the origin in split-relative local time (array context available), then placed at the split point. Pushes the shard selection. |
->polar |
angle radius -- |
Place the active object on a circle: x = radius·cos angle, y = radius·sin angle. |
f |
-- number |
Normalized fraction ordinal/(n-1) in [0, 1]; 0 for a single element. Pairs with mix. |
f0 |
-- number |
Cyclic fraction ordinal/n in [0, 1); handy for rings (f0 tau *). |
fc |
-- number |
Centered fraction 2f-1 in [-1, 1]. |
ic |
-- number |
Centered index ordinal - (n-1)/2, symmetric around 0. |
col, row |
-- number |
Current column/row inside grid. |
cols, rows |
-- number |
Grid dimensions inside grid. |
colc, rowc |
-- number |
Centered column/row: col-(cols-1)/2, row-(rows-1)/2. |
Data visualization
Dense data is an explicit target for RectSL. A colormap turns a value in [0, 1] into a perceptually-uniform colour; a Data value is a plain numeric list you can normalize and bind to size/colour; the chart words fold both into ready-made bar charts, heatmaps, and scatter plots. All three emit ordinary rects: each, ->stagger-time, group and tracks all still apply to the result.
The scene below makes five figures, each in a line or two. The first shows a colormap:
24 thin rects laid across the wall, each filled with viridis sampled at its own
fraction. The second is a bar chart of eight literal numbers. Because bars returns a
selection, the next two lines fade the bars in, 0.08 s apart, without changing the chart
word. The third
computes its own field (48 samples of a sine) and tiles it into an 8×6 heatmap; the
fourth plots y against x; the last reads a list's min, max, length and one element
without consuming it, then charts it anyway.
# a colour legend: 24 thin rects sweeping one colormap across the wall
"swatch" rect 0.6 3 ->wh
24 '( fc 11 * 0 ->offset "viridis" f colormap solid ->fill ) array
;
# a bar chart from literal data, staggered fade-in, coloured by value
'( 3 7 2 9 5 8 4 6 ) data normalize "magma" bars
'( '( 0=0 0.6=1 ) ease-out ->alpha ) each
0.08 ->stagger-time
# a heatmap of a computed field (linspace + data-map fill the grid)
0 1 48 linspace '( 6 * sin 1 + ) data-map 8 6 "turbo" heat
# a scatter plot: y against x, coloured by y
0 1 20 linspace dup '( 3 * sin ) data-map "plasma" scatter
# data helpers: extent / dmin / dmax / data-len / data-at read without consuming
'( 10 20 30 40 ) data
dup extent drop drop dup dmin drop dup dmax drop
dup data-len drop dup 2 data-at drop normalize "coolwarm" bars
Worked: Data visualization renders each of these on its own — Colormap legend, Bar chart from data, Heatmap of a computed field, Scatter plot, and Reading a Data value.
Colormaps
| Word | Stack | Notes |
|---|---|---|
colormap, cmap |
name t -- color |
Look up a named ramp at t in [0, 1] (clamped). Pushes a Color usable anywhere colour is (solid, linear, ->fill, rgb). Names: viridis, magma, inferno, plasma, turbo, coolwarm (diverging), grayscale. |
Data
| Word | Stack | Notes |
|---|---|---|
data |
quote -- data |
Run quote and collect every Number it leaves into a Data list ('( 3 1 4 1 5 ) data, or computed: '( 8 times [ i i * ] ) data). |
linspace |
lo hi count -- data |
count evenly spaced values from lo to hi inclusive. |
normalize |
data -- data |
Rescale a list to [0, 1] by its own min/max (a flat list maps to 0.5). |
extent |
data -- min max |
Push the list's minimum then maximum. |
dmin, dmax |
data -- number |
Returns the minimum value in a data list; dmax returns the maximum. These reduce the list to one number. |
data-len |
data -- number |
Pushes the number of values in the data list. Use it to drive loops over the collected samples. |
data-at |
data i -- number |
Reads the sample at the supplied index. Out-of-range indices are clamped to the available range. |
data-map |
data quote -- data |
Transform each value through quote (the value is on the stack; i/f array context is available). |
Chart idioms
| Word | Stack | Notes |
|---|---|---|
bars |
data name -- selection |
Bottom-anchored bar chart spanning the wall; bar height and colour both from the value normalized by the list's extent, coloured with colormap name. Pushes the selection of bars. |
heat, heatmap |
data cols rows name -- selection |
Row-major grid heatmap filling the wall (row 0 at top); each cell coloured with colormap name by its normalized value. Pushes the selection of cells. |
scatter |
xdata ydata name -- selection |
Plot (->x, ->y) points as small squares, each axis normalized to the wall; colour follows normalized ->y through colormap name. Pushes the selection of points. |
Plotting layer
The chart idioms above each normalize by their own extent and fill the wall. The plotting layer adds a shared coordinate frame so several series overlay in one domain, with axes, ticks, numeric labels, and legends. Set a ->domain (or let the first series adopt its own extent) and optional ->frame/->margins; then axes, the domain-aware series (line/plot, area, points), and colorbar/legend all map through it. bars, heat and scatter do not read that frame; they keep normalizing by their own extent, and the two sets of words sit side by side in one scene. RectSL has no text primitive, so tick and colorbar labels are numerals drawn as a 3×5 grid of rects: numbers only, no titles or series names.
The next figure overlays three series. The domain is stated once (x from 0 to 10, y from
0 to 1), with margins around it for the tick numerals. axes draws the rules, ticks at
round numbers, and gridlines first, so the data paints over them. Then the
same 40-sample sine appears as a filled area, a cosine as a polyline, and a coarser
12-sample sine as points; all three land in the one coordinate system, so they can be
compared rather than merely stacked. A continuous colour bar and a five-swatch key
finish it.
#rectsl
->wall 16 10 abstract
->duration 3
->fps 30
# one shared domain, axes underneath, three series overlaid, plus legends
0 10 ->xdomain 0 1 ->ydomain # or `0 10 0 1 ->domain` in one go
0.12 0.14 0.12 0.06 ->margins # or just `->frame` for sensible defaults
axes
0 10 40 linspace dup '( 0.7 * sin 1 + 2 / ) data-map "viridis" area
0 10 40 linspace dup '( 0.55 * cos 1 + 2 / ) data-map "plasma" line
0 10 12 linspace dup '( 0.9 * sin 1 + 2 / ) data-map "turbo" points
"viridis" colorbar
5 "turbo" legend
Worked: A plotted figure,
which shows the same figure both with ->frame/->domain and with the axes set by hand.
| Word | Stack | Notes |
|---|---|---|
->domain |
x0 x1 y0 y1 -- |
Set an explicit shared x and y data domain. Series and axes map data → the plot frame through it. |
->xdomain, ->ydomain |
lo hi -- |
Set just the x (or y) domain. |
->margins |
l r b t -- |
Reserve plot-frame margins as wall fractions (left/right/bottom/top) — room for axis labels and a colorbar. |
->frame |
-- |
Apply sensible default margins (used only when none are set, so an explicit ->margins wins). |
axes |
-- selection |
Draw x/y axis rules, nice-number ticks, numeric tick labels (in the margins), and faint gridlines for the current domain. Draw it before the series so they paint on top. |
line, plot |
xdata ydata name -- selection |
Polyline through the shared domain as rotated thin rects, coloured along colormap name by point order. |
area |
xdata ydata name -- selection |
Column-filled area from the ->y = 0 baseline to each sample, coloured by normalized ->y. |
points |
xdata ydata name -- selection |
Domain-aware scatter (shares the frame with other series); colour follows normalized ->y. |
colorbar |
name -- selection |
A continuous colour ramp for name in the right margin, with min/mid/max numerals from the y domain. |
legend |
count name -- selection |
A key of count discrete colour swatches sampling colormap name, stacked top-right. |
Musicality: patterns as scene structure
RectSL's array primitives distribute copies in space. The musical layer distributes them in
time, using Field's Tidal-inspired pattern language. A pattern can be used
anywhere a track can, and each pattern event, with its onset and duration, can become a ->live
window.
All of the mini-notation works: polymeter {a b c}%8, Euclid
bd(3,8), per-cycle alternation <a b>, ? degrade, . grouping, @ elongation.
Clock
A cycle is the pattern language's bar. ->cps states cycles per second directly; ->tempo states
it as a musician would. The two spellings of tempo commute: ->tempo remembers its bpm, so
->beats-per-cycle re-derives from it whichever order they appear in.
Below, no time in the program is written in seconds. ->tempo 120 with four beats to
the cycle makes a bar two seconds long, and ->duration-cycles 4 makes the clip four
bars, or eight seconds. The two rects then compute positions from musical units: one beat is 0.5 s, two cycles is 4 s, cycle-secs is 2, and
scene-cycles is 4. Change the tempo and every one of those numbers moves with it.
->tempo 120 # a 4/4 bar at two seconds
->beats-per-cycle 4
->duration-cycles 4 # …so the piece is eight seconds long, unwritten
"beat" rect 0.6 ->square 1 beats 8 * 2 cycles 4 * ->xy ;
"cyc" rect 0.6 ->square cycle-secs 4 * scene-cycles 1 - ->xy ;
The four scene-level clock words are prefix: they read a number forward out of the source. The four conversions are ordinary stack words.
| Word | Stack / reads | Notes |
|---|---|---|
->cps |
->cps <n> |
Cycles per second. Default 1, so a cycle is a second and a scene that never mentions the clock is unaffected. |
->tempo |
->tempo <bpm> |
Beats per minute; cps = bpm / 60 / beatsPerCycle. |
->beats-per-cycle |
->beats-per-cycle <n> |
Beats in a cycle. Four (a 4/4 bar) by default. |
->duration-cycles |
->duration-cycles <n> |
Scene duration in cycles rather than seconds. Set the tempo first. |
beats |
n -- seconds |
Converts a beat count to seconds using the scene clock. Set tempo before converting durations. |
cycles |
n -- seconds |
Converts a cycle count to seconds using the scene clock. The result can be assigned to an ordinary time channel. |
cycle-secs |
-- seconds |
Pushes the duration of one cycle in seconds. It is the reciprocal of cycles per second. |
scene-cycles |
-- number |
Pushes the scene’s total duration in cycles. This combines scene duration with the current clock. |
pat: one copy per event
pat is array indexed by rhythm instead of by ordinal. "bd*4" is four events a bar,
so over four bars the kick template below becomes sixteen copies, one per strike, each
alive for its own quarter-note. Its body is authored in local
time, where 0 is that copy's own birth: every strike drops from y=5.5 to 0 over 0.34 s
and flashes up and out over half a second. step is the index within the bar, so the
sixteen strikes also march across the wall and restart at each bar line.
The second rect reads the event rather than just counting it: which sample name fired
(atom, atom?), how long its whole is (dur), which cycle it fell in (cyc), and any
parameter the notation attached to it ("n" param). "bd:2 sd cp*2" carries a 2 on the
first event, which is where that opacity comes from.
->tempo 120
->duration-cycles 4
# one template, sixteen strikes: four a bar, each alive for its own quarter
"kick" rect 1.3 ->square 0xff5a3c solid ->fill
"bd*4" '(
step 1.5 - 4.2 * ->x
'( 0=5.5 0.34=0 ) ease-out ->y
'( 0=0 0.10=1 0.5=0 ) linear ->alpha
) pat
;
# the per-event context: which atom, how long, which cycle, what it carries
"ctx" rect 0.5 ->square
"bd:2 sd cp*2" '(
t 2 * 9 - ->x
cyc 2 * step 0.4 * + 3 - ->y
"n" param 0.1 * 0.4 + ->alpha
"bd:2" atom? 0.4 * ->rotate
dur 3 * 0.3 + ->scale
atom ->tag
) pat
;
Worked: Four to the floor is pat at its plainest
against a clock; The rest of the surface
puts every per-event context word on screen at once.
| Word | Stack | Notes |
|---|---|---|
pat |
[template] pattern quote -- selection |
Reproduce the template once per event, each copy ->live for exactly that event's window. The quote authors the copy's flight in local time (0 is its own birth), like ->split. Anything the quote creates is given the same window, so pat composes with array, grid and ->split. An all-rests pattern shows nothing. |
t |
-- seconds |
Pushes the current pattern event’s onset in seconds. Use it inside the quote run for that event. |
dur |
-- seconds |
Pushes the whole event’s duration in seconds. A clipped event fragment does not change its whole duration. |
cyc |
-- number |
Pushes the cycle containing the event’s onset. Use it to vary a pattern across repeated bars. |
step |
-- number |
Pushes the event index within its cycle. The index restarts at the next cycle. |
atom |
-- string |
The event's mini-notation value: bd, 808:4, c'maj7. |
atom? |
name -- bool |
1 when this event's atom is name. |
param |
key -- number |
A named parameter carried on the event (gain, pan, n, note, …). |
param? |
key -- bool |
1 when the event carries that parameter. |
i, n, f, f0, fc and ic work inside a pat body exactly as they do inside array.
Patterns as values
mini turns text into a rhythm; the signal words build continuous patterns. Both coerce to a
track wherever RectSL wants one: sine ->alpha and "0 0.25 0.5" ->rot need no conversion
word. A bare string is read as mini-notation only when it contains numbers, so a misplaced
name still errors rather than silently becoming zero.
Seven rects, seven kinds of pattern, all of them assigned straight into ->x with no
conversion word. smooth takes a raw sine, scaled to span the wall, and a second copy of
it into rotation. stepped is the same sine put through segment 8, which samples it
eight times a bar, so it holds and then jumps instead of easing. steps is a bare mini-notation string of numbers. held uses
hold-pat so the rest in "-6 ~ 6" keeps the previous value instead of dropping to
zero. The last three remap a triangle into [-5, 5], round a saw onto a quarter grid,
and centre seeded Perlin noise around zero.
->tempo 90
->duration-cycles 4
"smooth" rect 1 ->square sine 12 * 6 - ->x 3.4 ->y sine tau * ->rot ;
"stepped" rect 1 ->square sine 8 segment 12 * 6 - ->x 1.6 ->y ;
"steps" rect 0.9 ->square "0 3 -3 6" ->x -0.2 ->y ; # a string with numbers is mini-notation
"held" rect 0.9 ->square "-6 ~ 6" mini nums hold-pat ->x -2 ->y ;
"remapped" rect 0.8 ->square tri -5 5 pat-range ->x -3.6 ->y ;
"quantised" rect 0.8 ->square saw 0.25 quantise 16 * 8 - ->x -4.6 ->y ;
"noisy" rect 0.8 ->square 7 perlin bipolar 6 * ->x 4.4 ->y ;
Worked: Signals, stepped and smooth, and
Kolakoski rhythms for kolakoski/kolakoski-gaps
against euclid.
| Word | Stack | Notes |
|---|---|---|
mini |
text -- pattern |
Parses the text on the stack as pattern mini-notation. The result is a pattern value for later transformation or placement. |
sine, cosine, saw, isaw, tri, square |
-- signal |
Continuous signals over [0,1], one cycle long. |
rand, perlin |
seed -- signal |
Deterministic noise — granularity-free, identical on any machine. |
nums |
pattern -- signal |
Read a rhythm's atoms as numbers. |
texts |
signal -- pattern |
Converts numeric signal values to text pattern atoms. This is the reverse direction of nums. |
segment |
signal n -- signal |
Sample a continuous signal into n events per cycle — how a smooth curve becomes stepped. |
pat-range |
signal lo hi -- signal |
Remap [0,1] to [lo,hi]. (range is already the loop word.) |
quantise, quantize |
signal step -- signal |
Round values to a grid. |
bipolar |
signal -- signal |
[0,1] to [-1,1]. |
euclid |
pulses steps rotation -- pattern |
A Euclidean rhythm as events — the onsets only, not a mask. |
kolakoski |
steps rotation -- pattern |
A Kolakoski rhythm as events. The terms of the self-describing word 1 2 2 1 1 2 … read as alternating on/off run lengths, so the figure's own run lengths spell the word back: x..xx.x..x..xx.x over sixteen steps. |
kolakoski-gaps |
steps rotation -- pattern |
kolakoski, reading each term as the gap to the next onset instead of a run length: xx.x.xxx.xx.x.xx — eleven onsets in sixteen steps against the run reading's eight, and never more than one rest between hits. |
hold-pat |
signal -- track |
Sample-and-hold: a rest keeps the previous value instead of falling to zero. Pure — it looks backwards, it does not remember. |
Pattern transforms
These time transforms work on rhythms and signals alike, and make one bar differ from the next.
The scene below has six rows, all the same three-hit figure, each varied a different way. plain is the
reference. faster doubles its speed but only on cycles 0, 3, 6…, so the row lurches
once every three bars. walking rotates the bar a further quarter each cycle, with no
quote needed. echoed superimposes a copy shifted an eighth of a
cycle, with each of its events doubled. swung delays every second subdivision of an
eighth-note grid in exact rational time, so the row is swung yet still quantised.
sometimes squeezes a deterministic half of its events into the back of their own slots,
turning four-on-the-floor off-beat without moving the grid.
->cps 1
->duration-cycles 8
"plain" rect 0.7 ->square "bd*3" '( f0 14 * 7 - 4.2 ->xy ) pat ;
"faster" rect 0.7 ->square "bd*3" mini 3 '( 2 fast ) every '( f0 14 * 7 - 2.4 ->xy ) pat ;
"walking" rect 0.7 ->square "bd sd hh cp" mini 4 iter '( f0 14 * 7 - 0.6 ->xy ) pat ;
"echoed" rect 0.7 ->square "bd*3" mini 1 8 '( 2 ply ) off '( f0 14 * 7 - -1.2 ->xy ) pat ;
"swung" rect 0.7 ->square "bd*8" mini 4 swing '( f0 14 * 7 - -3 ->xy ) pat ;
"sometimes" rect 0.7 ->square "bd*4" mini 0.5 '( press ) sometimes-by '( f0 14 * 7 - -4.6 ->xy ) pat ;
Worked: Variation operators is exactly this scene animated; The rest of the surface covers the transforms not shown here.
| Word | Stack | Notes |
|---|---|---|
fast, slow |
pattern n -- pattern |
Absolute-time, so slow spans cycles rather than dropping material. |
rev, palindrome |
pattern -- pattern |
Reverse; reverse every other cycle. |
iter, iter-back |
pattern n -- pattern |
Rotate the bar one 1/n further each cycle. The walking feel, with no quote needed. |
ply |
pattern n -- pattern |
Repeat each event n times in its own slot. |
stut |
pattern n gapDen -- pattern |
n strikes per event, a 1/gapDen of a cycle apart. |
linger |
pattern num den -- pattern |
Repeat the first num/den of the cycle for the whole cycle. |
pat-rot |
pattern n -- pattern |
Rotate values against the rhythm, keeping the onsets. |
shuffle |
pattern n -- pattern |
Deterministically reorder n slots per cycle. |
degrade-by |
pattern p -- pattern |
Drop events with probability p, deterministically. |
swing, swing-by |
pattern n -- pattern / pattern num den n -- pattern |
Delay every second subdivision of a 2n grid, in exact rational time — swung but still quantised. |
press, press-by |
pattern -- pattern / pattern num den -- pattern |
Squeeze each event into the later part of its own slot: four-on-the-floor becomes off-beat without touching the grid. |
pat-delay |
pattern num den -- pattern |
Shift by num/den of a cycle. |
every |
pattern k quote -- pattern |
Apply the quote only on cycles 0, k, 2k, …. |
off |
pattern num den quote -- pattern |
Superimpose a copy shifted num/den of a cycle, transformed by the quote. |
superimpose |
pattern quote -- pattern |
Superimpose a transformed copy, unshifted. |
inside, outside |
pattern n quote -- pattern |
Apply the quote at a slower / faster time base. |
sometimes-by |
pattern p quote -- pattern |
Transform a proportion p of events. The two halves partition exactly — nothing is both transformed and untouched, nothing is dropped. |
stack-pat, cat-pat |
a b -- pattern |
Play together; play one per half-cycle. |
The quote in every/off/superimpose/inside/outside/sometimes-by runs once, at compile
time. For that reason Tidal's chunk, which transforms a different slice of the bar on every
cycle, is not available; iter gives a similar walking feel.
Variation on a selection
These apply the same ideas to scene objects rather than to rhythms, which makes them useful on material that has nothing to do with patterns.
flash is a full-wall white rect at zero opacity, behind everything, which comes up to
0.14 only on every fourth bar; any mutator can be made conditional this way. twin gets
a mirrored, dimmer partner on the other side, the spatial version of stereo pan. echo
gets a copy of itself half a second late and fainter. row is eight copies with
staggered entry times, every odd one pushed a further 0.6 of a step late (swing), and a
repeatable quarter of them dropped out per object, per cycle.
->tempo 90
->duration-cycles 4
"flash" rect 16 9 ->wh 0 0 ->xy 0xffffff solid ->fill 0 ->alpha ->back
selected 4 '( 0.14 ->alpha ) ->every # only on every fourth bar
;
"twin" rect 0.7 ->square 5 2.6 ->xy
selected '( 0.45 ->alpha ) ->jux # a mirrored, dimmer partner
;
"echo" rect 0.6 ->square '( 0=-8 4=8 ) linear ->x 0.4 ->y
selected 0.5 '( 0.4 ->alpha ) ->off # a delayed, transformed copy
;
"row" rect 0.5 ->square -7 -5.4 ->xy
8 '( ic 1.6 * 0 ->offset ) array
0.12 0.6 ->stagger-swing
0.25 ->degrade
;
Worked: Four to the floor for ->every and
->euclid; Signals, stepped and smooth
for ->jux, ->off, ->stagger-swing and ->degrade.
| Word | Stack | Notes |
|---|---|---|
->every |
selection k quote -- selection |
Let the quote take effect only on cycles 0, k, 2k, …. Both versions of every track are kept and chosen between per sample, so any mutator becomes conditional. |
->off |
selection seconds quote -- selection |
Superimpose a delayed, transformed echo. |
->jux |
selection quote -- selection |
A mirrored twin, transformed by the quote — left/right being the spatial reading of stereo pan. |
->degrade |
selection p -- selection |
Deterministic per-object, per-cycle dropout. |
->stagger-swing |
selection step amount -- selection |
->stagger-time, with every odd copy pushed a further amount of a step late. |
Rhythm in space
strip is pat spending the subdivision on where rather than on when: one cycle
spans the wall, and each copy is centred on its event and as wide as the event is long.
So "bd [sd sd] hh*4", which pat would read as a rhythm, comes out as a ruler: one
wide mark, two half-width, four quarter-width.
The second rect lays sixteen equal slots across the wall and then lets the figure decide which stay lit. The off-steps are hidden rather than deleted; RectSL has no node removal.
->tempo 120
->duration-cycles 2
# the same notation, spent on *where* instead of on *when*
"tick" rect 0.34 ->square
"bd [sd sd] hh*4" '( -3.9 ->y 0.55 ->alpha ) strip
;
# sixteen slots laid out in space; the figure decides which stay lit
"slot" rect 0.55 1.8 ->wh
16 '( f 15 * 7.5 - 2.8 ->xy ) array
16 0 ->kolakoski # or `5 16 0 ->euclid`
;
Worked: Four to the floor reads pat and strip
side by side on the same string; Kolakoski rhythms
compares the two maskings.
| Word | Stack | Notes |
|---|---|---|
strip |
[template] pattern quote -- selection |
Lay one copy per event across the wall instead of across time. One cycle spans the wall; each copy is centred on its event and as wide as the event is long. The body sees the same t/dur/step/atom context a pat body does. |
->euclid |
selection pulses steps rotation -- selection |
Keep the Euclidean onsets of a selection and hide the rest. The off-steps are hidden rather than deleted — RectSL has no node removal. |
->kolakoski |
selection steps rotation -- selection |
The same layout on the Kolakoski figure — its run-length reading, which is the one that reads as a rhythm along a row. Turn steps up past the selection and the mask wraps, as ->euclid's does. |
Distribution curves (Functions bridge)
Linear mix is rarely the shape you want across an array. RectSL borrows afield.math.functions.Functions, a tested library of easings, distributions and noise defined over [0, 1]. That is exactly the range of the array fraction f. ease shapes a scalar/track through the full Penner family; the curve words push a reusable Fn value you sample at f (or any x). The curves are graph-backed, so the Functions rewrite/evolve tooling can optimise an array's distribution toward a target.
Three arrays, three distributions. The first spreads nine copies evenly and eases their
opacity across the array with an elastic overshoot, so the fade overshoots and settles
rather than ramping. The second is spaced evenly too, but takes its opacity from a
gaussian sampled at the array fraction (bright in the middle, dark at the edges), and
its rotation from seeded value noise, which scatters the same way every run. The
third shows the two curve combinators: tile 3 repeats one bell into a comb of three
identical bumps across sixteen copies, and reflect mirrors a noise curve about 0.5
before it nudges each copy sideways.
# alpha eased across the array with an elastic overshoot
"c" rect 1 ->square
9 '( ic 1.4 * 0 ->offset f "elastic-out" ease ->alpha ) array
;
# a gaussian falloff and reproducible noise scatter, sampled at f
"d" rect 0.6 ->square 0 -3 ->xy
12 '(
ic 1.1 * 0 ->offset
gaussian f sample ->alpha
7 noise f sample 2 * ->rotate
) array
;
# `reflect` mirrors a curve about 0.5; `tile` repeats it k times across the array
"e" rect 0.4 ->square 0 3 ->xy
16 '(
ic 0.9 * 0 ->offset
gaussian 3 tile f sample 2 * 0.2 + ->scale
7 noise reflect f sample 0.3 * 0 ->translate
) array
;
Worked: Distribution curves
for the gaussian comb and
eased orbit,
Curve transforms for tile and reflect, and
Evolving a distribution for
what the graph-backed curves buy you.
| Word | Stack | Notes |
|---|---|---|
ease |
->x name -- ->x / track name -- track |
Shape a value through a named Penner easing. name is any of linear, quad-in/out/in-out, cubic-…, quart-…, quint-…, sine-…, expo-…, circ-…, back-…, elastic-…, bounce-…, smoothstep, smootherstep. A constant eases to a number; a keyed track eases its interpolation. |
gaussian |
-- Fn |
A bell curve centred at 0.5 — bright in the middle of the array, dark at the edges. |
noise |
seed -- Fn |
Seeded value-noise curve — deterministic, reproducible scatter. |
fbm |
seed -- Fn |
Seeded fractal (fBm) noise curve — richer, multi-octave scatter. |
sample |
Fn x -- number |
Evaluate a curve at x (use the array fraction f). |
reflect |
Fn -- Fn |
Mirror a curve about 0.5: reflect(->x) = fn(1-x). |
tile |
Fn k -- Fn |
Repeat a curve k times across [0, 1]. |
Note: gaussian/noise/fbm here build CPU Fn curves on the RectSL stack; the fbm/palette seen inside material '( … ) quotes are StackSL GPU words in a separate language: same names, different context.
Kotlin escape (experimental)
For anything the built-in words don't cover, {{ … }} drops into Kotlin (in a with(Functions){ … } scope) anywhere a value is expected. Whatever the Kotlin returns — a Number, an Fn, or a Track<Double> — is pushed onto the stack: the whole Functions API is available inline. The escape also sees the array context (i, n, f, f0, fc, ic) and can vary per copy:
# a curve sampled at f, and a per-copy value computed straight in Kotlin
"c" rect 1 ->square
9 '( {{ i * 1.2 }} 0 ->offset {{ sin(2) through gain(0.5) }} f sample ->alpha ) array
;
Each distinct escape is compiled once, when the scene compiles, and then called like an ordinary function, so an escape inside an array body is not recompiled per copy. Escapes need a Kotlin engine: a Field box has one, so they work while live-editing (outside a box, pass one with compileRectSL(source, engine = …)). Graphics renders and gallery .rectsl programs can use escapes too; see the inline-functions example.
Stack and scalar operators
One grid, and every number in it computed rather than typed. Each cell knows its own
centred column and row, so colc dup * rowc dup * + sqrt is its distance out from the
middle. From that distance the cell derives its opacity (a smoothstepped falloff,
floored at 0.05 and clamped), its size (a power curve, so cells shrink faster towards the
edge), and its rotation (the two axes averaged, then rounded to quarter-turns of π, so
the field falls into bands).
There are no variables: dup, over, swap and drop let a value on the stack be used
more than once.
12 7 '(
"c" rect
colc 1.1 * rowc 1.1 * ->xy
colc dup * rowc dup * + sqrt # distance from the middle
dup 0 5 smoothstep neg 1 + 0.05 max 0 1 clamp 0.9 * 0.1 + ->alpha
5 min neg 5 + 5 / 0.4 pow 0.4 * 0.4 + ->square
col cols 1 - / row rows 1 - / over + 0.5 * swap drop
4 * ceil 4 / pi * ->rot
) grid
Worked: Grid field is this scene animated, and Selection & logic covers the comparison and boolean words. The operator-coverage set uses every word in this cheatsheet at least once.
| Word | Stack | Notes |
|---|---|---|
dup |
a -- a a |
Duplicates the top value. The original stays available below its copy. |
drop |
a -- |
Discards the top value while leaving the rest of the stack in place. |
swap |
a b -- b a |
Exchanges the top two values. Use it when a following operation expects the opposite operand order. |
over |
a b -- a b a |
Copies the second value to the top. Both original values remain on the stack. |
+, add |
a b -- out |
Adds two constants or tracks. Track arithmetic combines values at the sampled time. |
-, sub |
a b -- out |
Subtracts the top operand from the one below it. The operands can be constants or tracks. |
*, mul |
a b -- out |
Multiplies constants or sampled track values. A constant factor scales a track’s values. |
/, div |
a b -- out |
Divides the lower operand by the top one with protection against zero. Operand order follows the stack signature. |
mod, pow, min, max |
a b -- out |
Applies scalar modulo, power, minimum or maximum according to the selected word. The top value is the second operand. |
neg, negate, abs, floor, ceil, fract, sqrt, sin, cos |
a -- out |
Applies the selected unary math operation to one scalar or track. Trigonometric functions use radians. |
clamp |
value low high -- out |
Bounds a value between the supplied low and high limits. Push the value, then the two bounds. |
sat, saturate |
value -- out |
Clamps a scalar or track to [0,1]. saturate is the equivalent long form. |
mix, lerp |
a b t -- out |
Linearly interpolates between a and b by t. Zero selects a and one selects b. |
smoothstep |
low high x -- out |
Builds a Hermite transition between the lower and upper edges. Values outside the interval reach the end values. |
<, lt, <=, lte, >, gt, >=, gte, =, eq, !=, <>, neq |
a b -- bool |
Numeric/string comparisons where supported. |
and, or, not |
bool... -- bool |
Boolean operators using nonzero truth. |
pi, tau |
-- number |
Pushes pi; tau pushes a full turn, twice pi. Both are useful with angles expressed in radians. |
Lowering to GLSL: a scene as a signed distance field
A RectSL scene can be lowered to a self-contained GLSL function (no uniforms, no buffers, no samplers) that answers how far is this point from the nearest visible rectangle, at this instant:
import afield.lang.rectsl.*
val program = RectAnimationOracle.compile(source).program
val sdf = RectSLGlsl.emitSdf(program, functionName = "scene")
sdf.glsl // float scene(vec2 p, float t) { … }, plus its helpers
sdf.rectCount // how many rects the field unions
sdf.trackFunctionCount // distinct animation curves it had to emit
sdf.notes // fidelity caveats; empty means nothing was approximated
Why so little of the language has to come along
The stack program runs once, when the scene compiles. array, grid, radial,
sunflower, the selection words, each / filter / map, the chart and plotting
layers and the {{ … }} Kotlin escapes all finish there, leaving a flat list of rects
whose channels are tracks. Only that list reaches the GPU, so a hundred-copy
phyllotaxis arrives as a hundred rects and every word in the language lowers.
What comes out
This scene
#rectsl
->wall 8 5 abstract
->duration 4
"bar" rect
0.9 2.6 ->wh
'( 0=-2.6 4=2.6 ) ->x
'( 0=0 2=1.1 4=0 ) tcb ->y
'( 0=0 4=1.5708 ) ->rot
;
lowers to three curve functions, one shape, and a union. The ->y curve is
shown; the ->x and ->rot curves are elided:
float rectslTrack1(float t) {
if (t <= 0.0) return 0.0;
if (t >= 4.0) return 0.0;
if (t < 2.0) {
float u = clamp((t - 0.0) / 2.0, 0.0, 1.0);
float u2 = u * u;
float u3 = u2 * u;
return (2.0 * u3 - 3.0 * u2 + 1.0) * 0.0 + (u3 - 2.0 * u2 + u) * 0.55
+ (-2.0 * u3 + 3.0 * u2) * 1.1 + (u3 - u2) * 0.0;
}
// ... the 2..4 span, same shape, different constants
return 0.0;
}
// bar
float rectslShape0(vec2 p, float t) {
vec2 c = vec2(0.0);
float rot = 0.0;
c += vec2(rectslTrack0(t), rectslTrack1(t));
rot += rectslTrack2(t);
return rectslRect(p, c, abs(0.5 * vec2(0.9, 2.6)), rot);
}
// 1 rect(s), unioned. Positive outside, negative inside.
float scene(vec2 p, float t) {
return rectslShape0(p, t);
}
Nothing is sampled into a table. The TCB track comes out as cubic Hermite spans
with their tangents solved on the CPU, so each span costs the shader four multiply-adds.
Easings are emitted as the Penner closed forms, time warps as arithmetic on t,
and RectSL's guards travel with them: a / still returns 0 on a zero
divisor, and mod still truncates where GLSL's mod() would floor.
Constants fold, and equal tracks share a function. A twelve-copy array driven
by one animated template emits one curve and twelve literals, not twelve
copies of the curve.
What survives the lowering
Only the shape survives. A RectSL scene paints sorted rects over each other with per-rect alpha, fill and material, but a distance field only says where the shape is, so fill, material and draw order are dropped.
Visibility is not dropped: a rect gated to zero by ->appear, ->vanish, or
->live contributes no silhouette, exactly as it draws nothing on the CPU.
What a distance field can do that a quad cannot
All of these are one line over the same float:
float d = scene(p, t);
float fill = 1.0 - smoothstep(0.0, 0.01, d);
// fixed 0.01 scene-unit feather
float outline = 1.0 - smoothstep(0.0, 0.01, abs(d) - 0.03);
float glow = exp(-8.0 * max(d, 0.0));
// falls off outside the shape
vec2 normal = normalize(vec2(dFdx(d), dFdy(d)));
// for beveling or lighting
Because t is an argument rather than a frame index, and every track is
closed-form, a fragment can ask the scene about instants that were never
rendered. That is true motion blur, from the shape itself:
float blurred = 1e9;
for (int i = 0; i < 8; ++i) {
blurred = min(blurred, scene(p, t + float(i) * (1.0 / 480.0)));
}
Rects can also merge into each other, which RectSL itself cannot express. Pass
RectSLSdfOptions(blendRadius = k) and the min inside the union tree becomes a
polynomial smooth minimum, and the copies of an array fuse into one body as they
pass each other. (Rects gated invisible are excluded from the blend; their 1e9
sentinel would otherwise smear across the neighbourhood.)
The looks, rendered
You do not have to write any of the above. RectSLSdfLook is that vocabulary as
data, RectSLSdfShader.fragment(scene, look) assembles a whole fragment shader
from it, and RectSLSdfLook.PRESETS carries one worked example per capability:
| Preset | What it demonstrates |
|---|---|
fill |
Antialiased interior, one pixel of edge at any resolution. |
outline |
A band |d| < w at any w. No stroke geometry. |
glow |
exp(-k·max(d,0)) — falloff with distance, outside the shape. |
contours |
Iso-distance lines, inside and out. The proof it is a distance. |
relief |
Per-pixel normals from the gradient, lit. |
blend |
smin between rects (via blendRadius), so copies fuse. |
carve |
max(d, -c) against a second emitted scene. |
motion-blur |
24 instants inside one frame, from the closed-form tracks. |
All eight are rendered on
the examples page,
next to a raster-versus-SDF comparison and a resolution pair. Here are two of
them, the same scene under min and under smin:
relief — hard creases where the copies meet under
min.
blend — the same scene, blendRadius = 0.55.The contours look draws the field itself: iso-distance lines every 0.22 wall units,
continuing inside the shape because the distance is signed.

To put it on screen and moving, open the
rectsl_sdf_live starting point: two
boxes, a Window + Renderer and one box that compiles a scene, emits the field,
wraps it in a look, and draws it with a fullscreen triangle and a clock.
Fidelity
The shape is exact and the arithmetic is not quite, in a way you can plan around.
On a GPU the sampler works in double and the shader in float, so expect the
two to part company at about 1e-6 of a wall unit.
Agents: what the build checks
The emitted geometry is checked against the CPU sampler to 1e-9 across the
whole gallery, every gallery scene is compiled through shaderc as part of the
build, and the GPU agreement guard is 1e-5. dev_docs/RECTSL_SDF_AGREEMENT.md
carries the probe-shader design, the per-class deviation figures, the
->degrade hash census, and which of all of it is asserted rather than printed.
Almost nothing needs approximating, for the reason
above: easings, time warps and
TCB tracks all reach the shader as closed forms. Even ->degrade lowers exactly —
its 64-bit hash is a uvec2 lane pair and its threshold test is an integer
comparison in disguise.
The one thing that is sampled is a pattern query, and it has to be: a
PatPattern is a (PatArc) -> List<PatEvent> lambda, so from the outside the
emitter cannot tell a "bd*4" from a sine, let alone find a closed form for it.
emitSdf returns one RectSLSdfBake per sampled track — the classification, the
rate, and the measurement it made of itself — and the sentence in sdf.notes is
that report read aloud. Three classifications, and what each means for you:
fidelity |
the track | what to expect |
|---|---|---|
STEPPED |
a rhythm: "bd*4", "0 3 -3 6", segment, quantise, square |
exact away from an edge, and each edge is found by bisection rather than snapped to the sample grid. It lands within a microsecond, which is PatTrack's own query resolution |
SAMPLED |
a signal: sine, cosine, tri, perlin, saw, isaw |
a hold chain and a ramp are both built and the closer one emitted. Small but not zero; no plateaus means no exactness to claim |
ALIASED |
rand |
it hashes the instant, so it has no value between samples. Raising the rate does not help and lowering it does not hurt. Reported rather than fixed, because no rate fixes it |