Skip to content

JavaScript Scripting

Overview

In addition to a scene's GLSL and JSON files, you can add an optional JavaScript scripting file. This script will be run on every frame before main.glsl, calculating variables on a frame-wide basis and passing them into the shader as uniforms.

Unlike GLSL, objects and data declared in the JavaScript persist across multiple frames, enabling useful functionality like timers and Object Oriented Programming. The JavaScript engine is also equipped with a powerful event system and built-in functions to update controls, allowing complex control over your scene.

Check out this video tutorial to see JavaScript in action:

Update and Setup Functions

There are two main functions involved in a script.js file: the setup() function and the update(dt) function. They are both optional, but you must define at least one for anything to happen in the script.

The setup() function which will be run once when the scene loads. This is useful for initializing variables and registering event handlers. By registering event handlers in the setup function, you can completely forgo the update function.

The update(dt) function which will be run on every frame. The argument dt stores the time elapsed since the previous frame, which is useful for a variety of purposes (physics simulations, timers, consistent speeds, etc.).

Getting/Setting Uniforms

One of the primary functions of script.js is to take the current scene uniforms as input and create new uniforms as output.

Accessing Input Uniforms

All of the scene's uniforms are available as global variables in the script, so you can access them like you would in the GLSL. This includes built-in standard GLSL uniforms, audio reactive uniforms, and any uniforms created by the controls defined in scene.json. It does not include uniforms that apply to individual pixels and passes, like _uvc, PASSINDEX, and RENDERSIZE. Vector uniforms are available as objects with xyzw properties.

Here are some examples of these global variables:

  • TIME (standard uniform)
  • syn_BassLevel (audio reactive uniform)
  • mySlider (control uniform)
  • myXy.y (access the components of a vector uniform)

TIP: You can print out all the available global variables and functions to Synesthesia's console using the utility function printGlobalKeys()

Setting Output Uniforms

To set a uniform for the shader, use the setUniform() function. There are three ways to use this function: you can pass in up to four float values, pass in an array containing up to four values, or pass in an object containing xyzw or rgba properties.

setUniform(uniformName, x, [y], [z], [w]);
setUniform(uniformName, [x, y, z, w]);
setUniform(uniformName, { x, y, z, w });
setUniform(uniformName, { r, g, b, a });

This function will automatically create a uniform available in your shader called uniformName, determining the uniform type based on the amount of data you pass in.

Examples

// set a float uniform
setUniform("size", 0.5);
// set a vec2 uniform
setUniform("position", 0.5, 0.3);
setUniform("position", [0.5, 0.3]);
setUniform("position", { x: 0.5, y: 0.3 });
// set a vec3 uniform
setUniform("color", 0.1, 0.0, 1.0);
setUniform("color", [0.1, 0.0, 1.0]);
setUniform("color", { r: 0.1, g: 0.0, b: 1.0 });
// set a vec4 uniform
setUniform("colorWithAlpha", 0.1, 0.0, 1.0, 1.0);
setUniform("colorWithAlpha", [0.1, 0.0, 1.0, 1.0]);
setUniform("colorWithAlpha", { r: 0.1, g: 0.0, b: 1.0, a: 1.0 });

It is good practice to initialize all the output uniforms you'd like to use within the setup() function to ensure that they will always be defined in the shader.

Modifying Uniforms

The most common usage of script.js is to modify Synesthesia's built-in uniforms — especially the audio reactive variety. To achieve a desired effect, sometimes these uniforms need to be larger, faster, squared, combined, inverted, etc.

For example, let's say you want to modify syn_BassLevel to be more reactive, so you square it. You want its effects to be more subtle, so you scale it by 0.5. And you want it to turn on/off based on a toggle control you've created called "move_with_bass". Here's what your code might look like:

function update(dt) {
  var modified_BassLevel = 0.5 * Math.pow(syn_BassLevel, 2) * move_with_bass;
  setUniform("modified_BassLevel", modified_BassLevel);
}

For more examples of scenes that modify audio uniforms, check out kaleidoWHOA, MAN, Meta Experiment 3, Circles5, or Hills, Eels.

DEPRECATED: inputs and uniforms

If you look at the script.js code for some scenes in Synesthesia, you'll notice that uniforms are accessed and set using two global objects called inputs and uniforms. You can access all input uniforms as properties of inputs, and you can set uniforms by adding a property to the uniforms object.

These global objects are still supported and available in the JavaScript, but they have been deprecated in favor of global input variables and the setUniform function.

Updating Controls

Using the following built-in JavaScript functions, you can update the controls of a scene.

setControl()

Set the value of a control. You can include up to three values based on the dimension of the target control (include three to change colors, two to change xys, and one for anything else).

setControl(controlName, value1, [value2], [value3])

Params

  • controlName string - the name of the control (insensitive to case and spacing characters)
  • value1 float - the new value of the control, normalized between 0 and 1
  • [value2] float - the new value for the second dimension of a multidimensional control
  • [value3] float - the new value for the third dimension of a multidimensional control

Examples

setControl(mySlider, 0.5);
setControl(myXyPad, 0.5, 0.3);
setControl(myColor, 0.1, 0.5, 0.0);

setControlDimension()

Set a specific dimension of a multidimensional control (for xy or color controls).

setControlDimension(controlName, dimension, value)

Params

  • controlName string - the name of the control (insensitive to case and spacing characters)
  • dimension int - the index value of the dimension to target, between 0 and 2. For an xy control, dimension 0 would set the x axis and 1 would set the y axis
  • value float - the new value of the control dimension, normalized between 0 and 1

Examples

setControlDimension(myXyPad, 0, 0.8); // set the x-axis
setControlDimension(myColor, 2, 1.0); // set the blue channel

randomizeControl()

Randomize the value of a control.

randomizeControl(controlName)

Params

  • controlName string - the name of the control (insensitive to case and spacing characters)

defaultControl()

Set a control to its default value.

defaultControl(controlName)

Params

  • controlName string - the name of the control (insensitive to case and spacing characters)

Event System

Synesthesia's JavaScript engine includes an event system — by defining event handlers, you can run code conditionally based on the state of the script's input variables.

Registering Event Handlers

There are five built-in functions you can use to register different types of event handlers. These functions should be called within the setup() function, since they only need to be registered once. Event handler registration functions all have the signature:

eventType(target, callback)

Params

  • target string — the name of the input variable you'd like to track. You can use any of the script's global input uniforms (the name of a slider, audio uniform, standard uniform, etc.). This argument is insensitive to case and spacing characters like "_" and "-", so "my_slider", "MySlider", and "m-Y-s-L-i-D-e-R" would all work to track your slider
  • callback string — the name of the JavaScript callback function you'd like to call whenever the event occurs (must be an exact match)

Whenever an event occurs and an event handler callback function is called, two arguments will be passed in:

  • value — the current value of the target
  • previousValue — the value of the target in the previous frame

Here's a complete list of the event handler registration functions:

Function Effect
onChange(target, callback) callback will be called whenever target changes value
onOffToOn(target, callback) callback will be called whenever target changes from less than 0.5 to greater than 0.5
onOnToOff(target, callback) callback will be called whenever target changes from greater than 0.5 to less than 0.5
whileOn(target, callback) callback will be called on every frame that target is greater than 0.5
whileOff(target, callback) callback will be called on every frame that target is less than 0.5

Examples

Here are some examples of how the event system could be used, incorporating the built-in JavaScript functions to update controls:

// the following functions before setup() are all custom event handlers

function onMacroChange(value, previousValue) {
  setControl("param1", value);
  setControl("param2", value);
  setControl("param3", value);
}

function onPresetChange(value, previousValue) {
  // trigger one of three presets based on the dropdown index
  var dropdownIndex = value;
  if (dropdownIndex === 0) {
    setControl("param1", 0.5);
    setControl("param2", 1.0);
    setControl("param3", 0.1);
  } else if (dropdownIndex === 1) {
    setControl("param1", 1.0);
    setControl("param2", 0.5);
    setControl("param3", 0.7);
  } else if (dropdownIndex === 2) {
    setControl("param1", 0.8);
    setControl("param2", 0.0);
    setControl("param3", 0.9);
  }
}

function onLoudBass(value, previousValue) {
  if (value > 0.9) {
    randomizeControl("hue");
  }
}

function onChangeSeed(value, previousValue) {
  randomSeed = Math.random() * 100;
}

function onRandomizeColors(value, previousValue) {
  randomizeControl("color1");
  randomizeControl("color2");
  randomizeControl("color3");
}

function onDefaultColors(value, previousValue) {
  defaultControl("color1");
  defaultControl("color2");
  defaultControl("color3");
}

function whileBrightnessLFO(value, previousValue) {
  setControl("brightness", Math.sin(TIME*2)*0.5 + 0.5);
}

function defaultBrightness(value, previousValue) {
  defaultControl("brightness");
}

function whileDebug(value, previousValue) {
  print("random seed: " + randomSeed);
  print("bass level: " + syn_BassLevel);
}

var randomSeed = 0;

function setup() {
  // create a "macro" slider that sets the value of multiple other controls
  onChange("macro", "onMacroChange");
  // trigger a hard-coded preset based on the value of a dropdown
  onChange("preset_dropdown", "onPresetChange");
  // randomize the "hue" meta control whenever the bass gets loud
  onChange("syn_BassLevel", "onLoudBass");
  // update a JavaScript seed variable whenever a button is pressed
  onOffToOn("change_seed", "onChangeSeed");
  // randomize a subset of controls when a button called 'randomize_colors' is pressed
  onOffToOn("randomize_colors", "onRandomizeColors");
  // set those controls back to default when a different button is pressed
  onOffToOn("default_colors", "onDefaultColors");
  // use an LFO to control the "brightness" meta control when a toggle is on
  whileOn("brightness_LFO", "whileBrightnessLFO");
  // return brightness to its default value when the LFO toggle is turned off
  onOnToOff("brightness_LFO", "defaultBrightness");
  // print debug information whenever debug toggle is on
  whileOn("debug", "whileDebug");
}

Object Oriented Programming

Other use cases of script.js involve Object Oriented Programming (OOP), which allows complex functionality that would be otherwise impossible (or at least impractical) with shaders. Generally, this involves creating a custom class, constructing an instance, updating it each frame, and sending its properties into the shader as uniforms. An instance declared above update() will remain available until the scene stops, which allows you to create cohesive behavior over time.

Here are some examples of the custom classes that have been used in Synesthesia's built-in scenes:

  • camera movement (Molten, Alien Cavern, Deeper)
  • BPM counter (Alien Cavern, Deeper, Hex Array, etc.)
  • smooth counter (Hue Review, KIFS Flythrough, Lattix, etc.)
  • timer (Circles5, Circuit Bending, Voronoi Geode, etc.)
  • physics simulation (Biopsy)
  • introducing randomness (Stained Glass, Thresholder)

Printing to the Console

You can use print() or console.log() to print to Synesthesia's built-in developer console. This can be useful for debugging shaders, since you can display uniform values.

Here's a trick used to only print values on a periodic basis:

var frameCount = 0;
var printPeriod = 50;

function update(dt) {
  if (frameCount % printPeriod == 0){
    print("suh dude");
  }

  frameCount++;
}