The sculpture is made of up an array of modules. A module is one motor with an attached string and a ball at the end of the string.
A formation is a series of coordinated movements between the modules.
A formation is created by supplying one function. This function is called a tick callback function. It is fired for every module at every millisecond and returns an object containing what height the module (ball) should be at.
Tick callbacks can be found in /src/custom/formations.ts
. To create another formation, add another tick callback to the exported object.
The callback function accepts one parameter: the info
object. This contains data about the grid scultpure, the specific module, and more:
interface TickInfo {
globals: [key: string]: any; // Global values passed onto all of the functions. This is an empty object by default unless you specifically provide values while invoking the formation.
maxHeight: number; // Maximum height each module can reach
nx: number; // Number of modules in the sculpture grid in the x direction
ny: number; // Number of modules in the sculpture grid in the y direction
timeElapsed: number; // Time passed since the formation has started
totalDuration: number; // Total time formation will go on for
x: number; // X-coordinate of the specific module this callback function is invoked for
y: number; // Y-coordinate of the specific module this callback function is invoked for
}
Up-to-date specification of data types can be found in /src/lib/tick.ts
The tick callback performs the logic to dictate where each module should be at what time using the tick info provided. The callback has the following type:
type TickCallback = (info: TickInfo) => Partial<MovePoint> | number | void;
interface MovePoint {
height: number; // Height from very bottom. 1 unit equals 1 meter.
easing: EASING; // Easing function to get to next point throughout the provided `wait` duration.
waitBefore: number; // How many milliseconds to wait before changing height
easeDuration: number; // How long it should take to ease to new height. 0 is instant.
waitAfter: number; // How many milliseconds to wait before calling the next tick callback
}
Up-to-date specification of data types can be found in /src/lib/tick.ts
It's important to note the Partial<MovePoint>
makes all properties in the return object optional. If height is not provided, it will default to the height of the previous tick callback (rendering the ease function useless). If easing function is not provided, it defaults to EASING.LINEAR
. A full list of possible easings can be found in /src/lib/tick.ts
. beforeWait
and afterWait
are how many milliseconds to delay before and after easing into the new height, respectively. These both default to 0
milliseconds. Finally, easeDuration
is how many milliseconds it should take to ease into the new height. This value defaults to 0
, which instantly teleports the module. Increase the easeDuration
to see the easing functions in action.
The tick callback can also return a number. This value is interpreted as the height; there is no difference between returning a number and returning an object with only the height
property. Just like returning an object, easing
will default to EASING.LINEAR
and wait/duration values default to 0
milliseconds.
If the callback function returns nothing, null, or other invalid input, the module will remain at the height it was previously. In practice, if no value has been previously returned for the current callback function, it will use the final position of the last formation. If there is no previous formation (like in the visualizer), it will default to max height of the sculpture.
Here is an example tick callback to create a formation:
info => {
// Some logic here..
return {
height: 1, // Raise ball 1 meter from its lowest point
easing: EASING.EASE_IN_OUT_QUAD, // Easing function to get from current position to 1 meter
wait: 1000 // Easing should take 1 second to get to 1 meter off the ground. No other tick callback functions will be invoked for this module for the next 1000 milliseconds.
};
}
There are many cases you might want a touch of randomness to your formation. Instead of using something like Math.random()
, use the random()
function (already imported in /src/custom/formations.ts
) to create a "deterministic" random number generator. Pass in the info
object and it will generate the same "random" numbers for each invocation for each individual module at each specific time. This allows for random numbers, but consistent values returned each time the formation is generated. This helps remove any uncertainty to make sure no unexpected behavior pops up transitioning from development to the physical structure.
Here is an example implementation generating random height
s and wait
ing at random times:
info => {
// This is just the random number generator. Not a random number itself!
// Call this function to generate a random decimal between 0 (inclusive) and 1 (exclusive)
const rng = random(info);
return {
height: rng() * info.maxHeight,
easing: EASING.EASE_IN_OUT_EXPO,
wait: Math.abs(rng() * 3000) + 1000
};
}