001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.wpilibj; 006 007import static edu.wpi.first.units.Units.Meters; 008import static edu.wpi.first.units.Units.Microsecond; 009import static edu.wpi.first.units.Units.Microseconds; 010import static edu.wpi.first.units.Units.Value; 011 012import edu.wpi.first.units.collections.LongToObjectHashMap; 013import edu.wpi.first.units.measure.Dimensionless; 014import edu.wpi.first.units.measure.Distance; 015import edu.wpi.first.units.measure.Frequency; 016import edu.wpi.first.units.measure.LinearVelocity; 017import edu.wpi.first.units.measure.Time; 018import edu.wpi.first.util.WPIUtilJNI; 019import edu.wpi.first.wpilibj.util.Color; 020import java.util.Map; 021import java.util.Objects; 022import java.util.function.BooleanSupplier; 023import java.util.function.DoubleSupplier; 024 025/** 026 * An LED pattern controls lights on an LED strip to command patterns of color that may change over 027 * time. Dynamic patterns should synchronize on an external clock for timed-based animations ({@link 028 * WPIUtilJNI#now()} is recommended, since it can be mocked in simulation and unit tests), or on 029 * some other dynamic input (see {@link #synchronizedBlink(BooleanSupplier)}, for example). 030 * 031 * <p>Patterns should be updated periodically in order for animations to play smoothly. For example, 032 * a hypothetical LED subsystem could create a {@code Command} that will continuously apply the 033 * pattern to its LED data buffer as part of the main periodic loop. 034 * 035 * <pre><code> 036 * public class LEDs extends SubsystemBase { 037 * private final AddressableLED m_led = new AddressableLED(0); 038 * private final AddressableLEDBuffer m_ledData = new AddressableLEDBuffer(120); 039 * 040 * public LEDs() { 041 * m_led.setLength(120); 042 * m_led.start(); 043 * } 044 * 045 * {@literal @}Override 046 * public void periodic() { 047 * m_led.writeData(m_ledData); 048 * } 049 * 050 * public Command runPattern(LEDPattern pattern) { 051 * return run(() -> pattern.applyTo(m_ledData)); 052 * } 053 * } 054 * </code></pre> 055 * 056 * <p>LED patterns are stateless, and as such can be applied to multiple LED strips (or different 057 * sections of the same LED strip, since the roboRIO can only drive a single LED strip). In this 058 * example, we split the single buffer into two views - one for the section of the LED strip on the 059 * left side of a robot, and another view for the section of LEDs on the right side. The same 060 * pattern is able to be applied to both sides. 061 * 062 * <pre><code> 063 * public class LEDs extends SubsystemBase { 064 * private final AddressableLED m_led = new AddressableLED(0); 065 * private final AddressableLEDBuffer m_ledData = new AddressableLEDBuffer(60); 066 * private final AddressableLEDBufferView m_leftData = m_ledData.createView(0, 29); 067 * private final AddressableLEDBufferView m_rightData = m_ledData.createView(30, 59).reversed(); 068 * 069 * public LEDs() { 070 * m_led.setLength(60); 071 * m_led.start(); 072 * } 073 * 074 * {@literal @}Override 075 * public void periodic() { 076 * m_led.writeData(m_ledData); 077 * } 078 * 079 * public Command runPattern(LEDPattern pattern) { 080 * // Use the single input pattern to drive both sides 081 * return runSplitPatterns(pattern, pattern); 082 * } 083 * 084 * public Command runSplitPatterns(LEDPattern left, LEDPattern right) { 085 * return run(() -> { 086 * left.applyTo(m_leftData); 087 * right.applyTo(m_rightData); 088 * }); 089 * } 090 * } 091 * </code></pre> 092 */ 093@FunctionalInterface 094public interface LEDPattern { 095 /** A functional interface for index mapping functions. */ 096 @FunctionalInterface 097 interface IndexMapper { 098 /** 099 * Maps the index. 100 * 101 * @param bufLen Length of the buffer 102 * @param index The index to map 103 * @return The mapped index 104 */ 105 int apply(int bufLen, int index); 106 } 107 108 /** 109 * Writes the pattern to an LED buffer. Dynamic animations should be called periodically (such as 110 * with a command or with a periodic method) to refresh the buffer over time. 111 * 112 * <p>This method is intentionally designed to use separate objects for reading and writing data. 113 * By splitting them up, we can easily modify the behavior of some base pattern to make it {@link 114 * #scrollAtRelativeSpeed(Frequency) scroll}, {@link #blink(Time, Time) blink}, or {@link 115 * #breathe(Time) breathe} by intercepting the data writes to transform their behavior to whatever 116 * we like. 117 * 118 * @param reader data reader for accessing buffer length and current colors 119 * @param writer data writer for setting new LED colors on the buffer 120 */ 121 void applyTo(LEDReader reader, LEDWriter writer); 122 123 /** 124 * Convenience for {@link #applyTo(LEDReader, LEDWriter)} when one object provides both a read and 125 * a write interface. This is most helpful for playing an animated pattern directly on an {@link 126 * AddressableLEDBuffer} for the sake of code clarity. 127 * 128 * <pre><code> 129 * AddressableLEDBuffer data = new AddressableLEDBuffer(120); 130 * LEDPattern pattern = ... 131 * 132 * void periodic() { 133 * pattern.applyTo(data); 134 * } 135 * </code></pre> 136 * 137 * @param readWriter the object to use for both reading and writing to a set of LEDs 138 * @param <T> the type of the object that can both read and write LED data 139 */ 140 default <T extends LEDReader & LEDWriter> void applyTo(T readWriter) { 141 applyTo(readWriter, readWriter); 142 } 143 144 /** 145 * Creates a pattern with remapped indices. 146 * 147 * @param indexMapper the index mapper 148 * @return the mapped pattern 149 */ 150 default LEDPattern mapIndex(IndexMapper indexMapper) { 151 return (reader, writer) -> { 152 int bufLen = reader.getLength(); 153 applyTo( 154 new LEDReader() { 155 @Override 156 public int getLength() { 157 return reader.getLength(); 158 } 159 160 @Override 161 public int getRed(int index) { 162 return reader.getRed(indexMapper.apply(bufLen, index)); 163 } 164 165 @Override 166 public int getGreen(int index) { 167 return reader.getGreen(indexMapper.apply(bufLen, index)); 168 } 169 170 @Override 171 public int getBlue(int index) { 172 return reader.getBlue(indexMapper.apply(bufLen, index)); 173 } 174 }, 175 (i, r, g, b) -> writer.setRGB(indexMapper.apply(bufLen, i), r, g, b)); 176 }; 177 } 178 179 /** 180 * Creates a pattern that displays this one in reverse. Scrolling patterns will scroll in the 181 * opposite direction (but at the same speed). It will treat the end of an LED strip as the start, 182 * and the start of the strip as the end. This can be useful for making ping-pong patterns that 183 * travel from one end of an LED strip to the other, then reverse direction and move back to the 184 * start. This can also be useful when working with LED strips connected in a serpentine pattern 185 * (where the start of one strip is connected to the end of the previous one); however, consider 186 * using a {@link AddressableLEDBufferView#reversed() reversed view} of the overall buffer for 187 * that segment rather than reversing patterns. 188 * 189 * @return the reverse pattern 190 * @see AddressableLEDBufferView#reversed() 191 */ 192 default LEDPattern reversed() { 193 return mapIndex((length, index) -> length - 1 - index); 194 } 195 196 /** 197 * Creates a pattern that plays this one, but offset by a certain number of LEDs. The offset 198 * pattern will wrap around, if necessary. 199 * 200 * @param offset how many LEDs to offset by 201 * @return the offset pattern 202 */ 203 default LEDPattern offsetBy(int offset) { 204 return mapIndex((length, index) -> Math.floorMod(index + offset, length)); 205 } 206 207 /** 208 * Creates a pattern that plays this one scrolling up the buffer. The velocity controls how fast 209 * the pattern returns back to its original position, and is in terms of the length of the LED 210 * strip; scrolling across a segment that is 10 LEDs long will travel twice as fast as on a 211 * segment that's only 5 LEDs long (assuming equal LED density on both segments). 212 * 213 * <p>For example, scrolling a pattern by one quarter of any LED strip's length per second, 214 * regardless of the total number of LEDs on that strip: 215 * 216 * <pre> 217 * LEDPattern rainbow = LEDPattern.rainbow(255, 255); 218 * LEDPattern scrollingRainbow = rainbow.scrollAtRelativeSpeed(Percent.per(Second).of(25)); 219 * </pre> 220 * 221 * @param velocity how fast the pattern should move, in terms of how long it takes to do a full 222 * scroll along the length of LEDs and return back to the starting position 223 * @return the scrolling pattern 224 */ 225 default LEDPattern scrollAtRelativeSpeed(Frequency velocity) { 226 final double periodMicros = velocity.asPeriod().in(Microseconds); 227 228 return mapIndex( 229 (bufLen, index) -> { 230 long now = RobotController.getTime(); 231 232 // index should move by (buf.length) / (period) 233 double t = (now % (long) periodMicros) / periodMicros; 234 int offset = (int) (t * bufLen); 235 236 return Math.floorMod(index + offset, bufLen); 237 }); 238 } 239 240 /** 241 * Creates a pattern that plays this one scrolling up an LED strip. A negative velocity makes the 242 * pattern play in reverse. 243 * 244 * <p>For example, scrolling a pattern at 4 inches per second along an LED strip with 60 LEDs per 245 * meter: 246 * 247 * <pre> 248 * // LEDs per meter, a known value taken from the spec sheet of our particular LED strip 249 * Distance LED_SPACING = Meters.of(1.0 / 60); 250 * 251 * LEDPattern rainbow = LEDPattern.rainbow(); 252 * LEDPattern scrollingRainbow = 253 * rainbow.scrollAtAbsoluteSpeed(InchesPerSecond.of(4), LED_SPACING); 254 * </pre> 255 * 256 * <p>Note that this pattern will scroll <i>faster</i> if applied to a less dense LED strip (such 257 * as 30 LEDs per meter), or <i>slower</i> if applied to a denser LED strip (such as 120 or 144 258 * LEDs per meter). 259 * 260 * @param velocity how fast the pattern should move along a physical LED strip 261 * @param ledSpacing the distance between adjacent LEDs on the physical LED strip 262 * @return the scrolling pattern 263 */ 264 default LEDPattern scrollAtAbsoluteSpeed(LinearVelocity velocity, Distance ledSpacing) { 265 // eg velocity = 10 m/s, spacing = 0.01m 266 // meters per micro = 1e-5 m/us 267 // micros per LED = 1e-2 m / (1e-5 m/us) = 1e-3 us 268 269 var metersPerMicro = velocity.in(Meters.per(Microsecond)); 270 var microsPerLED = (int) (ledSpacing.in(Meters) / metersPerMicro); 271 272 return mapIndex( 273 (bufLen, index) -> { 274 long now = RobotController.getTime(); 275 276 // every step in time that's a multiple of microsPerLED will increment the offset by 1 277 var offset = (int) (now / microsPerLED); 278 279 // floorMod so if the offset is negative, we still get positive outputs 280 return Math.floorMod(index + offset, bufLen); 281 }); 282 } 283 284 /** 285 * Creates a pattern that switches between playing this pattern and turning the entire LED strip 286 * off. 287 * 288 * @param onTime how long the pattern should play for, per cycle 289 * @param offTime how long the pattern should be turned off for, per cycle 290 * @return the blinking pattern 291 */ 292 default LEDPattern blink(Time onTime, Time offTime) { 293 final long totalTimeMicros = (long) (onTime.in(Microseconds) + offTime.in(Microseconds)); 294 final long onTimeMicros = (long) onTime.in(Microseconds); 295 296 return (reader, writer) -> { 297 if (RobotController.getTime() % totalTimeMicros < onTimeMicros) { 298 applyTo(reader, writer); 299 } else { 300 kOff.applyTo(reader, writer); 301 } 302 }; 303 } 304 305 /** 306 * Like {@link #blink(Time, Time) blink(onTime, offTime)}, but where the "off" time is exactly 307 * equal to the "on" time. 308 * 309 * @param onTime how long the pattern should play for (and be turned off for), per cycle 310 * @return the blinking pattern 311 */ 312 default LEDPattern blink(Time onTime) { 313 return blink(onTime, onTime); 314 } 315 316 /** 317 * Creates a pattern that blinks this one on and off in sync with a true/false signal. The pattern 318 * will play while the signal outputs {@code true}, and will turn off while the signal outputs 319 * {@code false}. 320 * 321 * @param signal the signal to synchronize with 322 * @return the blinking pattern 323 */ 324 default LEDPattern synchronizedBlink(BooleanSupplier signal) { 325 return (reader, writer) -> { 326 if (signal.getAsBoolean()) { 327 applyTo(reader, writer); 328 } else { 329 kOff.applyTo(reader, writer); 330 } 331 }; 332 } 333 334 /** 335 * Creates a pattern that brightens and dims this one over time. Brightness follows a sinusoidal 336 * pattern. 337 * 338 * @param period how fast the breathing pattern should complete a single cycle 339 * @return the breathing pattern 340 */ 341 default LEDPattern breathe(Time period) { 342 final long periodMicros = (long) period.in(Microseconds); 343 344 return (reader, writer) -> { 345 applyTo( 346 reader, 347 (i, r, g, b) -> { 348 // How far we are in the cycle, in the range [0, 1) 349 double t = (RobotController.getTime() % periodMicros) / (double) periodMicros; 350 double phase = t * 2 * Math.PI; 351 352 // Apply the cosine function and shift its output from [-1, 1] to [0, 1] 353 // Use cosine so the period starts at 100% brightness 354 double dim = (Math.cos(phase) + 1) / 2.0; 355 356 int output = Color.lerpRGB(0, 0, 0, r, g, b, dim); 357 358 writer.setRGB( 359 i, 360 Color.unpackRGB(output, Color.RGBChannel.kRed), 361 Color.unpackRGB(output, Color.RGBChannel.kGreen), 362 Color.unpackRGB(output, Color.RGBChannel.kBlue)); 363 }); 364 }; 365 } 366 367 /** 368 * Creates a pattern that plays this pattern overlaid on another. Anywhere this pattern sets an 369 * LED to off (or {@link Color#kBlack}), the base pattern will be displayed instead. 370 * 371 * @param base the base pattern to overlay on top of 372 * @return the combined overlay pattern 373 */ 374 default LEDPattern overlayOn(LEDPattern base) { 375 return (reader, writer) -> { 376 // write the base pattern down first... 377 base.applyTo(reader, writer); 378 379 // ... then, overwrite with the illuminated LEDs from the overlay 380 applyTo( 381 reader, 382 (i, r, g, b) -> { 383 if (r != 0 || g != 0 || b != 0) { 384 writer.setRGB(i, r, g, b); 385 } 386 }); 387 }; 388 } 389 390 /** 391 * Creates a pattern that displays outputs as a combination of this pattern and another. Color 392 * values are calculated as the average color of both patterns; if both patterns set the same LED 393 * to the same color, then it is set to that color, but if one pattern sets to one color and the 394 * other pattern sets it to off, then it will show the color of the first pattern but at 395 * approximately half brightness. This is different from {@link #overlayOn}, which will show the 396 * base pattern at full brightness if the overlay is set to off at that position. 397 * 398 * @param other the pattern to blend with 399 * @return the blended pattern 400 */ 401 default LEDPattern blend(LEDPattern other) { 402 return (reader, writer) -> { 403 applyTo(reader, writer); 404 405 other.applyTo( 406 reader, 407 (i, r, g, b) -> { 408 int blendedRGB = 409 Color.lerpRGB( 410 reader.getRed(i), reader.getGreen(i), reader.getBlue(i), r, g, b, 0.5); 411 412 writer.setRGB( 413 i, 414 Color.unpackRGB(blendedRGB, Color.RGBChannel.kRed), 415 Color.unpackRGB(blendedRGB, Color.RGBChannel.kGreen), 416 Color.unpackRGB(blendedRGB, Color.RGBChannel.kBlue)); 417 }); 418 }; 419 } 420 421 /** 422 * Similar to {@link #blend(LEDPattern)}, but performs a bitwise mask on each color channel rather 423 * than averaging the colors for each LED. This can be helpful for displaying only a portion of 424 * the base pattern by applying a mask that sets the desired area to white, and all other areas to 425 * black. However, it can also be used to display only certain color channels or hues; for 426 * example, masking with {@code LEDPattern.color(Color.kRed)} will turn off the green and blue 427 * channels on the output pattern, leaving only the red LEDs to be illuminated. 428 * 429 * @param mask the mask to apply 430 * @return the masked pattern 431 */ 432 default LEDPattern mask(LEDPattern mask) { 433 return (reader, writer) -> { 434 // Apply the current pattern down as normal... 435 applyTo(reader, writer); 436 437 mask.applyTo( 438 reader, 439 (i, r, g, b) -> { 440 // ... then perform a bitwise AND operation on each channel to apply the mask 441 writer.setRGB(i, r & reader.getRed(i), g & reader.getGreen(i), b & reader.getBlue(i)); 442 }); 443 }; 444 } 445 446 /** 447 * Creates a pattern that plays this one, but at a different brightness. Brightness multipliers 448 * are applied per-channel in the RGB space; no HSL or HSV conversions are applied. Multipliers 449 * are also uncapped, which may result in the original colors washing out and appearing less 450 * saturated or even just a bright white. 451 * 452 * <p>This method is predominantly intended for dimming LEDs to avoid painfully bright or 453 * distracting patterns from playing (apologies to the 2024 NE Greater Boston field staff). 454 * 455 * <p>For example, dimming can be done simply by adding a call to `atBrightness` at the end of a 456 * pattern: 457 * 458 * <pre> 459 * // Solid red, but at 50% brightness 460 * LEDPattern.solid(Color.kRed).atBrightness(Percent.of(50)); 461 * 462 * // Solid white, but at only 10% (i.e. ~0.5V) 463 * LEDPattern.solid(Color.kWhite).atBrightness(Percent.of(10)); 464 * </pre> 465 * 466 * @param relativeBrightness the multiplier to apply to all channels to modify brightness 467 * @return the input pattern, displayed at 468 */ 469 default LEDPattern atBrightness(Dimensionless relativeBrightness) { 470 double multiplier = relativeBrightness.in(Value); 471 472 return (reader, writer) -> { 473 applyTo( 474 reader, 475 (i, r, g, b) -> { 476 // Clamp RGB values to keep them in the range [0, 255]. 477 // Otherwise, the casts to byte would result in values like 256 wrapping to 0 478 479 writer.setRGB( 480 i, 481 (int) Math.clamp(r * multiplier, 0, 255), 482 (int) Math.clamp(g * multiplier, 0, 255), 483 (int) Math.clamp(b * multiplier, 0, 255)); 484 }); 485 }; 486 } 487 488 /** A pattern that turns off all LEDs. */ 489 LEDPattern kOff = solid(Color.kBlack); 490 491 /** 492 * Creates a pattern that displays a single static color along the entire length of the LED strip. 493 * 494 * @param color the color to display 495 * @return the pattern 496 */ 497 static LEDPattern solid(Color color) { 498 return (reader, writer) -> { 499 int bufLen = reader.getLength(); 500 for (int led = 0; led < bufLen; led++) { 501 writer.setLED(led, color); 502 } 503 }; 504 } 505 506 /** 507 * Creates a pattern that works as a mask layer for {@link #mask(LEDPattern)} that illuminates 508 * only the portion of the LED strip corresponding with some progress. The mask pattern will start 509 * from the base and set LEDs to white at a proportion equal to the progress returned by the 510 * function. Some usages for this could be for displaying progress of a flywheel to its target 511 * velocity, progress of a complex autonomous sequence, or the height of an elevator. 512 * 513 * <p>For example, creating a mask for displaying a red-to-blue gradient, starting from the red 514 * end, based on where an elevator is in its range of travel. 515 * 516 * <pre> 517 * LEDPattern basePattern = gradient(Color.kRed, Color.kBlue); 518 * LEDPattern progressPattern = 519 * basePattern.mask(progressMaskLayer(() -> elevator.getHeight() / elevator.maxHeight()); 520 * </pre> 521 * 522 * @param progressSupplier the function to call to determine the progress. This should return 523 * values in the range [0, 1]; any values outside that range will be clamped. 524 * @return the mask pattern 525 */ 526 static LEDPattern progressMaskLayer(DoubleSupplier progressSupplier) { 527 return (reader, writer) -> { 528 double progress = Math.clamp(progressSupplier.getAsDouble(), 0, 1); 529 530 int bufLen = reader.getLength(); 531 int max = (int) (bufLen * progress); 532 533 for (int led = 0; led < max; led++) { 534 writer.setLED(led, Color.kWhite); 535 } 536 537 for (int led = max; led < bufLen; led++) { 538 writer.setLED(led, Color.kBlack); 539 } 540 }; 541 } 542 543 /** 544 * Display a set of colors in steps across the length of the LED strip. No interpolation is done 545 * between colors. Colors are specified by the first LED on the strip to show that color. The last 546 * color in the map will be displayed all the way to the end of the strip. LEDs positioned before 547 * the first specified step will be turned off (you can think of this as if there's a 0 -> black 548 * step by default) 549 * 550 * <pre> 551 * // Display red from 0-33%, white from 33% - 67%, and blue from 67% to 100% 552 * steps(Map.of(0.00, Color.kRed, 0.33, Color.kWhite, 0.67, Color.kBlue)) 553 * 554 * // Half off, half on 555 * steps(Map.of(0.5, Color.kWhite)) 556 * </pre> 557 * 558 * @param steps a map of progress to the color to start displaying at that position along the LED 559 * strip 560 * @return a motionless step pattern 561 */ 562 static LEDPattern steps(Map<? extends Number, Color> steps) { 563 if (steps.isEmpty()) { 564 // no colors specified 565 DriverStation.reportWarning("Creating LED steps with no colors!", false); 566 return kOff; 567 } 568 569 if (steps.size() == 1 && steps.keySet().iterator().next().doubleValue() == 0) { 570 // only one color specified, just show a static color 571 DriverStation.reportWarning("Creating LED steps with only one color!", false); 572 return solid(steps.values().iterator().next()); 573 } 574 575 return (reader, writer) -> { 576 int bufLen = reader.getLength(); 577 578 // precompute relevant positions for this buffer so we don't need to do a check 579 // on every single LED index 580 var stopPositions = new LongToObjectHashMap<Color>(); 581 steps.forEach( 582 (progress, color) -> { 583 stopPositions.put((int) Math.floor(progress.doubleValue() * bufLen), color); 584 }); 585 586 Color currentColor = Color.kBlack; 587 for (int led = 0; led < bufLen; led++) { 588 currentColor = Objects.requireNonNullElse(stopPositions.get(led), currentColor); 589 590 writer.setLED(led, currentColor); 591 } 592 }; 593 } 594 595 /** Types of gradients. */ 596 enum GradientType { 597 /** 598 * A continuous gradient, where the gradient wraps around to allow for seamless scrolling 599 * effects. 600 */ 601 kContinuous, 602 603 /** 604 * A discontinuous gradient, where the first pixel is set to the first color of the gradient and 605 * the final pixel is set to the last color of the gradient. There is no wrapping effect, so 606 * scrolling effects will display an obvious seam. 607 */ 608 kDiscontinuous 609 } 610 611 /** 612 * Creates a pattern that displays a non-animated gradient of colors across the entire length of 613 * the LED strip. Colors are evenly distributed along the full length of the LED strip. The 614 * gradient type is configured with the {@code type} parameter, allowing the gradient to be either 615 * continuous (no seams, good for scrolling effects) or discontinuous (a clear seam is visible, 616 * but the gradient applies to the full length of the LED strip without needing to use some space 617 * for wrapping). 618 * 619 * @param type the type of gradient (continuous or discontinuous) 620 * @param colors the colors to display in the gradient 621 * @return a motionless gradient pattern 622 */ 623 static LEDPattern gradient(GradientType type, Color... colors) { 624 if (colors.length == 0) { 625 // Nothing to display 626 DriverStation.reportWarning("Creating a gradient with no colors!", false); 627 return kOff; 628 } 629 630 if (colors.length == 1) { 631 // No gradients with one color 632 DriverStation.reportWarning("Creating a gradient with only one color!", false); 633 return solid(colors[0]); 634 } 635 636 final int numSegments = colors.length; 637 638 return (reader, writer) -> { 639 int bufLen = reader.getLength(); 640 int ledsPerSegment = 641 switch (type) { 642 case kContinuous -> bufLen / numSegments; 643 case kDiscontinuous -> (bufLen - 1) / (numSegments - 1); 644 }; 645 646 for (int led = 0; led < bufLen; led++) { 647 int colorIndex = (led / ledsPerSegment) % numSegments; 648 int nextColorIndex = (colorIndex + 1) % numSegments; 649 double t = (led / (double) ledsPerSegment) % 1; 650 651 Color color = colors[colorIndex]; 652 Color nextColor = colors[nextColorIndex]; 653 int gradientColor = 654 Color.lerpRGB( 655 color.red, 656 color.green, 657 color.blue, 658 nextColor.red, 659 nextColor.green, 660 nextColor.blue, 661 t); 662 663 writer.setRGB( 664 led, 665 Color.unpackRGB(gradientColor, Color.RGBChannel.kRed), 666 Color.unpackRGB(gradientColor, Color.RGBChannel.kGreen), 667 Color.unpackRGB(gradientColor, Color.RGBChannel.kBlue)); 668 } 669 }; 670 } 671 672 /** 673 * Creates an LED pattern that displays a rainbow across the color wheel. The rainbow pattern will 674 * stretch across the entire length of the LED strip. 675 * 676 * @param saturation the saturation of the HSV colors, in [0, 255] 677 * @param value the value of the HSV colors, in [0, 255] 678 * @return the rainbow pattern 679 */ 680 static LEDPattern rainbow(int saturation, int value) { 681 return (reader, writer) -> { 682 int bufLen = reader.getLength(); 683 for (int i = 0; i < bufLen; i++) { 684 int hue = ((i * 180) / bufLen) % 180; 685 writer.setHSV(i, hue, saturation, value); 686 } 687 }; 688 } 689}