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 between0
and1
- [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, between0
and2
. For an xy control, dimension0
would set the x axis and1
would set the y axis - value
float
- the new value of the control dimension, normalized between0
and1
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 thetarget
previousValue
— the value of thetarget
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++;
}