diff --git a/.github/workflows/gridlights-build.yml b/.github/workflows/gridlights-build.yml new file mode 100644 index 0000000000..e9c147aac9 --- /dev/null +++ b/.github/workflows/gridlights-build.yml @@ -0,0 +1,62 @@ +name: GridLights Build + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + name: Build (${{ matrix.environment }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: + - esp32dev + # Add more GridLights target environments here as needed + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO toolchains and packages + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-platformio-${{ hashFiles('platformio.ini') }} + restore-keys: | + ${{ runner.os }}-platformio- + + - name: Cache .pio build directory + uses: actions/cache@v4 + with: + path: .pio + key: ${{ runner.os }}-pio-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'wled00/**') }} + restore-keys: | + ${{ runner.os }}-pio-${{ matrix.environment }}- + + - name: Install PlatformIO + run: pip install -r requirements.txt + + - name: Build ${{ matrix.environment }} + run: pio run -e ${{ matrix.environment }} + + - name: Upload firmware + uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.environment }} + path: .pio/build/${{ matrix.environment }}/firmware.bin + if-no-files-found: error diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000000..5e11487ecd --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,41 @@ +name: Version Check + +on: + pull_request: + branches: + - main + +jobs: + check-version-bump: + name: GRIDLIGHTS_VERSION bumped + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract version on PR branch + id: pr_version + run: | + VERSION=$(grep GRIDLIGHTS_VERSION platformio_override.ini | grep -oP '[0-9]+\.[0-9]+\.[0-9]+') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Extract version on main + id: main_version + run: | + VERSION=$(git show origin/main:platformio_override.ini | grep GRIDLIGHTS_VERSION | grep -oP '[0-9]+\.[0-9]+\.[0-9]+') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Fail if version not bumped + run: | + PR="${{ steps.pr_version.outputs.version }}" + MAIN="${{ steps.main_version.outputs.version }}" + echo "main: $MAIN" + echo "PR: $PR" + if [ "$PR" = "$MAIN" ]; then + echo "❌ GRIDLIGHTS_VERSION ($MAIN) must be bumped before merging." + echo " Update GRIDLIGHTS_VERSION in platformio_override.ini." + exit 1 + fi + echo "✅ Version bumped: $MAIN → $PR" diff --git a/.github/workflows/wled-ci.yml b/.github/workflows/wled-ci.yml deleted file mode 100644 index 2b599e6f66..0000000000 --- a/.github/workflows/wled-ci.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: PlatformIO CI - -on: [push, pull_request] - -jobs: - - get_default_envs: - name: Gather Environments - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Cache pip - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - name: Install PlatformIO - run: pip install -r requirements.txt - - name: Get default environments - id: envs - run: | - echo "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT - outputs: - environments: ${{ steps.envs.outputs.environments }} - - - build: - name: Build Enviornments - runs-on: ubuntu-latest - needs: get_default_envs - strategy: - fail-fast: false - matrix: - environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} - steps: - - uses: actions/checkout@v3 - - name: Cache pip - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - name: Cache PlatformIO - uses: actions/cache@v3 - with: - path: ~/.platformio - key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - name: Install PlatformIO - run: pip install -r requirements.txt - - name: Build firmware - env: - WLED_RELEASE: True - run: pio run -e ${{ matrix.environment }} - - uses: actions/upload-artifact@v2 - with: - name: firmware-${{ matrix.environment }} - path: | - build_output/firmware/*.bin - build_output/firmware/*.gz - - uses: actions/upload-artifact@v2 - if: startsWith(github.ref, 'refs/tags/') - with: - name: firmware-release - path: build_output/release/*.bin - release: - name: Create Release - runs-on: ubuntu-latest - needs: [get_default_envs, build] - if: startsWith(github.ref, 'refs/tags/') - steps: - - uses: actions/download-artifact@v2 - with: - name: firmware-release - - name: Create draft release - uses: softprops/action-gh-release@v1 - with: - draft: True - files: | - *.bin - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c85fae0c22..b687696b95 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ .vscode esp01-update.sh -platformio_override.ini replace_fs.py wled-update.sh diff --git a/firmware.bin b/firmware.bin new file mode 100644 index 0000000000..1c5671afd6 Binary files /dev/null and b/firmware.bin differ diff --git a/platformio.ini b/platformio.ini index 2ef4ae01bf..def3a93658 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,7 +11,7 @@ # CI binaries ; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth # ESP32 variant builds are temporarily excluded from CI due to toolchain issues on the GitHub Actions Linux environment -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi +; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi # Release binaries ; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB @@ -30,7 +30,7 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_compat, esp8266_2 ; default_envs = h803wf ; default_envs = d1_mini_debug ; default_envs = d1_mini_ota -; default_envs = esp32dev +default_envs = esp32dev ; default_envs = esp8285_4CH_MagicHome ; default_envs = esp8285_H801 ; default_envs = d1_mini_5CH_Shojo_PCB diff --git a/platformio_override.ini b/platformio_override.ini new file mode 100644 index 0000000000..94a2c20aad --- /dev/null +++ b/platformio_override.ini @@ -0,0 +1,11 @@ +; GridLights hardware configuration +; Overrides for the GridLights ESP32 board on top of upstream WLED platformio.ini + +[env:esp32dev] +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 + -D LEDPIN=4 + -D DEFAULT_LED_COUNT=37 + -D DEFAULT_LED_TYPE=30 + -D RLYPIN=5 + -D RLYMDE=0 + -D GRIDLIGHTS_VERSION=\"2.2.1\" diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cb8813c930..868ec6312b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7469,6 +7469,2555 @@ uint16_t mode_2Ddistortionwaves() { static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@!,Scale;;;2"; +struct Frame { + const uint8_t *data; + uint16_t width; // Pattern width + uint16_t height; // Pattern height + uint16_t baseDuration; // Base duration in ms (modified by speed) + uint16_t basePulseFreq; // Base pulse frequency in Hz (modified by intensity) + uint8_t brightness; // Brightness level (0-255) +}; + +// Define frames as arrays of 0s and 1s (binary map) +const uint8_t frame0[] = { + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 0, 1, 0, 0, 1, 0, 1, + 1, 0, 0, 1, 1, 0, 0, 1, + 1, 0, 0, 1, 1, 0, 0, 1, + 1, 0, 1, 0, 0, 1, 0, 1, + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, +}; + +const uint8_t frame1[] = { + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, +}; + +const uint8_t frame2[] = { + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, +}; + +uint16_t mode_custom_shapes(const Frame frames[], uint16_t frameCount) { + static uint32_t lastFrameTime = 0; + static uint8_t currentFrame = 0; + static uint32_t lastStrobeTime = 0; // Tracks strobe toggling + static bool strobeState = true; // Tracks LED flashing state + + // Reset on first call of new effect + if (SEGENV.call == 0) { + currentFrame = 0; + lastFrameTime = 0; + lastStrobeTime = 0; + strobeState = true; + } + + // Validate input parameters + if (!frames || frameCount == 0) { + return FRAMETIME; + } + + // Validate frame count and current frame + if (frameCount == 0 || currentFrame >= frameCount) { + currentFrame = 0; + } + + uint32_t currentTime = millis(); + + // Retrieve current frame settings + const Frame &frame = frames[currentFrame]; + + // Apply speed slider with 3 discrete settings + uint32_t frameTime = frame.baseDuration; + if (SEGMENT.speed == 0) { + frameTime = frame.baseDuration * 2; // 2x slower + } else if (SEGMENT.speed == 255) { + frameTime = frame.baseDuration / 2; // 2x faster + } + // else: 1-254 = normal speed (use baseDuration as-is) + + // Update frame index if needed + if (currentTime - lastFrameTime > frameTime) { + lastFrameTime = currentTime; + currentFrame = (currentFrame + 1) % frameCount; + } + + // Determine strobe behavior for the current frame + uint32_t cycleTime = (frame.basePulseFreq > 0) ? (1000 / frame.basePulseFreq) : 0; + + if (frame.basePulseFreq > 0 && (currentTime - lastStrobeTime > cycleTime / 2)) { + lastStrobeTime = currentTime; + strobeState = !strobeState; // Toggle strobe state + } else if (frame.basePulseFreq == 0) { + strobeState = true; // Always on + } + + // Get the current frame data + const uint8_t *currentColors = frame.data; + uint32_t patternSize = (uint32_t)frame.width * frame.height; + if (patternSize == 0 || patternSize > 10000) { // Reasonable upper limit + return FRAMETIME; + } + + // Get primary color + uint32_t primaryColor = SEGCOLOR(0); + if (primaryColor == BLACK) { + primaryColor = SEGMENT.color_from_palette(0, true, PALETTE_SOLID_WRAP, 0); + } + + // Apply frame brightness + if (frame.brightness < 255) { + uint8_t r = (R(primaryColor) * frame.brightness) >> 8; + uint8_t g = (G(primaryColor) * frame.brightness) >> 8; + uint8_t b = (B(primaryColor) * frame.brightness) >> 8; + uint8_t w = (W(primaryColor) * frame.brightness) >> 8; + primaryColor = RGBW32(r, g, b, w); + } + + // Apply the frame and strobe effect + for (uint16_t i = 0; i < SEGLEN; i++) { + uint8_t color = currentColors[i % patternSize]; + if (!strobeState || color == 0) { + SEGMENT.setPixelColor(i, BLACK); // Turn off LED + } else { + SEGMENT.setPixelColor(i, primaryColor); + } + } + + return FRAMETIME; +} + +uint16_t mode_hertz_testing() { + // Hardware-reliable frequency steps (tested up to 20Hz) + // Focus on physiologically useful frequencies for closed-eye effects + const uint16_t frequencies[] = { + 1, 2, 4, 5, 8, 10, 12, 15, 20 + }; + const uint8_t freqCount = sizeof(frequencies) / sizeof(frequencies[0]); + + // Map speed slider to frequency array index + uint8_t freqIndex = map(SEGMENT.speed, 0, 255, 0, freqCount - 1); + uint16_t targetHz = frequencies[freqIndex]; + + // Use intensity to control brightness + uint8_t brightness = SEGMENT.intensity; + + // Calculate clean period (always whole milliseconds) + uint32_t period = 1000 / targetHz; + uint32_t halfPeriod = period / 2; + + // Simple, stable timing calculation + uint32_t now = millis(); + uint32_t phase = now % period; + bool isOn = (phase < halfPeriod); + + // Set color + uint32_t color = BLACK; + if (isOn && brightness > 0) { + // Use simple white for consistent performance + uint8_t val = brightness; + color = ((uint32_t)val << 16) | ((uint32_t)val << 8) | val; + } + + // Adaptive pixel count for performance + uint16_t maxPixels; + if (targetHz >= 40) { + maxPixels = 100; // High frequency: fewer pixels + } else if (targetHz >= 20) { + maxPixels = 200; // Medium frequency: medium pixels + } else { + maxPixels = 500; // Low frequency: more pixels + } + + uint16_t pixelCount = (SEGLEN < maxPixels) ? SEGLEN : maxPixels; + + // Update pixels + for (uint16_t i = 0; i < pixelCount; i++) { + SEGMENT.setPixelColor(i, color); + } + + return FRAMETIME; +} + +uint16_t mode_high_frequency_test() { + // Dedicated high-frequency testing (25-50Hz) + // Minimal processing for maximum performance + const uint16_t highFreqs[] = { 25, 30, 40, 50 }; + const uint8_t freqCount = sizeof(highFreqs) / sizeof(highFreqs[0]); + + uint8_t freqIndex = map(SEGMENT.speed, 0, 255, 0, freqCount - 1); + uint16_t targetHz = highFreqs[freqIndex]; + uint8_t brightness = SEGMENT.intensity; + + // Ultra-simple timing for maximum performance + uint32_t period = 1000 / targetHz; + uint32_t now = millis(); + bool isOn = ((now / period) & 1) == 0; + + // Minimal pixel count for high frequency + uint16_t pixelCount = (SEGLEN < 50) ? SEGLEN : 50; + + uint32_t color = BLACK; + if (isOn && brightness > 0) { + uint8_t val = brightness; + color = ((uint32_t)val << 16) | ((uint32_t)val << 8) | val; + } + + for (uint16_t i = 0; i < pixelCount; i++) { + SEGMENT.setPixelColor(i, color); + } + + return FRAMETIME; +} + +// uint16_t mode_custom_shapes(const Frame frames[], uint16_t frameCount) { +// static bool strobeOn = true; // Tracks whether the strobe is "on" or "off" + +// static uint32_t lastFrameTime = 0; +// static uint8_t currentFrame = 0; // Frame index: 0, 1, or 2 +// // const uint8_t *frames[] = {frame0, frame1, frame2}; +// // const uint16_t frameCount = sizeof(frames) / sizeof(frames[0]); + +// // Map SEGMENT.speed to pulse frequency and frame duration +// uint16_t minPulseFrequency = 1; // Minimum pulse frequency in Hz +// uint16_t maxPulseFrequency = 50; // Maximum pulse frequency in Hz +// uint16_t pulseFrequency = (SEGMENT.intensity == 0) ? 0 : map(SEGMENT.intensity, 1, 255, minPulseFrequency, maxPulseFrequency); + +// // Map SEGMENT.speed to frame switch duration +// uint32_t minFrameTime = 100; // Minimum time per frame in ms (10 fps) +// uint32_t maxFrameTime = 1000; // Maximum time per frame in ms (1 fps) +// uint32_t frameTime = map(SEGMENT.speed, 0, 255, maxFrameTime, minFrameTime); + +// // beginning of strobe like effect +// uint32_t cycleTime = (pulseFrequency > 0) ? (1000 / pulseFrequency) : 1000; + +// // Determine strobe state (on/off) based on cycleTime +// uint32_t currentTime = millis(); +// if (pulseFrequency > 0) { +// strobeOn = (currentTime % cycleTime) < (cycleTime / 2); // On for half the cycle +// } else { +// strobeOn = true; // Always on if pulseFrequency is 0 +// } + +// // Use custom durations if the first frame has a nonzero duration +// uint32_t effectiveFrameTime = (frames[0].duration > 0) ? frames[currentFrame].duration * 1000 : frameTime; + +// // Switch frames based on frameTime +// if (currentTime - lastFrameTime > frameTime) { +// lastFrameTime = currentTime; +// currentFrame = (currentFrame + 1) % frameCount; +// } + +// // this piece uses the sign wave calculation +// /** + +// // Calculate brightness based on pulse frequency +// uint16_t pulsePeriod = 0; // Declare pulsePeriod here +// uint8_t brightness; // Declare brightness here + +// if (pulseFrequency > 0) { +// pulsePeriod = 1000 / pulseFrequency; +// brightness = (sin((millis() % pulsePeriod) * (PI * 2) / pulsePeriod) + 1) * 127.5; +// } else { +// brightness = 255; // Full brightness, no strobing +// } + +// // Switch frames based on frameTime +// if (millis() - lastFrameTime > frameTime) { +// lastFrameTime = millis(); +// currentFrame = (currentFrame + 1) % frameCount; +// } +// */ + + +// // Get the current frame +// const uint8_t *currentColors = frames[currentFrame].data; + +// // Apply the frame and strobe effect +// for (uint16_t i = 0; i < SEGLEN; i++) { +// uint8_t color = currentColors[i]; +// if (!strobeOn || color == 0) { +// SEGMENT.setPixelColor(i, 0); // Off +// } else { +// uint32_t ledColor = SEGMENT.color_from_palette(0, true, PALETTE_SOLID_WRAP, 0); +// // Apply brightness to "on" LEDs +// // uint8_t r = ((ledColor >> 16) & 0xFF) * brightness / 255; +// // uint8_t g = ((ledColor >> 8) & 0xFF) * brightness / 255; +// // uint8_t b = (ledColor & 0xFF) * brightness / 255; +// // SEGMENT.setPixelColor(i, r, g, b); +// SEGMENT.setPixelColor(i, ledColor); +// } +// } + +// return FRAMETIME; +// } +// Metadata for the custom effect +static const char _data_FX_MODE_CUSTOM[] PROGMEM = "Custom Squares@!,!,,,,Smooth;;!"; + +uint16_t mode_custom_squares() { + const Frame sframes[] = { + { frame0, 8, 8, 2000, 5, 255 }, // 8x8 pattern, 2s base duration, 5Hz base pulse, full brightness + { frame1, 8, 8, 3000, 3, 255 }, // 8x8 pattern, 3s base duration, 3Hz base pulse, full brightness + { frame2, 8, 8, 1000, 8, 255 }, // 8x8 pattern, 1s base duration, 8Hz base pulse, full brightness + }; + const uint16_t frameCount = sizeof(sframes) / sizeof(sframes[0]); + return mode_custom_shapes(sframes, frameCount); +} + +// uint16_t mode_custom_circles() { +// const uint8_t *frames[] = {frame0, frame1, frame2}; // Create an array of pointers +// return mode_custom_shapes(frames); +// } + +// Diamond Spin + +// Define frames as arrays of 0s and 1s (binary map) +const uint8_t dsframe0[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dsframe1[] = { + 0, 0, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dsframe2[] = { + 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dsframe3[] = { + 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, +}; + +const uint8_t dsframe4[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, 0, + 0, 0, 1, 0, 1, 0, 0, + 0, 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dsframe5[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 1, 1, 0, 1, 1, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +uint16_t mode_custom_diamond_spin() { + static uint32_t lastFrameTime = 0; + static uint8_t currentFrame = 0; + static uint32_t lastStrobeTime = 0; + static bool strobeState = true; + + // Frame sequence for diamond spin effect + const uint8_t *frameSequence[] = { + dsframe0, dsframe1, dsframe2, dsframe3, dsframe4, dsframe5 + }; + const uint8_t frameCount = sizeof(frameSequence) / sizeof(frameSequence[0]); + + // Reset on first call + if (SEGENV.call == 0) { + currentFrame = 0; + lastFrameTime = 0; + lastStrobeTime = 0; + strobeState = true; + } + + uint32_t currentTime = millis(); + + // Map speed slider to frame duration (100ms to 2000ms) + uint32_t frameDuration = map(SEGMENT.speed, 0, 255, 2000, 100); + + // Update frame index if needed + if (currentTime - lastFrameTime > frameDuration) { + lastFrameTime = currentTime; + currentFrame = (currentFrame + 1) % frameCount; + } + + // Map intensity slider to pulse frequency (0-50 Hz) + uint16_t pulseFrequency = map(SEGMENT.intensity, 0, 255, 0, 50); + + // Handle strobe timing + if (pulseFrequency > 0) { + uint32_t cycleTime = 1000 / pulseFrequency; + if (currentTime - lastStrobeTime > cycleTime / 2) { + lastStrobeTime = currentTime; + strobeState = !strobeState; + } + } else { + strobeState = true; // Always on when intensity is 0 + } + + // Get primary color + uint32_t primaryColor = SEGCOLOR(0); + if (primaryColor == BLACK) { + primaryColor = SEGMENT.color_from_palette(0, true, PALETTE_SOLID_WRAP, 0); + } + + // Apply pattern to LEDs + const uint8_t *currentPattern = frameSequence[currentFrame]; + for (uint16_t i = 0; i < SEGLEN && i < 37; i++) { + uint8_t pixelValue = currentPattern[i]; + if (!strobeState || pixelValue == 0) { + SEGMENT.setPixelColor(i, BLACK); + } else { + SEGMENT.setPixelColor(i, primaryColor); + } + } + + return FRAMETIME; +} + +// Metadata for the custom effect +static const char _data_FX_MODE_CUSTOM_DIAMOND_SPIN[] PROGMEM = "Diamond Spin@Speed,Frequency;;!;"; + + +// DIAMOND SPIN + +// Define frames as arrays of 0s and 1s (binary map) +const uint8_t dframe0[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dframe1[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, + 0, 0, 0, 1, 0, 1, 0, + 0, 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dframe2[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, 0, + 0, 1, 1, 1, 0, + 0, 0, 0, 0, +}; + +const uint8_t dframe3[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 1, 0, 1, 0, 0, + 1, 1, 1, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dframe4[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, 0, + 0, 1, 0, 1, 0, 0, 0, + 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dframe5[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, 0, + 0, 1, 0, 1, 0, 0, 0, + 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dframe6[] = { + 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 0, 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +const uint8_t dframe7[] = { + 0, 0, 0, 0, + 0, 0, 1, 1, 1, + 0, 0, 1, 0, 1, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, +}; + +uint16_t mode_custom_drunk_diamond_spin() { + const Frame dframes[] = { + { dframe0, 37, 1, 2000, 10, 255 }, // 39 element pattern, 2s base duration, 10Hz base pulse, full brightness + { dframe1, 37, 1, 500, 6, 255 }, // 39 element pattern, 0.5s base duration, 6Hz base pulse, full brightness + { dframe2, 37, 1, 2000, 7, 255 }, // 39 element pattern, 2s base duration, 7Hz base pulse, full brightness + { dframe3, 37, 1, 2000, 8, 255 }, // 39 element pattern, 2s base duration, 8Hz base pulse, full brightness + { dframe4, 37, 1, 500, 0, 255 }, // 39 element pattern, 0.5s base duration, no pulse, full brightness + { dframe5, 37, 1, 2000, 0, 255 }, // 39 element pattern, 2s base duration, no pulse, full brightness + { dframe6, 37, 1, 500, 0, 255 }, // 39 element pattern, 0.5s base duration, no pulse, full brightness + { dframe7, 37, 1, 2000, 10, 255 } // 39 element pattern, 2s base duration, 10Hz base pulse, full brightness + }; + const uint16_t frameCount = sizeof(dframes) / sizeof(dframes[0]); + return mode_custom_shapes(dframes, frameCount); +} + +// Metadata for the custom effect +static const char _data_FX_MODE_CUSTOM_D_DIAMOND_SPIN[] PROGMEM = "Drunk Diamond Spin@!,!,,,,Smooth;;!"; + + +const uint8_t bframe0[] = { + 1, 1, 1, 0, + 0, 0, 0, 1, 0, + 0, 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, 0, + 0, 1, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + 1, 1, 1, 0 +}; + +const uint8_t bframe1[] = { + 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 1, 1 +}; + +const uint8_t bframe2[] = { + 1, 0, 0, 1, + 0, 1, 0, 0, 0, + 0, 1, 1, 0, 1, 0, + 0, 0, 0, 1, 0, 0, 0, + 0, 1, 0, 1, 1, 0, + 0, 0, 0, 1, 0, + 1, 0, 0, 1 +}; + +uint16_t mode_custom_ben() { + const Frame bframes[] = { + { bframe0, 37, 1, 4000, 1, 255 }, // 35 element pattern, 4s base duration, 1Hz pulse, full brightness + { bframe1, 37, 1, 4000, 5, 255 }, // 35 element pattern, 4s base duration, 5Hz pulse, full brightness + { bframe2, 37, 1, 4000, 20, 255 }, // 35 element pattern, 4s base duration, 20Hz pulse, full brightness + }; + const uint16_t frameCount = sizeof(bframes) / sizeof(bframes[0]); + return mode_custom_shapes(bframes, frameCount); +} + +// Metadata for the custom effect +static const char _data_FX_MODE_CUSTOM_BEN[] PROGMEM = "Ben@!,!,,,,Smooth;;!"; + +// NOVAS EFFECT - Hexagon animation with ping-pong pattern +// Define frames for Novas effect (hexagon patterns, 35 elements each) +const uint8_t novas_frame0[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0 +}; + +const uint8_t novas_frame1[] = { + 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0 +}; + +const uint8_t novas_frame2[] = { + 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0 +}; + +const uint8_t novas_frame3[] = { + 0, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 1, 0, + 0, 1, 1, 0, 1, 1, 0, + 0, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 0 +}; + +const uint8_t novas_frame4[] = { + 0, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 0 +}; + +const uint8_t novas_frame5[] = { + 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1 +}; + +const uint8_t novas_frame6[] = { + 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1 +}; + +uint16_t mode_custom_novas() { + // Extended sequence with varying durations and Hz values + const Frame novasFrames[] = { + // First section: 5 seconds, 30Hz (13 frames) + { novas_frame0, 37, 1, 5000, 30, 255 }, + { novas_frame1, 37, 1, 5000, 30, 255 }, + { novas_frame2, 37, 1, 5000, 30, 255 }, + { novas_frame3, 37, 1, 5000, 30, 255 }, + { novas_frame4, 37, 1, 5000, 30, 255 }, + { novas_frame5, 37, 1, 5000, 30, 255 }, + { novas_frame6, 37, 1, 5000, 30, 255 }, + { novas_frame5, 37, 1, 5000, 30, 255 }, + { novas_frame4, 37, 1, 5000, 30, 255 }, + { novas_frame3, 37, 1, 5000, 30, 255 }, + { novas_frame2, 37, 1, 5000, 30, 255 }, + { novas_frame1, 37, 1, 5000, 30, 255 }, + { novas_frame0, 37, 1, 5000, 30, 255 }, + + // Second section: 5 seconds, 20Hz (12 frames) + { novas_frame1, 37, 1, 5000, 20, 255 }, + { novas_frame2, 37, 1, 5000, 20, 255 }, + { novas_frame3, 37, 1, 5000, 20, 255 }, + { novas_frame4, 37, 1, 5000, 20, 255 }, + { novas_frame5, 37, 1, 5000, 20, 255 }, + { novas_frame6, 37, 1, 5000, 20, 255 }, + { novas_frame5, 37, 1, 5000, 20, 255 }, + { novas_frame4, 37, 1, 5000, 20, 255 }, + { novas_frame3, 37, 1, 5000, 20, 255 }, + { novas_frame2, 37, 1, 5000, 20, 255 }, + { novas_frame1, 37, 1, 5000, 20, 255 }, + { novas_frame0, 37, 1, 5000, 20, 255 }, + + // Third section: 5 seconds, 15Hz (12 frames) + { novas_frame1, 37, 1, 5000, 15, 255 }, + { novas_frame2, 37, 1, 5000, 15, 255 }, + { novas_frame3, 37, 1, 5000, 15, 255 }, + { novas_frame4, 37, 1, 5000, 15, 255 }, + { novas_frame5, 37, 1, 5000, 15, 255 }, + { novas_frame6, 37, 1, 5000, 15, 255 }, + { novas_frame5, 37, 1, 5000, 15, 255 }, + { novas_frame4, 37, 1, 5000, 15, 255 }, + { novas_frame3, 37, 1, 5000, 15, 255 }, + { novas_frame2, 37, 1, 5000, 15, 255 }, + { novas_frame1, 37, 1, 5000, 15, 255 }, + { novas_frame0, 37, 1, 5000, 15, 255 }, + + // Fourth section: 10 seconds, 10Hz (12 frames) + { novas_frame1, 37, 1, 10000, 10, 255 }, + { novas_frame2, 37, 1, 10000, 10, 255 }, + { novas_frame3, 37, 1, 10000, 10, 255 }, + { novas_frame4, 37, 1, 10000, 10, 255 }, + { novas_frame5, 37, 1, 10000, 10, 255 }, + { novas_frame6, 37, 1, 10000, 10, 255 }, + { novas_frame5, 37, 1, 10000, 10, 255 }, + { novas_frame4, 37, 1, 10000, 10, 255 }, + { novas_frame3, 37, 1, 10000, 10, 255 }, + { novas_frame2, 37, 1, 10000, 10, 255 }, + { novas_frame1, 37, 1, 10000, 10, 255 }, + { novas_frame0, 37, 1, 10000, 10, 255 }, + + // Fifth section: 10 seconds, 6Hz (12 frames) + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame6, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame0, 37, 1, 10000, 6, 255 }, + }; + const uint16_t frameCount = sizeof(novasFrames) / sizeof(novasFrames[0]); + return mode_custom_shapes(novasFrames, frameCount); +} + +// Metadata for the Novas effect +static const char _data_FX_MODE_CUSTOM_NOVAS[] PROGMEM = "Novas@Speed,!,,,,Smooth;;!"; + +// NOVAS (3-9 Hz) EFFECT - Same pattern with different Hz values +uint16_t mode_custom_novas_3_9hz() { + // Extended sequence with 3-9 Hz frequency range + const Frame novasFrames[] = { + // First section: 5 seconds, 3Hz (13 frames) + { novas_frame0, 37, 1, 5000, 3, 255 }, + { novas_frame1, 37, 1, 5000, 3, 255 }, + { novas_frame2, 37, 1, 5000, 3, 255 }, + { novas_frame3, 37, 1, 5000, 3, 255 }, + { novas_frame4, 37, 1, 5000, 3, 255 }, + { novas_frame5, 37, 1, 5000, 3, 255 }, + { novas_frame6, 37, 1, 5000, 3, 255 }, + { novas_frame5, 37, 1, 5000, 3, 255 }, + { novas_frame4, 37, 1, 5000, 3, 255 }, + { novas_frame3, 37, 1, 5000, 3, 255 }, + { novas_frame2, 37, 1, 5000, 3, 255 }, + { novas_frame1, 37, 1, 5000, 3, 255 }, + { novas_frame0, 37, 1, 5000, 3, 255 }, + + // Second section: 10 seconds, 6Hz (12 frames) + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame6, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame0, 37, 1, 10000, 6, 255 }, + + // Third section: 15 seconds, 9Hz (11 frames) + { novas_frame1, 37, 1, 15000, 9, 255 }, + { novas_frame2, 37, 1, 15000, 9, 255 }, + { novas_frame3, 37, 1, 15000, 9, 255 }, + { novas_frame4, 37, 1, 15000, 9, 255 }, + { novas_frame5, 37, 1, 15000, 9, 255 }, + { novas_frame6, 37, 1, 15000, 9, 255 }, + { novas_frame5, 37, 1, 15000, 9, 255 }, + { novas_frame4, 37, 1, 15000, 9, 255 }, + { novas_frame3, 37, 1, 15000, 9, 255 }, + { novas_frame2, 37, 1, 15000, 9, 255 }, + { novas_frame1, 37, 1, 15000, 9, 255 }, + + // Fourth section: 10 seconds, 9Hz (1 frame) + { novas_frame0, 37, 1, 10000, 9, 255 }, + + // Fifth section: 10 seconds, 6Hz (11 frames) + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame6, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame1, 37, 1, 10000, 6, 255 }, + }; + const uint16_t frameCount = sizeof(novasFrames) / sizeof(novasFrames[0]); + return mode_custom_shapes(novasFrames, frameCount); +} + +// Metadata for the Novas (3-9 Hz) effect +static const char _data_FX_MODE_CUSTOM_NOVAS_3_9HZ[] PROGMEM = "Novas (3-9 Hz)@Speed,!,,,,Smooth;;!"; + +// NOVAS (3-9 Hz, Inverted Time) EFFECT - Inverted time progression +uint16_t mode_custom_novas_inverted() { + const Frame novasFrames[] = { + // First section: 15 seconds, 3Hz (13 frames) + { novas_frame0, 37, 1, 15000, 3, 255 }, + { novas_frame1, 37, 1, 15000, 3, 255 }, + { novas_frame2, 37, 1, 15000, 3, 255 }, + { novas_frame3, 37, 1, 15000, 3, 255 }, + { novas_frame4, 37, 1, 15000, 3, 255 }, + { novas_frame5, 37, 1, 15000, 3, 255 }, + { novas_frame6, 37, 1, 15000, 3, 255 }, + { novas_frame5, 37, 1, 15000, 3, 255 }, + { novas_frame4, 37, 1, 15000, 3, 255 }, + { novas_frame3, 37, 1, 15000, 3, 255 }, + { novas_frame2, 37, 1, 15000, 3, 255 }, + { novas_frame1, 37, 1, 15000, 3, 255 }, + { novas_frame0, 37, 1, 15000, 3, 255 }, + + // Second section: 10 seconds, 6Hz (12 frames) + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame6, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame0, 37, 1, 10000, 6, 255 }, + + // Third section: 5 seconds, 9Hz (11 frames) + { novas_frame1, 37, 1, 5000, 9, 255 }, + { novas_frame2, 37, 1, 5000, 9, 255 }, + { novas_frame3, 37, 1, 5000, 9, 255 }, + { novas_frame4, 37, 1, 5000, 9, 255 }, + { novas_frame5, 37, 1, 5000, 9, 255 }, + { novas_frame6, 37, 1, 5000, 9, 255 }, + { novas_frame5, 37, 1, 5000, 9, 255 }, + { novas_frame4, 37, 1, 5000, 9, 255 }, + { novas_frame3, 37, 1, 5000, 9, 255 }, + { novas_frame2, 37, 1, 5000, 9, 255 }, + { novas_frame1, 37, 1, 5000, 9, 255 }, + + // Fourth section: 10 seconds, 9Hz (1 frame) + { novas_frame0, 37, 1, 10000, 9, 255 }, + + // Fifth section: 10 seconds, 6Hz (11 frames) + { novas_frame1, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame6, 37, 1, 10000, 6, 255 }, + { novas_frame5, 37, 1, 10000, 6, 255 }, + { novas_frame4, 37, 1, 10000, 6, 255 }, + { novas_frame3, 37, 1, 10000, 6, 255 }, + { novas_frame2, 37, 1, 10000, 6, 255 }, + { novas_frame1, 37, 1, 10000, 6, 255 }, + }; + const uint16_t frameCount = sizeof(novasFrames) / sizeof(novasFrames[0]); + return mode_custom_shapes(novasFrames, frameCount); +} + +// Metadata for the Novas (3-9 Hz, Inverted Time) effect +static const char _data_FX_MODE_CUSTOM_NOVAS_INVERTED[] PROGMEM = "Novas (3-9 Hz, Inverted Time)@Speed,!,,,,Smooth;;!"; + +// BLACK HOLE EFFECT - Inverted hexagon patterns with frequency progression +// Define frames for Black Hole effect (inverted patterns, 35 elements each) +const uint8_t blackhole_frame0[] = { + 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1 +}; + +const uint8_t blackhole_frame1[] = { + 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1 +}; + +const uint8_t blackhole_frame2[] = { + 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1 +}; + +const uint8_t blackhole_frame3[] = { + 1, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 1 +}; + +const uint8_t blackhole_frame4[] = { + 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1 +}; + +const uint8_t blackhole_frame5[] = { + 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1 +}; + +const uint8_t blackhole_frame6[] = { + 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1 +}; + +uint16_t mode_black_hole() { + const Frame pulseFrames[] = { + // First section: 10 seconds, 6Hz (6 frames) + { blackhole_frame0, 37, 1, 10000, 6, 255 }, + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + { blackhole_frame3, 37, 1, 10000, 6, 255 }, + { blackhole_frame4, 37, 1, 10000, 6, 255 }, + { blackhole_frame5, 37, 1, 10000, 6, 255 }, + + // Second section: 10 seconds, 9Hz (5 frames) + { blackhole_frame6, 37, 1, 10000, 9, 255 }, + { blackhole_frame5, 37, 1, 10000, 9, 255 }, + { blackhole_frame4, 37, 1, 10000, 9, 255 }, + { blackhole_frame3, 37, 1, 10000, 9, 255 }, + { blackhole_frame2, 37, 1, 10000, 9, 255 }, + + // Third section: 10 seconds, 15Hz (5 frames) + { blackhole_frame1, 37, 1, 10000, 15, 255 }, + { blackhole_frame0, 37, 1, 10000, 15, 255 }, + { blackhole_frame1, 37, 1, 10000, 15, 255 }, + { blackhole_frame2, 37, 1, 10000, 15, 255 }, + { blackhole_frame3, 37, 1, 10000, 15, 255 }, + + // Fourth section: 10 seconds, 24Hz (5 frames) + { blackhole_frame4, 37, 1, 10000, 24, 255 }, + { blackhole_frame5, 37, 1, 10000, 24, 255 }, + { blackhole_frame6, 37, 1, 10000, 24, 255 }, + { blackhole_frame5, 37, 1, 10000, 24, 255 }, + { blackhole_frame4, 37, 1, 10000, 24, 255 }, + + // Fifth section: 10 seconds, 39Hz (5 frames) + { blackhole_frame3, 37, 1, 10000, 39, 255 }, + { blackhole_frame2, 37, 1, 10000, 39, 255 }, + { blackhole_frame1, 37, 1, 10000, 39, 255 }, + { blackhole_frame0, 37, 1, 10000, 39, 255 }, + { blackhole_frame1, 37, 1, 10000, 39, 255 }, + + // Sixth section: 10 seconds, 24Hz (5 frames) - descending + { blackhole_frame2, 37, 1, 10000, 24, 255 }, + { blackhole_frame3, 37, 1, 10000, 24, 255 }, + { blackhole_frame4, 37, 1, 10000, 24, 255 }, + { blackhole_frame5, 37, 1, 10000, 24, 255 }, + { blackhole_frame6, 37, 1, 10000, 24, 255 }, + + // Seventh section: 10 seconds, 15Hz (5 frames) + { blackhole_frame5, 37, 1, 10000, 15, 255 }, + { blackhole_frame4, 37, 1, 10000, 15, 255 }, + { blackhole_frame3, 37, 1, 10000, 15, 255 }, + { blackhole_frame2, 37, 1, 10000, 15, 255 }, + { blackhole_frame1, 37, 1, 10000, 15, 255 }, + + // Eighth section: 10 seconds, 9Hz (10 frames) + { blackhole_frame0, 37, 1, 10000, 9, 255 }, + { blackhole_frame1, 37, 1, 10000, 9, 255 }, + { blackhole_frame2, 37, 1, 10000, 9, 255 }, + { blackhole_frame3, 37, 1, 10000, 9, 255 }, + { blackhole_frame4, 37, 1, 10000, 9, 255 }, + { blackhole_frame5, 37, 1, 10000, 9, 255 }, + { blackhole_frame6, 37, 1, 10000, 9, 255 }, + { blackhole_frame5, 37, 1, 10000, 9, 255 }, + { blackhole_frame4, 37, 1, 10000, 9, 255 }, + { blackhole_frame3, 37, 1, 10000, 9, 255 }, + + // Ninth section: 10 seconds, 6Hz (5 frames) + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame0, 37, 1, 10000, 6, 255 }, + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + + // Tenth section: 10 seconds, 3Hz (16 frames) + { blackhole_frame3, 37, 1, 10000, 3, 255 }, + { blackhole_frame4, 37, 1, 10000, 3, 255 }, + { blackhole_frame5, 37, 1, 10000, 3, 255 }, + { blackhole_frame6, 37, 1, 10000, 3, 255 }, + { blackhole_frame5, 37, 1, 10000, 3, 255 }, + { blackhole_frame4, 37, 1, 10000, 3, 255 }, + { blackhole_frame3, 37, 1, 10000, 3, 255 }, + { blackhole_frame2, 37, 1, 10000, 3, 255 }, + { blackhole_frame1, 37, 1, 10000, 3, 255 }, + { blackhole_frame0, 37, 1, 10000, 3, 255 }, + { blackhole_frame1, 37, 1, 10000, 3, 255 }, + { blackhole_frame2, 37, 1, 10000, 3, 255 }, + { blackhole_frame3, 37, 1, 10000, 3, 255 }, + { blackhole_frame4, 37, 1, 10000, 3, 255 }, + { blackhole_frame5, 37, 1, 10000, 3, 255 }, + { blackhole_frame6, 37, 1, 10000, 3, 255 }, + }; + const uint16_t frameCount = sizeof(pulseFrames) / sizeof(pulseFrames[0]); + return mode_custom_shapes(pulseFrames, frameCount); +} + +// Metadata for the Black Hole effect +static const char _data_FX_MODE_BLACK_HOLE[] PROGMEM = "Black Hole@Speed,!,,,,Smooth;;!"; + +// BLACK HOLE 3 EFFECT - Constant 3Hz frequency version +uint16_t mode_black_hole_3() { + const Frame blackhole3Frames[] = { + // First section: 5 seconds, 3Hz (37 frames) + { blackhole_frame0, 37, 1, 5000, 3, 255 }, + { blackhole_frame1, 37, 1, 5000, 3, 255 }, + { blackhole_frame2, 37, 1, 5000, 3, 255 }, + { blackhole_frame3, 37, 1, 5000, 3, 255 }, + { blackhole_frame4, 37, 1, 5000, 3, 255 }, + { blackhole_frame5, 37, 1, 5000, 3, 255 }, + { blackhole_frame6, 37, 1, 5000, 3, 255 }, + { blackhole_frame5, 37, 1, 5000, 3, 255 }, + { blackhole_frame4, 37, 1, 5000, 3, 255 }, + { blackhole_frame3, 37, 1, 5000, 3, 255 }, + { blackhole_frame2, 37, 1, 5000, 3, 255 }, + { blackhole_frame1, 37, 1, 5000, 3, 255 }, + { blackhole_frame0, 37, 1, 5000, 3, 255 }, + { blackhole_frame1, 37, 1, 5000, 3, 255 }, + { blackhole_frame2, 37, 1, 5000, 3, 255 }, + { blackhole_frame3, 37, 1, 5000, 3, 255 }, + { blackhole_frame4, 37, 1, 5000, 3, 255 }, + { blackhole_frame5, 37, 1, 5000, 3, 255 }, + { blackhole_frame6, 37, 1, 5000, 3, 255 }, + { blackhole_frame5, 37, 1, 5000, 3, 255 }, + { blackhole_frame4, 37, 1, 5000, 3, 255 }, + { blackhole_frame3, 37, 1, 5000, 3, 255 }, + { blackhole_frame2, 37, 1, 5000, 3, 255 }, + { blackhole_frame1, 37, 1, 5000, 3, 255 }, + { blackhole_frame0, 37, 1, 5000, 3, 255 }, + { blackhole_frame1, 37, 1, 5000, 3, 255 }, + { blackhole_frame2, 37, 1, 5000, 3, 255 }, + { blackhole_frame3, 37, 1, 5000, 3, 255 }, + { blackhole_frame4, 37, 1, 5000, 3, 255 }, + { blackhole_frame5, 37, 1, 5000, 3, 255 }, + { blackhole_frame6, 37, 1, 5000, 3, 255 }, + { blackhole_frame5, 37, 1, 5000, 3, 255 }, + { blackhole_frame4, 37, 1, 5000, 3, 255 }, + { blackhole_frame3, 37, 1, 5000, 3, 255 }, + { blackhole_frame2, 37, 1, 5000, 3, 255 }, + { blackhole_frame1, 37, 1, 5000, 3, 255 } + }; + const uint16_t frameCount = sizeof(blackhole3Frames) / sizeof(blackhole3Frames[0]); + return mode_custom_shapes(blackhole3Frames, frameCount); +} + +// Metadata for the Black Hole 3 effect +static const char _data_FX_MODE_BLACK_HOLE_3[] PROGMEM = "Black Hole 3@Speed,!,,,,Smooth;;!"; + +// BLACK HOLE 6 EFFECT - Constant 6Hz frequency version +uint16_t mode_black_hole_6() { + const Frame blackhole6Frames[] = { + // First section: 5 seconds, 6Hz (37 frames) + { blackhole_frame0, 37, 1, 5000, 6, 255 }, + { blackhole_frame1, 37, 1, 5000, 6, 255 }, + { blackhole_frame2, 37, 1, 5000, 6, 255 }, + { blackhole_frame3, 37, 1, 5000, 6, 255 }, + { blackhole_frame4, 37, 1, 5000, 6, 255 }, + { blackhole_frame5, 37, 1, 5000, 6, 255 }, + { blackhole_frame6, 37, 1, 5000, 6, 255 }, + { blackhole_frame5, 37, 1, 5000, 6, 255 }, + { blackhole_frame4, 37, 1, 5000, 6, 255 }, + { blackhole_frame3, 37, 1, 5000, 6, 255 }, + { blackhole_frame2, 37, 1, 5000, 6, 255 }, + { blackhole_frame1, 37, 1, 5000, 6, 255 }, + { blackhole_frame0, 37, 1, 5000, 6, 255 }, + { blackhole_frame1, 37, 1, 5000, 6, 255 }, + { blackhole_frame2, 37, 1, 5000, 6, 255 }, + { blackhole_frame3, 37, 1, 5000, 6, 255 }, + { blackhole_frame4, 37, 1, 5000, 6, 255 }, + { blackhole_frame5, 37, 1, 5000, 6, 255 }, + { blackhole_frame6, 37, 1, 5000, 6, 255 }, + { blackhole_frame5, 37, 1, 5000, 6, 255 }, + { blackhole_frame4, 37, 1, 5000, 6, 255 }, + { blackhole_frame3, 37, 1, 5000, 6, 255 }, + { blackhole_frame2, 37, 1, 5000, 6, 255 }, + { blackhole_frame1, 37, 1, 5000, 6, 255 }, + { blackhole_frame0, 37, 1, 5000, 6, 255 }, + { blackhole_frame1, 37, 1, 5000, 6, 255 }, + { blackhole_frame2, 37, 1, 5000, 6, 255 }, + { blackhole_frame3, 37, 1, 5000, 6, 255 }, + { blackhole_frame4, 37, 1, 5000, 6, 255 }, + { blackhole_frame5, 37, 1, 5000, 6, 255 }, + { blackhole_frame6, 37, 1, 5000, 6, 255 }, + { blackhole_frame5, 37, 1, 5000, 6, 255 }, + { blackhole_frame4, 37, 1, 5000, 6, 255 }, + { blackhole_frame3, 37, 1, 5000, 6, 255 }, + { blackhole_frame2, 37, 1, 5000, 6, 255 }, + { blackhole_frame1, 37, 1, 5000, 6, 255 }, + { blackhole_frame0, 37, 1, 5000, 6, 255 }, + + // Second section: 10 seconds, 6Hz (30 frames) + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + { blackhole_frame3, 37, 1, 10000, 6, 255 }, + { blackhole_frame4, 37, 1, 10000, 6, 255 }, + { blackhole_frame5, 37, 1, 10000, 6, 255 }, + { blackhole_frame6, 37, 1, 10000, 6, 255 }, + { blackhole_frame5, 37, 1, 10000, 6, 255 }, + { blackhole_frame4, 37, 1, 10000, 6, 255 }, + { blackhole_frame3, 37, 1, 10000, 6, 255 }, + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame0, 37, 1, 10000, 6, 255 }, + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + { blackhole_frame3, 37, 1, 10000, 6, 255 }, + { blackhole_frame4, 37, 1, 10000, 6, 255 }, + { blackhole_frame5, 37, 1, 10000, 6, 255 }, + { blackhole_frame6, 37, 1, 10000, 6, 255 }, + { blackhole_frame5, 37, 1, 10000, 6, 255 }, + { blackhole_frame4, 37, 1, 10000, 6, 255 }, + { blackhole_frame3, 37, 1, 10000, 6, 255 }, + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame0, 37, 1, 10000, 6, 255 }, + { blackhole_frame1, 37, 1, 10000, 6, 255 }, + { blackhole_frame2, 37, 1, 10000, 6, 255 }, + { blackhole_frame3, 37, 1, 10000, 6, 255 }, + { blackhole_frame4, 37, 1, 10000, 6, 255 }, + { blackhole_frame5, 37, 1, 10000, 6, 255 }, + { blackhole_frame6, 37, 1, 10000, 6, 255 }, + }; + const uint16_t frameCount = sizeof(blackhole6Frames) / sizeof(blackhole6Frames[0]); + return mode_custom_shapes(blackhole6Frames, frameCount); +} + +// Metadata for the Black Hole 6 effect +static const char _data_FX_MODE_BLACK_HOLE_6[] PROGMEM = "Black Hole 6@Speed,!,,,,Smooth;;!"; + +// BLACK HOLE 9 EFFECT - Variable frequency version (10Hz->15Hz->20Hz->30Hz->20Hz) +uint16_t mode_black_hole_9() { + const Frame blackhole9Frames[] = { + // First section: 5 seconds, 10Hz (13 frames) + { blackhole_frame0, 37, 1, 5000, 10, 255 }, + { blackhole_frame1, 37, 1, 5000, 10, 255 }, + { blackhole_frame2, 37, 1, 5000, 10, 255 }, + { blackhole_frame3, 37, 1, 5000, 10, 255 }, + { blackhole_frame4, 37, 1, 5000, 10, 255 }, + { blackhole_frame5, 37, 1, 5000, 10, 255 }, + { blackhole_frame6, 37, 1, 5000, 10, 255 }, + { blackhole_frame5, 37, 1, 5000, 10, 255 }, + { blackhole_frame4, 37, 1, 5000, 10, 255 }, + { blackhole_frame3, 37, 1, 5000, 10, 255 }, + { blackhole_frame2, 37, 1, 5000, 10, 255 }, + { blackhole_frame1, 37, 1, 5000, 10, 255 }, + { blackhole_frame0, 37, 1, 5000, 10, 255 }, + + // Second section: 5 seconds, 15Hz (12 frames) + { blackhole_frame1, 37, 1, 5000, 15, 255 }, + { blackhole_frame2, 37, 1, 5000, 15, 255 }, + { blackhole_frame3, 37, 1, 5000, 15, 255 }, + { blackhole_frame4, 37, 1, 5000, 15, 255 }, + { blackhole_frame5, 37, 1, 5000, 15, 255 }, + { blackhole_frame6, 37, 1, 5000, 15, 255 }, + { blackhole_frame5, 37, 1, 5000, 15, 255 }, + { blackhole_frame4, 37, 1, 5000, 15, 255 }, + { blackhole_frame3, 37, 1, 5000, 15, 255 }, + { blackhole_frame2, 37, 1, 5000, 15, 255 }, + { blackhole_frame1, 37, 1, 5000, 15, 255 }, + { blackhole_frame0, 37, 1, 5000, 15, 255 }, + + // Third section: 5 seconds, 20Hz (12 frames) + { blackhole_frame1, 37, 1, 5000, 20, 255 }, + { blackhole_frame2, 37, 1, 5000, 20, 255 }, + { blackhole_frame3, 37, 1, 5000, 20, 255 }, + { blackhole_frame4, 37, 1, 5000, 20, 255 }, + { blackhole_frame5, 37, 1, 5000, 20, 255 }, + { blackhole_frame6, 37, 1, 5000, 20, 255 }, + { blackhole_frame5, 37, 1, 5000, 20, 255 }, + { blackhole_frame4, 37, 1, 5000, 20, 255 }, + { blackhole_frame3, 37, 1, 5000, 20, 255 }, + { blackhole_frame2, 37, 1, 5000, 20, 255 }, + { blackhole_frame1, 37, 1, 5000, 20, 255 }, + { blackhole_frame0, 37, 1, 5000, 20, 255 }, + + // Fourth section: 10 seconds, 30Hz (12 frames) + { blackhole_frame1, 37, 1, 10000, 30, 255 }, + { blackhole_frame2, 37, 1, 10000, 30, 255 }, + { blackhole_frame3, 37, 1, 10000, 30, 255 }, + { blackhole_frame4, 37, 1, 10000, 30, 255 }, + { blackhole_frame5, 37, 1, 10000, 30, 255 }, + { blackhole_frame6, 37, 1, 10000, 30, 255 }, + { blackhole_frame5, 37, 1, 10000, 30, 255 }, + { blackhole_frame4, 37, 1, 10000, 30, 255 }, + { blackhole_frame3, 37, 1, 10000, 30, 255 }, + { blackhole_frame2, 37, 1, 10000, 30, 255 }, + { blackhole_frame1, 37, 1, 10000, 30, 255 }, + { blackhole_frame0, 37, 1, 10000, 30, 255 }, + + // Fifth section: 10 seconds, 20Hz (19 frames) + { blackhole_frame1, 37, 1, 10000, 20, 255 }, + { blackhole_frame2, 37, 1, 10000, 20, 255 }, + { blackhole_frame3, 37, 1, 10000, 20, 255 }, + { blackhole_frame4, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + { blackhole_frame6, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + { blackhole_frame4, 37, 1, 10000, 20, 255 }, + { blackhole_frame3, 37, 1, 10000, 20, 255 }, + { blackhole_frame2, 37, 1, 10000, 20, 255 }, + { blackhole_frame1, 37, 1, 10000, 20, 255 }, + { blackhole_frame0, 37, 1, 10000, 20, 255 }, + { blackhole_frame1, 37, 1, 10000, 20, 255 }, + { blackhole_frame2, 37, 1, 10000, 20, 255 }, + { blackhole_frame3, 37, 1, 10000, 20, 255 }, + { blackhole_frame4, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + { blackhole_frame6, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + }; + const uint16_t frameCount = sizeof(blackhole9Frames) / sizeof(blackhole9Frames[0]); + return mode_custom_shapes(blackhole9Frames, frameCount); +} + +// Metadata for the Black Hole 9 effect +static const char _data_FX_MODE_BLACK_HOLE_9[] PROGMEM = "Black Hole 9@Speed,!,,,,Smooth;;!"; + +// BLACK HOLE 15 EFFECT - Variable frequency version (10Hz->15Hz->20Hz->30Hz->20Hz) +uint16_t mode_black_hole_15() { + const Frame blackhole15Frames[] = { + // First section: 5 seconds, 10Hz (13 frames) + { blackhole_frame0, 37, 1, 5000, 10, 255 }, + { blackhole_frame1, 37, 1, 5000, 10, 255 }, + { blackhole_frame2, 37, 1, 5000, 10, 255 }, + { blackhole_frame3, 37, 1, 5000, 10, 255 }, + { blackhole_frame4, 37, 1, 5000, 10, 255 }, + { blackhole_frame5, 37, 1, 5000, 10, 255 }, + { blackhole_frame6, 37, 1, 5000, 10, 255 }, + { blackhole_frame5, 37, 1, 5000, 10, 255 }, + { blackhole_frame4, 37, 1, 5000, 10, 255 }, + { blackhole_frame3, 37, 1, 5000, 10, 255 }, + { blackhole_frame2, 37, 1, 5000, 10, 255 }, + { blackhole_frame1, 37, 1, 5000, 10, 255 }, + { blackhole_frame0, 37, 1, 5000, 10, 255 }, + + // Second section: 5 seconds, 15Hz (12 frames) + { blackhole_frame1, 37, 1, 5000, 15, 255 }, + { blackhole_frame2, 37, 1, 5000, 15, 255 }, + { blackhole_frame3, 37, 1, 5000, 15, 255 }, + { blackhole_frame4, 37, 1, 5000, 15, 255 }, + { blackhole_frame5, 37, 1, 5000, 15, 255 }, + { blackhole_frame6, 37, 1, 5000, 15, 255 }, + { blackhole_frame5, 37, 1, 5000, 15, 255 }, + { blackhole_frame4, 37, 1, 5000, 15, 255 }, + { blackhole_frame3, 37, 1, 5000, 15, 255 }, + { blackhole_frame2, 37, 1, 5000, 15, 255 }, + { blackhole_frame1, 37, 1, 5000, 15, 255 }, + { blackhole_frame0, 37, 1, 5000, 15, 255 }, + + // Third section: 5 seconds, 20Hz (12 frames) + { blackhole_frame1, 37, 1, 5000, 20, 255 }, + { blackhole_frame2, 37, 1, 5000, 20, 255 }, + { blackhole_frame3, 37, 1, 5000, 20, 255 }, + { blackhole_frame4, 37, 1, 5000, 20, 255 }, + { blackhole_frame5, 37, 1, 5000, 20, 255 }, + { blackhole_frame6, 37, 1, 5000, 20, 255 }, + { blackhole_frame5, 37, 1, 5000, 20, 255 }, + { blackhole_frame4, 37, 1, 5000, 20, 255 }, + { blackhole_frame3, 37, 1, 5000, 20, 255 }, + { blackhole_frame2, 37, 1, 5000, 20, 255 }, + { blackhole_frame1, 37, 1, 5000, 20, 255 }, + { blackhole_frame0, 37, 1, 5000, 20, 255 }, + + // Fourth section: 10 seconds, 30Hz (12 frames) + { blackhole_frame1, 37, 1, 10000, 30, 255 }, + { blackhole_frame2, 37, 1, 10000, 30, 255 }, + { blackhole_frame3, 37, 1, 10000, 30, 255 }, + { blackhole_frame4, 37, 1, 10000, 30, 255 }, + { blackhole_frame5, 37, 1, 10000, 30, 255 }, + { blackhole_frame6, 37, 1, 10000, 30, 255 }, + { blackhole_frame5, 37, 1, 10000, 30, 255 }, + { blackhole_frame4, 37, 1, 10000, 30, 255 }, + { blackhole_frame3, 37, 1, 10000, 30, 255 }, + { blackhole_frame2, 37, 1, 10000, 30, 255 }, + { blackhole_frame1, 37, 1, 10000, 30, 255 }, + { blackhole_frame0, 37, 1, 10000, 30, 255 }, + + // Fifth section: 10 seconds, 20Hz (19 frames) + { blackhole_frame1, 37, 1, 10000, 20, 255 }, + { blackhole_frame2, 37, 1, 10000, 20, 255 }, + { blackhole_frame3, 37, 1, 10000, 20, 255 }, + { blackhole_frame4, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + { blackhole_frame6, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + { blackhole_frame4, 37, 1, 10000, 20, 255 }, + { blackhole_frame3, 37, 1, 10000, 20, 255 }, + { blackhole_frame2, 37, 1, 10000, 20, 255 }, + { blackhole_frame1, 37, 1, 10000, 20, 255 }, + { blackhole_frame0, 37, 1, 10000, 20, 255 }, + { blackhole_frame1, 37, 1, 10000, 20, 255 }, + { blackhole_frame2, 37, 1, 10000, 20, 255 }, + { blackhole_frame3, 37, 1, 10000, 20, 255 }, + { blackhole_frame4, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + { blackhole_frame6, 37, 1, 10000, 20, 255 }, + { blackhole_frame5, 37, 1, 10000, 20, 255 }, + }; + const uint16_t frameCount = sizeof(blackhole15Frames) / sizeof(blackhole15Frames[0]); + return mode_custom_shapes(blackhole15Frames, frameCount); +} + +// Metadata for the Black Hole 15 effect +static const char _data_FX_MODE_BLACK_HOLE_15[] PROGMEM = "Black Hole 15@Speed,!,,,,Smooth;;!"; + +// BLACK HOLE CUSTOM - User-controllable speed and frequency +uint16_t mode_black_hole_custom() { + static uint32_t lastFrameTime = 0; + static uint8_t currentFrame = 0; + static uint32_t lastStrobeTime = 0; + static bool strobeState = true; + + // Frame sequence for black hole effect + const uint8_t *frameSequence[] = { + blackhole_frame0, blackhole_frame1, blackhole_frame2, blackhole_frame3, + blackhole_frame4, blackhole_frame5, blackhole_frame6, blackhole_frame5, + blackhole_frame4, blackhole_frame3, blackhole_frame2, blackhole_frame1 + }; + const uint8_t frameCount = sizeof(frameSequence) / sizeof(frameSequence[0]); + + // Reset on first call + if (SEGENV.call == 0) { + currentFrame = 0; + lastFrameTime = 0; + lastStrobeTime = 0; + strobeState = true; + } + + uint32_t currentTime = millis(); + + // Map speed slider to frame duration (100ms to 2000ms) + uint32_t frameDuration = map(SEGMENT.speed, 0, 255, 2000, 100); + + // Update frame index if needed + if (currentTime - lastFrameTime > frameDuration) { + lastFrameTime = currentTime; + currentFrame = (currentFrame + 1) % frameCount; + } + + // Map intensity slider to pulse frequency (0-50 Hz) + uint16_t pulseFrequency = map(SEGMENT.intensity, 0, 255, 0, 50); + + // Handle strobe timing + if (pulseFrequency > 0) { + uint32_t cycleTime = 1000 / pulseFrequency; + if (currentTime - lastStrobeTime > cycleTime / 2) { + lastStrobeTime = currentTime; + strobeState = !strobeState; + } + } else { + strobeState = true; // Always on when intensity is 0 + } + + // Get primary color + uint32_t primaryColor = SEGCOLOR(0); + if (primaryColor == BLACK) { + primaryColor = SEGMENT.color_from_palette(0, true, PALETTE_SOLID_WRAP, 0); + } + + // Apply pattern to LEDs + const uint8_t *currentPattern = frameSequence[currentFrame]; + for (uint16_t i = 0; i < SEGLEN && i < 37; i++) { + uint8_t pixelValue = currentPattern[i]; + if (!strobeState || pixelValue == 0) { + SEGMENT.setPixelColor(i, BLACK); + } else { + SEGMENT.setPixelColor(i, primaryColor); + } + } + + return FRAMETIME; +} + +// Metadata for Black Hole Custom effect +static const char _data_FX_MODE_BLACK_HOLE_CUSTOM[] PROGMEM = "Black Hole (Custom)@Speed,Frequency;;!;"; + +// Metadata for hertz testing effect +static const char _data_FX_MODE_HERTZ_TESTING[] PROGMEM = "Phosphene Pulse@Frequency Steps,Brightness;;!;"; + +// ============================================================================= +// ADDITIONAL TEST EFFECTS +// ============================================================================= + +// 1. BREATHING SQUARE EFFECT - Simple expanding/contracting square pattern +const uint8_t square1[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +const uint8_t square2[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1, 0, 0, + 0, 0, 1, 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, 1, 0, 0, + 0, 0, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +const uint8_t square3[] = { + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, +}; + +uint16_t mode_breathing_square() { + const Frame breathFrames[] = { + { square1, 8, 8, 800, 0, 255 }, // Small center square, 0.8s duration, full brightness + { square2, 8, 8, 600, 0, 255 }, // Medium square, 0.6s duration, full brightness + { square3, 8, 8, 800, 0, 255 }, // Large square, 0.8s duration, full brightness + { square2, 8, 8, 600, 0, 255 }, // Back to medium, 0.6s duration, full brightness + }; + const uint16_t frameCount = sizeof(breathFrames) / sizeof(breathFrames[0]); + return mode_custom_shapes(breathFrames, frameCount); +} + +// 2. SPIRAL WAVE EFFECT - Rotating spiral pattern +const uint8_t spiral1[] = { + 1, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 1, 1, +}; + +const uint8_t spiral2[] = { + 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, +}; + +const uint8_t spiral3[] = { + 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, +}; + +const uint8_t spiral4[] = { + 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, +}; + +uint16_t mode_spiral_wave() { + const Frame spiralFrames[] = { + { spiral1, 8, 8, 200, 0, 255 }, // Fast rotation, 0.2s per frame, full brightness + { spiral2, 8, 8, 200, 0, 255 }, // full brightness + { spiral3, 8, 8, 200, 0, 255 }, // full brightness + { spiral4, 8, 8, 200, 0, 255 }, // full brightness + }; + const uint16_t frameCount = sizeof(spiralFrames) / sizeof(spiralFrames[0]); + return mode_custom_shapes(spiralFrames, frameCount); +} + +// 3. PULSING CROSS EFFECT - Cross pattern with varying intensity +const uint8_t cross1[] = { + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, +}; + +const uint8_t cross2[] = { + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 0, 0, +}; + +const uint8_t cross3[] = { + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, +}; + +uint16_t mode_pulsing_cross() { + const Frame crossFrames[] = { + { cross1, 7, 7, 600, 0, 255 }, // Thin cross, 0.6s duration, full brightness + { cross2, 7, 7, 400, 0, 255 }, // Thick cross, 0.4s duration, full brightness + { cross3, 7, 7, 300, 0, 255 }, // Full brightness, 0.3s duration + { cross2, 7, 7, 400, 0, 255 }, // Back to thick cross, full brightness + }; + const uint16_t frameCount = sizeof(crossFrames) / sizeof(crossFrames[0]); + return mode_custom_shapes(crossFrames, frameCount); +} + +// 4. LINEAR STRIPE EFFECT - Horizontal stripes moving vertically +const uint8_t stripe1[] = { + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +const uint8_t stripe2[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, +}; + +const uint8_t stripe3[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +uint16_t mode_moving_stripes() { + const Frame stripeFrames[] = { + { stripe1, 8, 8, 300, 0, 255 }, // Pattern 1, 0.3s duration, full brightness + { stripe2, 8, 8, 300, 0, 255 }, // Pattern 2, 0.3s duration, full brightness + { stripe3, 8, 8, 300, 0, 255 }, // Pattern 3, 0.3s duration, full brightness + }; + const uint16_t frameCount = sizeof(stripeFrames) / sizeof(stripeFrames[0]); + return mode_custom_shapes(stripeFrames, frameCount); +} + +// 5. CORNER FLASH EFFECT - Alternating corner patterns for stress testing +const uint8_t corner1[] = { + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 0, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 0, 1, 1, +}; + +const uint8_t corner2[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +uint16_t mode_corner_flash() { + const Frame cornerFrames[] = { + { corner1, 8, 8, 150, 0, 255 }, // Corners lit, fast 0.15s duration, full brightness + { corner2, 8, 8, 150, 0, 255 }, // Center lit, fast 0.15s duration, full brightness + }; + const uint16_t frameCount = sizeof(cornerFrames) / sizeof(cornerFrames[0]); + return mode_custom_shapes(cornerFrames, frameCount); +} + +// Effect metadata +static const char _data_FX_MODE_BREATHING_SQUARE[] PROGMEM = "Breathing Square@Speed,Brightness;;!;"; +static const char _data_FX_MODE_SPIRAL_WAVE[] PROGMEM = "Spiral Wave@Speed,Brightness;;!;"; +static const char _data_FX_MODE_PULSING_CROSS[] PROGMEM = "Pulsing Cross@Speed,Brightness;;!;"; +static const char _data_FX_MODE_MOVING_STRIPES[] PROGMEM = "Moving Stripes@Speed,Brightness;;!;"; +static const char _data_FX_MODE_CORNER_FLASH[] PROGMEM = "Corner Flash@Speed,Brightness;;!;"; + +// Hz-controlled versions metadata +static const char _data_FX_MODE_BREATHING_SQUARE_HZ[] PROGMEM = "Breathing Square Hz@Pattern Speed,Flash Hz (0-50);;!;"; +static const char _data_FX_MODE_SPIRAL_WAVE_HZ[] PROGMEM = "Spiral Wave Hz@Pattern Speed,Flash Hz (0-50);;!;"; +static const char _data_FX_MODE_PULSING_CROSS_HZ[] PROGMEM = "Pulsing Cross Hz@Pattern Speed,Flash Hz (0-50);;!;"; +static const char _data_FX_MODE_MOVING_STRIPES_HZ[] PROGMEM = "Moving Stripes Hz@Pattern Speed,Flash Hz (0-50);;!;"; +static const char _data_FX_MODE_CORNER_FLASH_HZ[] PROGMEM = "Corner Flash Hz@Pattern Speed,Flash Hz (0-50);;!;"; + +// Metadata for high frequency testing +static const char _data_FX_MODE_HIGH_FREQ_TEST[] PROGMEM = "High Freq Test@Speed (25-50Hz),Brightness;;!;"; + +// ============================================================================= +// JOURNEY EFFECTS +// ============================================================================= + +// aSimplestar +const uint8_t aSIMPLESTAR200[] = { + 1,0,0,1, + 0,1,0,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 0,0,1,1,0,0, + 0,1,0,1,0, + 1,0,0,1 +}; +const uint8_t aSimplestar30[] = { + 1,0,0,1, + 0,1,0,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 0,0,1,1,0,0, + 0,1,0,1,0, + 1,0,0,1 +}; +const uint8_t aSimplestar60[] = { + 1,0,0,1, + 0,1,0,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 0,0,1,1,0,0, + 0,1,0,1,0, + 1,0,0,1 +}; +const uint8_t aSimplestar90[] = { + 1,0,0,1, + 0,1,0,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 0,0,1,1,0,0, + 0,1,0,1,0, + 1,0,0,1 +}; +const uint8_t aSimplestar120[] = { + 1,0,0,1, + 0,1,0,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 0,0,1,1,0,0, + 0,1,0,1,0, + 1,0,0,1 +}; +uint16_t mode_aSimplestar3() { + const Frame frames[] = { { aSimplestar30, 37, 1, 60, 3, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSimplestar6() { + const Frame frames[] = { { aSimplestar60, 37, 1, 60, 6, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSimplestar9() { + const Frame frames[] = { { aSimplestar90, 37, 1, 60, 9, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSimplestar12() { + const Frame frames[] = { { aSimplestar120, 37, 1, 60, 12, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSIMPLESTAR20() { + const Frame frames[] = { { aSIMPLESTAR200, 37, 1, 60, 20, 255 } }; + return mode_custom_shapes(frames, 1); +} +static const char _data_FX_MODE_ASIMPLESTAR3[] PROGMEM = "aSimplestar3@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIMPLESTAR6[] PROGMEM = "aSimplestar6@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIMPLESTAR9[] PROGMEM = "aSimplestar9@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIMPLESTAR12[] PROGMEM = "aSimplestar12@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIMPLESTAR20[] PROGMEM = "aSIMPLESTAR20@!,!,,,,Smooth;;!"; + +// aFull +const uint8_t aFull30[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aFull60[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aFull90[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aFull120[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aFull200[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +uint16_t mode_aFull3() { + const Frame frames[] = { { aFull30, 37, 1, 60, 3, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aFull6() { + const Frame frames[] = { { aFull60, 37, 1, 60, 6, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aFull9() { + const Frame frames[] = { { aFull90, 37, 1, 60, 9, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aFull12() { + const Frame frames[] = { { aFull120, 37, 1, 60, 12, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aFull20() { + const Frame frames[] = { { aFull200, 37, 1, 60, 20, 255 } }; + return mode_custom_shapes(frames, 1); +} +static const char _data_FX_MODE_AFULL3[] PROGMEM = "aFull3@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AFULL6[] PROGMEM = "aFull6@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AFULL9[] PROGMEM = "aFull9@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AFULL12[] PROGMEM = "aFull12@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AFULL20[] PROGMEM = "aFull20@!,!,,,,Smooth;;!"; + +// aMerkabah +const uint8_t aMerkabah30[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aMerkabah60[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aMerkabah90[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aMerkabah120[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +const uint8_t aMerkabah200[] = { + 0,0,0,0, + 0,0,1,0,0, + 0,1,1,1,1,0, + 0,0,1,1,1,0,0, + 0,1,1,1,1,0, + 0,0,1,0,0, + 0,0,0,0 +}; +uint16_t mode_aMerkabah3() { + const Frame frames[] = { { aMerkabah30, 37, 1, 60, 3, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aMerkabah6() { + const Frame frames[] = { { aMerkabah60, 37, 1, 60, 6, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aMerkabah9() { + const Frame frames[] = { { aMerkabah90, 37, 1, 60, 9, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aMerkabah12() { + const Frame frames[] = { { aMerkabah120, 37, 1, 60, 12, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aMerkabah20() { + const Frame frames[] = { { aMerkabah200, 37, 1, 60, 20, 255 } }; + return mode_custom_shapes(frames, 1); +} +static const char _data_FX_MODE_AMERKABAH3[] PROGMEM = "aMerkabah3@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AMERKABAH6[] PROGMEM = "aMerkabah6@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AMERKABAH9[] PROGMEM = "aMerkabah9@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AMERKABAH12[] PROGMEM = "aMerkabah12@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AMERKABAH20[] PROGMEM = "aMerkabah20@!,!,,,,Smooth;;!"; + +// aSidewaysMerkabah +const uint8_t aSidewaysMerkabah30[] = { + 0,0,0,0, + 0,1,0,1,0, + 0,0,1,1,0,0, + 0,1,1,1,1,1,0, + 0,0,1,1,0,0, + 0,1,0,1,0, + 0,0,0,0 +}; +const uint8_t aSidewaysMerkabah60[] = { + 0,0,0,0, + 0,1,0,1,0, + 0,0,1,1,0,0, + 0,1,1,1,1,1,0, + 0,0,1,1,0,0, + 0,1,0,1,0, + 0,0,0,0 +}; +const uint8_t aSidewaysMerkabah90[] = { + 0,0,0,0, + 0,1,0,1,0, + 0,0,1,1,0,0, + 0,1,1,1,1,1,0, + 0,0,1,1,0,0, + 0,1,0,1,0, + 0,0,0,0 +}; +const uint8_t aSidewaysMerkabah120[] = { + 0,0,0,0, + 0,1,0,1,0, + 0,0,1,1,0,0, + 0,1,1,1,1,1,0, + 0,0,1,1,0,0, + 0,1,0,1,0, + 0,0,0,0 +}; +const uint8_t aSidewaysMerkabah200[] = { + 0,0,0,0, + 0,1,0,1,0, + 0,0,1,1,0,0, + 0,1,1,1,1,1,0, + 0,0,1,1,0,0, + 0,1,0,1,0, + 0,0,0,0 +}; +uint16_t mode_aSidewaysMerkabah3() { + const Frame frames[] = { { aSidewaysMerkabah30, 37, 1, 60, 3, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSidewaysMerkabah6() { + const Frame frames[] = { { aSidewaysMerkabah60, 37, 1, 60, 6, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSidewaysMerkabah9() { + const Frame frames[] = { { aSidewaysMerkabah90, 37, 1, 60, 9, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSidewaysMerkabah12() { + const Frame frames[] = { { aSidewaysMerkabah120, 37, 1, 60, 12, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aSidewaysMerkabah20() { + const Frame frames[] = { { aSidewaysMerkabah200, 37, 1, 60, 20, 255 } }; + return mode_custom_shapes(frames, 1); +} +static const char _data_FX_MODE_ASIDEWAYSMERKABAH3[] PROGMEM = "aSidewaysMerkabah3@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIDEWAYSMERKABAH6[] PROGMEM = "aSidewaysMerkabah6@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIDEWAYSMERKABAH9[] PROGMEM = "aSidewaysMerkabah9@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIDEWAYSMERKABAH12[] PROGMEM = "aSidewaysMerkabah12@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ASIDEWAYSMERKABAH20[] PROGMEM = "aSidewaysMerkabah20@!,!,,,,Smooth;;!"; + +// aHourglass +const uint8_t aHourglass30[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 0,0,0,1,0,0,0, + 0,0,1,1,0,0, + 0,1,1,1,0, + 1,1,1,1 +}; +const uint8_t aHourglass60[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 0,0,0,1,0,0,0, + 0,0,1,1,0,0, + 0,1,1,1,0, + 1,1,1,1 +}; +const uint8_t aHourglass90[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 0,0,0,1,0,0,0, + 0,0,1,1,0,0, + 0,1,1,1,0, + 1,1,1,1 +}; +const uint8_t aHourglass120[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 0,0,0,1,0,0,0, + 0,0,1,1,0,0, + 0,1,1,1,0, + 1,1,1,1 +}; +const uint8_t aHourglass200[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 0,0,0,1,0,0,0, + 0,0,1,1,0,0, + 0,1,1,1,0, + 1,1,1,1 +}; +uint16_t mode_aHourglass3() { + const Frame frames[] = { { aHourglass30, 37, 1, 60, 3, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aHourglass6() { + const Frame frames[] = { { aHourglass60, 37, 1, 60, 6, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aHourglass9() { + const Frame frames[] = { { aHourglass90, 37, 1, 60, 9, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aHourglass12() { + const Frame frames[] = { { aHourglass120, 37, 1, 60, 12, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aHourglass20() { + const Frame frames[] = { { aHourglass200, 37, 1, 60, 20, 255 } }; + return mode_custom_shapes(frames, 1); +} +static const char _data_FX_MODE_AHOURGLASS3[] PROGMEM = "aHourglass3@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AHOURGLASS6[] PROGMEM = "aHourglass6@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AHOURGLASS9[] PROGMEM = "aHourglass9@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AHOURGLASS12[] PROGMEM = "aHourglass12@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_AHOURGLASS20[] PROGMEM = "aHourglass20@!,!,,,,Smooth;;!"; + +// AlmostNuke +const uint8_t AlmostNuke30[] = { + 0,0,0,0, + 1,0,0,0,1, + 1,1,0,0,1,1, + 0,0,0,1,0,0,0, + 0,0,0,0,0,0, + 0,0,1,0,0, + 0,1,1,0 +}; +const uint8_t AlmostNuke60[] = { + 0,0,0,0, + 1,0,0,0,1, + 1,1,0,0,1,1, + 0,0,0,1,0,0,0, + 0,0,0,0,0,0, + 0,0,1,0,0, + 0,1,1,0 +}; +const uint8_t AlmostNuke90[] = { + 0,0,0,0, + 1,0,0,0,1, + 1,1,0,0,1,1, + 0,0,0,1,0,0,0, + 0,0,0,0,0,0, + 0,0,1,0,0, + 0,1,1,0 +}; +const uint8_t AlmostNuke120[] = { + 0,0,0,0, + 1,0,0,0,1, + 1,1,0,0,1,1, + 0,0,0,1,0,0,0, + 0,0,0,0,0,0, + 0,0,1,0,0, + 0,1,1,0 +}; +const uint8_t AlmostNuke200[] = { + 0,0,0,0, + 1,0,0,0,1, + 1,1,0,0,1,1, + 0,0,0,1,0,0,0, + 0,0,0,0,0,0, + 0,0,1,0,0, + 0,1,1,0 +}; +uint16_t mode_AlmostNuke3() { + const Frame frames[] = { { AlmostNuke30, 37, 1, 60, 3, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_AlmostNuke6() { + const Frame frames[] = { { AlmostNuke60, 37, 1, 60, 6, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_AlmostNuke9() { + const Frame frames[] = { { AlmostNuke90, 37, 1, 60, 9, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_AlmostNuke12() { + const Frame frames[] = { { AlmostNuke120, 37, 1, 60, 12, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_AlmostNuke20() { + const Frame frames[] = { { AlmostNuke200, 37, 1, 60, 20, 255 } }; + return mode_custom_shapes(frames, 1); +} +static const char _data_FX_MODE_ALMOSTNUKE3[] PROGMEM = "AlmostNuke3@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ALMOSTNUKE6[] PROGMEM = "AlmostNuke6@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ALMOSTNUKE9[] PROGMEM = "AlmostNuke9@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ALMOSTNUKE12[] PROGMEM = "AlmostNuke12@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ALMOSTNUKE20[] PROGMEM = "AlmostNuke20@!,!,,,,Smooth;;!"; + +// aNuke +const uint8_t aNuke30[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 1,1,1,1,1,1, + 1,1,0,1,1, + 1,0,0,1 +}; +const uint8_t aNuke60[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 1,1,1,1,1,1, + 1,1,0,1,1, + 1,0,0,1 +}; +const uint8_t aNuke90[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 1,1,1,1,1,1, + 1,1,0,1,1, + 1,0,0,1 +}; +const uint8_t aNuke120[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 1,1,1,1,1,1, + 1,1,0,1,1, + 1,0,0,1 +}; +const uint8_t aNuke200[] = { + 1,1,1,1, + 0,1,1,1,0, + 0,0,1,1,0,0, + 1,1,1,1,1,1,1, + 1,1,1,1,1,1, + 1,1,0,1,1, + 1,0,0,1 +}; +uint16_t mode_aNuke3() { + const Frame frames[] = { { aNuke30, 37, 1, 60, 3, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aNuke6() { + const Frame frames[] = { { aNuke60, 37, 1, 60, 6, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aNuke9() { + const Frame frames[] = { { aNuke90, 37, 1, 60, 9, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aNuke12() { + const Frame frames[] = { { aNuke120, 37, 1, 60, 12, 255 } }; + return mode_custom_shapes(frames, 1); +} +uint16_t mode_aNuke20() { + const Frame frames[] = { { aNuke200, 37, 1, 60, 20, 255 } }; + return mode_custom_shapes(frames, 1); +} +static const char _data_FX_MODE_ANUKE3[] PROGMEM = "aNuke3@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ANUKE6[] PROGMEM = "aNuke6@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ANUKE9[] PROGMEM = "aNuke9@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ANUKE12[] PROGMEM = "aNuke12@!,!,,,,Smooth;;!"; +static const char _data_FX_MODE_ANUKE20[] PROGMEM = "aNuke20@!,!,,,,Smooth;;!"; + +// ============================================================================= +// HZ-CONTROLLED PATTERN EFFECTS +// ============================================================================= +// These effects combine pattern animation with Hz-based flashing +// Speed slider controls pattern animation speed +// Intensity slider controls flash frequency (0-50Hz) + +// Helper function to apply Hz-based flashing to any pattern +uint16_t mode_pattern_with_hz(const Frame frames[], uint16_t frameCount) { + // Get flash frequency from intensity slider (0-50Hz) + uint16_t flashHz = 0; + if (SEGMENT.intensity > 0) { + flashHz = map(SEGMENT.intensity, 1, 255, 1, 50); + } + + // Determine if we should show the pattern based on Hz timing + bool showPattern = true; + if (flashHz > 0) { + uint32_t period = 1000 / flashHz; + uint32_t halfPeriod = period / 2; + uint32_t now = millis(); + uint32_t phase = now % period; + showPattern = (phase < halfPeriod); + } + + // If not showing pattern, black out all pixels and return + if (!showPattern) { + for (uint16_t i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, BLACK); + } + return FRAMETIME; + } + + // Otherwise, render the pattern normally + // This code is adapted from mode_custom_shapes with timing fix for high Hz + if (!frames || frameCount == 0 || frameCount > 255) { + return FRAMETIME; + } + + if (SEGENV.call == 0) { + SEGENV.aux0 = millis(); + SEGENV.aux1 = 0; + SEGENV.step = 0; + } + + uint16_t currentFrame = SEGENV.aux1; + if (currentFrame >= frameCount) { + currentFrame = 0; + SEGENV.aux1 = 0; + } + + const Frame &frame = frames[currentFrame]; + if (!frame.data) { + SEGENV.aux1 = (currentFrame + 1) % frameCount; + return FRAMETIME; + } + + uint32_t patternSize = (uint32_t)frame.width * frame.height; + if (patternSize == 0 || patternSize > 10000) { + SEGENV.aux1 = (currentFrame + 1) % frameCount; + return FRAMETIME; + } + + // Speed-based timing for pattern animation + uint32_t baseDuration = (frame.baseDuration > 0) ? frame.baseDuration : 1000; + uint32_t frameTime; + + if (SEGMENT.speed < 128) { + frameTime = map(SEGMENT.speed, 0, 127, baseDuration * 4, baseDuration); + } else { + frameTime = map(SEGMENT.speed, 128, 255, baseDuration, baseDuration / 4); + } + + // Allow faster timing for Hz effects (10ms minimum instead of 33ms) + frameTime = constrain(frameTime, 10, 10000); + + uint32_t now = millis(); + bool shouldAdvanceFrame = false; + + if (now >= SEGENV.aux0) { + shouldAdvanceFrame = (now - SEGENV.aux0) >= frameTime; + } else { + shouldAdvanceFrame = true; + } + + if (shouldAdvanceFrame) { + SEGENV.aux0 = now; + SEGENV.aux1 = (currentFrame + 1) % frameCount; + } + + // Apply pattern + uint16_t segmentLength = SEGLEN; + if (segmentLength > 1000) { + segmentLength = 1000; + } + + uint32_t primaryColor = SEGCOLOR(0); + if (primaryColor == BLACK) { + primaryColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 0); + if (primaryColor == BLACK) { + primaryColor = WHITE; + } + } + + for (uint16_t i = 0; i < segmentLength; i++) { + uint32_t patternIndex = i % patternSize; + + if (frame.data) { + uint8_t patternValue = frame.data[patternIndex]; + + if (patternValue == 0) { + SEGMENT.setPixelColor(i, BLACK); + } else { + SEGMENT.setPixelColor(i, primaryColor); + } + } else { + SEGMENT.setPixelColor(i, BLACK); + } + } + + return FRAMETIME; +} + +// 1. BREATHING SQUARE HZ - Breathing square with Hz control +uint16_t mode_breathing_square_hz() { + const Frame breathFrames[] = { + { square1, 8, 8, 800, 0, 255 }, // full brightness + { square2, 8, 8, 600, 0, 255 }, // full brightness + { square3, 8, 8, 800, 0, 255 }, // full brightness + { square2, 8, 8, 600, 0, 255 }, // full brightness + }; + const uint16_t frameCount = sizeof(breathFrames) / sizeof(breathFrames[0]); + return mode_pattern_with_hz(breathFrames, frameCount); +} + +// 2. SPIRAL WAVE HZ - Spiral wave with Hz control +uint16_t mode_spiral_wave_hz() { + const Frame spiralFrames[] = { + { spiral1, 8, 8, 200, 0, 255 }, // full brightness + { spiral2, 8, 8, 200, 0, 255 }, // full brightness + { spiral3, 8, 8, 200, 0, 255 }, // full brightness + { spiral4, 8, 8, 200, 0, 255 }, // full brightness + }; + const uint16_t frameCount = sizeof(spiralFrames) / sizeof(spiralFrames[0]); + return mode_pattern_with_hz(spiralFrames, frameCount); +} + +// 3. PULSING CROSS HZ - Pulsing cross with Hz control +uint16_t mode_pulsing_cross_hz() { + const Frame crossFrames[] = { + { cross1, 7, 7, 600, 0, 255 }, // full brightness + { cross2, 7, 7, 400, 0, 255 }, // full brightness + { cross3, 7, 7, 300, 0, 255 }, // full brightness + { cross2, 7, 7, 400, 0, 255 }, // full brightness + }; + const uint16_t frameCount = sizeof(crossFrames) / sizeof(crossFrames[0]); + return mode_pattern_with_hz(crossFrames, frameCount); +} + +// 4. MOVING STRIPES HZ - Moving stripes with Hz control +uint16_t mode_moving_stripes_hz() { + const Frame stripeFrames[] = { + { stripe1, 8, 8, 300, 0, 255 }, // full brightness + { stripe2, 8, 8, 300, 0, 255 }, // full brightness + { stripe3, 8, 8, 300, 0, 255 }, // full brightness + }; + const uint16_t frameCount = sizeof(stripeFrames) / sizeof(stripeFrames[0]); + return mode_pattern_with_hz(stripeFrames, frameCount); +} + +// 5. CORNER FLASH HZ - Corner flash with Hz control +uint16_t mode_corner_flash_hz() { + const Frame cornerFrames[] = { + { corner1, 8, 8, 150, 0, 255 }, // full brightness + { corner2, 8, 8, 150, 0, 255 }, // full brightness + }; + const uint16_t frameCount = sizeof(cornerFrames) / sizeof(cornerFrames[0]); + return mode_pattern_with_hz(cornerFrames, frameCount); +} + +// STATIC HZ TEST - Pure Hz testing with smooth 0-50Hz control +uint16_t mode_static_hz_test() { + // Get flash frequency from intensity slider (0-50Hz) + // Intensity 0 = static on (no flashing) + // Intensity 255 = 50Hz + uint16_t flashHz = 0; + if (SEGMENT.intensity > 0) { + flashHz = map(SEGMENT.intensity, 1, 255, 1, 50); + } + + // Get brightness from speed slider + uint8_t brightness = SEGMENT.speed; + + // Determine if we should be on based on Hz timing + bool isOn = true; + if (flashHz > 0) { + uint32_t period = 1000 / flashHz; + uint32_t halfPeriod = period / 2; + uint32_t now = millis(); + uint32_t phase = now % period; + isOn = (phase < halfPeriod); + } + + // Set color based on state + uint32_t color = BLACK; + if (isOn && brightness > 0) { + // Get primary color or use white + color = SEGCOLOR(0); + if (color == BLACK) { + // Create white with brightness + uint8_t val = brightness; + color = ((uint32_t)val << 16) | ((uint32_t)val << 8) | val; + } else { + // Scale existing color by brightness + uint8_t r = (color >> 16 & 0xFF) * brightness / 255; + uint8_t g = (color >> 8 & 0xFF) * brightness / 255; + uint8_t b = (color & 0xFF) * brightness / 255; + color = ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; + } + } + + // Limit pixel count based on frequency for performance + uint16_t maxPixels = SEGLEN; + if (flashHz >= 40) { + maxPixels = min((uint16_t)100, SEGLEN); // 40-50Hz: max 100 pixels + } else if (flashHz >= 30) { + maxPixels = min((uint16_t)200, SEGLEN); // 30-40Hz: max 200 pixels + } else if (flashHz >= 20) { + maxPixels = min((uint16_t)500, SEGLEN); // 20-30Hz: max 500 pixels + } + // Below 20Hz: no limit needed + + // Update pixels + for (uint16_t i = 0; i < maxPixels; i++) { + SEGMENT.setPixelColor(i, color); + } + + // Clear any remaining pixels if we limited the count + for (uint16_t i = maxPixels; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, BLACK); + } + + return FRAMETIME; +} + +// Metadata for static Hz test +static const char _data_FX_MODE_STATIC_HZ_TEST[] PROGMEM = "Static Hz Test@Brightness,Flash Hz (0-50);;!;"; + + +// Custom spin +// me +// Define frames as arrays of color values +const uint8_t cframe0[] = { + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, +}; + +const uint8_t cframe1[] = { + 0, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 0, +}; + +const uint8_t cframe2[] = { + 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 1, 1, +}; + +const uint8_t cframe3[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}; + +const uint8_t cframe4[] = { + 0, 0, 0, 0, 0, 0, 1, 1, + 0, 0, 0, 0, 0, 1, 1, 0, + 0, 0, 0, 0, 1, 1, 0, 0, + 0, 0, 0, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 0, 0, 0, + 0, 0, 1, 1, 0, 0, 0, 0, + 0, 1, 1, 0, 0, 0, 0, 0, + 1, 1, 0, 0, 0, 0, 0, 0, +}; + +const uint8_t cframe5[] = { + 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 1, 1, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 1, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, +}; + +uint16_t mode_custom_circles() { + const Frame cframes[] = { + { cframe0, 8, 8, 2000, 0, 255 }, // 8x8 pattern, 2s base duration, no pulse, full brightness + { cframe1, 8, 8, 500, 0, 255 }, // 8x8 pattern, 0.5s base duration, no pulse, full brightness + { cframe2, 8, 8, 2000, 0, 255 }, // 8x8 pattern, 2s base duration, no pulse, full brightness + { cframe3, 8, 8, 2000, 0, 255 }, // 8x8 pattern, 2s base duration, no pulse, full brightness + { cframe4, 8, 8, 500, 0, 255 }, // 8x8 pattern, 0.5s base duration, no pulse, full brightness + { cframe5, 8, 8, 2000, 0, 255 }, // 8x8 pattern, 2s base duration, no pulse, full brightness + { cframe0, 8, 8, 200, 0, 255 }, // 8x8 pattern, 0.2s base duration, no pulse, full brightness + { cframe1, 8, 8, 8000, 4, 255 }, // 8x8 pattern, 8s base duration, 4Hz base pulse, full brightness + { cframe5, 8, 8, 2000, 0, 255 }, // 8x8 pattern, 2s base duration, no pulse, full brightness + { cframe3, 8, 8, 200, 0, 255 }, // 8x8 pattern, 0.2s base duration, no pulse, full brightness + { cframe4, 8, 8, 500, 0, 255 }, // 8x8 pattern, 0.5s base duration, no pulse, full brightness + { cframe5, 8, 8, 2000, 0, 255 }, // 8x8 pattern, 2s base duration, no pulse, full brightness + }; + const uint16_t frameCount = sizeof(cframes) / sizeof(cframes[0]); + return mode_custom_shapes(cframes, frameCount); + // static uint32_t lastFrameTime = 0; + // static uint8_t currentFrame = 0; // Frame index: 0, 1, or 2 + // const uint32_t *frames[] = {cframe0, cframe1, cframe2, cframe3, cframe4, cframe5}; + // const uint16_t frameCount = sizeof(frames) / sizeof(frames[0]); + // uint16_t pulseFrequency = 20; // Hz + // uint16_t pulsePeriod = 1000 / pulseFrequency; // Pulse duration in ms + // uint8_t brightness = (sin((millis() % pulsePeriod) * (PI * 2) / pulsePeriod) + 1) * 127.5; + + // // Switch frames every 500ms + // if (millis() - lastFrameTime > 500) { + // lastFrameTime = millis(); + // currentFrame = (currentFrame + 1) % frameCount; + // } + + // // Get the current frame + // const uint32_t *currentColors = frames[currentFrame]; + + // // Apply the frame and strobe effect + // for (uint16_t i = 0; i < SEGLEN; i++) { + // uint32_t color = currentColors[i]; + // if (color == 0x000000) { + // SEGMENT.setPixelColor(i, 0); // Off + // } else { + // // Apply brightness to "on" LEDs + // uint8_t r = ((color >> 16) & 0xFF) * brightness / 255; + // uint8_t g = ((color >> 8) & 0xFF) * brightness / 255; + // uint8_t b = (color & 0xFF) * brightness / 255; + // SEGMENT.setPixelColor(i, r, g, b); + // } + // } + + // return FRAMETIME; +} +// Metadata for the custom effect +static const char _data_FX_MODE_CIRCLE[] PROGMEM = "Custom Circles@!,!,,,,Smooth;;!"; + + + //Soap //@Stepko //Idea from https://www.youtube.com/watch?v=DiHBgITrZck&ab_channel=StefanPetrick @@ -7695,6 +10244,71 @@ void WS2812FX::setupEffectData() { } // now replace all pre-allocated effects // --- 1D non-audio effects --- + addEffect(FX_MODE_CUSTOM_BEN, &mode_custom_ben, _data_FX_MODE_CUSTOM_BEN); + addEffect(FX_MODE_CUSTOM_NOVAS, &mode_custom_novas, _data_FX_MODE_CUSTOM_NOVAS); + addEffect(FX_MODE_CUSTOM_NOVAS_3_9HZ, &mode_custom_novas_3_9hz, _data_FX_MODE_CUSTOM_NOVAS_3_9HZ); + addEffect(FX_MODE_CUSTOM_NOVAS_INVERTED, &mode_custom_novas_inverted, _data_FX_MODE_CUSTOM_NOVAS_INVERTED); + addEffect(FX_MODE_BLACK_HOLE, &mode_black_hole, _data_FX_MODE_BLACK_HOLE); + addEffect(FX_MODE_BLACK_HOLE_3, &mode_black_hole_3, _data_FX_MODE_BLACK_HOLE_3); + addEffect(FX_MODE_BLACK_HOLE_6, &mode_black_hole_6, _data_FX_MODE_BLACK_HOLE_6); + addEffect(FX_MODE_BLACK_HOLE_9, &mode_black_hole_9, _data_FX_MODE_BLACK_HOLE_9); + addEffect(FX_MODE_BLACK_HOLE_15, &mode_black_hole_15, _data_FX_MODE_BLACK_HOLE_15); + addEffect(FX_MODE_BLACK_HOLE_CUSTOM, &mode_black_hole_custom, _data_FX_MODE_BLACK_HOLE_CUSTOM); + addEffect(FX_MODE_HERTZ_TESTING, &mode_hertz_testing, _data_FX_MODE_HERTZ_TESTING); + addEffect(FX_MODE_HIGH_FREQ_TEST, &mode_high_frequency_test, _data_FX_MODE_HIGH_FREQ_TEST); + addEffect(FX_MODE_CUSTOM_D_DIAMOND_SPIN, &mode_custom_drunk_diamond_spin, _data_FX_MODE_CUSTOM_D_DIAMOND_SPIN); + addEffect(FX_MODE_CUSTOM_DIAMOND_SPIN, &mode_custom_diamond_spin, _data_FX_MODE_CUSTOM_DIAMOND_SPIN); + addEffect(FX_MODE_CUSTOM_SQUARES, &mode_custom_squares, _data_FX_MODE_CUSTOM); + addEffect(FX_MODE_CUSTOM_CIRCLES, &mode_custom_circles, _data_FX_MODE_CIRCLE); + // New test effects + addEffect(FX_MODE_BREATHING_SQUARE, &mode_breathing_square, _data_FX_MODE_BREATHING_SQUARE); + addEffect(FX_MODE_SPIRAL_WAVE, &mode_spiral_wave, _data_FX_MODE_SPIRAL_WAVE); + addEffect(FX_MODE_PULSING_CROSS, &mode_pulsing_cross, _data_FX_MODE_PULSING_CROSS); + addEffect(FX_MODE_MOVING_STRIPES, &mode_moving_stripes, _data_FX_MODE_MOVING_STRIPES); + addEffect(FX_MODE_CORNER_FLASH, &mode_corner_flash, _data_FX_MODE_CORNER_FLASH); + // Hz-controlled pattern effects + addEffect(FX_MODE_BREATHING_SQUARE_HZ, &mode_breathing_square_hz, _data_FX_MODE_BREATHING_SQUARE_HZ); + addEffect(FX_MODE_SPIRAL_WAVE_HZ, &mode_spiral_wave_hz, _data_FX_MODE_SPIRAL_WAVE_HZ); + addEffect(FX_MODE_PULSING_CROSS_HZ, &mode_pulsing_cross_hz, _data_FX_MODE_PULSING_CROSS_HZ); + addEffect(FX_MODE_MOVING_STRIPES_HZ, &mode_moving_stripes_hz, _data_FX_MODE_MOVING_STRIPES_HZ); + addEffect(FX_MODE_CORNER_FLASH_HZ, &mode_corner_flash_hz, _data_FX_MODE_CORNER_FLASH_HZ); + addEffect(FX_MODE_STATIC_HZ_TEST, &mode_static_hz_test, _data_FX_MODE_STATIC_HZ_TEST); + // Journey effects + addEffect(FX_MODE_ASIMPLESTAR3, &mode_aSimplestar3, _data_FX_MODE_ASIMPLESTAR3); + addEffect(FX_MODE_ASIMPLESTAR6, &mode_aSimplestar6, _data_FX_MODE_ASIMPLESTAR6); + addEffect(FX_MODE_ASIMPLESTAR9, &mode_aSimplestar9, _data_FX_MODE_ASIMPLESTAR9); + addEffect(FX_MODE_ASIMPLESTAR12, &mode_aSimplestar12, _data_FX_MODE_ASIMPLESTAR12); + addEffect(FX_MODE_ASIMPLESTAR20, &mode_aSIMPLESTAR20, _data_FX_MODE_ASIMPLESTAR20); + addEffect(FX_MODE_AFULL3, &mode_aFull3, _data_FX_MODE_AFULL3); + addEffect(FX_MODE_AFULL6, &mode_aFull6, _data_FX_MODE_AFULL6); + addEffect(FX_MODE_AFULL9, &mode_aFull9, _data_FX_MODE_AFULL9); + addEffect(FX_MODE_AFULL12, &mode_aFull12, _data_FX_MODE_AFULL12); + addEffect(FX_MODE_AFULL20, &mode_aFull20, _data_FX_MODE_AFULL20); + addEffect(FX_MODE_AMERKABAH3, &mode_aMerkabah3, _data_FX_MODE_AMERKABAH3); + addEffect(FX_MODE_AMERKABAH6, &mode_aMerkabah6, _data_FX_MODE_AMERKABAH6); + addEffect(FX_MODE_AMERKABAH9, &mode_aMerkabah9, _data_FX_MODE_AMERKABAH9); + addEffect(FX_MODE_AMERKABAH12, &mode_aMerkabah12, _data_FX_MODE_AMERKABAH12); + addEffect(FX_MODE_AMERKABAH20, &mode_aMerkabah20, _data_FX_MODE_AMERKABAH20); + addEffect(FX_MODE_ASIDEWAYSMERKABAH3, &mode_aSidewaysMerkabah3, _data_FX_MODE_ASIDEWAYSMERKABAH3); + addEffect(FX_MODE_ASIDEWAYSMERKABAH6, &mode_aSidewaysMerkabah6, _data_FX_MODE_ASIDEWAYSMERKABAH6); + addEffect(FX_MODE_ASIDEWAYSMERKABAH9, &mode_aSidewaysMerkabah9, _data_FX_MODE_ASIDEWAYSMERKABAH9); + addEffect(FX_MODE_ASIDEWAYSMERKABAH12, &mode_aSidewaysMerkabah12, _data_FX_MODE_ASIDEWAYSMERKABAH12); + addEffect(FX_MODE_ASIDEWAYSMERKABAH20, &mode_aSidewaysMerkabah20, _data_FX_MODE_ASIDEWAYSMERKABAH20); + addEffect(FX_MODE_AHOURGLASS3, &mode_aHourglass3, _data_FX_MODE_AHOURGLASS3); + addEffect(FX_MODE_AHOURGLASS6, &mode_aHourglass6, _data_FX_MODE_AHOURGLASS6); + addEffect(FX_MODE_AHOURGLASS9, &mode_aHourglass9, _data_FX_MODE_AHOURGLASS9); + addEffect(FX_MODE_AHOURGLASS12, &mode_aHourglass12, _data_FX_MODE_AHOURGLASS12); + addEffect(FX_MODE_AHOURGLASS20, &mode_aHourglass20, _data_FX_MODE_AHOURGLASS20); + addEffect(FX_MODE_ALMOSTNUKE3, &mode_AlmostNuke3, _data_FX_MODE_ALMOSTNUKE3); + addEffect(FX_MODE_ALMOSTNUKE6, &mode_AlmostNuke6, _data_FX_MODE_ALMOSTNUKE6); + addEffect(FX_MODE_ALMOSTNUKE9, &mode_AlmostNuke9, _data_FX_MODE_ALMOSTNUKE9); + addEffect(FX_MODE_ALMOSTNUKE12, &mode_AlmostNuke12, _data_FX_MODE_ALMOSTNUKE12); + addEffect(FX_MODE_ALMOSTNUKE20, &mode_AlmostNuke20, _data_FX_MODE_ALMOSTNUKE20); + addEffect(FX_MODE_ANUKE3, &mode_aNuke3, _data_FX_MODE_ANUKE3); + addEffect(FX_MODE_ANUKE6, &mode_aNuke6, _data_FX_MODE_ANUKE6); + addEffect(FX_MODE_ANUKE9, &mode_aNuke9, _data_FX_MODE_ANUKE9); + addEffect(FX_MODE_ANUKE12, &mode_aNuke12, _data_FX_MODE_ANUKE12); + addEffect(FX_MODE_ANUKE20, &mode_aNuke20, _data_FX_MODE_ANUKE20); addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); diff --git a/wled00/FX.h b/wled00/FX.h index 167ed8f0f0..7a6796ba19 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -57,7 +57,7 @@ #endif /* Not used in all effects yet */ -#define WLED_FPS 42 +#define WLED_FPS 100 #define FRAMETIME_FIXED (1000/WLED_FPS) //#define FRAMETIME _frametime #define FRAMETIME strip.getFrameTime() @@ -314,8 +314,71 @@ #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#define FX_MODE_CUSTOM_SQUARES 187 +#define FX_MODE_CUSTOM_CIRCLES 188 +#define FX_MODE_CUSTOM_DIAMOND_SPIN 189 +#define FX_MODE_CUSTOM_D_DIAMOND_SPIN 190 +#define FX_MODE_CUSTOM_BEN 191 +#define FX_MODE_HERTZ_TESTING 192 +#define FX_MODE_HIGH_FREQ_TEST 193 +#define FX_MODE_BREATHING_SQUARE 194 +#define FX_MODE_SPIRAL_WAVE 195 +#define FX_MODE_PULSING_CROSS 196 +#define FX_MODE_MOVING_STRIPES 197 +#define FX_MODE_CORNER_FLASH 198 +#define FX_MODE_BREATHING_SQUARE_HZ 199 +#define FX_MODE_SPIRAL_WAVE_HZ 200 +#define FX_MODE_PULSING_CROSS_HZ 201 +#define FX_MODE_MOVING_STRIPES_HZ 202 +#define FX_MODE_CORNER_FLASH_HZ 203 +#define FX_MODE_STATIC_HZ_TEST 204 +#define FX_MODE_CUSTOM_NOVAS 205 +#define FX_MODE_CUSTOM_NOVAS_3_9HZ 206 +#define FX_MODE_CUSTOM_NOVAS_INVERTED 207 +#define FX_MODE_BLACK_HOLE 208 +#define FX_MODE_BLACK_HOLE_3 209 +#define FX_MODE_BLACK_HOLE_6 210 +#define FX_MODE_BLACK_HOLE_9 211 +#define FX_MODE_BLACK_HOLE_15 212 +#define FX_MODE_BLACK_HOLE_CUSTOM 213 + +#define FX_MODE_ASIMPLESTAR3 214 +#define FX_MODE_ASIMPLESTAR6 215 +#define FX_MODE_ASIMPLESTAR9 216 +#define FX_MODE_ASIMPLESTAR12 217 +#define FX_MODE_ASIMPLESTAR20 218 +#define FX_MODE_AFULL3 219 +#define FX_MODE_AFULL6 220 +#define FX_MODE_AFULL9 221 +#define FX_MODE_AFULL12 222 +#define FX_MODE_AFULL20 223 +#define FX_MODE_AMERKABAH3 224 +#define FX_MODE_AMERKABAH6 225 +#define FX_MODE_AMERKABAH9 226 +#define FX_MODE_AMERKABAH12 227 +#define FX_MODE_AMERKABAH20 228 +#define FX_MODE_ASIDEWAYSMERKABAH3 229 +#define FX_MODE_ASIDEWAYSMERKABAH6 230 +#define FX_MODE_ASIDEWAYSMERKABAH9 231 +#define FX_MODE_ASIDEWAYSMERKABAH12 232 +#define FX_MODE_ASIDEWAYSMERKABAH20 233 +#define FX_MODE_AHOURGLASS3 234 +#define FX_MODE_AHOURGLASS6 235 +#define FX_MODE_AHOURGLASS9 236 +#define FX_MODE_AHOURGLASS12 237 +#define FX_MODE_AHOURGLASS20 238 +#define FX_MODE_ALMOSTNUKE3 239 +#define FX_MODE_ALMOSTNUKE6 240 +#define FX_MODE_ALMOSTNUKE9 241 +#define FX_MODE_ALMOSTNUKE12 242 +#define FX_MODE_ALMOSTNUKE20 243 +#define FX_MODE_ANUKE3 244 +#define FX_MODE_ANUKE6 245 +#define FX_MODE_ANUKE9 246 +#define FX_MODE_ANUKE12 247 +#define FX_MODE_ANUKE20 248 + +#define MODE_COUNT 249 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 58e2fdfa57..bb0446fe6a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -145,7 +145,6 @@ Segment& Segment::operator= (Segment &&orig) noexcept { } bool Segment::allocateData(size_t len) { - if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; @@ -660,7 +659,7 @@ uint16_t Segment::virtualLength() const { return vLength; } -void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) +void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) { if (!isActive()) return; // not active #ifndef WLED_DISABLE_2D @@ -1160,6 +1159,8 @@ void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; if (nowUp - _lastShow < MIN_SHOW_DELAY) return; + // Added to allow updates at the desired frame rate + _lastShow = nowUp; bool doShow = false; _isServicing = true; diff --git a/wled00/json.cpp b/wled00/json.cpp index af8ed89ef6..b3b2763fa2 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -612,6 +612,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme void serializeInfo(JsonObject root) { root[F("ver")] = versionString; + root[F("gl_ver")] = F(GRIDLIGHTS_VERSION); root[F("vid")] = VERSION; //root[F("cn")] = WLED_CODENAME; diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 8ba6b1a565..3e1b37eb90 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -95,7 +95,8 @@ void WLED::loop() #endif if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) // block stuff if WARLS/Adalight is enabled { - if (apActive) dnsServer.processNextRequest(); + // Captive portal disabled + // if (apActive) dnsServer.processNextRequest(); #ifndef WLED_DISABLE_OTA if (WLED_CONNECTED && aOtaEnabled && !otaLock && correctPIN) ArduinoOTA.handle(); #endif @@ -566,8 +567,9 @@ void WLED::initAP(bool resetAP) e131.begin(false, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT); ddp.begin(false, DDP_DEFAULT_PORT); - dnsServer.setErrorReplyCode(DNSReplyCode::NoError); - dnsServer.start(53, "*", WiFi.softAPIP()); + // Captive portal disabled - no automatic splash page + // dnsServer.setErrorReplyCode(DNSReplyCode::NoError); + // dnsServer.start(53, "*", WiFi.softAPIP()); } apActive = true; } @@ -888,7 +890,8 @@ void WLED::handleConnection() // shut down AP if (apBehavior != AP_BEHAVIOR_ALWAYS && apActive) { - dnsServer.stop(); + // Captive portal disabled + // dnsServer.stop(); WiFi.softAPdisconnect(true); apActive = false; DEBUG_PRINTLN(F("Access point disabled (handle)."));