In JavaScript, generators are functions that can be exited and later re-entered. Their context (variable bindings) are saved across re-entrances. This makes them ideal for behaviors because they ensure that the behavior is interruptable, that logic executes at the right time, and that the main thread is never blocked.
You should have at least a basic understanding of generator functions before proceeding.
Ayva's do()
method accepts behaviors in the form of a generator function. When using a generator function, the yield
keyword can be used to pause execution until any moves that have been queued are finished. A value may optionally be passed to yield
to perform an action as well. That value may take the following forms:
yield <number>
A number is interpreted as the number of seconds to wait after all moves have finished:
ayva.do(function*() {
ayva.$.stroke(0, 1).execute(); // Stroke down.
ayva.$.stroke(1, 1).execute(); // Stroke up.
yield 0.5; // Wait half a second.
});
yield <single-axis-move>
A single-axis move can be yielded directly (see the Motion API):
ayva.do(function*() {
yield { to: 0, speed: 1 }; // Stroke down.
yield { to: 1, speed: 0.5 }; // Stroke up.
});
yield <multi-axis-move>
A multi-axis move can be yielded by passing an array of moves (see the Motion API: Multiaxis Movements):
ayva.do(function*() {
yield [
{ to: 0, speed: 1 },
{ axis: 'twist', to: 0 }
];
yield [
{ to: 1, speed: 0.5 },
{ axis: 'twist', to: 1 }
];
});
yield <move-builder>
A move builder can be yielded directly (see Syntactic Sugar ($)):
ayva.do(function*() {
const { stroke } = ayva.$; // Get a move builder.
yield stroke(0, 1).twist(0).pitch(0.75); // Stroke down.
yield stroke(1, 1).twist(1).pitch(0.25); // Stroke up.
});
yield <Promise>
A Promise can be yielded. Execution will continue after all previous moves have finished and the specified Promise has resolved.
ayva.do(function*() {
ayva.$.stroke(0, 1).execute();
ayva.$.stroke(1, 1).execute();
yield new Promise ((resolve) => { /* Some asynchronous operation */ });
});
yield <null> or <undefined>
Yielding null
or undefined
will simply pause execution until any moves that have been queued are finished.
ayva.do(function*() {
ayva.$.stroke(0, 1).execute();
ayva.$.stroke(1, 1).execute();
yield;
console.log('This will print after the above strokes are complete.');
});
Object Oriented Programming
You can package your generator behavior into a class to make it more reusable and/or configurable by extending the GeneratorBehavior
base class. GeneratorBehavior
is available as part of the standard distribution, but to use it within an ES6 module, you must import it. For example, in a browser:
import { Ayva, GeneratorBehavior } from 'https://unpkg.com/ayvajs';
or from within a Node.js app:
import { Ayva, GeneratorBehavior } from 'ayvajs';
*generate()
When you extend a GeneratorBehavior
, you must implement the *generate()
method. The *generate()
method takes an optional Ayva instance as the argument and is expected to perform and/or yield the actions that constitute a single iteration of the behavior.
class MyStroke extends GeneratorBehavior {
constructor (speed) {
super(); // Must call super constructor.
this.speed = speed;
}
* generate (ayva) {
const { stroke } = ayva.$; // Get a move builder.
yield stroke(0, this.speed); // Stroke down.
yield stroke(1, this.speed); // Stroke up.
yield 0.5; // Wait 0.5 seconds.
}
}
ayva.do(new MyStroke(2)); // 2 strokes per second.
Behavior Composition
A GeneratorBehavior
can incorporate another GeneratorBehavior
by using a yield* expression.
The following example demonstrates using ClassicStroke
inside a GeneratorBehavior
(ClassicStroke
is a subclass of GeneratorBehavior
):
class MyCompositeBehavior extends GeneratorBehavior {
constructor (speed) {
super();
this.speed = speed;
}
* generate (ayva) {
const fullSpeedStroke = new ClassicStroke(0, 1, this.speed);
const halfSpeedStroke = new ClassicStroke(0, 1, this.speed / 2);
yield* fullSpeedStroke(ayva, 4); // 4 full speed strokes.
yield* halfSpeedStroke(ayva, 4); // 4 half speed strokes.
}
}
ayva.do(new MyCompositeBehavior(2));
Notice that when we used ClassicStroke
in a yield*
expression, we called it like a function:
yield* fullSpeedStroke(ayva, 4);
This is because a GeneratorBehavior
is designed to be a callable object. i.e. It can behave like a function. When invoked, it will automatically call its *generate()
function to return the generator for the behavior. The first parameter is the Ayva instance to pass to *generate()
, and the second (optional) parameter is the number of iterations to perform (default = 1).
yield*
will give control to the sub behavior until the specified number of iterations have completed.
bind()
It is sometimes tedious to always have to pass the Ayva instance to a generator behavior when it is invoked. To prevent having to do this, a GeneratorBehavior
can be bound to an Ayva instance using the bind()
method (not to be confused with Function.prototype.bind(), although the effect is similar).
Here is the previous example rewritten using bind()
:
class MyCompositeBehavior extends GeneratorBehavior {
constructor (speed) {
super();
this.speed = speed;
}
* generate (ayva) {
// Create and bind the strokes to the Ayva instance.
const fullSpeedStroke = new ClassicStroke(0, 1, this.speed).bind(ayva);
const halfSpeedStroke = new ClassicStroke(0, 1, this.speed / 2).bind(ayva);
// The ayva parameter can now be omitted.
yield* fullSpeedStroke(4); // 4 full speed strokes.
yield* halfSpeedStroke(4); // 4 half speed strokes.
}
}
ayva.do(new MyCompositeBehavior(2));
If only one iteration needs to be performed, and a generator behavior has been bound to an Ayva instance (or doesn't need one), it can itself be yielded without having to invoke it. For example:
class MyBehavior extends GeneratorBehavior {
/* ... */
}
const behavior = new MyBehavior();
ayva.do(function*() {
/* ... */
yield* behavior;
});
next()
One limitation of a yield*
expression is that it passes control to the supplied generator until that generator is finished. There is therefore no way for the parent behavior to take back control. GeneratorBehaviors
have a next()
method that allows for finer control if needed. It returns only the next action of a behavior so control can be taken back at any time. The following example demonstrates this by creating a stroke that sometimes pauses for half a second in between movements:
class MyStroke extends GeneratorBehavior {
* generate (ayva) {
// Create a stroke where up strokes are performed at half speed.
const stroke = new ClassicStroke(0, 1, [1, 0.5]).bind(ayva);
while (true) {
yield* stroke;
}
}
}
class MyCompositeBehavior extends GeneratorBehavior {
* generate (ayva) {
const myStroke = new MyStroke().bind(ayva);
while (true) {
// Yield only the next action of MyStroke (a single stroke).
yield myStroke.next();
if (Math.random() < 0.5) {
// 50% chance of pausing for half a second in between strokes.
yield 0.5;
}
}
}
}
ayva.do(new MyCompositeBehavior());
Loops
Warning: Be careful when using loops such as while inside of Generator Behaviors. If a behavior does not yield a value you could end up with an infinite loop that blocks the main thread! The following example demonstrates this:
class SubBehavior {
* generate (ayva) {
ayva.$.stroke(0, 1).execute();
ayva.$.stroke(1, 1).execute();
}
}
class ParentBehavior {
* generate (ayva) {
const child = new SubBehavior().bind(ayva);
while (true) {
// Because child does not yield a value, this will block!!!
yield* child;
}
}
}
ayva.do(new ParentBehavior());
To fix this example, either SubBehavior could yield a value, or an additional empty yield could be added inside of the loop:
while (true) {
yield* child;
yield; // Adding an additional yield here ensures execution gets paused even if child does not yield a value.
}
Tempest Stroke
TempestStrokes
contain some convenience methods to allow incorporating them into GeneratorBehaviors
more seamlessly.
start()
The start()
method returns a generator that moves the device into the starting position of a TempestStroke
:
ayva.do(function*() {
const stroke = new TempestStroke('down-forward', 30).bind(ayva);
yield* stroke.start();
while (true) {
yield* stroke;
}
});
By default the speed of the movement is 1 unit per second. An object may be passed to the start()
method to override any properties of the movement. The properties available for override are those supplied to movement descriptors according to the Motion API. Here is an example that makes the start move last for 2 seconds with a parabolic ramp:
ayva.do(function*() {
const stroke = new TempestStroke('down-forward', 30).bind(ayva);
yield* stroke.start({
duration: 2,
value: Ayva.RAMP_PARABOLIC
});
while (true) {
yield* stroke;
}
});
Note: The start()
method's first parameter would normally be the Ayva instance, but in these examples the usage of bind()
allows us to omit it.
transition()
The transition()
method returns a new TempestStroke
that includes a transition at the beginning:
ayva.do(function*() {
const stroke = new TempestStroke('down-forward', 45).bind(ayva);
// Perform two down-forward strokes.
// Note: Each iteration of a TempestStroke is 180 degrees.
yield* stroke.start();
yield* stroke(4);
// Transition into an orbit-grind at 60 bpm over three seconds.
const nextStroke = stroke.transition('orbit-grind', 60, 3);
while (true) {
yield* nextStroke;
}
});
Note: In this example the first parameter of transition()
was a named stroke, but it could also be a stroke config—just like the first parameter of TempestStroke's constructor.
Whew!
That's all for the Behavior API! Feel free to explore the API Documentation to discover anything not covered here. 😊