![]() |
HitLib 1.5.0
VEX V5 LED animation library for PROS
|
New in 1.5. Pattern Studio's Animate tab builds animations rather than picking them, and hitlib::LedStrand::compose() plays what it builds.
A custom animation is a Composition: an ordered stack of up to eight layers over a table of up to eight drivers. A layer says what to draw, where on the strip, when, and how it lands on the layers below it. A driver is one thing the animation watches - a sensor, a song, an oscillator, or a byte your own code sets - reduced to a single 0-255 value each tick. A binding ties one to a layer's parameter.
Every preset animation is expressible as a one-layer composition, and the Design tab's Open as Custom button performs that conversion on whatever you have configured. That is not a claim in a document: Pattern Studio's test suite runs both and compares them frame for frame.
A composition replaces the base animation. Brightness, the overlay buffer, the splice mask and the profile system all keep working over it, because the evaluator writes the same buffer a preset writes and leaves the strand in the same state afterwards.
The composition and everything it points at must outlive the strand - Pattern Studio emits all of it as namespace-scope constants, the same way it emits a Profile. Re-installing the composition already playing is a no-op, so a Sequencer phase that re-issues it every 200 ms costs a pointer comparison rather than rebuilding every pattern five times a second.
Calling any preset - rainbow(), pulse(), off() - takes the base buffer back.
setDriverLevel() is the composition's setLevel(): it stores a byte, so it is cheap enough to call every control-loop iteration. The export names each driver, so robot code says what it means and a reordered driver becomes a compile error rather than a wrong animation:
A composition's MUSIC drivers and its layers' timing windows run on one clock, so composeMusicSeek() moves both and composeMusicPause() holds both: a paused song leaves every window where it is, and resuming carries on from there. Motion inside a layer - a scroll, a flash - keeps running.
| Kind | What it draws |
|---|---|
| OFF | nothing - its background, or transparency under KEY |
| SOLID | one colour |
| PULSE | a run of colour, scrolling or bouncing |
| FLASH | on and off, on its own timing |
| FLOW | a two-colour gradient, scrolling |
| RAINBOW | the hue wheel, scrolling |
| TWINKLE | sparks from a palette |
| BITSCROLL | a pattern of coloured runs - several, unlike the preset |
| GAUGE | a colour scale, as a whole-span colour or a bar |
| METER | the level meter, scoped to a span |
| FRAMES | a hand-drawn strip: across is pixel, down is time. Each frame holds still or scrolls at the layer's speed, its own choice |
Each layer has a span (start, width, and an optional stride that repeats it along the strip), a blend, an opacity, an optional timing window with fades, and up to four bindings.
A repeat is never wider than its stride, so a span wider than the stride - width 0, "to the end", included - shows its first stride pixels in each repeat, and no pixel is blended twice. A continuous stride is one animation seen through the repeats rather than a copy in each: its scroll, bounce or sparkle runs along the whole strip and passes through every window.
A GAUGE with no stops of its own ramps color at emptyAt to color2 at fullAt, as a gauge splice region does.
OVER replaces. KEY is the one that makes stacking work: a pulse over a wash paints only its lit run and lets the wash through the gaps, where OVER would blank them. Every generator reports a coverage per pixel - a bitscroll gap, an unlit meter pixel, a half-faded spark - and KEY is what reads it. ADD, SCREEN and MULTIPLY do the usual; MASK uses this layer's brightness to decide how much of the layers below survives. Opacity and fades blend each of them in from "not there" - a mask at half opacity does half of what it does at full.
A converted preset blends OVER, deliberately: OVER ignores coverage, so the layer paints its own background exactly as the preset paints its own. Every layer after the first defaults to KEY.
startMs/endMs place a layer inside the composition's loop; an end at or before the start runs across the loop point. Two layers with overlapping windows and matching fades are a crossfade - there is no separate crossfade mechanism.
A window the loop never reaches - one that starts at or past the loop's length, as shortening a loop can leave behind - never opens, and one that ends past the loop point ends at it.
A binding maps a driver's 0-255 onto a parameter between atZero and atFull; atFull below atZero reverses it. An optional 17-point curve reshapes the driver first. A curve that eases makes a binding an envelope; a curve that steps makes it a threshold, which is how this design gets conditionals without a node graph.
Bindable: opacity, speed, run length, level, start, width, density, and the mix between a layer's two colours. A speed binding moves the pixels per step; the time between steps (Speed::everyMs()) stays the layer's own.
Audited against src/led_strand.cpp and asserted frame-for-frame by Pattern Studio's test suite.
| Preset | Result |
|---|---|
| off, setColor | exact |
| pulse, incl. bounce and invert | exact |
| flash, incl. sub-tick periods | exact |
| flow, both seam modes, both directions | exact |
| rainbow, any speed | exact (and a layer can reverse it, which the preset cannot) |
| bitscroll, all tiling and bounce modes | exact |
| levelFill + setLevel / levelSource | exact, partial edge pixel included |
| musicSync | exact |
| Gauge regions, both styles, both scale blends | exact |
| twinkle | algorithmically exact, not frame-identical - see below |
| spliceMaskCustom, up to 8 regions | exact (one layer per region) |
| spliceMask alternating | approximate - within one refresh tick per toggle |
TwinkleState::reset() seeds one process-wide std::rand() shared by every strand, so the preset is not frame-reproducible against itself, let alone against the desktop preview's generator. The spawn rate, fade and hold are identical; the individual sparks are not. Compared statistically instead.
Stated here rather than discovered halfway through a build.
A worst-case composition - eight layers over 64 pixels with four curved bindings each - is about 20,000 integer operations per tick, roughly 60 µs, and allocates nothing, a layer restarting its window included. Set against the 12 ms that flushBuffer() already spends pacing the ADI driver under a process-wide lock, that is about half a percent of what a tick costs before the composition is there at all.
The budgets it actually spends are flash - under 2 KB of descriptor for a full eight-layer composition, about a fifth of one baked song table - and a one-time scratch arena sized in compose(). The Animate tab shows both.