Mirrors are relatively simple and a useful first step for more advanced forms. Once a value is read from some input, it must be processed. This might involve steps such as:
Computed values are typically stored in state, where other parts of your code then use the state, triggered by either the update, or at a set interval.
At this point values will need to be mapped to the output domain. For example, if we store the value of a slider on a normalised range of 0..1, we could map it to colour saturation:
const saturation = Math.round(slider * 100);
const hsl = `hsl(100, ${saturation}%, 50%)`;
// ie. slider == 0.5, we will get:
// hsl(100, 50%, 50%);
Or maybe the x position of an element, based on the width of the window:
const x = Math.round(slider * window.innerWidth);
const pos = { x, y: 100 };
// ie. if slider == 0.5 and window width is 500, we will get:
// { x: 250, y: 100 }
The source for the demos is available.
↑For both Data and Dynamics Mirrors, it might be that the data we express is derived or computed from one or more raw data sources.
In the below example, it is the speed of the slider movement that is expressed, not its position.
This example could be considered a Data Mirror, because there is no mirroring of dynamics - the saturation visualises the computed ‘speed’ value, but itself does not really have any quality of ‘speed’.
Speed, in this case, is simple enough to calculate and to model. But some dynamics are richer and more ambiguous, so it is helpful to articulate them as clearly as possible.
For example, perhaps we want ‘chaotic touch movements’ to be expressed as ‘chaotic’ sound output. What do we mean by ‘chaotic’? Is it:
Once we have decided these dynamics, the next goal is to arrive at some scalar (ie. a number of 0..1) or bipolar value (-1…1) that becomes a measure of ‘chaos’. In a sense, we invent a virtual ‘chaos sensor’.
To get to this, we consider:
x & y from pointermove events)x,y)Let us say the ‘chaos’ value is based on speed of movement and bendiness of movement. We can figure out and test these separately before trying to combine them.
In code, this might look a little like:
import { scale, clamp } from 'https://unpkg.com/ixfx/dist/numbers.js';
function onPointerMove(evt) {
const { lastMove, speedTracker } = state;
// Calculate speed based on current event and last one
const speed = calculateSpeed(evt, lastMove);
// Add current speed to averager
speedTracker.seen(speed);
// Difference of current speed from average
const difference = Math.abs(speedTracker.avg - speed);
// After empirically finding out range of 'difference', set
// that to the min/max for scale. In this case 0...100.
// 'clamp' is used to make sure 0..1 is not exceeded.
const chaosSpeed = clamp(scale(difference, 0, 100));
// Save new state
saveState({
lastMove: evt,
chaosSpeed,
});
}
We could follow essentially the same process as for speed, but using angle of movement instead of speed. Once again, we want to end up with a nice 0..1 scale. Or maybe it would make more sense to model as a bipolar value on a -1..1 scale, where 0 means ‘no bending’, -1 maximum bend in one direction, 1 in the other.
But assuming we now have two values on a 0..1 scale, we could combine them into one combined ‘chaos’ value. This could be a simple average of both values:
const chaos = (chaosSpeed + chaosBendiness) / 2;
…or perhaps they are weighted, eg. the final chaos value is 60% determined by the ‘speed chaos’ and 40% by the ‘bendiness chaos’ values:
const chaos = chaosSpeed * 0.6 + chaosBendiness * 0.4;
↑