#include <FastLED.h>
#include "shapes.h"
////////////////////////////////////////////////////////////
// Pin and LED definitions
////////////////////////////////////////////////////////////
#define LED_PIN_STRIP_1 4
#define LED_PIN_STRIP_2 5
#define LED_PIN_STRIP_3 6
#define LED_PIN_STRIP_4 7
#define LED_PIN_STRIP_5 8
#define LED_PIN_STRIP_6 9
#define LED_PIN_STRIP_7 10
#define LED_PIN_TAIL_1 2
#define LED_PIN_TAIL_2 3
#define SMALL_RING_LEDS 28
#define BASE_RING_LEDS 42
#define LARGE_RING_LEDS 51
#define LED_GROUPS 7
#define LEDS_IN_TAIL 50
const uint16_t ledsInGroup = SMALL_RING_LEDS + BASE_RING_LEDS + LARGE_RING_LEDS;
CRGB leds_1[121];
CRGB leds_2[121];
CRGB leds_3[121];
CRGB leds_4[121];
CRGB leds_5[121];
CRGB leds_6[121];
CRGB leds_7[121];
CRGB leds_tail_1[LEDS_IN_TAIL];
CRGB leds_tail_2[LEDS_IN_TAIL];
CRGB* ledGroups[LED_GROUPS] = { leds_1, leds_2, leds_3, leds_4, leds_5, leds_6, leds_7 };
////////////////////////////////////////////////////////////
// Global Animation State
////////////////////////////////////////////////////////////
const Animation* currentAnimSet = nullptr;
uint8_t currentAnimIndex = 0;
uint8_t currentFrame = 0;
uint8_t nextFrame = 0;
uint8_t framesInCurrentAnim = 0;
static int currentMode = -1;
static int desiredMode = -1;
static int sensor1 = 0, sensor2 = 0;
const uint8_t MORPH_STEPS = 20; // how many steps in each frame blend
uint8_t morphStep = 0;
bool morphInProgress = false;
uint16_t morphIntervalMs;
float currentGlobalAlpha = 0;
static uint16_t morphID = 0;
bool reverseAnimation = false;
const Animation* transitionAnimFrom = nullptr;
uint8_t transitionFrameFrom = 0;
const Animation* transitionAnimTo = nullptr;
uint8_t transitionMorphStep;
bool transitionInProgress = false;
uint16_t transitionDurationMs = 0;
uint8_t nextAnimIndex = 0;
// Delay
bool frameDelayActive = false;
uint32_t frameDelayStartTime = 0;
uint16_t frameDelayRandom = 0;
const uint16_t frameDelay = 1500;
// Hold
// Gradients
DEFINE_GRADIENT_PALETTE(NorthernLightsPalette) {
0, 0, 207, 82,
62, 3, 46, 62,
143, 25, 100, 106,
192, 0, 198, 144,
255, 0, 223, 150
};
DEFINE_GRADIENT_PALETTE(WarmNorthernLightsPalette) {
0, 207, 34, 44,
61, 233, 160, 58,
130, 219, 61, 49,
187, 233, 160, 58,
255, 26, 26, 26
};
CRGBPalette16 northernLightsPal = NorthernLightsPalette;
CRGBPalette16 warmNorthernLightsPal = WarmNorthernLightsPalette;
// Noise variables
uint16_t noiseX = 0;
const uint8_t NOISE_SCALE = 50; // Adjust noise granularity
const uint8_t NOISE_SPEED = 20; // Adjust noise movement speed
const uint8_t NOISE_BLEND = 150; // 25% blend (0-255)
const uint8_t DUAL_NOISE_BLEND = 0;
// You can adjust these as desired
static uint16_t tailOffset1 = 0;
static uint16_t tailOffset2 = 0;
// Noise parameters for the tails (you can reuse NOISE_SCALE, NOISE_SPEED, etc. if you like)
const uint8_t TAIL_NOISE_SCALE = 60; // adjust granularity
const uint8_t TAIL_NOISE_SPEED = 10; // how fast the noise flows
////////////////////////////////////////////////////////////
// Get color from current frame
////////////////////////////////////////////////////////////
// Example 16-bit -> 8-bit hash
// (Adapted from public domain, or you can roll your own.)
static uint8_t simpleHash8(uint16_t x) {
x ^= (x >> 4);
x *= 0x27d4; // some prime
x ^= (x >> 5);
return (uint8_t)x;
}
CRGB getColorInAnimationFrame(const Animation& anim, uint8_t frameIndex, uint8_t group, uint16_t led)
{
if (frameIndex >= anim.numFrames) return CRGB::Black;
const Shape &shape = anim.frames[frameIndex];
for (uint8_t i = 0; i < shape.numSegments; i++) {
const LedSegment &seg = shape.segments[i];
if (seg.group == group && led >= seg.startIndex && led <= seg.endIndex) {
// Convert to RGB first
CRGB rgb;
hsv2rgb_rainbow(seg.color, rgb);
// Generate noise color (same for all channels for cohesion)
uint16_t noise = inoise8(led * NOISE_SCALE - noiseX, 0);
CRGB brightNoiseColor = ColorFromPalette(northernLightsPal, noise);
CRGB warmNoiseColor = ColorFromPalette(warmNorthernLightsPal, noise);
CRGB noiseColor = blend(brightNoiseColor, warmNoiseColor, DUAL_NOISE_BLEND);
// Simple blend between base color and noise
return blend(rgb, noiseColor, NOISE_BLEND);
}
}
return CRGB::Black;
}
////////////////////////////////////////////////////////////
// Blends from frameFrom to frameTo by "alpha" (0.0-1.0)
////////////////////////////////////////////////////////////
/* void blendAnimationFrames(const Animation& anim, uint8_t frameFrom, uint8_t frameTo)
{
for (uint8_t g = 0; g < LED_GROUPS; g++) {
for (uint16_t led = 0; led < ledsInGroup; led++) {
CRGB colorA = getColorInAnimationFrame(anim, frameFrom, g, led);
CRGB colorB = getColorInAnimationFrame(anim, frameTo, g, led);
// Randomly decide which frame's color to use based on alpha
if (random8() < (uint8_t)(currentGlobalAlpha * 255)) {
ledGroups[g][led] = colorB; // Use color from frameTo
} else {
ledGroups[g][led] = colorA; // Use color from frameFrom
}
}
}
} */
void blendAnimationFrames(const Animation& anim, uint8_t frameFrom, uint8_t frameTo)
{
for (uint8_t g = 0; g < LED_GROUPS; g++) {
for (uint16_t led = 0; led < ledsInGroup; led++) {
CRGB colorA = getColorInAnimationFrame(anim, frameFrom, g, led);
CRGB colorB = getColorInAnimationFrame(anim, frameTo, g, led);
uint8_t blendAmount = (uint8_t)(currentGlobalAlpha * 255);
CRGB crossFaded = blend(colorA, colorB, blendAmount);
ledGroups[g][led] = crossFaded;
}
}
}
/* void blendAnimationFrames(const Animation& anim, uint8_t frameFrom, uint8_t frameTo)
{
// if up to 1.0, some LEDs can start near the end
const float maxShift = 0.75f; // try 0.5 or so to avoid super abrupt changes
for (uint8_t g = 0; g < LED_GROUPS; g++) {
for (uint16_t led = 0; led < ledsInGroup; led++) {
// Base colors
CRGB colorA = getColorInAnimationFrame(anim, frameFrom, g, led);
CRGB colorB = getColorInAnimationFrame(anim, frameTo, g, led);
// 1) Determine a pseudo-random shift [0..maxShift]
uint16_t seed = (uint16_t)(g * 37 + led * 13 + morphID * 17);
uint8_t rnd = simpleHash8(seed);
float rnd01 = rnd / 255.0f;
float shift = rnd01 * maxShift; // in [0..maxShift]
// 2) Map globalAlpha [0..1] into localAlpha, offset by shift
// so the LED only starts morphing after globalAlpha >= shift.
float localTimeRaw = (currentGlobalAlpha - shift) / (1.0f - shift);
// clamp to [0..1]
if (localTimeRaw < 0.0f) localTimeRaw = 0.0f;
if (localTimeRaw > 1.0f) localTimeRaw = 1.0f;
// optional: apply an easing function
float localAlpha = easeInOutCubic(localTimeRaw);
// Convert [0..1] => [0..255] for blend()
uint8_t blendAmount = (uint8_t)(localAlpha * 255);
ledGroups[g][led] = blend(colorA, colorB, blendAmount);
}
}
} */
////////////////////////////////////////////////////////////
// Keep current frame active (to not stop noise)
////////////////////////////////////////////////////////////
void playCurrentFrame() {
for (uint8_t g = 0; g < LED_GROUPS; g++) {
for (uint16_t led = 0; led < ledsInGroup; led++) {
ledGroups[g][led] = getColorInAnimationFrame(currentAnimSet[currentAnimIndex], nextFrame, g, led);
}
}
}
////////////////////////////////////////////////////////////
// Initializes a new morph process
////////////////////////////////////////////////////////////
/* void startMorph(const Animation& anim, uint8_t fromFrame, uint8_t toFrame, uint16_t durationMs)
{
morphStep = 0;
currentGlobalAlpha = 0;
morphInProgress = true;
morphIntervalMs = durationMs / MORPH_STEPS;
blendAnimationFrames(anim, fromFrame, toFrame);
} */
void startMorph(const Animation& anim, uint8_t fromFrame, uint8_t toFrame, uint16_t durationMs)
{
/* morphID++; */
morphStep = 0;
morphInProgress = true;
morphIntervalMs = durationMs / MORPH_STEPS;
currentGlobalAlpha = 0;
// Immediately show the first blend with new offsets
blendAnimationFrames(anim, fromFrame, toFrame);
}
////////////////////////////////////////////////////////////
// Call this frequently; does 1 step if it's time
////////////////////////////////////////////////////////////
void updateMorph()
{
static uint32_t lastMorphMs = 0;
uint32_t now = millis();
if (!morphInProgress) return;
if (now - lastMorphMs >= morphIntervalMs) {
lastMorphMs = now;
morphStep++;
if (morphStep > MORPH_STEPS) {
morphInProgress = false;
} else {
const Animation& anim = currentAnimSet[currentAnimIndex];
currentGlobalAlpha = (float)morphStep / (float)MORPH_STEPS;
blendAnimationFrames(anim, currentFrame, nextFrame);
}
}
}
////////////////////////////////////////////////////////////
// Decide nextFrame based on reverseAnimation or not
////////////////////////////////////////////////////////////
uint8_t pickNextFrame(uint8_t current, uint8_t totalFrames, bool reverseFlag)
{
if (!reverseFlag) {
return (current + 1) % totalFrames;
}
static int8_t direction = 1;
int next = (int)current + direction;
if (next >= totalFrames) {
next = totalFrames - 2;
direction = -1;
} else if (next < 0) {
next = 1;
direction = 1;
}
return (uint8_t)next;
}
////////////////////////////////////////////////////////////
// Switch to idle, partly, or active if needed
////////////////////////////////////////////////////////////
void setAnimationSet(const Animation* newSet, uint8_t index = 0) {
if (newSet == currentAnimSet || morphInProgress) return;
// Cancel ongoing transition if any
if (transitionInProgress) {
currentAnimSet = transitionAnimTo;
currentAnimIndex = nextAnimIndex;
transitionInProgress = false;
}
// Setup transition
transitionAnimFrom = currentAnimSet;
transitionFrameFrom = nextFrame;
transitionAnimTo = newSet;
nextAnimIndex = index;
transitionMorphStep = 0;
transitionInProgress = true;
currentGlobalAlpha = 0.0;
// Show the first transition frame
crossfadeAnimations(*transitionAnimFrom, transitionFrameFrom, *transitionAnimTo, 0);
}
////////////////////////////////////////////////////////////
// Crossfade between animation sets
////////////////////////////////////////////////////////////
void crossfadeAnimations(const Animation& animFrom, uint8_t frameFrom,
const Animation& animTo, uint8_t frameTo) {
for (uint8_t g = 0; g < LED_GROUPS; g++) {
for (uint16_t led = 0; led < ledsInGroup; led++) {
CRGB colorA = getColorInAnimationFrame(animFrom, frameFrom, g, led);
CRGB colorB = getColorInAnimationFrame(animTo, frameTo, g, led);
uint8_t blendAmount = (uint8_t)(currentGlobalAlpha * 255);
ledGroups[g][led] = blend(colorA, colorB, blendAmount);
}
}
}
////////////////////////////////////////////////////////////
// Update transition between animetion sets
////////////////////////////////////////////////////////////
void updateTransition() {
static uint32_t lastTransitionMs = 0;
uint32_t now = millis();
if (!transitionInProgress) return;
if (now - lastTransitionMs >= (transitionDurationMs / MORPH_STEPS)) {
lastTransitionMs = now;
transitionMorphStep++;
if (transitionMorphStep > MORPH_STEPS) {
// Transition complete
currentAnimSet = transitionAnimTo;
currentAnimIndex = nextAnimIndex;
currentFrame = 0;
nextFrame = pickNextFrame(currentFrame, currentAnimSet[currentAnimIndex].numFrames,
currentAnimSet[currentAnimIndex].reverse);
transitionInProgress = false;
morphInProgress = false; // Reset morph state
} else {
currentGlobalAlpha = (float)transitionMorphStep / MORPH_STEPS;
crossfadeAnimations(*transitionAnimFrom, transitionFrameFrom,
*transitionAnimTo, 0);
}
}
}
////////////////////////////////////////////////////////////
// Mouthpiece functionality
////////////////////////////////////////////////////////////
CRGB getTailNoiseColor(uint16_t position, uint16_t offset)
{
uint16_t noiseValue = inoise8(position * TAIL_NOISE_SCALE - offset, 0);
CRGB brightNoiseColor = ColorFromPalette(northernLightsPal, noiseValue);
CRGB warmNoiseColor = ColorFromPalette(warmNorthernLightsPal, noiseValue);
CRGB noiseColor = blend(brightNoiseColor, warmNoiseColor, DUAL_NOISE_BLEND);
CHSV baseHSV = transitionInProgress ? transitionAnimTo[0]
.frames[0]
.segments[0]
.color : currentAnimSet[currentAnimIndex]
.frames[currentFrame]
.segments[0]
.color;
CRGB baseRGB;
hsv2rgb_rainbow(baseHSV, baseRGB);
return blend(baseRGB, noiseColor, NOISE_BLEND);
}
void updateSingleTail(CRGB* tailLeds, uint16_t numLeds,
int sensorValue, uint16_t &offset)
{
// threshold
const int blowThreshold = 50;
bool isBlowing = (sensorValue > blowThreshold);
// Increase or decrease offset depending on blow input
offset = isBlowing ? offset - TAIL_NOISE_SPEED : offset + TAIL_NOISE_SPEED;
const uint16_t fadeEndIndex = (numLeds > 10) ? 10 : numLeds;
for (uint16_t i = 0; i < numLeds; i++) {
CRGB color = getTailNoiseColor(i, offset);
if (i < fadeEndIndex) {
float fadeFactor = float(i) / 10.0;
color.nscale8(uint8_t(fadeFactor * 255)); // Scale brightness
}
if (transitionInProgress) {
color.nscale8(uint8_t(currentGlobalAlpha * 255));
}
tailLeds[i] = color;
}
}
////////////////////////////////////////////////////////////
// Helper functions
////////////////////////////////////////////////////////////
float easeInOutCubic(float x) {
// x in [0..1]
return (x < 0.5f)
? 4.0f * x * x * x // first half
: 1.0f - powf(-2.0f * x + 2.0f, 3) / 2.0f; // second half
}
// Let’s define a helper boolean:
bool isPartlyOrActive(int mode) {
return (mode == 1 || mode == 2);
}
////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////
void setup() {
Serial.begin(9600);
FastLED.addLeds<WS2812, LED_PIN_STRIP_1, GRB>(leds_1, ledsInGroup);
FastLED.addLeds<WS2812, LED_PIN_STRIP_2, GRB>(leds_2, ledsInGroup);
FastLED.addLeds<WS2812, LED_PIN_STRIP_3, GRB>(leds_3, ledsInGroup);
FastLED.addLeds<WS2812, LED_PIN_STRIP_4, GRB>(leds_4, ledsInGroup);
FastLED.addLeds<WS2812, LED_PIN_STRIP_5, GRB>(leds_5, ledsInGroup);
FastLED.addLeds<WS2812, LED_PIN_STRIP_6, GRB>(leds_6, ledsInGroup);
FastLED.addLeds<WS2812, LED_PIN_STRIP_7, GRB>(leds_7, ledsInGroup);
FastLED.addLeds<WS2812, LED_PIN_TAIL_1, GRB>(leds_tail_1, LEDS_IN_TAIL);
FastLED.addLeds<WS2812, LED_PIN_TAIL_2, GRB>(leds_tail_2, LEDS_IN_TAIL);
setAnimationSet(idleAnimations, 0);
sensor1 = analogRead(A0);
sensor2 = analogRead(A1);
}
////////////////////////////////////////////////////////////
// Loop
////////////////////////////////////////////////////////////
void loop() {
sensor1 = analogRead(A0);
sensor2 = analogRead(A1);
EVERY_N_MILLISECONDS(20) {
noiseX += NOISE_SPEED;
updateSingleTail(leds_tail_1, LEDS_IN_TAIL, sensor1, tailOffset1);
updateSingleTail(leds_tail_2, LEDS_IN_TAIL, sensor2, tailOffset2);
}
// Decide newMode from sensors
int diff = abs(sensor1 - sensor2);
int newMode = -1;
if (sensor1 < 50 && sensor2 < 50) {
newMode = 0; // idle
} else if (sensor1 > 50 && sensor2 > 50 && diff < 25) {
newMode = 2; // active
} else {
newMode = 1; // partly
}
if (newMode != desiredMode) {
desiredMode = newMode;
}
// Continue with ring morphing / transitions
if (transitionInProgress) {
updateTransition();
} else {
if (!morphInProgress && !frameDelayActive) {
const Animation& anim = currentAnimSet[currentAnimIndex];
nextFrame = pickNextFrame(currentFrame, anim.numFrames, anim.reverse);
startMorph(anim, currentFrame, nextFrame, 200);
} else {
playCurrentFrame();
}
updateMorph();
if (!morphInProgress && !frameDelayActive) {
frameDelayActive = true;
frameDelayStartTime = millis();
}
if (frameDelayActive && (millis() - frameDelayStartTime >= currentAnimSet[currentAnimIndex].frameDelay + frameDelayRandom)) {
frameDelayActive = false;
currentFrame = nextFrame;
}
}
if (!transitionInProgress && !morphInProgress) {
bool currentIsPartlyOrActive = isPartlyOrActive(currentMode);
if (currentIsPartlyOrActive) {
if (currentFrame == 0 && (desiredMode != currentMode)) {
currentMode = desiredMode;
switch (currentMode) {
case 0: setAnimationSet(idleAnimations, 0); break;
case 1: setAnimationSet(partlyAnimations, 0); break;
case 2: setAnimationSet(activeAnimations, 0); break;
}
}
} else {
if (desiredMode != currentMode) {
currentMode = desiredMode;
switch (currentMode) {
case 0: setAnimationSet(idleAnimations, 0); break;
case 1: setAnimationSet(partlyAnimations, 0); break;
case 2: setAnimationSet(activeAnimations, 0); break;
}
}
}
}
FastLED.show();
}