Contents ↓

Device Development

This chapter will guide you through creating the main animation tools in Chromatik. Basic familiarity with the concepts presented in Coding with LX is assumed.

Patterns

The most basic unit for generation of animations in LX is the LXPattern class. Conceptually, an LXPattern is a pixel shader. It is provided a rendering buffer to fill with a color value for every individual LED in the installation. A real-time run loop generates and updates these colors in real time.

Scaffolding

The basic scaffolding of a pattern class is as follows.

@LXCategory("Examples")
@LXComponent.Name("Example")
public class ExamplePattern extends LXPattern {

  // A basic constructor should pass lx to the super constructor
  public ExamplePattern(LX lx) {
    super(lx);
  }

  @Override
  public void run(double deltaMs) {
    // This method is called in real-time when the pattern is active, with
    // deltaMs indicating how many milliseconds have passed since the previous frame
  }
}

Annotations

Interfaces

Rendering

The core render loop should visit every point in the model and generate a color based upon the geometric coordinates of the point, or whatever other factors may be taken into account. Here the LXColor methods are useful to construct color values.

@Override
public void run(double deltaMs) {
  for (LXPoint p : model.points) {
    float hue = /* compute hue for this position, 0-360 */
    float sat = /* compute saturation for this position, 0-100 */
    float brightness = /* compute brightness for this position, 0-100 */
    colors[p.index] = LXColor.hsb(hue, sat, brightness);
  }
}

The colors array will have been provided to this pattern object by the framework. Its previous contents should be disregarded. In general, it is the responbility of the pattern to fill every point represented by model.points.

Parameters and Modulators

A pattern may expose Parameters which give the user control over its behavior, as well as using Modulators to generate changing values over time. These should be added as fields to the class and registered in the constructor.

// This is a parameter with default value 5, range 0-100
public final CompoundParameter example =
  new CompoundParameter("Param", 5, 0, 100)
  .setDescription("This is an example parameter");

// This is an LFO oscillating in a sin wave from 0 to 1 every second (1000ms)
public final SinLFO lfo = new SinLFO(0, 1, 1000);

public ExamplePattern(LX lx) {
  super(lx);
  addParameter("example", this.example);
  startModulator(this.lfo);
}

@Override
public void run(double deltaMs) {
  // Parameter and modulator values are automatically updated and can be
  // retrieved at runtime
  final float example = this.example.getValuef();
  final float lfo = this.lfo.getValuef();
  ...
}

Example

Here is a complete example class that generates a visual stripe with position and width specified by parameters.

@LXCategory("Examples")
@LXComponent.Name("Stripe")
public class StripePattern extends LXPattern {

  public final CompoundParameter xPos =
    new CompoundParameter("X-Pos", .5)
    .setUnits(CompoundParameter.Units.PERCENT_NORMALIZED)
    .setDescription("Sets the base position of the stripe");

  public final CompoundParameter fade =
    new CompoundParameter("Fade", .1)
    .setUnits(CompoundParameter.Units.PERCENT_NORMALIZED)
    .setDescription("Sets the fade width of the stripe");

  public StripePattern(LX lx) {
    super(lx);
    addParameter("xPos", this.xPos);
    addParameter("fade", this.fade);
  }

  @Override
  public void run(double deltaMs) {
    final float xPos = this.xPos.getValuef();
    final float fade = this.fade.getValuef();

    // Compute falloff factor based upon fade size
    final float falloff = 100 / fade;

    for (LXPoint p : model.points) {
      // Brightness falls off as distance from xPos increases
      float level = 100 - falloff * Math.abs(p.xn - xPos);

      // Set grayscale color, ensure value in range
      colors[p.index] = LXColor.gray(LXUtils.maxf(0, level));
    }
  }
}

Learn more from real examples in the LX Repository →

Effects

The second functional unit for animations in LX is the LXEffect class. An LXEffect implements a real-time run loop which modifies the pixel values of an already-computed animation. An effect comes after at least one Pattern and may be in a chain of many effects.

Scaffolding

The basic scaffolding of an effect class looks as follows:

@LXCategory("Examples")
@LXComponent.Name("Example")
public class ExampleEffect extends LXEffect {

  // A basic constructor should pass lx to the super constructor
  public ExampleEffect(LX lx) {
    super(lx);
  }

  @Override
  public void run(double deltaMs, double enabledAmount) {
    // This method is called in real-time when the effect is active, with
    // deltaMs indicating how many milliseconds have passed since the previous frame.

    // The enabledAmount parameter indicates with a value from 0-1 how strongly
    // to apply the effect. Damping is automatically enabled when the effect is turned
    // on or off.

    // The loop is not called when the effect is inactive.
  }
}

Annotations

Interfaces

Rendering

The core loop may perform whatever mutations it likes to the colors array, taking note that the previous values in colors are the source material that the effect is intended to modify.

@Override
public void run(double deltaMs, double enabledAmount) {
  for (LXPoint p : model.points) {
    colors[p.index] = /* perform some mutation of the color here */
  }
}

Note that the colors array is not owned by the effect object. At the time run is called, it will contain the values generated by whatever came before this effect in the processing chain. The effect will not have access to the values generated on this frame in future frames. If those values are needed, a secondary buffer must be used to store them (e.g. ModelBuffer).

Parameters and Modulators

A effect may expose Parameters and Modulators in identical fashion to Patterns.

@LXCategory("Examples")
@LXComponent.Name("Example")
public class ExampleEffect extends LXEffect {

  // Intensity parameter
  public final CompoundParameter intensity =
    new CompoundParameter("Intensity", .5)
    .setUnits(CompoundParameter.Units.PERCENT_NORMALIZED)
    .setDescription("Intensity of the effect");

  // This is an LFO oscillating in a sin wave from 0 to 1 every second (1000ms)
  public final SinLFO lfo = new SinLFO(0, 1, 1000);

  public ExampleEffect(LX lx) {
    super(lx);
    addParameter("intensity", this.intensity);
    startModulator(this.lfo);
  }

  @Override
  public void run(double deltaMs, double enabledAmount) {
    // Parameter and modulator values are automatically updated and can be
    // retrieved at runtime
    final float intensity = this.intensity.getValuef();
    final float lfo = this.lfo.getValuef();
    ...
  }
}

Example

Here is a complete example class that generates basic visual pulsing with the rate and intensity defined by parameters.

@LXCategory("Examples")
@LXComponent.Name("Pulse")
public class PulseEffect extends LXEffect {

  public final CompoundParameter intensity =
    new CompoundParameter("Intensity", .5)
    .setUnits(CompoundParameter.Units.PERCENT_NORMALIZED)
    .setDescription("Intensity of the effect");

  public final CompoundParameter period =
    new CompoundParameter("Period", 500, 5000)
    .setUnits(CompoundParameter.Units.MILLISECONDS)
    .setDescription("Period of the strobe oscillation");

  public final SinLFO lfo = new SinLFO(0, 1, this.period);

  public PulseEffect(LX lx) {
    super(lx);
    addParameter("intensity", this.intensity);
    addParameter("period", this.period);
    startModulator(this.lfo);
  }

  @Override
  public void run(double deltaMs, double enabledAmount) {
    // Parameter and modulator values are automatically updated and can be
    // retrieved at runtime
    final float intensity = this.intensity.getValuef();
    final float lfo = this.lfo.getValuef();

    // Compute strobe brightness level
    final float strobeLevel = 1 - lfo * intensity;

    // Take enabledAmount into account
    final double dampedLevel = LXUtils.lerp(1, strobeLevel, enabledAmount);

    // Make a mask color
    final int mask = LXColor.gray(100 * dampedLevel);
    final int alpha = LXColor.BLEND_ALPHA_FULL;

    // Multiply all colors against mask
    for (LXPoint p : model.points) {
      colors[p.index] = LXColor.multiply(colors[p.index], mask, alpha);
    }
  }
}

Learn more from real examples in the LX Repository →

Modulators

Custom user-facing modulator devices can also be developed. They similarly include a method that will be automatically invoked by the engine when the modulator is active to compute its new value based upon elapsed time.

Scaffolding

The basic scaffolding of a modulator class is as follows.

@LXCategory("Examples")
@LXModulator.Global("Example")
@LXModulator.Device("Example")
public class ExampleModulator extends LXModulator implements LXNormalizedParameter, LXOscComponent {

  // No-arg constructor passes default label
  public ExampleModulator() {
    super("Example");
  }

  @Override
  protected double computeValue(double deltaMs) {
    // Compute the new value for this modulator
    // based upon elapsed time
    return 0;
  }

  @Override
  public LXNormalizedParameter setNormalized(double value) {
    throw new UnsupportedOperationException("ExampleModulator doesn't support setNormalized()");
  }

  @Override
  public double getNormalized() {
    return getValue();
  }
}

Learn more from real examples in the LX Repository →

Annotations

Interfaces

Trigger Modulators

Modulators which implement a trigger output can use the LXTriggerSource interface to specify the trigger output.

@LXCategory("Examples")
@LXModulator.Global("Example Trigger")
@LXModulator.Device("Example Trigger")
public class ExampleTriggerModulator extends LXModulator implements LXOscComponent, LXTriggerSource {

  public final TriggerParameter trigOut =
    new TriggerParameter("Trig Out")
    .setDescription("Output trigger fires on some event");

  public ExampleTriggerModulator() {
    super("Trigger");
    addParameter("trigOut", this.trigOut);
  }

  @Override
  protected double computeValue(double deltaMs) {
    if (triggerConditionMet) {
      this.trigOut.trigger();
    }
    // Compute new value
    return 0;
  }

  public BooleanParameter getTriggerSource() {
    return this.trigOut;
  }
  ...
}

Non-Mapping Sources

Modulators which implement some kind of utility functionality but do not actually serve as a valid modulation mapping source can indicate this via the setMappingSource method.

public class UtilityModulator extends LXModulator implements LXOscComponent {
  public UtilityModulator() {
    super("Utility");
    setMappingSource(false);
  }
}