Skip to content

Demo sketches

The demo sketches have a particular pattern that bears explaining.

In overview, it looks like the following, where ’…’ is omitted detail.

1
const settings = Object.freeze({ ... }); // Properties that capture settings
2
3
/**
4
* @typedef {Readonly<{
5
* ...
6
* }>} State
7
*/
8
9
/** @type State */
10
let state = { ... }; // Properties that capture state
11
12
function update() { ...} // Compute state
13
function use() { ... } // Use the state
14
function setup() { ... } // One-time setup
15
function saveState (s) { ... } // Helper function to save state

Settings

Settings is an immutable object containing settings for the sketch that do not change during its execution.

For example:

1
const settings = Object.freeze({
2
thingEl: /** @type HTMLElement */(document.getElementById(`thing`)),
3
range: [0.1, 0.8],
4
oscillatorFreq: 0.4
5
})

You’ll notice a /** ... */ comment on line 2, and then parenthesis wrapping document.getElementById. This is a type annotation hinting to your code editor to assume the return type of getElementById is a HTMLElement rather than default of Element | null

Settings will often contain things like parameter ranges: min and max values etc when cleaning input data. It’s also a good place to capture references to HTML elements, as we do in the above example. That way if you change the shape of your HTML, there’s only one place in the Javascript to update.

State

State contains data and functions which together establish the current state of the sketch. In principle, the state embodies all the data needed to recreate whatever is happening.

Since we’re using Javascript, we create type annotation for the state. This helps to ensure we don’t set the wrong types to properties, and it ‘lights up’ the editing experience in other ways.

The definition is inside of a specially-formatted comment. Once you get past the weird start and end parts on lines 2 and 7, it’s just a list of name: type pairs (lines 3-6). For example:

1
/**
2
* @typedef {Readonly<{
3
* hue: number,
4
* name: string,
5
* enabled: boolean,
6
* previous: number[]
7
* }>} State
8
*/

We then initialise the state, and include another comment to tell the editor it is of type State:

/** @type State */
let state = {
hue: 0,
name: 'asdf',
enabled: true,
previous: []
}

To keep immutability in a simple way, we introduce the saveState function:

/**
* @param {Partial<State>} changes
*/
function saveState (changes) {
state = Object.freeze({
...state,
...changes
})
}

Now, anywhere you want to update the state safely, you can call saveState, passing just the properties to change.

saveState({ enabled: false });

Update

Depending on the logic of your sketch, it might make sense to have a central update function which does the calculations necessary for updating state. For example:

function update() {
let { x } = state;
x += 1;
if (x > 100) x = 0;
// At the end of 'update' we save all the things we've wanted to change
saveState({ x });
}

In the above update, we get the current x value, update it by one, reset if we reach 100, and then save the value back to state.

update might be called using a timer at a set tempo, or perhaps based on an events that drive the behaviour of the sketch. It depends on the intended interactivity.

Use

In the sketches, the convention is that use() makes use of whatever is in the state. It might be to pan audio, draw on the canvas, update the HTML of an element or whatever. In the case of animation, it’s typical then for use() to be called at a high rate using window.requestAnimationFrame or similar.

The key here is that updating the state and using the state are two separated. This gives some advantages:

  • Updating/using can be desynchronised. Eg using state at a high rate while animating the canvas, while updating state at the rate that which events happen
  • The flow of the sketch becomes clearer. When & where data is being ingested & processed, and when & where it’s used.
  • Where possible, computations are done once and assigned to state rather than being recomputed for each update
  • Testing and debugging is easier because fake data can be put into the state and use() won’t need to be changed

Code within use() should ideally:

  • Never modify the state
  • Only access state and settings
  • Avoid computation. Rather, it consumes computed results stored in state.

For example:

function use() {
// Grab things from settings & state
const { thingEl } = settings;
const { hue } = state;
thingEl.style.backgroundColor = `hsl(${hue*360}, 50%, 50%)`;
}

You can call use at the end of update, if the two are meant to be synchronised.

Setup

Another convention used in the sketches is a function called setup() which runs once when the sketch loads. It is usually used for wiring up events and initialising a main loop if necessary.

For example:

function setup() {
const { updateSpeedMs, remote } = settings;
document.getElementById(`blah`).addEventListener(`pointerdown`, () => {
saveState({ down: true });
})
document.getElementById(`blah`).addEventListener(`pointerup`, () => {
saveState({ down: false });
});
// Kick off update loop
setInterval(() => {
update();
}, 10);
}
setup(); // Call it

All together

Here’s a simple example of all of these things together:

import { Remote } from "https://unpkg.com/@clinth/remote@latest/dist/index.mjs";
const settings = Object.freeze({
updateSpeedMs: 1000,
remote: new Remote()
});
/**
* @typedef {Readonly<{
* magic: number
* }>} State */
/** @type State */
let state = {
magic: 0
});
function use() {
const { magic } = state;
// Broadcast magic number
r.broadcast({ magic });
}
function update() {
saveState({
magic: Math.random()
});
use();
}
function setup() {
const { updateSpeedMs, remote } = settings;
// Call 'update' in a loop
setInterval(() => {
update();
}, updateSpeedMs);
}
/**
* Save state
* @param {Partial<State>} changes
*/
function saveState (changes) {
state = Object.freeze({
...state,
...s
});
return state;
}