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}