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