agentleFS
piemontepiemonte/skills/te-pattern/SKILL.md

te-pattern

Skill1 starsChanged 7 months agoMITView on GitHub

Use when creating Java LED patterns for LXStudio-TE — TEPerformancePattern, audio reactivity, color system, variable-speed time, common controls, and pattern organization.

The file

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 0 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Add #pragma auto to any .fs file in te-app/resources/shaders/ and it will automatically appear in the "Auto Shaders" UI category.

    line 478
  2. Very specific:

    Call addCommonControls(): after all control configuration in the constructor

    line 552
  3. Very specific:

    Create file at te-app/src/main/java/titanicsend/pattern/[yourname]/PatternName.java

    line 568
  4. Very specific:

    Call super(lx) or super(lx, TEShaderView.ALL_EDGES)

    line 573
  5. Very specific:

    Use getSpeed(), getSize(), calcColor(), etc.

    line 577
  6. Very specific:

    Write to colors[point.index] for each LED

    line 578
  7. Very specific:

    Run mvn spotless:apply before committing

    line 579

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

TE Pattern Development

This reference covers creating Java patterns for Titanic's End, from basic LED animation through audio-reactive performance patterns and GPU shader wrappers.

Class Hierarchy

LXPattern (LX framework)
    └── DmxPattern (titanicsend.dmx.pattern)
        └── TEPattern (titanicsend.pattern)
            └── TEAudioPattern (titanicsend.pattern)
                └── TEPerformancePattern (titanicsend.pattern)
                    ├── GLShaderPattern (titanicsend.pattern.glengine)
                    └── [Your Pattern Here]
ClassUse When
TEPatternDirect model access, no audio, no common controls
TEAudioPatternNeed audio analysis but custom control layout
TEPerformancePatternFull-featured: common controls + audio + color + time
GLShaderPatternWrapping a GLSL fragment shader

Almost all new patterns should extend TEPerformancePattern.

Full Annotated Example: Afterglow.java

package titanicsend.pattern.piemonte;

import heronarts.lx.LX;
import heronarts.lx.LXCategory;
import heronarts.lx.color.LXColor;
import heronarts.lx.modulator.SawLFO;
import heronarts.lx.parameter.LXParameter;
import java.util.*;
import titanicsend.model.TEEdgeModel;
import titanicsend.pattern.TEControlTag;
import titanicsend.pattern.TEPerformancePattern;

@LXCategory("Edge FG")                          // UI category
public class Afterglow extends TEPerformancePattern {

  // Modulator: sawtooth wave cycling 0..1
  private final SawLFO phase = new SawLFO(0, 1, 1000);
  private Map<Integer, Float> edgePhaseOffsets = new HashMap<>();
  private final Random random = new Random();

  public Afterglow(LX lx) {
    super(lx);                                   // Always call super(lx)

    // Start the phase modulator
    startModulator(phase);

    // Configure common control ranges for this pattern
    controls.setRange(TEControlTag.SPEED, 0.5, 0, 1);
    controls.setRange(TEControlTag.SIZE, 5, 1, 20);
    controls.setRange(TEControlTag.QUANTITY, 0.3, 0, 1);
    controls.setRange(TEControlTag.WOW1, 0.75, 0.5, 1.0);
    controls.setRange(TEControlTag.WOW2, 5, 1.0, 10.0);
    controls.setUnits(TEControlTag.SIZE, LXParameter.Units.INTEGER);

    // Hide controls this pattern doesn't use
    controls.markUnused(TEControlTag.ANGLE);
    controls.markUnused(TEControlTag.XPOS);
    controls.markUnused(TEControlTag.YPOS);
    controls.markUnused(TEControlTag.SPIN);
    controls.markUnused(TEControlTag.WOWTRIGGER);

    // Add controls to UI (must call after configuration)
    addCommonControls();
  }

  @Override
  protected void runTEAudioPattern(double deltaMs) {
    // Read common control values
    double speed = getSpeed();
    double dotSize = getSize();
    double fadeDistance = getQuantity();
    double spacing = getWow1();
    int density = (int) Math.ceil(getWow2());

    // Update phase rate based on speed
    phase.setPeriod(1000.0 / Math.max(speed, 0.001));

    // Iterate edges
    for (TEEdgeModel edge : modelTE.getEdges().values()) {
      float offset = edgePhaseOffsets.computeIfAbsent(
          edge.hashCode(), k -> random.nextFloat());

      for (TEEdgeModel.Point ep : edge.edgePoints) {
        // ep.n = fractional position along edge (0.0..1.0)
        double pos = (ep.n + phase.getValue() + offset) % 1.0;

        // Calculate brightness based on dot pattern
        double brightness = 0;
        for (int d = 0; d < density; d++) {
          double dotPos = (d * spacing / density) % 1.0;
          double dist = Math.abs(pos - dotPos);
          dist = Math.min(dist, 1.0 - dist); // Wrap around
          brightness = Math.max(brightness,
              1.0 - (dist / Math.max(fadeDistance * dotSize / 100.0, 0.001)));
        }

        brightness = Math.max(0, Math.min(1, brightness));
        colors[ep.point.index] = LXColor.gray(brightness * 100);
      }
    }
  }
}

TEPattern Base Class

Path: te-app/src/main/java/titanicsend/pattern/TEPattern.java

Key Fields

public TEWholeModel modelTE;     // The complete vehicle model

Key Methods

MethodReturnsDescription
getModelTE()TEWholeModelAccess the vehicle model
wholeNote()doubleFraction into current measure (0..1)
phrase()doubleFraction into current phrase (0..1)
sinePhaseOnBeat()doubleSine modulator synced to beat (0..1)
measure()doubleFraction into variable-length measure
getTempo()TempoEngine tempo object
getEqualizer()GraphicMeterFrequency analyzer
getSwatchColor(TEColorType)intColor from current palette
setEdges(int color)voidSet all edge points to color
clearEdges()voidClear all edge points
clearPixels()voidClear all pixels (uses Arrays.fill, faster than looping)
getXn(LXPoint p)floatNormalized X coordinate for point

TEAudioPattern

Path: te-app/src/main/java/titanicsend/pattern/TEAudioPattern.java

Extends TEPattern with real-time audio analysis.

Audio Fields

FieldTypeDescription
volumeLeveldoubleInstantaneous normalized volume (0..1)
bassLeveldoubleAverage of low frequency bands
trebleLeveldoubleAverage of high frequency bands
volumeRatiodoubleCurrent volume / moving average
bassRatiodoubleCurrent bass / moving average
trebleRatiodoubleCurrent treble / moving average
bassHitbooleanTrue if bass transient detected this frame

Audio Methods

MethodReturnsDescription
computeAudio(deltaMs)voidUpdate audio analysis (call once per frame)
getBassLevel()doubleCurrent bass level
getTrebleLevel()doubleCurrent treble level
bassHit()booleanIs this frame a bass transient?
resetBassGate()voidReset beat detection timing
getVolumeRatiof()floatVolume ratio as float
getBassRatio()doubleBass ratio
getTrebleRatio()doubleTreble ratio

Bass Hit Detection

The bass hit detector triggers when:

  1. Current bass > 1.2x the moving average
  2. Current bass > previous frame's bass (rising edge)
  3. Retrigger timeout has elapsed (default: 80% of eighth note duration)
@Override
protected void runTEAudioPattern(double deltaMs) {
    computeAudio(deltaMs);  // Update audio state

    if (bassHit()) {
        // Trigger visual effect on beat
    }

    // Use volumeRatio for reactive brightness
    double brightness = Math.min(1.0, volumeRatio);
}

TEPerformancePattern

Path: te-app/src/main/java/titanicsend/pattern/TEPerformancePattern.java

The primary base class for new patterns. Provides common controls, variable-speed time, color system, and audio reactivity.

Constructor Pattern

public MyPattern(LX lx) {
    super(lx);
    // or: super(lx, TEShaderView.ALL_EDGES);  // Set default view

    // Configure controls (before addCommonControls)
    controls.setRange(TEControlTag.SIZE, 1.0, 0.1, 10.0);
    controls.setLabel(TEControlTag.WOW1, "Glow");
    controls.markUnused(TEControlTag.SPIN);

    addCommonControls();  // Must call after configuration
}

@Override
protected void runTEAudioPattern(double deltaMs) {
    // Pattern logic here
    // Use getSpeed(), getSize(), calcColor(), etc.
}

Variable-Speed Time

Time advances at a rate linked to tempo and the Speed control.

MethodReturnsDescription
getTime()doubleCurrent time in seconds (can be negative)
getTimeMs()doubleCurrent time in milliseconds
getDeltaMs()doubleTime delta since last frame (ms)

Speed-to-Tempo Mapping:

Speed ValueMusical DurationBeats
0.25Whole note4 beats
0.5Half note2 beats
1.0Quarter note1 beat
2.0Eighth note1/2 beat
3.0Eighth triplet1/3 beat
4.0Sixteenth note1/4 beat

Speed is multiplied by BPM (converted to beats/sec) to scale virtual time. At 120 BPM with speed=1.0, time advances at 2x real-time (2 beats/sec).

// Enable reverse playback (negative speed values)
allowBidirectionalTime(true);

// Get tempo-synced time for animation
double t = getTime();  // Continuous, speed-adjusted seconds

Rotation

MethodReturnsDescription
getRotationAngleFromSpeed()doubleBeat-linked rotation from Speed (radians)
getRotationAngleFromSpin()doubleBeat-linked rotation from Spin (radians)
getStaticRotationAngle()doubleStatic offset from Angle control (radians)
setMaxRotationSpeed(rps)voidSet max rotation (radians/sec)
retrigger(TEControlTag)voidRestart SPEED or SPIN timer

Color System

Primary & Secondary Colors

MethodReturnsDescription
calcColor()intPrimary color with brightness applied
calcColor(setBrightness)intPrimary color, optional brightness
calcColor2()intSecondary color with brightness
calcColor2(setBrightness)intSecondary color, optional brightness
getGradientColor(lerp)intInterpolated palette color (0..1)
getSwatchColor(TEColorType)intColor from swatch by type
getCurrentPalette()FloatBuffer5 RGB colors (15 floats)

Colors are cached per frame and only recalculated when the palette changes. Call expireColors() to force recalculation.

// Use primary color
int color = calcColor();

// Use gradient across a normalized position
int gradColor = getGradientColor((float) ep.n);

// Get secondary color (offset from primary)
int color2 = calcColor2();

LinkedColorParameter

For patterns needing direct palette access, use TEColorParameter which integrates with the swatch system and supports both static colors and palette-linked modes.

Control Getters

MethodRangeDescription
getSpeed()-4..4Speed with global sync support
getXPos()-1..1X position
getYPos()-1..1Y position
getSize()0.01..5.0Size multiplier
getQuantity()0..1Quantity/density
getSpin()-1..1Spin control
getBrightness()0..1Brightness
getWow1()0..1First WoW parameter
getWow2()0..1Second WoW parameter
getWowTrigger()booleanWoW trigger state
getTwist()booleanAxis swap state
getLevelReactivity()0..1Audio level reactivity
getFrequencyReactivity()0..1Frequency reactivity

Event Callbacks

@Override
protected void onWowTrigger(boolean on) {
    // Called when WoW trigger activates/deactivates
    if (on) {
        // Trigger one-shot effect
    }
}

@Override
protected void onTwist(boolean on) {
    // Called when Twist toggles
}

TECommonControls

Path: te-app/src/main/java/titanicsend/pattern/TECommonControls.java

Manages the standardized control interface for all performance patterns.

Control Tags (TEControlTag)

TagPathLabelDefaultRange
SPEEDte_speedSpeed0.5-4..4 (bipolar, exp 1.75)
XPOSte_xposxPos0-1..1 (bipolar, center-biased)
YPOSte_yposyPos0-1..1 (bipolar, center-biased)
SIZEte_sizeSize1.00.01..5.0
QUANTITYte_quantityQuantity0.50..1
SPINte_spinSpin0-1..1 (bipolar, exp 2)
BRIGHTNESSte_brightnessBrightness1.00..1
WOW1te_wow1Wow100..1
WOW2te_wow2Wow200..1
WOWTRIGGERte_wowtriggerWowTriggerfalseboolean
ANGLEte_angleAngle0-PI..PI (displays degrees)
LEVELREACTIVITYte_levelLvlReact0.10..1
FREQREACTIVITYte_freqFreqReact0.10..1
TWISTte_twistTwistfalseboolean

Configuration Methods

// Change range and default value
controls.setRange(TEControlTag.SIZE, 5.0, 1.0, 20.0);

// Change display label
controls.setLabel(TEControlTag.WOW1, "Glow");

// Set response curve exponent
controls.setExponent(TEControlTag.SPEED, 2.0);

// Mark unused (hides from UI)
controls.markUnused(TEControlTag.SPIN);

// Set display units
controls.setUnits(TEControlTag.SIZE, LXParameter.Units.INTEGER);

// Set normalization curve
controls.setNormalizationCurve(TEControlTag.SPEED,
    LXParameter.NormalizationCurve.BIAS_CENTER);

Remote Controls Order (MIDI Surface)

Controls appear on MIDI surfaces in this order:

  1. LEVELREACTIVITY
  2. FREQREACTIVITY
  3. View
  4. SPEED
  5. XPOS
  6. YPOS
  7. QUANTITY
  8. SIZE
  9. ANGLE
  10. SPIN
  11. PANIC
  12. Preset
  13. WOW1
  14. WOW2
  15. WOWTRIGGER
  16. Color Offset
  17. Capture Defaults

Panic Button

The panic control resets all parameters:

  • Position, size, quantity, angle, spin → defaults
  • Brightness → 100%
  • Color saturation → 100%
  • WoW and reactivity → cleared

TEShaderView

Path: te-app/src/main/java/titanicsend/pattern/yoffa/framework/TEShaderView.java

Controls which model points a pattern renders to.

Enum ValueDescription
ALL_POINTSAll points (edges + panels), default
ALL_EDGESEdge strips only
ALL_PANELSPanel surfaces only
ALL_PANELS_INDIVIDUALPanels with per-panel normalization
DOUBLE_LARGELarge panel sections
SPLIT_PANEL_SECTIONSSplit panel variation
// Set in constructor
public MyPattern(LX lx) {
    super(lx, TEShaderView.ALL_EDGES);
    // ...
}

// Or override
@Override
public TEShaderView getDefaultView() {
    return TEShaderView.ALL_PANELS;
}

GLShaderPattern (Shader Wrapper)

Path: te-app/src/main/java/titanicsend/pattern/glengine/GLShaderPattern.java

Wraps GLSL fragment shaders as TE patterns. Extends TEPerformancePattern.

Minimal Java Wrapper

package titanicsend.pattern.glengine;

import heronarts.lx.LX;
import heronarts.lx.LXCategory;

@LXCategory("Native Shaders")
public class MyShaderPattern extends GLShaderPattern {
    public MyShaderPattern(LX lx) {
        super(lx, TEShaderView.ALL_POINTS);
        addShader("my_shader.fs");
    }
}

The wrapper automatically:

  • Maps all TECommonControls to shader uniforms
  • Updates audio data each frame
  • Transfers rendered pixels to the LX color buffer

Shader Chaining

public MyChainedPattern(LX lx) {
    super(lx);
    addShader("background.fs");  // Renders first
    addShader("overlay.fs");     // Composites on top
}

Auto Shaders (No Java Wrapper Needed)

Add #pragma auto to any .fs file in te-app/resources/shaders/ and it will automatically appear in the "Auto Shaders" UI category. See te-shader.md for details.

GLEngine Audio (Static Getters)

Path: te-app/src/main/java/titanicsend/pattern/glengine/GLEngine.java

Global audio state accessible from any pattern:

Static MethodDescription
GLEngine.getVolume()Instantaneous volume (0..1)
GLEngine.getBassLevel()Bass level
GLEngine.getTrebleLevel()Treble level
GLEngine.getVolumeRatio()Volume / moving average
GLEngine.getBassRatio()Bass / moving average
GLEngine.getTrebleRatio()Treble / moving average
GLEngine.getSinPhaseOnBeat()Sine wave on beat (0..1)
GLEngine.getAvgBass()Bass moving average

Audio texture (for shaders): 512x2 pixels, GL_R32F format.

  • Row 0: FFT frequency bins (0-511)
  • Row 1: Waveform samples (-1.0 to 1.0)

Audio Stem Uniforms

The PerFrameBlock includes audio stem splitter values (range -1.0 to 1.0):

UniformDescription
stemBassRMS energy of bass stem
stemDrumsRMS energy of drums stem
stemVocalsRMS energy of vocals stem
stemOtherRMS energy of other/instrumental content
stemDrumHitsRMS energy of drum hits/transients

File Organization

Patterns are organized by developer under te-app/src/main/java/titanicsend/pattern/:

titanicsend/pattern/
├── TEPattern.java
├── TEAudioPattern.java
├── TEPerformancePattern.java
├── TECommonControls.java
├── TEControlTag.java              # in jon/ subdirectory
├── glengine/
│   ├── GLShaderPattern.java
│   └── GLEngine.java
├── piemonte/
│   └── Afterglow.java
├── jon/
│   └── ...
├── yoffa/
│   └── framework/
│       └── TEShaderView.java
└── [your-name]/
    └── YourPattern.java

Conventions

  1. Package by developer: titanicsend.pattern.yourname
  2. @LXCategory annotation: Required. Common categories:
    • "Edge FG" — Edge foreground patterns
    • "Edge BG" — Edge background patterns
    • "Panel FG" — Panel foreground patterns
    • "Panel BG" — Panel background patterns
    • "Combo FG" — Both edges and panels
    • "Mothership" — Mothership-specific patterns
    • "Native Shaders" — Java-wrapped GLSL shaders
    • "Auto Shaders" — Auto-loaded GLSL shaders
  3. Code style: Spotless with Google Java Format (enforced by CI)
  4. Always extend TEPerformancePattern unless you have a specific reason not to
  5. Call addCommonControls() after all control configuration in the constructor
  6. Call computeAudio(deltaMs) at the start of runTEAudioPattern() if you need audio data
  7. Use clearPixels() to clear the frame — never manually loop over model.points setting TRANSPARENT
  8. Use helpers from the class hierarchy — check TEPattern, TEAudioPattern, and TEPerformancePattern for existing utility methods (e.g., clearPixels(), clearEdges(), setEdges(), calcColor(), getGradientColor()) before writing manual loops
  9. Speed parameter is bipolar — range it from -value to +value so the dial centers at zero (no movement). Negative speed should reverse the animation direction. Avoid using SawLFO with speed-dependent period when bipolar speed is needed; instead, accumulate phase manually:
    // Bipolar speed: negative reverses, zero stops
    controls.setRange(TEControlTag.SPEED, 0.5, -1, 1);
    
    // In runTEAudioPattern:
    phase += (deltaMs / periodMs) * getSpeed();
    phase = phase - Math.floor(phase); // wrap to 0..1

Creating a New Pattern: Checklist

  1. Create file at te-app/src/main/java/titanicsend/pattern/[yourname]/PatternName.java
  2. Set package to titanicsend.pattern.[yourname]
  3. Add @LXCategory("...") annotation
  4. Extend TEPerformancePattern
  5. In constructor:
    • Call super(lx) or super(lx, TEShaderView.ALL_EDGES)
    • Configure controls via controls.setRange(), controls.setLabel(), controls.markUnused()
    • Call addCommonControls()
  6. Override runTEAudioPattern(double deltaMs)
  7. Use getSpeed(), getSize(), calcColor(), etc. for control values
  8. Write to colors[point.index] for each LED
  9. Run mvn spotless:apply before committing

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.