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

would become more rigid and generic in s-expressions:

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.