CLEAR

Reactive Programming

In this guide we'll cover the basics of reactive programming within Spark AR Studio, a model of programming that avoids frame-by-frame code execution, improving application performance.

We'll look at:

  • The theory behind reactive and imperative programming.
  • The practicality of transitioning from imperative to reactive.
  • The concept of signal binding and how it can be used to make code more efficient.
  • Monitoring and converting values to signals.

Why Reactive Programming?

Spark AR Studio's implementation of reactive programming allows you to create relationships between objects, assets and values. This means that you don't have to execute code every frame when performing common tasks such as animating content, looking for user input, or realigning a mask to a face.

Reactive programming is also compatible with visual programming, reducing the frequency of calls made into the scripting engine.


Transitioning from Imperative to Reactive Programming

Theory

If you've not heard of reactive programming before, or have scripted in an environment with an update loop called every frame, chances are you've been using what's known as imperative programming.

Transitioning from imperative to reactive programming requires you to shift the way that you think, because of how values are propagated throughout your program.

For example, consider this code:

 y = 2x + 4; 

With imperative programming, the value of y is set when the method is executed. As the value of x changes, y remains the same. The value of y is only changed if and when a new value is assigned to it.

With reactive programming, we bind the value of y to the result of the equation. When the value of x changes, a signal is generated. That signal causes the value of y to be updated.

Practical Use

Let's use a real world example, updating the position of an object based on the rotation of a user's face.

In imperative programming we would need to write script to modify the object's values every frame, with application performance suffering as a result.

// Imperative Pseudocode - THIS CODE WILL NOT COMPILE
// Function called every frame
function update() {
  myObject.x = face.rotation.y.currentValue;
}

However in reactive programming we can bind the value to a signal and the object will update automatically when the face moves.

// Reactive Pseudocode - THIS CODE WILL NOT COMPILE
// The y rotation of the user's face is bound to the x position of the object once
myObject.x = face.rotation.y;

Here the updating happens in native code, as opposed to script, so it has minimal impact on application performance.


Signal Binding

One of the key ways in which reactive programming helps to improve performance is that it lets you treat values as signals. A signal is a special object containing a value that changes over time.

When signals are bound to variables or another object's properties, changes to the values are propagated in native code, eliminating the need for a context switch into the scripting engine.

The example below shows you how you can bind the signals from a source object - the user's face, to move a target object - a plane.

// Load in the required modules
const FaceTracking = require('FaceTracking');
const Scene = require('Scene');

// Locate a plane we've added to our scene
const myPlane = Scene.root.find('plane0');

// Bind the users's face rotation in the Y-axis to the X-position of our plane.
myPlane.transform.x = FaceTracking.face(0).cameraTransform.rotationY;
//[Reactive object] - [Signal]

When the object at the root of an expression (the lvalue) is a Reactive object, this statement is not a simple assignment, like in standard JavaScript, but rather a binding.

This means:

  • The r value shouldn't be a simple scalar or string, but a ScalarSignal or StringSignal.
  • Rather than assigning a value, you're binding the signal to that property. When the value underlying the signal is updated, so is the value of the property.

So rather than programming in the usual imperative style, where you use conditional logic to control the flow of data throughout a program and perform an assignment every time you need it, you're programming in a declarative style. We can see this in action in the following example:

// Load in the required modules
const FaceTracking = require('FaceTracking');
const Scene = require('Scene');

// Find a plane object in our scene
const plane = Scene.root.find('plane0');

// Set the plane to hidden if the mouth is open more than 50%
plane.hidden = FaceTracking.face(0).mouth.openness.gt(0.5);

The gt() method belongs to the ScalarSignal class and is used the same as the greater-than (>) sign.

We declare a signal path, binding a signal - the mouth openness, to a property - the hidden value of a plane, and let the framework do the work of shuttling data around for you.

You still have conditionals for flow control within these signal paths, but they are part of the signal path, not statements surrounding the signal path (as in the case of an imperative if-else or switch statement). The example above would look something like this in imperative:

// Imperative Pseudocode - THIS CODE WILL NOT COMPILE
// Function called every frame
function update() {
  if(mouth.openness > 0.5) {
    plane.hidden = true;
  }
  else {
    plane.hidden = false;
  }
}

Monitoring Signals

You can use the Diagnostics.watch() function to add specific signals to the watch view in Spark AR Studio.

// 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.




Converting Values to Signals

The Reactive.val() function causes a JavaScript numeric, string, or boolean type to be converted to a ScalarSignal, StringSignal or BoolSignal object that may be bound to a property of a Reactive object.

The following code demonstrates how to assign numeric or string values to the properties of a Reactive object:

// Load in the required modules
const Diagnostics = require('Diagnostics');
const Reactive = require('Reactive');

// Define some values
const aScalar = 5;
const aString = 'hello';
const aBool = true;

// Log the values
Diagnostics.log(aScalar);
Diagnostics.log(aString);
Diagnostics.log(aBool);

// Log the signals, converted from the values
Diagnostics.log(Reactive.val(aScalar));
Diagnostics.log(Reactive.val(aString));
Diagnostics.log(Reactive.val(aBool));

This code gives the following Console output:

>> 5
>> hello
>> true
>> ScalarSignal {
>>   """
>>     The `ScalarSignal` class monitors a numerical value.
>>   """
>>   Properties:
... list of properties and methods ...
}
>> StringSignal {
>>   """
>>     The `StringSignal` class monitors a string value.
>>   """
>>   Properties:
... list of properties and methods ...
}
>> BoolSignal {
>>   """
>>     The `BoolSignal` class monitors a boolean value.
>>   """
>>   Properties:
... list of properties and methods ...
}

Keep in mind that in most cases, values are implicitly converted into signals. You should only need to use Reactive.val() as a last resort.

// Load in the required modules
const FaceTracking = require('FaceTracking');
const Reactive = require('Reactive');

var mouthOpen = FaceTracking.face(0).mouth.openness.gt(Reactive.val(0.1)); // Works
var mouthOpen2 = FaceTracking.face(0).mouth.openness.gt(0.1); // Also works

Next Steps

Now you've learned about reactive programming its time to see it in action with our tutorial on how to create an effect with Scripting.