HitLib 1.5.0
VEX V5 LED animation library for PROS
Loading...
Searching...
No Matches
Animation Reference

Every animation below is available on hitlib::LedStrand. Nearly all of them are on hitlib::LedGroup too, where the call fans out to every strand in the group simultaneously; the exceptions are noted where they come up - overlayTwinkle(), overlayBitscroll() and the per-region gauge helpers (motorHeatGauge(), setRegionLevel()) are strand-only, so reach a strand through group[i] for those.

All calls are thread-safe and take effect on the next refresh tick.

Everything below can also be built - and combined, and driven from a sensor - as a custom animation in Pattern Studio's Animate tab. See Custom Animations.


Speed

Every animation that moves - flow, rainbow, pulse, bitscroll, and their overlay and splice-region versions - takes a hitlib::LedStrand::Speed. A bare number is pixels per refresh tick, which is what speed has always meant:

strand.rainbow(1); // a pixel every tick - 50 a second at the default 20 ms
strand.rainbow(3); // three pixels every tick

A pixel a tick is the slowest a bare number can go. For anything slower, give the time between steps instead:

strand.rainbow(Speed::everyMs(100)); // a pixel every 100 ms
strand.pulse(0xFF0000, 5, Speed::everyMs(250)); // a slow crawl
strand.flow(0xFF00DD, 0x000000, Speed::everyMs(40, 2)); // two pixels every 40 ms
How fast a moving animation travels: so many pixels, so often.
Definition led_strand.hpp:101

Like flash()'s durations, the step time is rounded to whole refresh ticks, so the pace stays even: a 100 ms step on a 20 ms strand moves on every fifth tick, never on a mix of fourth and sixth ones. A splice region takes one the same way, as .speed = Speed::everyMs(100).


Static

strand.setColor(0xFF0000); // solid red
strand.off(); // all pixels off (alias for setColor(0))

Flow (gradient scroll)

strand.flow(0xFF00DD, 0x000000, /*speed*/ 1);
strand.flow(0xFF00DD, 0x000000, /*speed*/ 1, /*invert*/ true);
strand.flow(0xFF00DD, 0x000000, /*speed*/ 1, /*invert*/ false, /*seamless*/ false);

Generates a gradient between two colors and scrolls it continuously. invert reverses the scroll direction. seamless (default true) loops the gradient back to the first color instead of cutting straight from the second color back to the first at the wrap.


Rainbow

strand.rainbow(/*speed*/ 1);

Full HSV rainbow scrolled across the strip.


Pulse

// Moving run of color over a background
strand.pulse(0xFF0000, /*runLength*/ 5, /*speed*/ 1);
strand.pulse(0xFF0000, 5, 1, /*bgColor*/ 0x330000);
// Bounce, run reverses direction at each end
strand.pulse(0xFF0000, 5, 1, 0x000000, /*invert*/ false, /*bounce*/ true);
// Slower than a pixel a tick - see Speed above
strand.pulse(0xFF0000, 5, hitlib::LedStrand::Speed::everyMs(120));
static constexpr Speed everyMs(uint16_t ms, uint8_t pixelsPerStep=1)
pixelsPerStep pixels (default one) every ms milliseconds.
Definition led_strand.hpp:111

Flash

strand.flash(0x00FF00, /*onMs*/ 100, /*offMs*/ 100);
strand.flash(0x00FF00, 100, 400, /*bgColor*/ 0x003300); // short blip, long gap

Blinks the whole strip: every LED lights at once for onMs, then the whole strip shows bgColor for offMs, and the cycle repeats.

On and off times are independent, so blink rate and duty cycle are set separately. 100, 100 is an even 5 Hz blink, while 100, 400 keeps the same brief flash at 2 Hz. Durations are rounded to whole refresh ticks and clamped to a minimum of one tick, so a strand can't be asked to blink faster than its refresh interval (its group's, once it is in a hitlib::LedGroup). They are also capped at 65535 ticks, about 22 minutes at 20 ms, so a huge onMs such as UINT32_MAX stays lit that long rather than wrapping round to one tick.


Twinkle

strand.twinkle(
{0xFFFFFF, 0xFFDD88}, // color palette
/*densityPct*/ 30, // 0–100, percentage of LEDs lit at once
/*fadeStep*/ 16, // brightness step per tick (higher = faster fade)
/*bgColor*/ 0x000000
);

Randomly spawns sparkles from the palette that fade in, hold briefly, then fade out.


Bitscroll

// Segments scroll off one end and wrap around
strand.bitscroll(
{{0xFF0000, 4}, {0x00FF00, 4}, {0x0000FF, 4}},
/*speed*/ 2,
/*invert*/ false,
/*bgColor*/ 0x000000,
/*bounce*/ false,
/*spacing*/ 2, // gap pixels between segments (default 5)
/*repeating*/ true // tile pattern vs a single copy
);
// Bounce: pattern rocks back and forth
strand.bitscroll(segments, 2, false, 0, /*bounce*/ true);
// Bounce a single copy of the pattern instead of a tiled one
strand.bitscroll(segments, 2, false, 0, /*bounce*/ true, /*spacing*/ 5,
/*repeating*/ false);

repeating applies to both travel styles: with true the pattern tiles across the whole strip, with false a single copy travels the strip on its own.


Overlay Animations

A second animation buffer, independent of the base animation. Shown directly in splice mask regions that set useOverlay.

strand.overlaySetColor(0xFFFFFF);
strand.overlayRainbow(1);
strand.overlayPulse(0x0000FF, 5, 1);
strand.overlayFlow(0xFF0000, 0x0000FF, 1);
strand.overlayFlow(0xFF0000, 0x0000FF, 1, /*seamless*/ false);
strand.overlayFlash(0xFFFFFF, 100, 100);
strand.overlayTwinkle({0xFFFFFF, 0x88CCFF}, 30, 16);
strand.overlayBitscroll({{0x00FFAA, 3}}, 1);

The overlay carries the same vocabulary as the base animations, minus bounce: it is a single scrolling buffer, and bouncing needs a wider master pattern to slide a window over. Use the base animation when you want that.


Level Meter

Fills the strip from one end in proportion to a 0-255 level, and dims the LED at the edge of the fill part-way so a short strand still shows a smooth ramp rather than one step per pixel.

// Set the colors once...
strand.levelFill(0x00FF88); // one color
strand.levelFill(0x00FF00, 0xFF0000, /*gradient*/ true); // green at the bottom, red at the top
strand.levelFill(0x00FF88, 0x000000, false, 0x050505, /*invert*/ true); // fills from the far end
// ...then drive it from wherever the number comes from.
strand.setLevel(intakeVelocity * 255 / maxVelocity);

With gradient, the two colors are laid out across the whole strip, not across the lit part, so a given pixel is always the same color no matter how full the meter is, as a VU-style scale needs; a gradient stretched over the fill would recolor every pixel on every update.

Three things can move a meter, and they are mutually exclusive - whichever was set up last owns it, so a strip handed to a song never has a stale sensor fighting it for the same pixels:

Set up with Moved by
By hand levelFill() your code calling setLevel()
From a value levelSource() the strand, once per tick
From a song musicSync() the baked envelope, against the wall clock

Following a value

levelSource() hands the strand a reader and the range it maps onto the strip. From then on the bar tracks the value with no code in your control loop at all:

pros::Motor intake(11);
strand.levelFill(0x00FF00, 0xFF0000, /*gradient*/ true); // green -> red scale
strand.levelSource([] { return intake.get_temperature(); },
20.0, 70.0); // cool -> shutdown hot

The two bounds are in whatever units the reader already speaks - degrees, percent, millimetres, RPM - so nothing has to be scaled into 0-255 first:

// A fuel gauge, empty when the battery is.
strand.levelSource([] { return pros::battery::get_capacity(); }, 0.0, 100.0);
// A bar that fills once per revolution and starts over.
strand.levelSource([] { return arm.get_position(); }, 0.0, 360.0, /*wrap*/ true);
// Full at 0: a "distance remaining" bar that drains as the number climbs.
strand.levelSource([] { return (double)goal.get(); }, 2000.0, 0.0);
// A noisy reading, glided rather than snapped.
strand.levelSource([] { return intake.get_actual_velocity(); },
0.0, 200.0, false, /*smoothing*/ 60);

Set it up once - in initialize(), or in a mode's onActivate so the meter follows that mode - and call clearLevelSource() or setLevel() to take it back by hand.

A few details worth knowing:

  • Outside the range the bar clamps to empty or full, so a gauge parks at either end. wrap cycles instead, for a continuously turning motor or a heading wants: 450° of a 0-360 range shows a quarter full, not full.
  • smoothing (0-99) is how much of the previous frame's fill to keep each tick. The bar still reaches its target, it just takes a few ticks - and a wrapping meter takes the short way round rather than sweeping back down the whole strip to roll over.
  • The reader runs on the LED task, not yours, so it must not block. It has to return a double: a reading that comes back as an integer needs saying so, [] { return (double)rot.get_angle(); }.
  • An unplugged device reports PROS_ERR_F, which is infinity. The meter holds where it is rather than slamming to full.

Pattern Studio has this as its Fill animation: pick what the bar follows from a list of the things a V5 robot actually has - battery capacity, motor heat, position, velocity or efficiency, a rotation sensor, an IMU heading, a distance sensor, a potentiometer - and the exported header carries the reader already written, including the #include it needs. Anything not on the list exports as a LevelFn hook to assign from your own code, and the export's banner spells out the assignment.


Music Sync

The same meter, driven by a song instead of by hand.

The V5 can neither hear audio nor decode a song, so it is resolved on the desktop. Pattern Studio reads an audio file - MP3, M4A, FLAC, OGG, WAV - or a MIDI, measures its loudness in three frequency bands, and exports each band a strand uses as a table of one byte per frame. On the robot, playback is an array lookup and a lerp per tick, no matter how long the song is.

#include "my_show.hpp" // exported by Pattern Studio, defines music::anthemBass
void startShow(hitlib::LedStrand& s) {
hitlib::profiles::music::anthemBass,
0x00FF88, 0xFF0044, /*gradient*/ true, // fill colors
0x000000, // unfilled background
/*invert*/ false,
/*sensitivity*/ 100, // % gain on the envelope
/*loop*/ false
);
}
Driver for a single WS2812B-compatible LED strip on a VEX ADI port.
Definition led_strand.hpp:53
void musicSync(const MusicTrack &track, uint32_t color, uint32_t color2=0x000000, bool gradient=false, uint32_t bgColor=0x000000, bool invert=false, uint8_t sensitivity=100, bool loop=false)
Fill the meter from a baked music envelope, and start playing it.

Playback is anchored to the wall clock at the moment musicSync() runs, so call it when the music starts, typically from a mode's onActivate. Samples are interpolated between frames, so the fill stays smooth even when the envelope's frame rate is coarser than refreshMs.

Bands

Each strand picks the part of the song it follows, so a robot with two strips can run them off one track without them moving together:

Band What it follows
Bass Kick and bass. Pumps on the beat, and what most strips want.
Mid Vocals, snare, guitars. Follows the body of the arrangement.
Treble Hats and cymbals. Sparse and sparkly.
Full mix Everything at once. Steadiest, least rhythmic.

Only the bands a design actually uses are exported, so following the bass alone costs one table rather than four.

Sensitivity

A percentage gain applied to every sample: above 100 the quiet passages reach further up the strip (and loud ones clip at full), below 100 only the peaks register. It is the one knob worth retuning on the robot, so it has its own setter and needs no re-export:

strand.setSensitivity(160); // punchier
strand.musicSeek(30000); // jump 30 s in
strand.musicPause(); // hold the meter where it is
strand.musicPause(false); // and carry on

Everything about how the song drives the strip - which band, how hard it punches, how fast it falls away - is decided in Pattern Studio and baked into the table. A three-minute song at the default 25 ms frame is about 7 KB of flash per band.


Splice Mask

Overrides part of the strip, either as equal alternating bins sharing the overlay buffer (spliceMask) or as arbitrarily placed regions that each animate independently (spliceMaskCustom). The two forms are mutually exclusive - whichever was called most recently is what's active.

// Two halves: left shows animation, right shows bgColor
strand.spliceMask(1);
// Alternating, toggles every 100 ms (creates a strobe/interleave effect)
strand.spliceMask(3, false, /*alternating*/ true, /*periodMs*/ 100);
// Masked bins show the overlay animation instead of a solid color
strand.overlayRainbow(1);
strand.spliceMask(1, false, false, 100, 0x000000, /*useOverlay*/ true);
// Custom: arbitrary, non-alternating regions, each with its own animation --
// unlike the overlay above, these run independently and simultaneously.
using Kind = LedStrand::SpliceRegionAnimKind;
strand.spliceMaskCustom({
{.start = 0, .width = 5, .kind = Kind::SOLID, .color = 0xFF0000}, // solid red
{.start = 10, .width = 8, .kind = Kind::RAINBOW, .speed = 1}, // its own rainbow
{.start = 20, .width = 6, .kind = Kind::PULSE, .color = 0x00FF00, .runLength = 3, .speed = 2},
{.start = 30, .width = 6, .kind = Kind::TWINKLE, .palette = {0xFFFFFF}, .densityPct = 40},
{.start = 40, .width = 8, .kind = Kind::BITSCROLL, .color = 0x00FFAA, .speed = 1},
});
// Clear (either kind)
strand.clearSpliceMask();

Region animation kinds

SpliceRegionAnimKind has nine values. Each region reads only the fields its kind uses, so the rest can be left at their defaults:

Kind Shows Fields it reads
OFF Nothing - the region stays dark. The default. -
SOLID One flat color. color
PULSE A run of color travelling over a background. color, bgColor, runLength, speed
FLASH The whole region blinking on and off. color, bgColor, onMs, offMs
FLOW A two-color gradient scrolling through the region. color, color2, speed, seamless
RAINBOW A full HSV rainbow across the region. speed
TWINKLE Sparkles drawn from a palette. palette, densityPct, fadeStep, bgColor
BITSCROLL Evenly sized runs scrolling through the region. color, bgColor, segmentWidth, spacing, repeating, speed, invert
GAUGE A meter colored off its own scale. See below. read, emptyAt, fullAt, wrap, smoothing, stops, style, blend, bgColor, invert

invert is narrower here than on the base animations: it reverses a BITSCROLL's scroll direction and picks which end a GAUGE BAR fills from, and every other kind ignores it. There is no bounce at all - a region scrolls a buffer its own width, and bouncing needs a wider master pattern to slide a window over.

Regions animate on their own clocks, so a FLASH region and a BITSCROLL region in the same mask do not have to agree on a period. A region's BITSCROLL takes a single segmentWidth rather than the base animation's list of BitScrollSegments, since a region is usually only a few pixels wide. SOLID and OFF cost nothing per tick: neither one allocates a buffer.

Gauge regions

A GAUGE region is the odd one out: it animates from a reading rather than from a clock. Each one polls its own value every tick and colors itself off its own scale, so a single strip can carry several independent meters – which the strand-wide level meter cannot do, there being one of those per strand.

The scale is a list of stops given in the reading's own units, so it says what it means. Below the first stop and above the last, the gauge holds that stop's color, so a scale never has to cover a range you don't care about. Two stops at the same reading make a hard edge: the one listed first is the color below it, the second the color above. stops is optional: leave it empty and the gauge falls back to color at emptyAt blending to color2 at fullAt, so a plain two-color gauge needs no scale at all.

using Kind = LedStrand::SpliceRegionAnimKind;
pros::Motor intake(11);
double intakeTemp() { return intake.get_temperature(); }
void showIntakeHeat(LedStrand& strand) {
strand.spliceMaskCustom({
{.start = 0, .width = 9, .kind = Kind::GAUGE,
.read = intakeTemp, .emptyAt = 20.0, .fullAt = 70.0, .smoothing = 80,
.stops = {{20.0, 0x00FF00}, {55.0, 0xFF7000}, {70.0, 0xFF00FF}}},
});
}

style picks what the region does with its level: HEAT (the default) colors every pixel it owns at once, and BAR fills the region proportionally instead, like a miniature levelFill(). blend picks what happens between two stops: LERP slides between their colors, and STEP holds each one until the next is actually reached – the honest choice when the stops are thresholds something crosses rather than points on a ramp. A reading exactly on a stop has reached it, which matters for a V5 motor: it reports temperature in 5 C steps that land right on the motor heat scale's stops.

Leave read as nullptr and the region is hand-driven instead, through setRegionLevel() – setLevel()'s counterpart for one region.

Motor heat, one segment per motor

The case gauges exist for: a strip under the drivebase, split into one segment per motor, each colored by how hot its own motor is. motorHeatGauge() is a gauge region arriving pre-loaded with the V5's own derating schedule, so the colors mean something specific rather than being a pretty ramp – green cold, yellow nearing the first cut, then orange, red and deep red for the 50%, 25% and 12.5% current limits, and magenta at 70 °C where the motor shuts down.

// Front to back down the left side, then the right - the order the LEDs run in.
pros::Motor drive[6] = {pros::Motor(1), pros::Motor(2), pros::Motor(3),
pros::Motor(11), pros::Motor(12), pros::Motor(13)};
// A LevelFn is a plain function pointer and so cannot capture an index, which
// is why each segment gets its own one-line reader.
double heat0() { return drive[0].get_temperature(); }
double heat1() { return drive[1].get_temperature(); }
double heat2() { return drive[2].get_temperature(); }
double heat3() { return drive[3].get_temperature(); }
double heat4() { return drive[4].get_temperature(); }
double heat5() { return drive[5].get_temperature(); }
LedStrand::LevelFn readers[6] = {heat0, heat1, heat2, heat3, heat4, heat5};
void showDriveHeat(LedStrand& strand) {
std::vector<LedStrand::SpliceRegion> segments;
for (uint8_t i = 0; i < 6; ++i) {
// 9 lit pixels out of every 10: the tenth is left uncovered as a dark
// divider, without which two neighbouring segments at similar
// temperatures read as one long one.
segments.push_back(LedStrand::motorHeatGauge(i * 10, 9, readers[i]));
}
strand.off(); // the dividers show the base, so keep it dark
strand.spliceMaskCustom(segments);
}

The V5 reports motor temperature in coarse steps rather than as a continuous reading, which is why the preset ships with smoothing at 80: without it a segment jumps from one stop color straight to the next instead of creeping between them.

Pattern Studio builds the whole thing without any of this being typed: set one region up as a Gauge on Motor Temperature (which brings the six stops with it), then press Divide to lay out however many equal segments the strip holds and change each one's port.


Brightness

strand.setBrightness(60); // 60% applied non-destructively at flush time
strand.setBrightness(100); // restore full brightness