A signal-based approach to javascript animations:

Signal.js is a little javascript utility meant to make it easy and fun to process streams of numbers with something similar to an effects rack. It really is just a 'thing' that takes numbers in one side and spits other numbers out from the other side. And I understand that might sound very generic, in fact it is. The reason why this came into being is another project I am working on, where I need to turn MIDI notes into some kind of a visual representation.

Now this is what a MIDI note looks like: Square wave Now imagine applying that to, say, the size of a circle on screen. The circle would just blink in and out of the page. Boring, right? So you make it go through a low-pass filter and you get something less flashy and more natural, like: Square wave after lowpass But then say your musical note needs to represent a wobbly noisy sound. Then you might want to blend in some white noise, maybe offset this stream of numbers and amplify it a bit. And then say you want to add a delay... The list goes on. What I needed was some library that quickly lets me define filters and effects similar to the ones I would find in a sound effects rack. And that in the same way lets me pipe them one after the other. So that's what Signal.js is.

Turns out you can use this for javascript animations in a broader sense, for anything related to user interactions that produce streams of numbers, for example scrolling or accelerometer data. But I might write about some better examples of that sort later.

Usage

How about a:

npm install signaljs --save
      

And then something to make sure all went well:


import Signal from 'signaljs'
//Get a list of available modules
var modulesNamesList = Signal.getModulesList();
      

That gets the configuration schemas for the available modules. This will contain some default values that you can use as configuration for a module instance. It will also contain extra information that can turn out useful if you want to build some visual configuration tool. Like the one I built on top of this page. But that I am too lazy right now to generalize it and make it useful :) But if you need something like that just ask and I'll feel motivated enough to do it!

Now you can do one of two things:

  • Create a configuration, which is basically an array of configuration objects, and pass that to Signal, which will build the pipeline for you,
  • Manually import and initialize modules, and .chain() them... This might be useful if you want to do some more complicated stuff.


import Signal from 'signaljs'

var thresholdConfig = Signal.getConfigurationSchemaForModule("Threshold");
thresholdConfig.threshold.value = 5;

var lowPassConfig = Signal.getConfigurationSchemaForModule("LowPass");

var signal = new Signal([lowPassConfig, thresholdConfig]);

var outputSignal = [];
var inputSignal = [1,2,3,4,5,100,7,8,9];

for(let val of inputSignal){ //Don't try this at home
    outputSignal.push(signal.push(val));
}

console.log(outputSignal);
//[0, 0, 0, 0, 0, 19.166666666666668, 17.428571428571427, 16.25, 15.444444444444445]

OR you can do this too:


import Signal from 'signaljs'

var Threshold = Signal.getModule("Threshold");
var thresholdConfig = Signal.getConfigurationSchemaForModule("Threshold");
thresholdConfig.threshold.value = 5;

var LowPass = Signal.getModule("LowPass");
var lowPassConfig = Signal.getConfigurationSchemaForModule("LowPass");

var lowPass = new LowPass(lowPassConfig);
var threshold = new Threshold(thresholdConfig);
lowPass.chain(threshold);

var outputSignal = [];
var inputSignal = [1,2,3,4,5,100,7,8,9];

for(let val of inputSignal){
    outputSignal.push(lowPass.queueSample(val));
}

console.log(outputSignal);
//[0, 0, 0, 0, 0, 19.166666666666668, 17.428571428571427, 16.25, 15.444444444444445]
      

Writing a new module

This is an example on how you define a module, and this one is literally the most complicated there is at the time of writing:


import SignalModule from '../SignalModule'

class LowPass extends SignalModule {

    constructor(configuration){
        /*SignalModule always gets initialized with a buffer, basically just a linked list of values of fixed length-
        When something gets pushed into a buffer that has reached its size limit, the oldest value is dropped.*/
        super(configuration.bufferSize.value);
        //Nothing else to add in this constructor but other modules might have more to say.
    }

    _processOutput(){
        //Computes the output. Yeah because you also need that.
        var sum = 0;
        for(let value of this._buffer.iterate()){
            sum += value;
        }
        return sum ? sum/this._buffer.length : 0;
    }

    static getConfigurationSchema(){
        /*Useful when you want to build a little editor with sliders
        that lets you modify your effects rack.
        You would use this metadata to build the UI.*/
        var conf = super.getConfigurationSchema();
        return Object.assign(conf, {
            type: LowPass.MODULE_NAME,
            bufferSize: {
                display: "Buffer Size",
                type: "number",
                range: [2,100],
                value: 100
            }
        });
    }
}

LowPass.MODULE_NAME = "LowPass";

export default LowPass;
      

And add it to the modules bag ModulesBag.js. Or forget to do it and bang your head on the keyboard until you remember about it. Such great design.

Now go build beautiful things! Or ugly ones! Just.... go, nothing left to read here. Except the license, which basically means do whatever you want:

License

The MIT License (MIT)