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
- @LXCategory organizes this pattern in the Device Chooser
- @LXComponent.Name gives a friendly user-facing name for the pattern
Interfaces
- LXDeviceComponent.Midi indicates that the pattern responds to MIDI events
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
- @LXCategory organizes this pattern in the Device Chooser
- @LXComponent.Name gives a friendly user-facing name for the pattern
Interfaces
- LXDeviceComponent.Midi indicates that the effect responds to MIDI events
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
- @LXCategory organizes this pattern in the Modulator Chooser
- @LXModulator.Global gives a name for this modulator if available in the global modulation section
- @LXModulator.Device gives a name for this modulator if available in the device modulation section
Interfaces
- LXNormalizedParameter indicates values are normalized
- LXOscComponent indicates that the modulator receives OSC messages
- LXMidiListener indicates that the modulator handles MIDI messages
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);
}
}