One of the ways Spark AR Studio makes use of a reactive programming model is by allowing you to treat values as signals, which are special objects containing a value that can change over time.
Signals can be bound to an object’s property so that when the value of the signal changes, the value of the object’s property is automatically updated.
There are signal equivalents for primitive data types including number (ScalarSignal
), string (StringSignal
) and boolean (BoolSignal
) values.
The syntax for binding a signal to an object is the same as performing a standard assignment.
In the example below we bind a signal from a source object - the user’s face, to a target object - a plane. As a result, the value of plane.transform.x
will be updated whenever the value of the FaceTracking.face(0).cameraTransform.rotationY
signal changes.
// Load in the required modules const Scene = require('Scene'); const FaceTracking = require('FaceTracking'); // Enables async/await in JS [part 1] (async function() { // Locate the plane we've added to the Scene panel const plane = await Scene.root.findFirst('plane0'); // Bind the user's face rotation in the Y-axis to the X-position of our plane plane.transform.x = FaceTracking.face(0).cameraTransform.rotationY; // Enables async/await in JS [part 2] })();
The statement is treated as a signal binding because the value left of the assignment operator (=) is a reactive object and the right value is a signal. If the right value were a standard scalar, string or boolean value the statement would be treated as a standard assignment.
When a number, string or boolean value is passed to a function or property setter that expects a signal, the value is implicitly converted to a constant signal:
// Load in the required modules const FaceTracking = require('FaceTracking'); // Enables async/await in JS [part 1] (async function() { // The numerical value 0.1 is automatically converted to a ScalarSignal const mouthOpen = FaceTracking.face(0).mouth.openness.gt(0.1); // Enables async/await in JS [part 2] })();
You can also explicitly convert a primitive data type to its signal equivalent with the val()
method exposed by the ReactiveModule
class (more on this class later). The example below converts a scalar (numerical) value to a ScalarSignal
.
// Load in the required modules const FaceTracking = require('FaceTracking'); const Reactive = require('Reactive'); // Enables async/await in JS [part 1] (async function() { // Convert the numerical value 0.1 to a ScalarSignal const scalarSignal = Reactive.val(0.1); // Use the converted value const mouthOpen = FaceTracking.face(0).mouth.openness.gt(scalarSignal); // Enables async/await in JS [part 2] })();
You can use this same method to convert a boolean to a BoolSignal
, or a string to a StringSignal
.
// Load in the required modules const Reactive = require('Reactive'); // Enables async/await in JS [part 1] (async function() { // Convert the bool value 'true' to a BoolSignal const boolSignal = Reactive.val(true); // Convert the string value to a StringSignal const stringSignal = Reactive.val("This is a string"); // Enables async/await in JS [part 2] })();
If you need to retrieve the value that a signal contains you can use the pinLastValue()
method exposed by the ScalarSignal
, StringSignal
and BoolSignal
classes:
// Load in the required modules const Diagnostics = require('Diagnostics'); const FaceTracking = require('FaceTracking'); // Enables async/await in JS [part 1] (async function() { // Get the value of mouth openness when this line of code is executed const mouthOpennessValue = FaceTracking.face(0).mouth.openness.pinLastValue(); // Log the value Diagnostics.log(mouthOpennessValue); // Enables async/await in JS [part 2] })();
As the value changes over time, this returns the last value the signal contained before the method was called.
Alternatively, events can be subscribed to via the subscribeWithSnapshot()
method, which provides the value of the signal for use in the callback function:
// Load in the required modules const Diagnostics = require('Diagnostics'); const FaceTracking = require('FaceTracking'); const TouchGestures = require('TouchGestures'); // Enables async/await in JS [part 1] (async function() { // Subscribe to tap gestures TouchGestures.onTap().subscribeWithSnapshot({ // Get the value of mouth openness when the tap gesture is detected 'val' : FaceTracking.face(0).mouth.openness }, function (gesture, snapshot) { // Log the value from the snapshot Diagnostics.log(snapshot.val); }); // Enables async/await in JS [part 2] })();
As their value changes over time, standard JavaScript operators such as +
, -
, *
and /
can’t be performed directly on signals.
Instead, these type of calculations are performed via methods exposed by the ReactiveModule
class and the ScalarSignal
class as seen in the example below.
// Load in modules const Diagnostics = require('Diagnostics'); const Reactive = require('Reactive'); // Enables async/await in JS [part 1] (async function() { // Create a new scalar signal with an initial value of 5 const originalValue = Reactive.val(5); // Add 1 to the signal with the add() method exposed by the ScalarSignal // class and store the result in a new variable const valuePlusOne = originalValue.add(1); // Add 2 to the signal with the add() method exposed by the ReactiveModule // class and store the result in a new variable const valuePlusTwo = Reactive.add(originalValue, 2); // Multiply the signal by 2 with the mul() method exposed by the ReactiveModule // class and store the result in a new variable const valueDoubled = Reactive.mul(originalValue, 2); // Log the signals' values to the console Diagnostics.log(originalValue.pinLastValue()); Diagnostics.log(valuePlusOne.pinLastValue()); Diagnostics.log(valuePlusTwo.pinLastValue()); Diagnostics.log(valueDoubled.pinLastValue()); // Enables async/await in JS [part 2] })();
Additionally, the BoolSignal
and StringSignal
classes expose their own set of methods for manipulating signals:
// Load in modules const Diagnostics = require('Diagnostics'); const Reactive = require('Reactive'); // Enables async/await in JS [part 1] (async function() { // Create a BoolSignal object const boolSignal = Reactive.val(true); // Perform a logical 'not' operation with boolSignal and store the result in a new variable const newBool = boolSignal.not(); // Create a StringSignal object const stringSignal = Reactive.val("This is a string"); // Concatenate 'stringSignal' with a new string and store the result in a new variable const newString = stringSignal.concat(" made longer."); // Log the signals' values to the console Diagnostics.log(boolSignal.pinLastValue()); Diagnostics.log(newBool.pinLastValue()); Diagnostics.log(stringSignal.pinLastValue()); Diagnostics.log(newString.pinLastValue()); // Enables async/await in JS [part 2] })();
The watch()
method exposed by the DiagnosticsModule
class allows you to add a signal to the watch view in Spark AR Studio to monitor how its value changes over time.
// Load in the required modules const Diagnostics = require('Diagnostics'); const FaceTracking = require('FaceTracking'); // Add the mouth openness signal to the watch view Diagnostics.watch("Mouth Openness - ", FaceTracking.face(0).mouth.openness);
The watch view appears in the top right of the console: