Tutorial

Generator Behaviors

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.
});

Try it out!

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.
});

Try it out!

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 }
  ];
});

Try it out!

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.
});

Try it out!

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.

Try it out!

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));

Try it out!

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()); 

Try it out!

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;
  }
});

Try it out!

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;
  }
});

Try it out!

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;
  }
});

Try it out!

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