Implementing Things

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:

  1. Create a data model that represents important properties of the Thing
  2. Create Things when necessary, adding them to state
  3. Update state of sketch, along with created Thing(s)
  4. Use the updated state and properties associated with Thing(s)

A 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';

Data model

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
 */

Create Things

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 });
}

Updating

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 });
}

Update cycle

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
  })
};

Representing the thing

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);
}

Many things

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.