S-Rausch
Symbolic everything
Symbolic everywhere
One of my favourite demos is Masagin from Farbrausch & Neuro, an 8 minute real-time animation done with code with a strong 2D vibe.
The demogroup Farbrausch have their tools code available in a public repo and paniq holds the repo for the masagin source code.
The demo uses a DSL tailored for its engine. It goes like this:
# 01_flutepart
bpm 130
song "01_flutepart.ogg"
bgcolor 1,1,1
# 0b 6b 8b
verb grow1:
scale color 1,1,1,0 -> 1,1,1,1 time 2b
move cam 1,1 -> 0,0 seg 6b
lifespan 32b
verb grow2:
move cam 0,0 -> 1,1 seg 4b->6b
move cam 1,1 -> 0,0 seg 6b->8b
lifespan 32b
verb grow3:
move cam 0,0 -> 1,1 seg 6b->8b
move cam 1,1 -> 0,0 seg 8b->16b
lifespan 32b
verb grow4:
move cam 0,0 -> 1,1 seg 8b->16b
move cam 1,1 -> 0,0 seg 16b->24b
lifespan 32b
verb grow5:
scale color 1,1,1,1 -> 1,1,1,0 time 28b->32b
move cam 0,0 -> 1,1 seg 16b->24b
move cam 1,1 -> 0,0 seg 24b->32b
lifespan 35b
fader bounce:
type spring
k 2000
mass 1.0
friction 0.6
verb entry:
scale 120%,0% -> 100%,100% fader bounce
fader bob:
type sine
verb monkin:
like entry
#move 0,0 -> 0,0.05 time 19b->20b loop fader bob
verb growtree:
like entry
verb asked1:
scale texture 0,0 -> 1,0 time 1b
scale texture 1,0 -> 0,0 time 10b->12b
lifespan 12b
verb slide:
scale texture 0,0 -> 1,0 time 1b
scale color 1,1,1,1 -> 1,1,1,0 time 12b->16b
lifespan 16b
verb q1:
scale texture 0,0 -> 1,0 time 1b
scale texture 1,0 -> 0,0 time 2b->4b
lifespan 4b
verb q2:
scale texture 0,0 -> 1,0 time 1b
scale texture 1,0 -> 0,0 time 3b->6b
lifespan 6b
verb tsaid:
scale color 1,1,1,0 -> 1,1,1,1 time 1b
verb rake:
scale texture 0,0 -> 1,0 seg 32b
move cam 1,1 -> 1,1 seg 27b
move cam 1,1 -> 0,0 seg 27b->32b
lifespan 32b
verb colorblend:
move color 0.227,1.0,0.3647 seg 31b
move color 0.227,1.0,0.3647 -> 0.227,0.0,1.0 seg 32b->35b
move color 0.227,0.0,1.0 seg 35b->64b
set bgcolor color
verb slidegrowmask:
options zclear1+zwrite+alpha+texture
scale texture 0,1 -> 1.5,1 time 4b
scale texture 1.5,1 -> 0,1 time 6b->8b
lifespan 8b
verb slidegrow:
options color+alpha+texture+zread
scale texture 0,1 -> 1.5,1 time 4b
scale texture 1.5,1 -> 0,1 time 6b->8b
lifespan 8b
verb slidegrowmask4:
options zclear1+zwrite+alpha+texture
scale texture 0,1 -> 1.5,1 time 2b
scale texture 1.5,1 -> 0,1 time 2b->4b
lifespan 4b
verb slidegrow4:
options color+alpha+texture+zread
scale texture 0,1 -> 1.5,1 time 2b
scale texture 1.5,1 -> 0,1 time 2b->4b
lifespan 4b
verb setx:
options color+zread+alpha
lifespan 8b
verb setx4:
options color+zread+alpha
lifespan 4b
verb setmask:
options zclear1+zwrite
lifespan 8b
verb setmask4:
options zclear1+zwrite
lifespan 4b
fader logmove:
type log
exp1 1.0
exp2 0.0
verb slide1:
move -0.8,0 -> 0.2,0.0 time 16b fader logmove
lifespan 8b
verb slide2:
move -0.05,0.03 -> 0,0.0 time 8b
lifespan 8b
verb slide2b:
move 0.0,0.0 -> -0.05,0.01 time 8b
lifespan 8b
verb fadeinout2:
scale color 1,1,1,0 -> 1,1,1,1 time 2b
scale color 1,1,1,1 -> 1,1,1,0 seg 6b->8b
lifespan 8b
verb fadeinout1:
scale color 1,1,1,0 -> 1,1,1,1 time 4b->6b
scale color 1,1,1,1 -> 1,1,1,0 seg 14b->16b
lifespan 16b
verb slide3:
move -0.5,-0.5
rotate -10º -> 0º time 8b
move 0.5,0.5
mul root
mul bud
mul parent_model
lifespan 8b
verb slide3b:
move -0.5,-0.5
rotate 0º -> 1º time 4b
move 0,0 -> 0.01,0 time 4b
move 0.5,0.5
mul root
mul bud
mul parent_model
lifespan 4b
verb slide4:
move -0.5,-0.5
scale 50% -> 100% time 8b
move 0.5,0.5
mul root
mul bud
mul parent_model
lifespan 8b
verb stage:
scale color 0,0,0,1
mul root
mul bud
mul parent_model
mul root modelcam
move modelcam 0.0,0.0
mul bud modelcam
mul parent_model modelcam
move color 1,1,1
set bgcolor color
move cam 1,1 seg 32b
lifespan (64b+8b)
alias M masagin
alias S slides
fader expmove:
type log
exp1 0
exp2 1
verb camup:
mul root
mul bud
mul parent_model
mul root modelcam
scale modelcam 100%
mul bud modelcam
mul parent_model modelcam
move cam 0,0 -> 1,1 seg 5b fader expmove
branch story:
rake M.theater delay 32b:
monkin@1 M.monk delay 34b lifespan 32b
asked1@2 M.asked delay 37b:
q1 M.what delay 43b
q2 M.buddha delay 46b
entry@3 M.tozan delay 38b lifespan 32b
growtree@5 M.tree delay 42b lifespan 32b
tsaid@4 M.tozansaid delay 53b lifespan 16b:
slide M.masagin(name=scene01end) delay 59b:
camup none delay 59b lifespan 8b
stage S.stage:
fadeinout1 S.presents delay 0b
fadeinout2 S.aprod delay 16b
fadeinout2 S.aninvito delay 24b
slide1 none delay 0b:
setmask S.nvision08 delay p+0b
slidegrow S.nvslide delay p+0b
slide2 none delay 8b:
setmask S.bp delay p+0b
slidegrow S.bpslide delay p+0b
slide2b none delay 8b:
slidegrowmask S.sceneslide delay p+0b
setx S.sceneorg delay p+0b
slide3 none delay 16b:
slidegrowmask S.frslide delay p+0b
setx S.farbrausch delay p+0b
slide3b none delay 20b:
slidegrowmask4 S.nslide delay p+0b
setx4 S.neuro delay p+0b
slide4 none delay 24b:
slidegrowmask S.nvscslide delay p+0b
setx S.nvscene delay p+0b
ref story
It is called MasaginScript and there is a manual explaining it here. It is Python-inspired with tab indentation syntax and is intentionally limited, it has no 3D features or scene graph representation, instead it is focused in describing animation of 2D tree-like things. Colors are described as matrices (instead of rgb values), and sync is done via beats (this is common in demos, but fun to see it laied out here explicitly).
S-DSL
DSLs are cool to make, you get to describe in a razor sharp way a specificaly narrowed language that hopefully makes hard tasks easy.
Describing the new syntax can be super fun and creative, looking for ways to describe composition blocks and trimming down generic things, while juggling concepts in a way that they can be easy to grasp by newcomers and enjoyable to work with.
I have been spending about... 3 years now (time flies) playing with WGSL and WebGPU APIs, and inventing DSL's and new declarative syntaxes, that end up in demos that sometimes can't even run.
Here is my attempt at bringing the whole WebGPU into a declarative space, with a à-lá C macro style DSL where each create* function drops the create name into the # (i.e. createRenderPipeline -> #renderPipeline):
#renderPipeline pipeline {
layout=auto
vertex={ entryPoint=vertexMain module=code }
fragment={
entryPoint=fragMain
module=code
targets=[{ format=preferredCanvasFormat }]
}
primitive={ topology=triangle-list }
}
#renderPass renderPipeline {
colorAttachments=[{
view=contextCurrentTexture
clearValue=[0, 0, 0, 0]
loadOp=clear
storeOp=store
}]
pipeline=pipeline
draw=3
}
#frame simpleTriangle {
perform=[renderPipeline]
}
#shaderModule code {
code="
@vertex
fn vertexMain(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
return vec4f(pos[VertexIndex], 0.0, 1.0);
}
@fragment
fn fragMain() -> @location(0) vec4f {
return vec4(1.0, 0.0, 0.0, 1.0);
}
"
}
Naturally MasaginScript resonated strongly with my inner self in many ways.
Greenspun's Tenth Rule of Programming
One of the fun chilled criticisms a lispic friend constantly made to the DSLs versions and approaches I took was adapting an adaptation of the famous "Greenspun" quote:
"Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp."
He mangled it in a more axiomatic way:
"All custom DSLs are fundamentally incomplete implementations of Lisp"
So I went and tried to define my custom DSLs through S-expressions and quickly found myself drowned in parenthesis.
S-thread
There is a thread I like here, some DSLs can be an ad hoc Lisp syntax or will evolve to the point where it requires macros, or tree-structured data, at which point it becomes harder for them to hide that it became a poorly designed dialect of Lisp.
I find that a cool part of writing a DSL is to balance which parts are statically analyzable and deliberately constrained as opposed to which parts can grow new control flow and would benefit from s-expressions and macros.
S-plain yourself
To see this convergence clearly, suppose that all DSL will need to be parsed and converted into a tree-like structure, an AST. These trees can naturally be described with symbolic expressions ("S-expressions"), which are the bread and butter of Lisp. In Lispic languages your code is not text that gets converted to a tree/AST because the code is the tree itself.
S-expressions are just nested lists (commonly defined with parenthesis), with them code becomes data (and mind-bendingly vice-versa): result = (x * 5) + y becomes (+ (* x 5) y).
DSLs evolve, and sometimes never notice that they are repeating this pattern as complexity and features increase. Some are just simple and finite and would never benefit from something like this.
S-Masagin
So the original MasaginScript would go from something like:
branch story:
rake M.theater delay 32b:
monkin@1 M.monk delay 34b lifespan 32b
asked1@2 M.asked delay 37b:
q1 M.what delay 43b
q2 M.buddha delay 46b
Into its s-expression shape:
(branch story
(rake M.theater (delay 32b)
(monkin M.monk (pin 1) (delay 34b) (lifespan 32b))
(asked1 M.asked (pin 2) (delay 37b)
(q1 M.what (delay 43b))
(q2 M.buddha (delay 46b)))))
Where finding all delays above a certain value is just walking the tree and checking the s-expression head for "delay".
The tradeoff is losing the more domain-specific syntax it has for a general minimal tree representation, things like
move cam 1,1 -> 0,0 seg 6b
would become more rigid and generic in s-expressions:
(move cam (from (1 1)) (to (0 0)) (seg 6b))
Conclusion
I find myself reaching for s-expressions quite often, and I've been working on stripping them further than what Lisp or Clojure offer. I'm a sucker for expressive declarative spaces, and what I've been exploring is a baseline s-expression framework where the vocabulary the user writes is closed, pure, and passive (so no closures, no macros, no eval, no side effects at the DSL surface). All the programmatic part (things like generating shapes, splicing, lowering one tree into another, etc) lives in the host language, behind explicit contracts. The client writes data and the host turns it into behavior (similar vibe to JSX and other declarative scene graphs). I'll try to write more about this in the meantime.