/* A dot animation that travels along rails (C) Voidstar Lab LLC 2021 */ #ifndef RIPPLE_H_ #define RIPPLE_H_ // WARNING: These slow things down enough to affect performance. Don't turn on unless you need them! //#define DEBUG_ADVANCEMENT // Print debug messages about ripples' movement //#define DEBUG_RENDERING // Print debug messages about translating logical to actual position #include "FastLED.h" #include "mapping.h" enum rippleState { dead, withinNode, // Ripple isn't drawn as it passes through a node to keep the speed consistent travelingUpwards, travelingDownwards }; enum rippleBehavior { weaksauce = 0, feisty = 1, angry = 2, alwaysTurnsRight = 3, alwaysTurnsLeft = 4 }; float fmap(float x, float in_min, float in_max, float out_min, float out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } class Ripple { public: Ripple(int id) : rippleId(id) { Serial.print("Instanced ripple #"); Serial.println(rippleId); } rippleState state = dead; unsigned long color; /* If within a node: 0 is node, 1 is direction If traveling, 0 is segment, 1 is LED position from bottom */ int position[2]; // Place the Ripple in a node void start(byte n, byte d, unsigned long c, float s, unsigned long l, byte b) { color = c; speed = s; lifespan = l; behavior = b; birthday = millis(); pressure = 0; state = withinNode; position[0] = n; position[1] = d; justStarted = true; Serial.print("Ripple "); Serial.print(rippleId); Serial.print(" starting at node "); Serial.print(position[0]); Serial.print(" direction "); Serial.println(position[1]); } void advance(byte ledColors[40][14][3]) { unsigned long age = millis() - birthday; if (state == dead) return; pressure += fmap(float(age), 0.0, float(lifespan), speed, 0.0); // Ripple slows down as it ages // TODO: Motion of ripple is severely affected by loop speed. Make it time invariant if (pressure < 1 && (state == travelingUpwards || state == travelingDownwards)) { // Ripple is visible but hasn't moved - render it to avoid flickering renderLed(ledColors, age); } while (pressure >= 1) { #ifdef DEBUG_ADVANCEMENT Serial.print("Ripple "); Serial.print(rippleId); Serial.println(" advancing:"); #endif switch (state) { case withinNode: { if (justStarted) { justStarted = false; } else { #ifdef DEBUG_ADVANCEMENT Serial.print(" Picking direction out of node "); Serial.print(position[0]); Serial.print(" with agr. "); Serial.println(behavior); #endif int newDirection = -1; int sharpLeft = (position[1] + 1) % 6; int wideLeft = (position[1] + 2) % 6; int forward = (position[1] + 3) % 6; int wideRight = (position[1] + 4) % 6; int sharpRight = (position[1] + 5) % 6; if (behavior <= 2) { // Semi-random aggressive turn mode // The more aggressive a ripple, the tighter turns it wants to make. // If there aren't any segments it can turn to, we need to adjust its behavior. byte anger = behavior; while (newDirection < 0) { if (anger == 0) { int forwardConnection = nodeConnections[position[0]][forward]; if (forwardConnection < 0) { // We can't go straight ahead - we need to take a more aggressive angle #ifdef DEBUG_ADVANCEMENT Serial.println(" Can't go straight - picking more agr. path"); #endif anger++; } else { #ifdef DEBUG_ADVANCEMENT Serial.println(" Going forward"); #endif newDirection = forward; } } if (anger == 1) { int leftConnection = nodeConnections[position[0]][wideLeft]; int rightConnection = nodeConnections[position[0]][wideRight]; if (leftConnection >= 0 && rightConnection >= 0) { #ifdef DEBUG_ADVANCEMENT Serial.println(" Turning left or right at random"); #endif newDirection = random(2) ? wideLeft : wideRight; } else if (leftConnection >= 0) { #ifdef DEBUG_ADVANCEMENT Serial.println(" Can only turn left"); #endif newDirection = wideLeft; } else if (rightConnection >= 0) { #ifdef DEBUG_ADVANCEMENT Serial.println(" Can only turn right"); #endif newDirection = wideRight; } else { #ifdef DEBUG_ADVANCEMENT Serial.println(" Can't make wide turn - picking more agr. path"); #endif anger++; // Can't take shallow turn - must become more aggressive } } if (anger == 2) { int leftConnection = nodeConnections[position[0]][sharpLeft]; int rightConnection = nodeConnections[position[0]][sharpRight]; if (leftConnection >= 0 && rightConnection >= 0) { #ifdef DEBUG_ADVANCEMENT Serial.println(" Turning left or right at random"); #endif newDirection = random(2) ? sharpLeft : sharpRight; } else if (leftConnection >= 0) { #ifdef DEBUG_ADVANCEMENT Serial.println(" Can only turn left"); #endif newDirection = sharpLeft; } else if (rightConnection >= 0) { #ifdef DEBUG_ADVANCEMENT Serial.println(" Can only turn right"); #endif newDirection = sharpRight; } else { #ifdef DEBUG_ADVANCEMENT Serial.println(" Can't make tight turn - picking less agr. path"); #endif anger--; // Can't take tight turn - must become less aggressive } } // Note that this can't handle some circumstances, // like a node with segments in nothing but the 0 and 3 positions. // Good thing we don't have any of those! } } else if (behavior == alwaysTurnsRight) { for (int i = 1; i < 6; i++) { int possibleDirection = (position[1] + i) % 6; if (nodeConnections[position[0]][possibleDirection] >= 0) { newDirection = possibleDirection; break; } } #ifdef DEBUG_ADVANCEMENT Serial.println(" Turning as rightward as possible"); #endif } else if (behavior == alwaysTurnsLeft) { for (int i = 5; i >= 1; i--) { int possibleDirection = (position[1] + i) % 6; if (nodeConnections[position[0]][possibleDirection] >= 0) { newDirection = possibleDirection; break; } } #ifdef DEBUG_ADVANCEMENT Serial.println(" Turning as leftward as possible"); #endif } #ifdef DEBUG_ADVANCEMENT Serial.print(" Leaving node "); Serial.print(position[0]); Serial.print(" in direction "); Serial.println(newDirection); #endif position[1] = newDirection; } position[0] = nodeConnections[position[0]][position[1]]; // Look up which segment we're on #ifdef DEBUG_ADVANCEMENT Serial.print(" and entering segment "); Serial.println(position[0]); #endif if (position[1] == 5 || position[1] == 0 || position[1] == 1) { // Top half of the node #ifdef DEBUG_ADVANCEMENT Serial.println(" (starting at bottom)"); #endif state = travelingUpwards; position[1] = 0; // Starting at bottom of segment } else { #ifdef DEBUG_ADVANCEMENT Serial.println(" (starting at top)"); #endif state = travelingDownwards; position[1] = 13; // Starting at top of 14-LED-long strip } break; } case travelingUpwards: { position[1]++; if (position[1] >= 14) { // We've reached the top! #ifdef DEBUG_ADVANCEMENT Serial.print(" Reached top of seg. "); Serial.println(position[0]); #endif // Enter the new node. int segment = position[0]; position[0] = segmentConnections[position[0]][0]; for (int i = 0; i < 6; i++) { // Figure out from which direction the ripple is entering the node. // Allows us to exit in an appropriately aggressive direction. int incomingConnection = nodeConnections[position[0]][i]; if (incomingConnection == segment) position[1] = i; } #ifdef DEBUG_ADVANCEMENT Serial.print(" Entering node "); Serial.print(position[0]); Serial.print(" from direction "); Serial.println(position[1]); #endif state = withinNode; } else { #ifdef DEBUG_ADVANCEMENT Serial.print(" Moved up to seg. "); Serial.print(position[0]); Serial.print(" LED "); Serial.println(position[1]); #endif } break; } case travelingDownwards: { position[1]--; if (position[1] < 0) { // We've reached the bottom! #ifdef DEBUG_ADVANCEMENT Serial.print(" Reached bottom of seg. "); Serial.println(position[0]); #endif // Enter the new node. int segment = position[0]; position[0] = segmentConnections[position[0]][1]; for (int i = 0; i < 6; i++) { // Figure out from which direction the ripple is entering the node. // Allows us to exit in an appropriately aggressive direction. int incomingConnection = nodeConnections[position[0]][i]; if (incomingConnection == segment) position[1] = i; } #ifdef DEBUG_ADVANCEMENT Serial.print(" Entering node "); Serial.print(position[0]); Serial.print(" from direction "); Serial.println(position[1]); #endif state = withinNode; } else { #ifdef DEBUG_ADVANCEMENT Serial.print(" Moved down to seg. "); Serial.print(position[0]); Serial.print(" LED "); Serial.println(position[1]); #endif } break; } default: break; } pressure -= 1; if (state == travelingUpwards || state == travelingDownwards) { // Ripple is visible - render it renderLed(ledColors, age); } } #ifdef DEBUG_ADVANCEMENT Serial.print(" Age is now "); Serial.print(age); Serial.print('/'); Serial.println(lifespan); #endif if (lifespan && age >= lifespan) { // We dead #ifdef DEBUG_ADVANCEMENT Serial.println(" Lifespan is up! Ripple is dead."); #endif state = dead; position[0] = position[1] = pressure = age = 0; } } private: float speed; // Each loop, ripples move this many LED's. unsigned long lifespan; // The ripple stops after this many milliseconds /* 0: Always goes straight ahead if possible 1: Can take 60-degree turns 2: Can take 120-degree turns */ byte behavior; bool justStarted = false; float pressure; // When Pressure reaches 1, ripple will move unsigned long birthday; // Used to track age of ripple static byte rippleCount; // Used to give them unique ID's byte rippleId; // Used to identify this ripple in debug output void renderLed(byte ledColors[40][14][3], unsigned long age) { int strip = ledAssignments[position[0]][0]; int led = ledAssignments[position[0]][2] + position[1]; int red = ledColors[position[0]][position[1]][0]; int green = ledColors[position[0]][position[1]][1]; int blue = ledColors[position[0]][position[1]][2]; ledColors[position[0]][position[1]][0] = byte(min(255, max(0, int(fmap(float(age), 0.0, float(lifespan), (color >> 8) & 0xFF, 0.0)) + red))); ledColors[position[0]][position[1]][1] = byte(min(255, max(0, int(fmap(float(age), 0.0, float(lifespan), (color >> 16) & 0xFF, 0.0)) + green))); ledColors[position[0]][position[1]][2] = byte(min(255, max(0, int(fmap(float(age), 0.0, float(lifespan), color & 0xFF, 0.0)) + blue))); #ifdef DEBUG_RENDERING Serial.print("Rendering ripple position ("); Serial.print(position[0]); Serial.print(','); Serial.print(position[1]); Serial.print(") at Strip "); Serial.print(strip); Serial.print(", LED "); Serial.print(led); Serial.print(", color 0x"); for (int i = 0; i < 3; i++) { if (ledColors[position[0]][position[1]][i] <= 0x0F) Serial.print('0'); Serial.print(ledColors[position[0]][position[1]][i], HEX); } Serial.println(); #endif } }; #endif