3 unstable releases

0.1.1 Oct 24, 2024
0.1.0 Sep 29, 2024
0.0.1 Sep 29, 2024

#500 in WebAssembly

AGPL-3.0-or-later

280KB
7.5K SLoC

Murrelet

Build Status status alpha crates.io

Along with this repo, this README is a work in progress!

The crates here are part of the livecode engine that I've been building and using to make nearly all the art as this.xor.that.

A demo of this (should be) running here. The code for creating the WASM for the website is in examples/foolish-guillemot, and the main.js is here.

A high-level overview of the software is published here.

Disclaimer

I wanted to open source my code so I could share some ideas of how I've been implementing my livecode software. So that means:

  • These libraries are in initial development and the API is not stable.

  • At this time, I'm not sure if I'll accept PRs on this repo. If there is interest, I might entertain spinning off a more manageable chunk of the code to maintain and document and all that.

  • I'm still learning Rust and computer graphics, so there will be funny weird things.

What code is included

This repo can be broken down into a few parts:

  • livecode macros and code: how I turn Rust sketches into YAML-controlled live performance.
    • general code for parsing and evaluating livecode expressions: murrelet_livecode, murrelet_livecode_macros
    • specific code for livecoding (hot-swapping configs, some generic parameters): murrelet_perform
    • platform-specific packages for adding more sources: murrelet_src_audio, murrelet_src_audio
  • murrelet_gpu: some cute little macros for managing and chaining shaders. (this is not live at the moment)
  • murrelet_svg, murrelet_draw: drawing logic. tbh, mostly included out of necessity for the demo.

livecode macros

The two main ones here are livecode and unitcells, but there's a few others.

Livecode

The Livecode macros makes it possible to control parameters of a struct by injecting some info about the world (time, audio, midi, etc), combined with expressions.

UnitCells

Unitcells can be used to dynamically create a list of things. The number of things and the arrangement (grid, symmetry) is controlled by sequencers (see murrelet_draw/sequencers for examples).

Experimental: Boop

This is.. not working. But I haven't wanted to delete the code yet nor have had a reason to get it to work, so it's broken for now. I do want to fix it or reimagine eventually!

Boop is a funny not-quite-working bit of code that's meant to help interpolate values and avoid hard jumps when you update a value.

The one implemented right now are ODEs, which let you to use some features from animation, like anticipation (going a little in the opposite direction before going in the intended direction).

Experimental: NestEdit

This is a way to access/update a value in a nested struct using a string.

I made this to explore the parameter space of something like wallpaper groups (which involve enums and strings). So I can have one piece that lists out different configurations.

GPU

There are a few macros here for building shaders.

The build_shader just hides some boilerplate of the fragment shader.

let gradient_def: String = build_shader! {
    (
        raw r###"
        let start: vec4<f32> = uniforms.more_info;
        let end: vec4<f32> = uniforms.more_info_other;
        let result = mix(start, end, tex_coords.x);
        "###;
    )
};
let gradient_red = prebuilt_graphics::new_shader_basic(c, "grad", &gradient_def);
gradient_red
    .update_uniforms_other_tuple(c, ([0.0, 0.0, 1.0, 0.04], [1.0, 0.0, 1.0, 0.04]));

and then build_shader_pipeline let's you take those graphics and write to input textures of others. (for extra fun, I use Fira font with arrow glyphs)

let example_pipeline = build_shader_pipeline! {
    gradient_red -> drawing_placeholder;
    drawing_placeholder -> DISPLAY;
};

How expressions work

this is a work in progress and is probably pretty sloppy with programming language terms sorry

State scope

Some interesting variables are injected in different scopes, making them available in different fields.

In a basic example, you need to know about just two scopes:

  • world: the context per frame. includes things like time, midi, audio, global functions, and the app > ctx field. You can use these variables in every field (except the time config).
  • unitcell: the context per unitcell, which includes information like the x and y location and a unique seed for each instance.

Detailed breakdown

That's an oversimplification. Here's roughly how the scopes should work:

These three do strictly build on top of each other

  • program-level: these are functions and variables set for every frame. It is hidden away in LiveCodeUtil, so you probably won't run into it.
  • timeless: same as World but excludes the t-based variable. Basically exclusively used to load the AppConfigTiming config.
  • world: same as above.

Once you're within a world, going deeper can get as complicated as you want using a combination of:

  • unitcell: same as above
  • lazyeval: These are variables that are evaluated in your sketch itself, which let's you add custom variables specific to the sketch. The config returns an expression you add your additional context to and then evaluate.

For example, you might set up a sketch where a unitcell sequencer might contain a second unitcell sequencer (using a different variable prefix) that draws things that combine the outer and inner unitcell's variables.

World

Right now this is built on top of evalexpr. By default, it has support for inputs using:

  • evalexpr (expr to combine)
  • time (some custom code)
  • clicks (pretty fun to control a sketch with an ipad!)

I also included packages of how I add platform-specific implementations (this is what I use on the native build, i.e. not the web)

  • murrelet_src_audio
  • murrelet_src_midi

Expression variables

To see how exactly the variables are defined, you generally want to look for the IsLivecodeSrc trait implementation.

Timing

The float variable t represents time in expressions. This is very useful for making things bounce and change to a bpm for live performances. I also use it to explore parameter spaces, like setting a field to s(ease(t, 0.25), 1.0, 20.0) to ease between 1.0 and 20.0.

The value of t is an abstraction that should increment by 1.0 every bar, given the definitions in the fields of AppConfigTiming, which might look something like this:

app:
  ...
  time:
    realtime: true
        fps: 60.0
        bpm: 135.0
        beats_per_bar: 4.0 # defaults to 4.0

The realtime flag

For live performances, this should probably be set to true so you can match the bpm of the music, regardless of if the visuals start rendering faster or slower. For recording a video, you might want realtime to be false to avoid jumps.

Aside: For generative art, I sometimes switch between them: the glitchiness based on how fast my machine is rendering can make nice textures of realtime: true, but other times I want the even spacing of realtime: false.

If realtime: true, it'll use bpm and beats_per_bar and the system's clock to figure out what t should be. If realtime: false, instead of the system time, it'll use the current frame number to compute t.

Dependencies

~14MB
~306K SLoC