Please see the thing starter for a skeleton sketch that’s ready to use as Thing implementation.
There’s also things starter for multiple things.
Additional demos:
An outline for making a Thing is:
stateA tip is to separate all your Thing-specific code into a separate Javascript file, eg thing.js. This helps to keep your main sketch concise, and makes a clearer division between the state and behaviour of things and the sketch itself.
You can import the functions into your main script.js (or wherever) with:
import * as Thing from './thing.js';
It is a good idea to define a ‘data model’ which describes the states it can be in, along with any additional properties. For example, let’s say we have a Thing which can be dragged around the screen. The data might look something like this:
{
dragging: false,
position: {
x: 0.5,
y: 0.5
},
mass: 1
}
Using type annotations, we could formalise the model into a type.
// thing.js
/**
* @typedef {Readonly<{
* dragging: boolean
* position: {x: number, y:number}
* mass: number
* }>} Thing
*/
Maybe you want a function to create a Thing. This could be called when initialising the sketch’s state variable, or maybe you want to spawn Things dynamically.
// thing.js
/**
* Generate a thing
* @returns {Thing}
*/
function create() {
return Object.freeze({
mass: 1,
position: { x: 0.5, y: 0.5 },
dragging: false
});
}
Where to stash the Thing? If your sketch only needs to track one, it could be in the state:
// script.js
import * as Thing from './thing.js';
const state = Object.freeze({
thing: Thing.create();
})
If you’re creating many things, you may want to start with an array:
// script.js
const state = Object.freeze({
/* @typedef {Thing[]} */
things: []
});
There could be many different reasons for when and how to generate things. It might be at startup of the sketch, or perhaps as a result of a user action.
// script.js
// Eg. create 20 things at startup
const setup = () => {
const things = [];
for (let i=0;i<20;i++) {
things.push(Thing.create());
}
setState({ things });
}
Since we’re using the immutability principle principle, we return a new, modified Thing rather than change the existing one.
// thing.js
/**
* Updates a thing
* @returns {Thing}
**/
const update = (thing) => {
// 1. Calculate new properties
// ...
// 2. Sanity check values
// ...
// 3. Return new Thing with new values merged
return Object.freeze({
...thing.
// new properties
})
}
Once the new thing is created, it needs to be set back to the state.
For a single thing, this is easy:
// script.js
import * as Thing from './thing.js';
const loop = () => {
const newThing = Thing.update(state.thing);
setState({ thing: newThing });
}
Or if you’re updating many things:
const loop = () => {
// Call 'Thing.update' for each existing thing,
// accumulating new things
const newThings = state.things.map(t => Thing.update(t));
// Save all the updated things to state
setState({ things: newThings });
}
We want to decouple the Thing from events or when data is reading, and have it updating at its own rate. The function that loops should recalculate properties as necessary, perhaps drawing in values from the sketch’s state or settings.
// script.js
import * as Thing from './thing.js';
const settings = {
meltRate: 0.999
};
let state = Object.freeze({
// This value is being modulated say by a sensor input
freezeRay: 1.10,
thing: generateThing()
});
const loop = () => {
const thing = Thing.update(state.thing, settings, state);
setState({thing});
window.requestAnimationFrame(loop);
}
loop();
// thing.js
/**
* Continually loops, returning a modified thing
* @returns {Thing}
*/
const update = (thing, settings, state) => {
const { meltRate } = settings;
const { freezeRay } = state; // Get thing from state
let { mass } = thing;
// Apply relevant state from the world
mass *= freezeRay;
// Apply the 'logic' of the thing
// - Our thing melts over time
mass *= meltRate;
// Make sure mass doesn't drop below zero
if (mass < 0) mass = 0;
return Object.freeze({
...thing,
mass
})
};
At some point we need to use data contained in the Thing, so we might have:
// thing.js
const use = (thing) => {
const { position, mass, dragState } = thing
//...do something with properties from thing...
};
And in script.js, call it:
// script.js
const use = (state) => {
const { thing } = state;
// Do other global 'use' things
// 'Use' the thing too...
Thing.use(thing);
}
const update = () => {
// do update stuff...
// use
use(state);
window.requestAnimationFrame(update);
}
Having a plain object to track the Thing and functions which take a Thing makes it easy to scale up and have arrays of Things:
// script.js
let state = Object.freeze({
things: []
});
const use = (state) => {
// Use state properties
// ...
// Use all the things
state.things.forEach(t => Thing.use(t));
};
const update = () => {
// Call 'update' for each thing, make an array of changed results
const things = state.things.map(t => Thing.update(t));
// Save
saveState({ things });
// Use state
use(state);
window.requestAnimationFrame(update);
}
update();
Or if you want to keep track of Things by name, use a Map. This could be useful if a given Thing is associated with something else, such as a pointer id.