diff --git a/.agents/rules/project-context.md b/.agents/rules/project-context.md index 3e4055c..cce2127 100644 --- a/.agents/rules/project-context.md +++ b/.agents/rules/project-context.md @@ -32,8 +32,8 @@ The board topologically sits behind the LED matrix, interfacing via a 2x8 HUB75 ### Hardware (Case) The current enclosure is modeled in Blender. `hardware/case/` is organized by printer bed size, for the v3.0 board: -1. `bed_330mm/`: one-piece snap-fit body (oneshot_v3_part1/2.stl) — H2S-class beds, the main design. -2. `bed_256mm/`: divided snap-fit, 5 parts (divided_v3_part1..5.stl) — P1S-class beds; print & assembly tested (#169), separate LED-panel mounting part adapts to varying panel bolt-hole positions. +1. `bed_330mm/encloser.stl`: one-piece snap-fit body, one STL — H2S-class beds, the main design. +2. `bed_256mm/encloser.stl`: the standard 256mm print — body frame, back panels, and LED-panel mount in one STL; print & assembly verified. `for_other_panels/divided_v3_part1..5.stl` is the adjustable-mount variant for non-standard panels (#169). 3. `knobs/knobs_15mm.stl` / `knobs/knobs_20mm.stl`: Black PLA knobs, shared by all body options. 4. `legacy_v2/`: every v2.x-board case (standard plates, oneshot_v2, divided_v2.1) — NOT compatible with the v3.0 board. @@ -58,7 +58,7 @@ The project maintains a strict dual-license approach to accommodate the differin For full details, reference `BUILD_GUIDE.md` §10. Summary: 1. **Cold boot reliability:** fixed in v2.0 with a 10k GPIO0 pullup. 2. **SMD silkscreen ambiguity:** fixed in v2.0. -3. **LED matrix alignment bumps:** still open; current workaround is trimming during assembly. +3. **LED matrix alignment bumps:** resolved by the v3 snap-fit enclosures (built-in recesses); trimming only applies to the legacy v2 glued plates. 4. **Encoder direction:** handled in firmware. 5. **Encoder shaft length:** 15mm and 20mm EC11 shafts are functionally identical — purely preference. The BOM reference part (Bourns PEC11R-4220F-S0024) is 20mm; both knob STLs are kept. diff --git a/BUILD_GUIDE.md b/BUILD_GUIDE.md index fc05abd..f65e00f 100644 --- a/BUILD_GUIDE.md +++ b/BUILD_GUIDE.md @@ -1,8 +1,6 @@ # Patternflow v3.0.0 -- Build Guide > **Building a v2.x board?** Use the [v2 build guide](BUILD_GUIDE_v2.md) instead — v2 and v3 parts are **not** interchangeable. -> -> 📷 A couple of photos (Sections 7 and 9) were shot on the v2.1-cut twin build (red board). The v3 procedure is identical; they'll be re-shot on a v3 unit soon. This guide walks you through building a Patternflow v3.0.0 from scratch. It assumes basic familiarity with through-hole soldering and 3D printing. @@ -83,7 +81,7 @@ The v3.0 board has **two power inputs on one board**. Pick one before ordering p | Power cable | Any USB cable, stripped, wires screwed in | Standard USB-C cable | | Difficulty | Easy — the classic Patternflow method | ⚠️ **Hard.** The Type-C THT signal pins are tightly pitched; a solder bridge here has shorted and **burned a board** ([#114](https://github.com/engmung/Patternflow/issues/114)). Flux, fine tip, magnification, patience. | -**If in doubt, take Path A.** The result is electrically identical. You can also populate both — the reference build in this guide does (see the finished-board photos in Section 5). +**If in doubt, take Path A.** The result is electrically identical. You can also populate both — the reference build in this guide does. ## 3. Order the PCB @@ -102,7 +100,7 @@ Case folders are named by **printer bed size** — see [`hardware/case/`](hardwa | Your printer bed | Print | Notes | |---|---|---| | **256 mm** (P1S / X1C / A1 class) | `bed_256mm/encloser.stl` | Body frame, back panels, and LED-panel mount in one STL. **White PLA.** ~10 h total. | -| **~330 mm+** (H2S class) | `bed_330mm/oneshot_v3_part1/2.stl` | One-piece snap-fit — fewer parts. **White PLA.** | +| **~330 mm+** (H2S class) | `bed_330mm/encloser.stl` | The one-piece original — body, closing part, and LED-panel mount in one STL, **no bonding at all**. **White PLA.** | | **Everyone** | `knobs/knobs_20mm.stl` | **Required for every build** — all four knobs in one plate, as its own separate print job. **Black PLA.** (15 mm-shaft encoders → `knobs_15mm.stl`.) | Two colors, on purpose: **body in white, knobs in black** — that contrast is the Patternflow look. Printing the knob plate as a separate job keeps it simple (no color changes mid-print). @@ -135,25 +133,7 @@ The whole PCB assembly is covered in **one YouTube video** — soldering order i **[▶ Watch on YouTube — Patternflow v3.0 PCB soldering walkthrough](https://youtu.be/NZCjMBCsDAc)** - - -*Everything laid out before soldering. (This photo predates the R1/R2 5.1kΩ resistors — add those two to the lineup if you're on Path B.)* - -Two things to keep next to the video: - -**Path B (USB-C) only — read the warning in Section 2 first**, then compare your joints against these two: - -| ❌ Like this, and it can short | ✅ Aim for this | -|---|---| -| | | - -And before first power, go over the joints with a multimeter: - - - -Done right, it looks like this (this build populated both power paths): - - +All parts are through-hole, and the video covers the complete order — including the USB-C connector, where the **first attempt (11:00) shows exactly what goes wrong** when you push too hard, and the second attempt (12:40) shows the fix. **Path B builders: read the warning in Section 2 first**, and go over the finished joints with a multimeter before first power. Don't plug the ESP32 DevKit in until after the first power check (Section 7). @@ -201,48 +181,25 @@ Sections 6, 7, and 9 are covered in **one YouTube video** — the full assembly, **[▶ Watch on YouTube — Patternflow v3.0 assembly & first power-on](https://youtu.be/J9C9bZgkNKs)** -By now the enclosure halves you bonded in Section 4 have cured and the board is soldered — time to bring them together: - - - -*The ESP32, the PCB, and the cables, ready to go in.* - -> Printed the **330 mm one-piece body**? Everything in this section is identical — the only thing you skipped is the bonding step in Section 4. +By now the enclosure halves you bonded in Section 4 have cured and the board is soldered — time to bring them together. The video above walks every step; in writing: -1. **Seat the LED panel in the enclosure with its `HUB-75E IN` connector toward the top** — that's the side the ribbon reaches `J1` from: +> Printed the **330 mm one-piece body**? Everything in this section is identical — the only thing you skipped is the bonding step in Section 4. - - - ⚠️ The panel insertion is very tight — near-zero clearance. Work it in slowly. +1. **Seat the LED panel in the enclosure with its `HUB-75E IN` connector toward the top** — that's the side the ribbon reaches `J1` from. ⚠️ The panel insertion is very tight — near-zero clearance. Work it in slowly. 2. **Fit the LED-panel mounting part and tighten the screws.** The mounting part is a separate piece precisely because panel bolt-hole positions vary between suppliers — match it to your panel first. - - 3. **Set the board into its bay and secure the encoders from the front**: attach each encoder's nut and tighten with a wrench or pliers — this locks the board against the front face. - - - *Photo from the v2 build — same encoders, same step.* - ### Finishing touches - The power bank slides into the internal compartment behind the board bay. -- The four black knobs press-fit onto the encoder shafts — save them for **last**, after the Section 9 checks pass: - - - - *Photo from the v2 build — same press-fit.* +- The four black knobs press-fit onto the encoder shafts — save them for **last**, after the Section 9 checks pass. ## 7. Wiring & First Power-Up 1. Connect the HUB75 ribbon from `J1` to the panel's `IN` connector. -2. Wire `J3` to the panel's power cable (+5V / GND — double-check polarity). - - - - *📷 Shot on the v2.1 twin build (red board) — the v3 wiring is identical. Photo will be refreshed.* - +2. Wire `J3` to the panel's power cable (+5V / GND — double-check polarity). The wiring run is shown at **06:57** in the [assembly video](https://youtu.be/J9C9bZgkNKs). 3. Power: Path A — stripped USB cable into `J4` (red = +5V, black = GND); Path B — USB-C cable into `USB1`. 4. **Before inserting the ESP32:** with power disconnected, continuity-check +5V↔GND at the `J3` terminals (open = good, like the Section 5 check). Then power up once and confirm ~5V across `J3`. 5. Power off, seat the ESP32 DevKit in its sockets (orientation per silkscreen), power on. @@ -287,11 +244,7 @@ The stock flasher image ships with **OSC disabled at compile time** — live con 4. Press-click each encoder once; long-press **K4** (~1s) to enter pattern select, rotate to browse, long-press again to exit. 5. Long-press **K1** for the global brightness mode; **K2** long-press shows the OSC info screen. 6. Power-cycle once and confirm it boots cleanly with no RESET press needed (see the GPIO0 note in Section 5 if it doesn't). -7. All good? **Close the back panel**: hook the right edge in first, then press along the snap-fit until it clicks shut. Press the knobs onto the shafts last. - - - - *📷 Shot on the v2.1 twin build — identical procedure on v3. Photo will be refreshed.* +7. All good? **Close the back panel**: hook the right edge in first, then press along the snap-fit until it clicks shut (shown at **09:11** in the [assembly video](https://youtu.be/J9C9bZgkNKs)). Press the knobs onto the shafts last. ## 10. Known Issues & Design Notes diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2f9c5..06b1046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to Patternflow will be documented in this file. ### Added - **v3.0 board promoted to the recommended revision** (`hardware/pcb/gerber/patternflow_v3.0_gerber.zip`). Fabricated, assembled, and verified ([#114](https://github.com/engmung/Patternflow/issues/114)): hybrid power input — USB-C (`USB1` + 5.1k CC pull-downs) or a back-side 2-pin screw terminal (`J4`) as the beginner bypass — all-through-hole, no SMD passives. **Not size-compatible with v2.x cases (and vice versa).** - **Machine-readable BOM** (`hardware/bom/bom_v3.0.csv`): every part by manufacturer part number, per-unit quantities, assembly-critical notes. The LED panel remains an AliExpress link (its mounting-screw positions match the case). +- **Firmware preset library + PFMem.** Curated patterns now live in `firmware/patternflow/presets/` — thirteen new presets (the June–July daily batch) join the library, and the three custom slots ship with fresh patterns. New `src/core_mem.h` (**PFMem**) allocates framebuffer-sized pattern state PSRAM-first, so a fully loaded preset image can no longer boot-loop the device on DRAM pressure. The browser-flasher image carries all of it and self-reports 3.0.0. - **v3 build guide** — now the main `BUILD_GUIDE.md`: [PCB soldering](https://youtu.be/NZCjMBCsDAc) and [assembly-to-first-power-on](https://youtu.be/J9C9bZgkNKs) each covered by a full video walkthrough, photo-documented print/case/wiring steps, netlist-derived pin reference. The v2 guide moved to `BUILD_GUIDE_v2.md` for existing v2.x builds. ### Changed diff --git a/README.md b/README.md index 14d8c81..43fcb30 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ - 📦 **Get one ready-made** — Patternflow is in **[pre-launch on Crowd Supply](https://www.crowdsupply.com/engmung/patternflow)**. Subscribe to be notified the moment the campaign goes live. - 🎛️ **Try it right now** — the **[Live Editor](https://patternflow.work/pattern)** runs a virtual Patternflow in your browser. Same knobs, same patterns, no hardware. -> **Moving fast.** The repo improves almost daily — [v2.1.0](https://github.com/engmung/Patternflow/releases/tag/v2.1.0) consolidated the v2.x line (its release page bundles everything a v2 builder needs), and the **v3.0 board is verified and now the recommended build**: USB-C power with a beginner-friendly screw-terminal bypass, zero SMD passives, snap-fit enclosure. The [build guide](BUILD_GUIDE.md) now covers v3.0 (video-assisted soldering included). Follow the [changelog](CHANGELOG.md) and the [journal](https://patternflow.work/journal) for what's current. +> **Moving fast.** [**v3.0.0 is out**](https://github.com/engmung/Patternflow/releases/tag/v3.0.0) — hybrid USB-C / screw-terminal power, zero SMD passives, a snap-fit enclosure with no more panel trimming, and a [video-first build guide](BUILD_GUIDE.md). On v2.x hardware? Everything you need stays bundled at [v2.1.0](https://github.com/engmung/Patternflow/releases/tag/v2.1.0). Follow the [changelog](CHANGELOG.md) and the [journal](https://patternflow.work/journal) for what's current. ## Quick facts diff --git a/docs/assembly/enclosure/3d-print.md b/docs/assembly/enclosure/3d-print.md index 2831d4a..30caab4 100644 --- a/docs/assembly/enclosure/3d-print.md +++ b/docs/assembly/enclosure/3d-print.md @@ -8,8 +8,9 @@ This path uses the current PLA enclosure files in `hardware/case/`, organized by | File | Contents | | --- | --- | -| `hardware/case/bed_330mm/oneshot_v3_part1/2.stl` | One-piece snap-fit body — H2S-class (330 mm+) beds | -| `hardware/case/bed_256mm/divided_v3_part1..5.stl` | Divided snap-fit, 5 parts — P1S-class (256 mm) beds, print/assembly tested ([#169](https://github.com/engmung/Patternflow/issues/169)) | +| `hardware/case/bed_330mm/encloser.stl` | One-piece snap-fit body — H2S-class (330 mm+) beds, one STL | +| `hardware/case/bed_256mm/encloser.stl` | The standard 256 mm print — P1S-class beds, print & assembly verified | +| `hardware/case/bed_256mm/for_other_panels/divided_v3_part1..5.stl` | Adjustable-mount variant for non-standard LED panels ([#169](https://github.com/engmung/Patternflow/issues/169)) | | `hardware/case/knobs/knobs_15mm.stl` | Knobs for 15mm encoder shafts | | `hardware/case/knobs/knobs_20mm.stl` | Knobs for 20mm encoder shafts (the BOM reference part is 20mm) | diff --git a/docs/releases/v3.0.0-launch.md b/docs/releases/v3.0.0-launch.md index bb832b9..8fe31e7 100644 --- a/docs/releases/v3.0.0-launch.md +++ b/docs/releases/v3.0.0-launch.md @@ -8,14 +8,7 @@ Everything for the v3.0.0 release is **pre-loaded**. When the final gate below p ## Gate 1 — swap the two v2.1-twin photos -While the v3 unit is open, re-shoot and overwrite (same filenames, same orientation): - -- [ ] `docs/build-guide/images/v3/14_wiring.jpg` (wiring, §7) -- [ ] `docs/build-guide/images/v3/15_close_back.jpg` (closing the back, §9) - -Then delete the three "shot on the v2.1 twin" notices in `BUILD_GUIDE.md`: the 📷 line in the top banner, and the two italic captions under those photos (§7 and §9). - -If any new photos need resizing: `python -c "from PIL import Image; im=Image.open(r'PATH'); im=im.resize((1600,int(im.height*1600/im.width))) if im.width>1600 else im; im.save(r'PATH','JPEG',quality=82,optimize=True)"` +- [x] **Resolved differently (2026-07-19):** the stale v2.1-twin photos were removed from the guide entirely — the [assembly video](https://youtu.be/J9C9bZgkNKs) covers those steps. No re-shoot needed. ## Gate 2 — last-minute text diff --git a/docs/releases/v3.0.0.md b/docs/releases/v3.0.0.md index b020429..31d7bdb 100644 --- a/docs/releases/v3.0.0.md +++ b/docs/releases/v3.0.0.md @@ -25,6 +25,7 @@ The biggest hardware revision since launch. The v3.0 board and enclosure were re ### Firmware - **One image for every board generation** — v2.x and v3.0 share an identical pin map, so the [browser flasher](https://patternflow.work) just works on both. No board selection to get wrong. +- **Bigger preset library, sturdier engine** — thirteen new presets from the June–July daily batch, three fresh custom-slot patterns, and a new PSRAM-first allocator (`PFMem`) so a fully loaded pattern image can't starve the Wi-Fi/DMA DRAM budget. ## Breaking changes diff --git a/firmware/patternflow/_TEMPLATE.h b/firmware/patternflow/_TEMPLATE.h index 6205955..f743b02 100644 --- a/firmware/patternflow/_TEMPLATE.h +++ b/firmware/patternflow/_TEMPLATE.h @@ -22,6 +22,9 @@ // #include "src/core_color.h" // PFColor:: hsvToRgb, buildPowLUT, ColorStop, sampleRamp // #include "src/core_noise.h" // PFNoise:: cellHash, valueNoise2D, perlin2D, fractal2D // #include "src/core_tables.h" // PFTables:: init, rT[]/thetaT[] — per-pixel radius/angle from center +// #include "src/core_mem.h" // PFMem:: allocFloats — REQUIRED for framebuffer-sized buffers: never +// // declare them as static arrays (they lock internal DRAM from boot); +// // allocate once in setup(): if (!buf) buf = PFMem::allocFloats(N); namespace TemplatePattern { const char* NAME = "Template"; diff --git a/firmware/patternflow/custom1.h b/firmware/patternflow/custom1.h index a3873cc..96b15d7 100644 --- a/firmware/patternflow/custom1.h +++ b/firmware/patternflow/custom1.h @@ -1,5 +1,4 @@ #pragma once - #include #include "config.h" #include "src/core_display.h" @@ -8,260 +7,118 @@ #include "src/core_math.h" #include "src/core_noise.h" -namespace FireflyHollow { - -const char* NAME = "Firefly Hollow"; -const char* const KNOB_LABELS[4] = {"Flies", "Speed", "Glow", "Wind"}; - -// Ramp LUT – DO NOT EDIT (generated from user ramp, 256 entries) -static const uint8_t RAMP_LUT[256][3] = { - {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,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0}, - {11,10,10},{26,24,24},{41,36,36},{57,47,47},{72,56,56},{87,64,64},{103,70,70},{118,75,75}, - {133,79,79},{149,81,81},{164,82,82},{179,81,81},{195,79,79},{210,75,75},{225,70,70},{241,64,64}, - {254,58,56},{254,68,47},{254,80,38},{254,94,29},{255,109,20},{255,126,11},{255,144,2},{255,165,38}, - {255,185,87},{255,206,138},{255,227,187},{255,248,238},{255,250,227},{255,243,183},{255,236,140},{255,229,96}, - {255,222,53},{255,215,9},{255,207,0},{255,200,0},{255,193,0},{255,185,0},{255,178,0},{255,171,0}, - {255,163,0},{255,156,0},{255,149,0},{255,141,0},{255,134,0},{255,127,0},{255,119,0},{255,112,0}, - {255,105,0},{255,97,0},{255,90,0},{255,83,0},{255,75,0},{255,68,0},{255,61,0},{255,53,0}, - {255,46,0},{255,39,0},{255,31,0},{255,24,0},{255,17,0},{255,9,0},{255,2,0},{255,48,39}, - {255,109,93},{255,164,147},{255,213,201},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, - {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, -}; - -// Knob state (initial values from Pattern Lab) -static float knobFlies = 34.0f; // 2..34 -static float knobSpeed = 2.275f; // 0.2..5 -static float knobGlow = 1.32f; // 0..2 -static float knobWind = 0.409f; // 0..2 - -static constexpr int MAX_FLIES = 16; -static int numFlies = 16; // active count (capped at 16) +namespace BinaryCascade { -// Firefly motion parameters (constant per firefly, set in setup) -static float fly_s1[MAX_FLIES]; -static float fly_s2[MAX_FLIES]; -static float fly_hb[MAX_FLIES]; +const char* NAME = "Binary Cascade"; +const char* const KNOB_LABELS[4] = {"Color Shift", "Speed", "Block Length", "Lane Chaos"}; -// Phase accumulators (each wraps at TWO_PI) -static float phase_fu[MAX_FLIES]; // main x-osc -static float phase_fv_main[MAX_FLIES]; // main y-osc -static float phase_fv_extra[MAX_FLIES];// secondary y-osc (freq 0.9) -static float phase_b[MAX_FLIES]; // brightness oscillation +static float s_colorShift = 0.2f; +static float s_speed = 3.0f; +static float s_blockLength = 2.0f; +static float s_chaos = 0.3f; +static float s_timeAcc = 0.0f; -// Output buffers used by draw() -static float fu[MAX_FLIES]; -static float fv[MAX_FLIES]; -static float fb[MAX_FLIES]; - -// Grass sway phases -static float grass_phase1 = 0.0f; // freq 1.8 -static float grass_phase2 = 0.0f; // freq 2.6 - -// Cloud drift accumulator (unwrapped – feeds noise coordinates, not sin) -static float cloudT = 0.0f; +// Per-lane random data (128 lanes for 256-pixel max width with laneWidth=2) +static constexpr int MAX_LANES = (PANEL_RES_W + 1) / 2; +static float s_laneOffsets[MAX_LANES]; +static float s_laneSpeedMults[MAX_LANES]; +static bool s_laneDataInit = false; void setup() { - PFMath::buildSinLUT(); // idempotent - - for (int i = 0; i < MAX_FLIES; ++i) { - // deterministic per-firefly constants (replace sin‑based hash with cellHash) - fly_s1[i] = 0.13f + 0.11f * PFNoise::cellHash(i, 0, 7); - fly_s2[i] = 0.09f + 0.13f * PFNoise::cellHash(i, 0, 8); - fly_hb[i] = PFNoise::cellHash(i, 0, 9); - - // random initial phases - phase_fu[i] = PFNoise::cellHash(i, 0, 10) * TWO_PI; - phase_fv_main[i] = PFNoise::cellHash(i, 0, 11) * TWO_PI; - phase_fv_extra[i]= PFNoise::cellHash(i, 0, 12) * TWO_PI; - phase_b[i] = PFNoise::cellHash(i, 0, 13) * TWO_PI; - - // initial values so first frame is valid - fu[i] = 0.5f + 0.44f * PFMath::fastSin(phase_fu[i] + i * 2.39f); - fv[i] = 0.62f - + 0.30f * PFMath::fastSin(phase_fv_main[i] + i * 5.17f) - + 0.05f * PFMath::fastSin(phase_fv_extra[i] + (float)i); - float p = PFMath::fastSin(phase_b[i] + i * 1.7f); - float pp = fmaxf(0.0f, p); - fb[i] = pp * pp * pp; // x^3 + PFMath::buildSinLUT(); + // Initialize lane data once + if (!s_laneDataInit) { + for (int i = 0; i < MAX_LANES; ++i) { + s_laneOffsets[i] = PFNoise::cellHash(i, 0, 42) * 100.0f; + s_laneSpeedMults[i] = 0.5f + PFNoise::cellHash(i, 1, 137) * 1.5f; + } + s_laneDataInit = true; } - - grass_phase1 = 0.0f; - grass_phase2 = 0.0f; - cloudT = 0.0f; } void update(float dt, const InputFrame& input) { - // ----- knobs ----- - knobFlies += input.knobDeltas[0] * 0.05f; - if (knobFlies < 2.0f) knobFlies = 2.0f; - if (knobFlies > 34.0f) knobFlies = 34.0f; - - knobSpeed += input.knobDeltas[1] * 0.1f; - if (knobSpeed < 0.2f) knobSpeed = 0.2f; - if (knobSpeed > 5.0f) knobSpeed = 5.0f; - - knobGlow += input.knobDeltas[2] * 0.05f; - if (knobGlow < 0.0f) knobGlow = 0.0f; - if (knobGlow > 2.0f) knobGlow = 2.0f; - - knobWind += input.knobDeltas[3] * 0.05f; - if (knobWind < 0.0f) knobWind = 0.0f; - if (knobWind > 2.0f) knobWind = 2.0f; - - int flies = (int)lroundf(knobFlies); - if (flies < 2) flies = 2; - if (flies > 16) flies = 16; - numFlies = flies; - - const float speed = knobSpeed; - - // ----- firefly phases & buffers ----- - for (int i = 0; i < MAX_FLIES; ++i) { - phase_fu[i] += dt * speed * fly_s1[i] * 2.0f; - phase_fv_main[i] += dt * speed * fly_s2[i] * 2.0f; - phase_fv_extra[i] += dt * speed * 0.9f; - float freqB = 0.8f + 0.5f * fly_hb[i]; - phase_b[i] += dt * speed * freqB; - - // wrap each phase at TWO_PI - while (phase_fu[i] > TWO_PI) phase_fu[i] -= TWO_PI; - while (phase_fv_main[i] > TWO_PI) phase_fv_main[i] -= TWO_PI; - while (phase_fv_extra[i] > TWO_PI) phase_fv_extra[i] -= TWO_PI; - while (phase_b[i] > TWO_PI) phase_b[i] -= TWO_PI; - - fu[i] = 0.5f + 0.44f * PFMath::fastSin(phase_fu[i] + i * 2.39f); - fv[i] = 0.62f - + 0.30f * PFMath::fastSin(phase_fv_main[i] + i * 5.17f) - + 0.05f * PFMath::fastSin(phase_fv_extra[i] + (float)i); - float p = PFMath::fastSin(phase_b[i] + i * 1.7f); - float pp = fmaxf(0.0f, p); - fb[i] = pp * pp * pp; - } - - // ----- grass phases ----- - grass_phase1 += dt * speed * 1.8f; - while (grass_phase1 > TWO_PI) grass_phase1 -= TWO_PI; - grass_phase2 += dt * speed * 2.6f; - while (grass_phase2 > TWO_PI) grass_phase2 -= TWO_PI; - - // ----- cloud drift (offset only, no sin → precision loss negligible) ----- - cloudT += dt * speed * (0.4f + knobWind * 0.8f); + s_colorShift += input.knobDeltas[0] * 0.05f; + s_speed += input.knobDeltas[1] * 0.1f; + s_blockLength += input.knobDeltas[2] * 0.05f; + s_chaos += input.knobDeltas[3] * 0.05f; + + s_colorShift = fmaxf(0.0f, fminf(1.0f, s_colorShift)); + s_speed = fmaxf(-5.0f, fminf(5.0f, s_speed)); + s_blockLength = fmaxf(0.1f, fminf(6.0f, s_blockLength)); + s_chaos = fmaxf(0.0f, fminf(1.0f, s_chaos)); + + // No buttons used in JS → no reset logic + + s_timeAcc += dt * s_speed; + // Time feeds laneSpeed * 20 (float multiplier, not integer). Common period: TWO_PI works if all multipliers are integer, + // but here multiplier is 20.0 * (1.0 + (speedMult-1)*chaos) — not integer. Wrap at a large common period. + // Use 1000π as a safe wrap period (all multiples divide evenly enough for visual continuity). + const float PERIOD = 1000.0f * PI; + if (s_timeAcc > PERIOD) s_timeAcc -= PERIOD; + if (s_timeAcc < 0.0f) s_timeAcc += PERIOD; } void draw() { - const int W = PANEL_RES_W; - const int H = PANEL_RES_H; + const float t = s_timeAcc; + const float blockLen = 4.0f + s_blockLength * 8.0f; + const float blockLen2 = blockLen * 2.0f; + const int laneWidth = 2; - const float vh = (float)W; // logical vertical extent - const float vw = (float)H; // logical horizontal extent - const float vh_half = vh * 0.5f; - - const float glowSq = knobGlow * knobGlow; - const float wind = knobWind; - const int n = numFlies; - - // ----- pre‑compute per‑row data (depends only on u = y) ----- - float hillTop[H]; - float grassGh[H]; - for (int u = 0; u < H; ++u) { - float hu = (float)u; - hillTop[u] = vh * 0.58f - + PFMath::fastSin(hu * 0.09f + 2.0f) * 5.0f - + PFMath::fastSin(hu * 0.023f) * 8.0f; - - int gx = u / 2; - float hh = PFNoise::cellHash(gx, 0, 91); // 0..1 - grassGh[u] = 10.0f + hh * 16.0f; - } + for (int x = 0; x < PANEL_RES_W; ++x) { + const int laneIdx = x / laneWidth; + if (laneIdx >= MAX_LANES) continue; - const float cloudDrift = cloudT * (0.4f + wind * 0.8f); + const float speedMult = s_laneSpeedMults[laneIdx]; + const float laneSpeed = t * (1.0f + (speedMult - 1.0f) * s_chaos); + const float yOffset = s_laneOffsets[laneIdx] + laneSpeed * 20.0f; - for (int y = 0; y < H; ++y) { - const int u = y; // logical x in JS - const float hill = hillTop[u]; - const float gh = grassGh[u]; + const bool isDivider = (x % laneWidth) == (laneWidth - 1); - for (int x = 0; x < W; ++x) { - const int v = W - 1 - x; // logical y in JS - const float g = (float)v / vh; // normalized 0..1 - - float val = 0.05f + 0.14f * g; - - // ----- clouds (only upper half of the view) ----- - if ((float)v < vh_half) { - float cn = PFNoise::valueNoise2D(u * 0.045f + cloudDrift * 0.12f, - v * 0.09f + cloudDrift * 0.015f) * 0.65f - + PFNoise::valueNoise2D(u * 0.11f - cloudDrift * 0.07f, - v * 0.22f + 40.0f) * 0.35f; - cn *= 1.0f - (g / 0.5f) * 0.7f; // g < 0.5 here - float cd = (cn - 0.38f) / 0.18f; - if (cd > 0.0f) { - float soft = (cd >= 1.0f) ? 1.0f - : cd * cd * (3.0f - 2.0f * cd); // smoothstep - val += soft * 0.13f * 1.5f; // lit = 1.5 - } - } - - // ----- distant hill silhouette ----- - if (v >= hill) { - val = 0.07f; + for (int y = 0; y < PANEL_RES_H; ++y) { + if (isDivider) { + PFCanvas::setPixel(x, y, 10, 10, 15); + continue; } - // ----- foreground grass (swaying) ----- - float sway = PFMath::fastSin(u * 0.3f + grass_phase1) * wind * 4.0f - + PFMath::fastSin(u * 0.9f - grass_phase2) * wind * 2.0f; - float grassTop = vh - gh + sway; - if (v >= grassTop) { - val = 0.015f; - if (v - grassTop < 1.2f) val = 0.11f; // moonlit tips - } + const float activeY = fmodf((float)y + yOffset, blockLen2); + const bool isBlock = (activeY < blockLen); + + uint8_t r = 0, g = 0, b = 0; + + if (isBlock) { + const float normY = activeY / blockLen; // 0..1 within block + + if (s_colorShift < 0.2f) { + // Monochrome + const uint8_t br = (uint8_t)(150.0f + normY * 105.0f); + r = br; g = br; b = br; + } else if (s_colorShift < 0.6f) { + // Matrix Green + r = 0; + g = (uint8_t)(180.0f + normY * 75.0f); + b = (uint8_t)(50.0f + (1.0f - normY) * 100.0f); + } else { + // Vaporwave + if (speedMult > 1.0f) { + r = 255; g = 50; b = 200; + } else { + r = 50; g = 200; b = 255; + } + } - // ----- fireflies (additive glow) ----- - for (int i = 0; i < n; ++i) { - float du = (float)u - fu[i] * vw; - float dv = (float)v - fv[i] * vh; - float dd = du * du + dv * dv; - if (dd < glowSq * 9.0f) { - val += fb[i] * expf(-dd / glowSq); + // Hard-edge sparkle/glitch at block edges + if (activeY < 1.0f || activeY > blockLen - 1.0f) { + r = 255; g = 255; b = 255; } + } else { + r = 0; g = 0; b = 0; } - // ----- clamp and map through ramp ----- - if (val < 0.0f) val = 0.0f; - if (val > 1.0f) val = 1.0f; - int idx = (int)(val * 255.0f + 0.5f); - if (idx < 0) idx = 0; - if (idx > 255) idx = 255; - - PFCanvas::setPixel(x, y, - RAMP_LUT[idx][0], - RAMP_LUT[idx][1], - RAMP_LUT[idx][2]); + PFCanvas::setPixel(x, y, r, g, b); } } PFCanvas::present(); } -} // namespace FireflyHollow \ No newline at end of file +} // namespace BinaryCascade \ No newline at end of file diff --git a/firmware/patternflow/custom2.h b/firmware/patternflow/custom2.h index 4993e8d..4ceae9c 100644 --- a/firmware/patternflow/custom2.h +++ b/firmware/patternflow/custom2.h @@ -1,483 +1,115 @@ #pragma once #include +#include +#include #include "config.h" #include "src/core_display.h" #include "src/core_encoders.h" #include "src/core_canvas.h" #include "src/core_math.h" -namespace BreakoutArcade { +namespace GridFracturePattern { -const char* NAME = "Breakout Arcade"; -const char* const KNOB_LABELS[4] = {"Paddle", "Speed", "Luck", "Glow"}; +const char* NAME = "Grid Fracture"; +const char* const KNOB_LABELS[4] = {"ROWS", "SPEED", "GRID SIZE", "SHARPEN"}; -static const uint8_t RAMP_LUT[256][3] = { - {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,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,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},{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,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},{4,4,4}, - {8,8,9},{12,12,13},{16,16,17},{20,20,22},{24,23,26},{28,27,31},{31,30,35},{34,33,39}, - {38,36,44},{41,39,48},{44,42,53},{46,44,57},{49,47,61},{52,49,66},{54,51,70},{56,53,75}, - {59,55,79},{61,56,83},{63,58,88},{65,59,92},{66,60,97},{68,61,101},{69,62,105},{71,63,110}, - {72,63,114},{73,63,119},{74,64,123},{74,64,127},{75,64,132},{76,63,136},{76,63,141},{76,63,145}, - {77,62,150},{77,61,154},{77,60,158},{76,59,163},{76,58,167},{76,56,172},{75,55,176},{74,53,180}, - {73,51,185},{73,49,189},{71,47,194},{70,44,198},{69,42,202},{67,39,207},{66,36,211},{64,33,216}, - {62,30,220},{60,27,224},{58,23,229},{56,20,233},{54,16,238},{51,12,242},{48,8,246},{46,4,251}, - {44,0,255},{56,0,255},{68,0,255},{81,0,255},{93,0,255},{105,0,255},{118,0,255},{130,0,255}, - {142,0,255},{155,0,255},{167,0,255},{179,0,255},{192,0,255},{204,0,255},{216,0,255},{229,0,255}, - {241,0,255},{253,0,255},{255,0,245},{255,0,232},{255,0,220},{255,0,208},{255,0,195},{255,0,183}, - {255,0,171},{255,0,158},{255,0,146},{255,0,134},{255,0,121},{255,0,109},{255,0,97},{255,0,84}, - {255,0,72},{255,0,60},{255,0,47},{255,0,35},{255,0,23},{255,0,10},{255,2,0},{255,14,0}, - {255,27,0},{255,39,0},{255,51,0},{255,64,0},{255,67,4},{255,71,9},{255,74,14},{255,78,18}, - {255,81,23},{255,84,27},{255,88,32},{255,91,37},{255,95,41},{255,98,46},{255,102,50},{255,105,55}, - {255,109,59},{255,112,64},{255,115,69},{255,119,73},{255,122,78},{255,126,82},{255,129,87},{255,133,92}, - {255,136,96},{255,139,101},{255,143,105},{255,146,110},{255,150,114},{255,153,119},{255,157,124},{255,160,128}, - {255,164,133},{255,167,137},{255,170,142},{255,174,147},{255,177,151},{255,181,156},{255,184,160},{255,188,165}, - {255,191,170},{255,194,174},{255,198,179},{255,201,183},{255,205,188},{255,208,192},{255,212,197},{255,215,202}, - {255,218,206},{255,222,211},{255,225,215},{255,229,220},{255,232,225},{255,236,229},{255,239,234},{255,243,238}, - {255,246,243},{255,249,248},{255,253,252},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, -}; - -static constexpr int VW = 64; -static constexpr int VH = 128; -static constexpr int ROWS = 8; -static constexpr int BRICK_W = 8; -static constexpr int BRICK_H = 5; -static constexpr int BRICK_TOP = 10; -static constexpr float PAD_W = 10.0f; -static constexpr float PAD_W_WIDE = 16.0f; -static constexpr float ITEM_FALL = 20.0f; -static constexpr float AI_SPD = 115.0f; +const float GRID_FRACTURE_ROWS_MIN = 1.0f; +const float GRID_FRACTURE_ROWS_MAX = 8.0f; +const float GRID_FRACTURE_ROWS_STEP = 0.05f; -static uint8_t dead[64]; -static float balls[32]; -static uint8_t ballOn[8]; -static float items[18]; -static uint8_t itemOn[6]; -static float glow[VW * VH]; +const float GRID_FRACTURE_SPEED_MIN = 0.1f; +const float GRID_FRACTURE_SPEED_MAX = 5.0f; +const float GRID_FRACTURE_SPEED_STEP = 0.10f; -static float knobPaddle = 46.116f; // raw knob value, only changed by encoder -static float paddlePos = 46.116f; // actual paddle position (manual or AI) -static float speed = 89.336f; -static float luck = 0.332f; -static float glowK = 0.355f; -static float tAccum = 0.0f; -static uint32_t rngState = 12345; -static float flash = 0.0f; -static int combo = 0; -static float idleT = 99.0f; -static float lastKnob = 46.116f; -static float aiTarget = 32.0f; -static float wideT = 0.0f; -static float fireT = 0.0f; -static float padW = PAD_W; -static bool fire = false; -static bool manual = false; +const float GRID_FRACTURE_GRIDSIZE_MIN = 4.0f; +const float GRID_FRACTURE_GRIDSIZE_MAX = 24.0f; +const float GRID_FRACTURE_GRIDSIZE_STEP = 0.05f; -static float rnd() { - rngState = (rngState * 48271u) % 2147483647u; - return (float)rngState / 2147483647.0f; -} +const float GRID_FRACTURE_SHARPEN_MIN = 1.0f; +const float GRID_FRACTURE_SHARPEN_MAX = 10.0f; +const float GRID_FRACTURE_SHARPEN_STEP = 0.05f; -static void depositGlow(float gx, float gy, float amt, float rad) { - int x0 = (int)fmaxf(0.0f, floorf(gx - rad)); - int x1 = (int)fminf(VW - 1.0f, ceilf(gx + rad)); - int y0 = (int)fmaxf(0.0f, floorf(gy - rad)); - int y1 = (int)fminf(VH - 1.0f, ceilf(gy + rad)); - for (int gy2 = y0; gy2 <= y1; ++gy2) { - for (int gx2 = x0; gx2 <= x1; ++gx2) { - float du = gx2 - gx; - float dv = gy2 - gy; - float f = 1.0f - sqrtf(du * du + dv * dv) / (rad + 0.001f); - if (f > 0.0f) { - int idx = gx2 + gy2 * VW; - float nv = glow[idx] + amt * f; - if (nv > 1.0f) nv = 1.0f; - glow[idx] = nv; - } - } - } -} +struct Params { + float rows; + float speed; + float gridSize; + float sharpen; + float timeAcc; +}; -static void multiball() { - for (int i = 0; i < 8; ++i) { - if (!ballOn[i]) continue; - for (int j = 0; j < 8; ++j) { - if (ballOn[j]) continue; - ballOn[j] = 1; - balls[j*4] = balls[i*4]; - balls[j*4+1] = balls[i*4+1]; - float a = 0.3f + rnd() * 0.4f; - balls[j*4+2] = -balls[i*4+2] * (0.7f + a); - balls[j*4+3] = balls[i*4+3]; - float dx = balls[j*4+2]; - float dy = balls[j*4+3]; - float len = sqrtf(dx*dx + dy*dy); - balls[j*4+2] = dx / len; - balls[j*4+3] = dy / len; - break; - } - } -} +Params params; void setup() { + params.rows = 3.0f; + params.speed = 2.0f; + params.gridSize = 12.0f; + params.sharpen = 4.0f; + params.timeAcc = 0.0f; + PFMath::buildSinLUT(); - memset(dead, 0, sizeof(dead)); - balls[0] = 32.0f; - balls[1] = 80.0f; - balls[2] = 0.55f; - balls[3] = -0.84f; - ballOn[0] = 1; - for (int i = 1; i < 8; ++i) ballOn[i] = 0; - memset(itemOn, 0, sizeof(itemOn)); - memset(glow, 0, sizeof(glow)); - knobPaddle = 46.116f; - paddlePos = 46.116f; - lastKnob = 46.116f; - idleT = 99.0f; - aiTarget = 32.0f; - wideT = 0.0f; - fireT = 0.0f; - padW = PAD_W; - fire = false; - manual = false; - flash = 0.0f; - combo = 0; } void update(float dt, const InputFrame& input) { - // --- Knob Paddle (encoder driven, used only for manual mode and idle detection) --- - knobPaddle += input.knobDeltas[0] * 0.05f; - if (knobPaddle < 2.0f) knobPaddle = 2.0f; - if (knobPaddle > 62.0f) knobPaddle = 62.0f; - - speed += input.knobDeltas[1] * 0.1f; - if (speed < 25.0f) speed = 25.0f; - if (speed > 200.0f) speed = 200.0f; - - luck += input.knobDeltas[2] * 0.05f; - if (luck < 0.0f) luck = 0.0f; - if (luck > 1.0f) luck = 1.0f; - - glowK += input.knobDeltas[3] * 0.05f; - if (glowK < 0.0f) glowK = 0.0f; - if (glowK > 1.0f) glowK = 1.0f; - - if (input.btnPressed[0]) { - memset(dead, 0, sizeof(dead)); - } - if (input.btnPressed[1]) { - multiball(); - flash = 1.0f; - } - if (input.btnPressed[2]) { - wideT = 6.0f; - } - if (input.btnPressed[3]) { - fireT = 4.0f; - flash = 0.8f; - } - - // --- Idle detection (only from knob, never from AI movement) --- - if (lastKnob < -100.0f) lastKnob = knobPaddle; - if (fabsf(knobPaddle - lastKnob) > 0.15f) { - idleT = 0.0f; - } else { - idleT += dt; - } - lastKnob = knobPaddle; - manual = (idleT < 5.0f); - - const int vw = VW; - const int vh = VH; - const int brickTop = BRICK_TOP; - - float decay = expf(-dt * (8.0f - 7.0f * glowK)); - for (int i = 0; i < vw * vh; ++i) { - glow[i] *= decay; - } - - wideT = fmaxf(0.0f, wideT - dt); - fireT = fmaxf(0.0f, fireT - dt); - padW = (wideT > 0.0f) ? PAD_W_WIDE : PAD_W; - fire = (fireT > 0.0f); - float padV = vh - 7.0f; - - // --- Paddle movement --- - if (manual) { - paddlePos = knobPaddle; - } else { - float diff = aiTarget - paddlePos; - float step = fminf(fabsf(diff), AI_SPD * dt); - paddlePos += (diff > 0.0f ? step : -step); - } - float minPad = padW * 0.5f; - float maxPad = vw - padW * 0.5f; - if (paddlePos < minPad) paddlePos = minPad; - if (paddlePos > maxPad) paddlePos = maxPad; - - int aliveBalls = 0; - float ballTX = -1.0f; - float ballETA = 1e9f; - - for (int i = 0; i < 8; ++i) { - if (!ballOn[i]) continue; - int o = i * 4; - balls[o] += balls[o+2] * speed * dt; - balls[o+1] += balls[o+3] * speed * dt; - float bx = balls[o]; - float by = balls[o+1]; - - if (bx < 2.0f) { balls[o] = bx = 2.0f; balls[o+2] = fabsf(balls[o+2]); } - if (bx > vw - 2.0f) { balls[o] = bx = vw - 2.0f; balls[o+2] = -fabsf(balls[o+2]); } - if (by < 2.0f) { balls[o+1] = by = 2.0f; balls[o+3] = fabsf(balls[o+3]); } - - if (by >= brickTop && by < brickTop + ROWS * BRICK_H) { - int col = (int)(bx / BRICK_W); - if (col < 0) col = 0; - if (col > 7) col = 7; - int row = (int)((by - brickTop) / BRICK_H); - int idx = row * 8 + col; - if (row >= 0 && row < ROWS && !dead[idx]) { - dead[idx] = 1; - if (!fire) balls[o+3] = -balls[o+3]; - combo++; - flash = fminf(1.0f, 0.25f + combo * 0.1f); - depositGlow(col * BRICK_W + BRICK_W * 0.5f, - brickTop + row * BRICK_H + BRICK_H * 0.5f, - 0.9f, fire ? 5.0f : 3.0f); - - if (rnd() < luck * 0.4f) { - for (int s = 0; s < 6; ++s) { - if (itemOn[s]) continue; - itemOn[s] = 1; - items[s*3] = col * BRICK_W + BRICK_W * 0.5f; - items[s*3+1] = brickTop + row * BRICK_H; - items[s*3+2] = floorf(rnd() * 3.0f); - break; - } - } - } - } - - if (by >= padV - 1.0f && by <= padV + 2.0f && balls[o+3] > 0.0f - && fabsf(bx - paddlePos) < padW * 0.5f + 1.0f) { - balls[o+3] = -fabsf(balls[o+3]); - balls[o+2] += (bx - paddlePos) / (padW * 0.5f) * 0.7f; - float dx = balls[o+2]; - float dy = balls[o+3]; - float len = sqrtf(dx*dx + dy*dy); - dx /= len; - dy /= len; - if (dy > -0.35f) dy = -0.35f; - len = sqrtf(dx*dx + dy*dy); - balls[o+2] = dx / len; - balls[o+3] = dy / len; - combo = 0; - } - - if (by > vh + 4.0f) { - ballOn[i] = 0; - continue; - } - - aliveBalls++; + params.rows = constrain(params.rows + input.knobDeltas[0] * GRID_FRACTURE_ROWS_STEP, GRID_FRACTURE_ROWS_MIN, GRID_FRACTURE_ROWS_MAX); + params.speed = constrain(params.speed + input.knobDeltas[1] * GRID_FRACTURE_SPEED_STEP, GRID_FRACTURE_SPEED_MIN, GRID_FRACTURE_SPEED_MAX); + params.gridSize = constrain(params.gridSize + input.knobDeltas[2] * GRID_FRACTURE_GRIDSIZE_STEP, GRID_FRACTURE_GRIDSIZE_MIN, GRID_FRACTURE_GRIDSIZE_MAX); + params.sharpen = constrain(params.sharpen + input.knobDeltas[3] * GRID_FRACTURE_SHARPEN_STEP, GRID_FRACTURE_SHARPEN_MIN, GRID_FRACTURE_SHARPEN_MAX); - if (balls[o+3] > 0.0f) { - float eta = (padV - by) / (balls[o+3] * speed); - if (eta >= 0.0f && eta < ballETA) { - ballETA = eta; - ballTX = bx; - } - } - - depositGlow(bx, by, fire ? 0.5f : 0.3f, fire ? 2.5f : 1.5f); - } - - if (aliveBalls == 0) { - ballOn[0] = 1; - balls[0] = vw * 0.5f; - balls[1] = vh * 0.55f; - balls[2] = rnd() > 0.5f ? 0.55f : -0.55f; - balls[3] = -0.84f; - flash = 1.0f; - combo = 0; - } - - int brickAlive = 0; - for (int r = 0; r < ROWS; ++r) { - for (int c = 0; c < 8; ++c) { - if (!dead[r * 8 + c]) brickAlive++; - } - } - if (brickAlive == 0) { - memset(dead, 0, sizeof(dead)); - flash = 1.0f; - depositGlow(vw * 0.5f, vh * 0.3f, 1.0f, 14.0f); - } - - float itemTX = -1.0f; - float itemETA = 1e9f; - for (int s = 0; s < 6; ++s) { - if (!itemOn[s]) continue; - items[s*3+1] += ITEM_FALL * dt; - float ix = items[s*3]; - float iy = items[s*3+1]; - - if (iy >= padV - 1.0f && iy <= padV + 3.0f && fabsf(ix - paddlePos) < padW * 0.5f + 1.5f) { - int ty = (int)items[s*3+2]; - if (ty == 0) multiball(); - else if (ty == 1) wideT = 6.0f; - else fireT = 4.0f; - flash = 1.0f; - depositGlow(ix, padV, 1.0f, 6.0f); - itemOn[s] = 0; - continue; - } - if (iy > vh + 3.0f) { - itemOn[s] = 0; - continue; - } - - float eta = (padV - iy) / ITEM_FALL; - if (eta >= 0.0f && eta < itemETA) { - itemETA = eta; - itemTX = ix; - } - } - - if (ballTX < 0.0f) { - aiTarget = (itemTX >= 0.0f) ? itemTX : vw * 0.5f; - } else if (itemTX >= 0.0f) { - float travelT = fabsf(itemTX - paddlePos) / AI_SPD; - if (itemETA + 0.35f < ballETA && travelT < itemETA + 0.2f) { - aiTarget = itemTX; - } else { - aiTarget = ballTX; - } - } else { - aiTarget = ballTX; - } - - flash = fmaxf(0.0f, flash - dt * 2.5f); - - tAccum += dt; - if (tAccum > TWO_PI) tAccum -= TWO_PI; + params.timeAcc += dt * params.speed; } void draw() { - const int W = PANEL_RES_W; // 128 - const int H = PANEL_RES_H; // 64 - const float vw = VW; - const float vh = VH; - const int brickTop = BRICK_TOP; - const float padV = vh - 7.0f; - const float t = tAccum; - - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - float u = (float)y; - float v = (float)(W - 1 - x); - - float val = 0.03f + 0.02f * fmodf(v, 2.0f) + flash * 0.15f; - - if (u < 1.0f || u > vw - 2.0f) val = 0.3f; - - if (v >= brickTop && v < brickTop + ROWS * BRICK_H) { - int col = (int)(u / BRICK_W); - int row = (int)((v - brickTop) / BRICK_H); - if (col >= 0 && col < 8 && row >= 0 && row < ROWS && !dead[row * 8 + col]) { - float lu = u - col * BRICK_W; - float lv = (v - brickTop) - row * BRICK_H; - bool border = (lu < 0.5f || lu > BRICK_W - 0.5f || lv < 0.5f || lv > BRICK_H - 0.5f); - val = border ? 0.30f : 0.80f - row * 0.05f; - if (fire) { - val += 0.12f * PFMath::fastSin(t * 10.0f + col * 2.0f + row); - } - } + const int w = PANEL_RES_W; + const int h = PANEL_RES_H; + const float t = params.timeAcc; + + const int rowCount = (int)floorf(params.rows); + const int cellSize = (int)floorf(params.gridSize); + const float sharp = params.sharpen; + + if (cellSize <= 0) return; + + for (int y = 0; y < h; y++) { + const int bandIdx = (y * rowCount) / h; + const float bandPhase = (float)bandIdx * 1.5f; + + const float xOffset = PFMath::fastSin(t + bandPhase) * 10.0f; + + for (int x = 0; x < w; x++) { + float targetX = fmodf((float)x + xOffset + (float)w, (float)w); + if (targetX < 0.0f) targetX += (float)w; + + const int blockX = (int)floorf(targetX / (float)cellSize); + const int blockY = y / cellSize; + + const float cx = ((float)blockX + 0.5f) * (float)cellSize; + const float cy = ((float)blockY + 0.5f) * (float)cellSize; + const float dx = targetX - cx; + const float dy = (float)y - cy; + const float dist = PFMath::approxLength(dx, dy); + + const float waveFreq = 0.2f + 0.1f * PFMath::fastSin((float)blockX * 0.5f + (float)blockY * 0.3f); + const float wave = PFMath::fastSin(dist * waveFreq - t * 3.0f + (float)(blockX + blockY)); + + float v = (wave + 1.0f) * 0.5f; + v = powf(v, sharp); + + const int borderX = (int)floorf(targetX) % cellSize; + const int borderY = y % cellSize; + if (borderX == 0 || borderY == 0 || borderX == cellSize - 1 || borderY == cellSize - 1) { + v *= 0.1f; } - if (v >= padV && v <= padV + 1.0f && fabsf(u - paddlePos) < padW * 0.5f) { - val = 0.9f; - if (padW > PAD_W + 1.0f && fabsf(u - paddlePos) > padW * 0.5f - 2.0f) { - val = 0.6f + 0.4f * PFMath::fastSin(t * 12.0f); - } - if (manual && fabsf(u - paddlePos) < 1.0f && fabsf(v - padV) < 0.5f) { - val = 0.7f + 0.3f * PFMath::fastSin(t * 8.0f); - } - } - - int glowIdx = (int)u + (int)v * (int)vw; - if (glowIdx >= 0 && glowIdx < (int)(vw * vh)) { - val += glow[glowIdx] * 0.85f; - } + v = constrain(0.05f + v * 1.2f, 0.0f, 1.0f); + uint8_t c = (uint8_t)(v * 255.0f); - val = fmaxf(0.0f, fminf(1.0f, val)); - int li = (int)(val * 255.0f + 0.5f); - if (li < 0) li = 0; - if (li > 255) li = 255; - PFCanvas::setPixel(x, y, RAMP_LUT[li][0], RAMP_LUT[li][1], RAMP_LUT[li][2]); - } - } - - auto plot = [&](float pu, float pv, float pval) { - int puInt = (int)roundf(pu); - int pvInt = (int)roundf(pv); - int px = W - 1 - pvInt; - int py = puInt; - if (px >= 0 && px < W && py >= 0 && py < H) { - pval = fmaxf(0.0f, fminf(1.0f, pval)); - int li = (int)(pval * 255.0f + 0.5f); - if (li < 0) li = 0; - if (li > 255) li = 255; - PFCanvas::setPixel(px, py, RAMP_LUT[li][0], RAMP_LUT[li][1], RAMP_LUT[li][2]); - } - }; - - for (int s = 0; s < 6; ++s) { - if (!itemOn[s]) continue; - float ix = items[s*3]; - float iy = items[s*3+1]; - int ty = (int)items[s*3+2]; - float blink = 0.6f + 0.4f * PFMath::fastSin(t * (6.0f + ty * 4.0f) + s); - plot(ix, iy, 1.0f); - plot(ix - 1, iy, blink); - plot(ix + 1, iy, blink); - plot(ix, iy - 1, blink); - plot(ix, iy + 1, blink); - if (ty == 0) { - plot(ix - 2, iy, blink * 0.5f); - plot(ix + 2, iy, blink * 0.5f); - } - } - - for (int i = 0; i < 8; ++i) { - if (!ballOn[i]) continue; - float bx = balls[i*4]; - float by = balls[i*4+1]; - plot(bx, by, 1.0f); - plot(bx - 1, by, 0.8f); - plot(bx + 1, by, 0.8f); - plot(bx, by - 1, 0.8f); - plot(bx, by + 1, 0.8f); - if (fire) { - plot(bx - 1, by - 1, 0.6f); - plot(bx + 1, by - 1, 0.6f); - plot(bx - 1, by + 1, 0.6f); - plot(bx + 1, by + 1, 0.6f); + PFCanvas::setPixel(x, y, c, c, c); } } PFCanvas::present(); } -} // namespace BreakoutArcade \ No newline at end of file +} // namespace GridFracturePattern + diff --git a/firmware/patternflow/custom3.h b/firmware/patternflow/custom3.h index 85dddc9..72a2ece 100644 --- a/firmware/patternflow/custom3.h +++ b/firmware/patternflow/custom3.h @@ -1,190 +1,143 @@ #pragma once #include +#include +#include #include "config.h" #include "src/core_display.h" #include "src/core_encoders.h" #include "src/core_canvas.h" #include "src/core_math.h" -#include "src/core_noise.h" - -namespace MidsummerSea { - -const char* NAME = "Midsummer Sea"; -const char* const KNOB_LABELS[4] = {"Waves", "Speed", "Sun", "Glitter"}; - -static const uint8_t RAMP_LUT[256][3] = { - {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,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,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},{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,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,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},{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,0,7}, - {3,0,17},{5,0,27},{6,0,37},{8,0,47},{10,0,57},{11,0,67},{13,0,77},{15,0,87}, - {16,0,97},{18,0,107},{20,0,117},{21,0,127},{23,0,137},{25,0,147},{26,0,157},{28,0,167}, - {30,0,177},{32,0,187},{33,0,197},{35,0,207},{37,0,217},{38,0,227},{40,0,237},{42,0,247}, - {45,1,253},{53,3,244},{60,5,234},{68,8,225},{76,10,215},{84,12,206},{92,15,196},{100,17,187}, - {107,19,177},{115,22,168},{123,24,159},{131,27,149},{139,29,140},{147,31,130},{154,34,121},{162,36,111}, - {170,38,102},{178,41,93},{186,43,83},{194,45,74},{202,48,64},{209,50,55},{217,53,45},{225,55,36}, - {233,57,27},{241,60,17},{249,62,8},{255,65,2},{255,71,10},{255,78,18},{255,84,27},{255,90,35}, - {255,97,44},{255,103,52},{255,109,60},{255,116,69},{255,122,77},{255,128,86},{255,134,94},{255,141,102}, - {255,147,111},{255,153,119},{255,160,128},{255,166,136},{255,172,144},{255,178,153},{255,185,161},{255,191,170}, - {255,197,178},{255,204,186},{255,210,195},{255,216,203},{255,223,212},{255,229,220},{255,235,228},{255,241,237}, - {255,248,245},{255,254,254},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, - {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + +namespace KineticRipplePattern { + +const char* NAME = "Kinetic Ripple"; +const char* const KNOB_LABELS[4] = {"CENTERS", "SPEED", "TIME WARP", "DENSITY"}; + +const float KINETIC_RIPPLE_WAVES_MIN = 1.0f; +const float KINETIC_RIPPLE_WAVES_MAX = 6.0f; +const float KINETIC_RIPPLE_WAVES_STEP = 0.05f; + +const float KINETIC_RIPPLE_SPEED_MIN = 0.1f; +const float KINETIC_RIPPLE_SPEED_MAX = 8.0f; +const float KINETIC_RIPPLE_SPEED_STEP = 0.10f; + +const float KINETIC_RIPPLE_WARP_MIN = 0.2f; +const float KINETIC_RIPPLE_WARP_MAX = 3.0f; +const float KINETIC_RIPPLE_WARP_STEP = 0.05f; + +const float KINETIC_RIPPLE_COMPLEXITY_MIN = 1.0f; +const float KINETIC_RIPPLE_COMPLEXITY_MAX = 5.0f; +const float KINETIC_RIPPLE_COMPLEXITY_STEP = 0.05f; + +struct Params { + float waves; + float speed; + float timeWarp; + float complexity; + float timeAcc; }; -static float waves = 0.374f; -static float speed = 1.954f; -static float sun = 0.683f; -static float glit = 0.311f; -static float t = 0.0f; +Params params; void setup() { + params.waves = 3.0f; + params.speed = 1.5f; + params.timeWarp = 1.0f; + params.complexity = 2.0f; + params.timeAcc = 0.0f; + PFMath::buildSinLUT(); } void update(float dt, const InputFrame& input) { - waves += input.knobDeltas[0] * 0.05f; - if (waves < 0.0f) waves = 0.0f; - if (waves > 1.0f) waves = 1.0f; - - speed += input.knobDeltas[1] * 0.1f; - if (speed < 0.1f) speed = 0.1f; - if (speed > 3.0f) speed = 3.0f; - - sun += input.knobDeltas[2] * 0.05f; - if (sun < 0.0f) sun = 0.0f; - if (sun > 1.0f) sun = 1.0f; - - glit += input.knobDeltas[3] * 0.05f; - if (glit < 0.0f) glit = 0.0f; - if (glit > 1.0f) glit = 1.0f; - - t += dt * speed; - // common period for all sine multipliers: 2000π - if (t > 2000.0f * TWO_PI) { - t -= 2000.0f * TWO_PI; - } + params.waves = constrain(params.waves + input.knobDeltas[0] * KINETIC_RIPPLE_WAVES_STEP, KINETIC_RIPPLE_WAVES_MIN, KINETIC_RIPPLE_WAVES_MAX); + params.speed = constrain(params.speed + input.knobDeltas[1] * KINETIC_RIPPLE_SPEED_STEP, KINETIC_RIPPLE_SPEED_MIN, KINETIC_RIPPLE_SPEED_MAX); + params.timeWarp = constrain(params.timeWarp + input.knobDeltas[2] * KINETIC_RIPPLE_WARP_STEP, KINETIC_RIPPLE_WARP_MIN, KINETIC_RIPPLE_WARP_MAX); + params.complexity = constrain(params.complexity + input.knobDeltas[3] * KINETIC_RIPPLE_COMPLEXITY_STEP, KINETIC_RIPPLE_COMPLEXITY_MIN, KINETIC_RIPPLE_COMPLEXITY_MAX); + + params.timeAcc += dt * params.speed; } void draw() { - const int W = PANEL_RES_W; // 128 - const int H = PANEL_RES_H; // 64 - const float vh = (float)(W); // portrait logic: vh = max(W,H) = 128 - const float vw = (float)(H); // 64 - const float horizon = floorf(vh * 0.34f); // 43 - const float beachTop = vh - floorf(vh * 0.14f); // 111 - const float sunU = vw * 0.5f; // 32.0 - const float sunV = horizon - 3.0f - sun * (horizon - 8.0f); // 40 - sun*35 - const float invHorizon = 1.0f / horizon; - const float beachRange = vh - beachTop; // 17 - - for (int y = 0; y < H; ++y) { - const float u = (float)y; // 0..63 - - for (int x = 0; x < W; ++x) { - // coordinate remap for vertical panel (landscape physical) - const float v = (float)(W - 1 - x); // 127..0 - - float val; - - if (v < horizon) { - // --- Sky --- - const float g = v * invHorizon; - val = 0.12f + 0.38f * g; - - const float du = u - sunU; - const float dv = (v - sunV) * 1.2f; - const float d = sqrtf(du * du + dv * dv); - if (d < 4.5f) { - val = 1.0f; + const int w = PANEL_RES_W; + const int h = PANEL_RES_H; + const float t = params.timeAcc; + + const int centers = (int)floorf(params.waves); + const float warp = params.timeWarp; + const float dens = params.complexity; + + const float cx1 = (float)w * 0.5f + PFMath::fastCos(t * 0.7f) * ((float)w * 0.25f); + const float cy1 = (float)h * 0.5f + PFMath::fastSin(t * 1.1f) * ((float)h * 0.25f); + const float cx2 = (float)w * 0.5f - PFMath::fastSin(t * 0.9f) * ((float)w * 0.3f); + const float cy2 = (float)h * 0.5f + PFMath::fastCos(t * 0.6f) * ((float)h * 0.2f); + + const float d1_freq = 0.1f * dens; + const float d2_freq = 0.08f * dens; + const float t_offset1 = -t * 2.5f; + const float t_offset2 = -t * 1.8f; + + for (int y = 0; y < h; y++) { + const float dy1 = (float)y - cy1; + const float dy2 = (float)y - cy2; + + for (int x = 0; x < w; x++) { + const float dx1 = (float)x - cx1; + const float dist1 = PFMath::approxLength(dx1, dy1); + + float tWarp1; + if (warp == 1.0f) { + tWarp1 = (dist1 * 0.02f) + t_offset1; + } else if (warp == 2.0f) { + float d1_scaled = dist1 * 0.02f; + tWarp1 = (d1_scaled * d1_scaled) + t_offset1; + } else { + tWarp1 = powf(dist1 * 0.02f, warp) + t_offset1; + } + const float v1 = PFMath::fastSin(dist1 * d1_freq + tWarp1); + + float finalWave = v1; + + if (centers > 1) { + const float dx2 = (float)x - cx2; + const float dist2 = PFMath::approxLength(dx2, dy2); + + float tWarp2; + if (warp == 1.0f) { + tWarp2 = (dist2 * 0.02f) + t_offset2; + } else if (warp == 2.0f) { + float d2_scaled = dist2 * 0.02f; + tWarp2 = (d2_scaled * d2_scaled) + t_offset2; } else { - val += 0.7f * expf(-d * 0.18f); + tWarp2 = powf(dist2 * 0.02f, warp) + t_offset2; } + const float v2 = PFMath::fastCos(dist2 * d2_freq + tWarp2); - const float cl = PFMath::fastSin(u * 0.11f + v * 0.9f + t * 0.25f) - + PFMath::fastSin(u * 0.05f - v * 0.5f + 2.0f); - if (cl > 1.2f && v < horizon * 0.8f) { - val += 0.12f; + if (centers == 2) { + finalWave = (v1 + v2) * 0.5f; + } else { + finalWave = fabsf(v1 * v2); } } - else if (v < beachTop) { - // --- Sea --- - const float depth = (v - horizon) / (beachTop - horizon); - const float persp = 1.0f / (depth + 0.09f); - const float wob = PFMath::fastSin(u * 0.25f + t * 1.3f) * 0.5f * waves; - const float band = PFMath::fastSin(persp * 2.6f + wob + t * (1.5f + depth * 2.0f)); - - val = 0.42f - 0.22f * depth - + band * (0.06f + 0.14f * waves) * (0.4f + depth); - - const float pathW = 2.5f + depth * 9.0f; - const float sway = PFMath::fastSin(v * 0.5f + t) * waves * 2.0f; - const bool inPath = fabsf(u - sunU + sway) < pathW; - if (inPath) { - val += 0.12f + 0.1f * (1.0f - depth); - } - // glitter sparkle - const int gx = (int)(u * 7.3f); - const int gy = (int)(v * 13.1f); - const int seed = (int)(floorf(t * 7.0f) * 17.7f); - const float sp = PFNoise::cellHash(gx, gy, seed); - const float thr = 1.0f - glit * (inPath ? 0.10f : 0.03f); - if (sp > thr && band > 0.2f) { - val = 1.0f; - } - } - else { - // --- Beach --- - const float s = (v - beachTop) / beachRange; - const int bx = (int)(u * 3.7f); - const int by = (int)(v * 5.1f); - const float sandHash = PFNoise::cellHash(bx, by); - val = 0.5f + 0.15f * s + 0.06f * sandHash; - - const float surge = PFMath::fastSin(t * 1.8f) * 0.5f + 0.5f; - const float edge = beachTop + 2.0f - + surge * beachRange * 0.55f * (0.4f + waves) - + PFMath::fastSin(u * 0.35f + t * 2.2f) * 2.5f * waves; - - if (v < edge) { - const float waterDepth = (edge - v) / beachRange; - val = 0.34f - 0.1f * waterDepth; - if (edge - v < 1.5f) { - val = 0.95f; - } - } - } + float v = (finalWave + 1.0f) * 0.5f; + + float bright; + if (v > 0.8f) bright = 1.0f; + else if (v > 0.5f) bright = 0.6f; + else if (v > 0.2f) bright = 0.25f; + else bright = 0.05f; + + // Amplify brightness dynamically to reach near-full intensity on a HUB75 LED Matrix + float amplified = constrain(bright * 1.15f, 0.0f, 1.0f); + uint8_t c = (uint8_t)(amplified * 255.0f); - // clamp and map through baked ramp - val = fmaxf(0.0f, fminf(1.0f, val)); - int li = (int)(val * 255.0f + 0.5f); - if (li < 0) li = 0; - if (li > 255) li = 255; - PFCanvas::setPixel(x, y, - RAMP_LUT[li][0], - RAMP_LUT[li][1], - RAMP_LUT[li][2]); + PFCanvas::setPixel(x, y, c, c, c); } } PFCanvas::present(); } -} // namespace MidsummerSea \ No newline at end of file +} // namespace KineticRipplePattern \ No newline at end of file diff --git a/firmware/patternflow/pattern_registry.h b/firmware/patternflow/pattern_registry.h index a56e3dd..d750d1d 100644 --- a/firmware/patternflow/pattern_registry.h +++ b/firmware/patternflow/pattern_registry.h @@ -32,6 +32,19 @@ #include "presets/preset_0531.h" #include "presets/preset_0601.h" #include "presets/preset_0602.h" +#include "presets/preset_0628.h" +#include "presets/preset_0629.h" +#include "presets/preset_0629_2.h" +#include "presets/preset_0701.h" +#include "presets/preset_0707.h" +#include "presets/preset_0710.h" +#include "presets/preset_0712.h" +#include "presets/preset_0712_2.h" +#include "presets/preset_0713.h" +#include "presets/preset_0715.h" +#include "presets/preset_0716.h" +#include "presets/preset_0718.h" +#include "presets/preset_0719.h" #include "presets/preset_a_big_hit.h" struct PatternEntry { @@ -55,9 +68,9 @@ struct PatternEntry { // ── CUSTOM (your own — edit these) ── PatternEntry customPatterns[] = { - PATTERN_ENTRY(FireflyHollow), - PATTERN_ENTRY(BreakoutArcade), - PATTERN_ENTRY(MidsummerSea), + PATTERN_ENTRY(BinaryCascade), + PATTERN_ENTRY(GridFracturePattern), + PATTERN_ENTRY(KineticRipplePattern), }; const int NUM_CUSTOM = sizeof(customPatterns) / sizeof(customPatterns[0]); @@ -83,6 +96,19 @@ PatternEntry presetPatterns[] = { PATTERN_ENTRY(Pattern0531), PATTERN_ENTRY(Pattern0601), PATTERN_ENTRY(Pattern0602), + PATTERN_ENTRY(RetroDigitalTapestry), + PATTERN_ENTRY(ChromaticAberrationVortexPattern), + PATTERN_ENTRY(VectorFieldParticleFlowPattern), + PATTERN_ENTRY(LissajousWeave), + PATTERN_ENTRY(UntitledPattern), + PATTERN_ENTRY(TileWaves), + PATTERN_ENTRY(MidsummerSea), + PATTERN_ENTRY(BreakoutArcade), + PATTERN_ENTRY(FireflyHollow), + PATTERN_ENTRY(PoincareSphere), + PATTERN_ENTRY(TriMarch), + PATTERN_ENTRY(WarpedWave), + PATTERN_ENTRY(MagVortex), PATTERN_ENTRY(PatternABigHit), }; const int NUM_PRESETS = sizeof(presetPatterns) / sizeof(presetPatterns[0]); diff --git a/firmware/patternflow/presets/preset_0628.h b/firmware/patternflow/presets/preset_0628.h new file mode 100644 index 0000000..dc0b822 --- /dev/null +++ b/firmware/patternflow/presets/preset_0628.h @@ -0,0 +1,117 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: Retro Digital Tapestry +// Author: Seunghun LEE +// Date: 2026-06-28 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_color.h" + +namespace RetroDigitalTapestry { + +const char* NAME = "Retro Digital Tapestry"; +const char* const KNOB_LABELS[4] = {"Cell Scale", "Speed", "Logic Mode", "Wave Mod"}; + +float cellScale = -0.101f; +float speed = 1.99f; +float logicMode = 1.001f; +float waveMod = 1.346f; +float timeAcc = 0.0f; + +void setup() { + PFMath::buildSinLUT(); + cellScale = -0.101f; + speed = 1.99f; + logicMode = 1.001f; + waveMod = 1.346f; + timeAcc = 0.0f; +} + +void update(float dt, const InputFrame& input) { + cellScale += input.knobDeltas[0] * 0.05f; + if (cellScale < -0.101f) cellScale = -0.101f; + if (cellScale > 0.5f) cellScale = 0.5f; + + speed += input.knobDeltas[1] * 0.1f; + if (speed < 0.1f) speed = 0.1f; + if (speed > 5.0f) speed = 5.0f; + + logicMode += input.knobDeltas[2] * 0.05f; + if (logicMode < 0.0f) logicMode = 0.0f; + if (logicMode > 1.001f) logicMode = 1.001f; + + waveMod += input.knobDeltas[3] * 0.05f; + if (waveMod < 0.0f) waveMod = 0.0f; + if (waveMod > 3.0f) waveMod = 3.0f; + + timeAcc += dt * speed * 2.0f; +} + +void draw() { + int scale = (int)(4.0f + cellScale * 24.0f); + if (scale < 1) scale = 1; + int mode = (int)(logicMode * 5.0f); + float waveF = 0.02f + waveMod * 0.08f; + float t = timeAcc; + + int floorT = (int)floorf(t); + int floorT15 = (int)floorf(t * 1.5f); + int floorT08 = (int)floorf(t * 0.8f); + + float minVal = floorf(50.0f * (PFMath::fastSin(t * 2.0f) * 0.5f + 0.5f)); + float hsv_v = 220.0f / 255.0f; + float hsv_s = (220.0f - minVal) / 220.0f; + + for (int y = 0; y < PANEL_RES_H; y++) { + int sy = y / scale; + float cosY = PFMath::fastCos(y * waveF - t); + + for (int x = 0; x < PANEL_RES_W; x++) { + int sx = x / scale; + int patternVal = 0; + + switch (mode) { + case 0: patternVal = (sx ^ sy) + floorT; break; + case 1: patternVal = (sx & sy) * 3 + floorT15; break; + case 2: patternVal = (sx * 7 + sy * 3) ^ floorT; break; + case 3: patternVal = (sx ^ (sy + floorT)) & 15; break; + default: patternVal = ((sx * sx + sy * sy) >> 2) + floorT08; break; + } + + float smoothS = PFMath::fastSin(x * waveF + t) * cosY; + bool bitActive = (patternVal & 8) != 0; + + uint8_t r = 0, g = 0, b = 0; + + if (bitActive) { + float hu = smoothS * 0.3f + 0.5f + (float)(patternVal % 16) / 32.0f; + hu = fmodf(hu, 1.0f); + if (hu < 0.0f) hu += 1.0f; + + PFColor::hsvToRgb(hu, hsv_s, hsv_v, r, g, b); + } else { + if ((x % scale == 0) || (y % scale == 0)) { + r = 20; g = 10; b = 40; + } + } + + PFCanvas::setPixel(x, y, r, g, b); + } + } + PFCanvas::present(); +} + +} // namespace RetroDigitalTapestry + +// ── Made with Patternflow · https://patternflow.work ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0629.h b/firmware/patternflow/presets/preset_0629.h new file mode 100644 index 0000000..c440516 --- /dev/null +++ b/firmware/patternflow/presets/preset_0629.h @@ -0,0 +1,146 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: Chromatic Aberration Vortex +// Author: Seunghun LEE +// Date: 2026-06-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" + +namespace ChromaticAberrationVortexPattern { + +const char* NAME = "Chromatic Vortex"; +const char* const KNOB_LABELS[4] = {"Split Dist", "Swirl Speed", "Wave Density", "Color Shift"}; + +const float CHROMATIC_VORTEX_SPLIT_MIN = 0.0f; +const float CHROMATIC_VORTEX_SPLIT_MAX = 1.0f; +const float CHROMATIC_VORTEX_SPLIT_STEP = 0.05f; + +const float CHROMATIC_VORTEX_SPEED_MIN = 0.1f; +const float CHROMATIC_VORTEX_SPEED_MAX = 10.0f; +const float CHROMATIC_VORTEX_SPEED_STEP = 0.10f; + +const float CHROMATIC_VORTEX_DENSITY_MIN = 0.0f; +const float CHROMATIC_VORTEX_DENSITY_MAX = 4.9f; +const float CHROMATIC_VORTEX_DENSITY_STEP = 0.05f; + +const float CHROMATIC_VORTEX_COLOR_BIAS_MIN = 0.0f; +const float CHROMATIC_VORTEX_COLOR_BIAS_MAX = 1.0f; +const float CHROMATIC_VORTEX_COLOR_BIAS_STEP = 0.05f; + +struct Params { + float split; + float speed; + float density; + float colorBias; + float timeAcc; +}; + +Params params; + +void setup() { + PFMath::buildSinLUT(); + params.split = 0.4f; + params.speed = 1.5f; + params.density = 2.0f; + params.colorBias = 0.5f; + params.timeAcc = 0.0f; +} + +void update(float dt, const InputFrame& input) { + params.split += input.knobDeltas[0] * CHROMATIC_VORTEX_SPLIT_STEP; + params.split = fmodf(params.split, 1.0f); + if (params.split < 0.0f) params.split += 1.0f; + + params.speed += input.knobDeltas[1] * CHROMATIC_VORTEX_SPEED_STEP; + params.speed = constrain(params.speed, CHROMATIC_VORTEX_SPEED_MIN, CHROMATIC_VORTEX_SPEED_MAX); + + params.density += input.knobDeltas[2] * CHROMATIC_VORTEX_DENSITY_STEP; + params.density = constrain(params.density, CHROMATIC_VORTEX_DENSITY_MIN, CHROMATIC_VORTEX_DENSITY_MAX); + + params.colorBias += input.knobDeltas[3] * CHROMATIC_VORTEX_COLOR_BIAS_STEP; + params.colorBias = fmodf(params.colorBias, 1.0f); + if (params.colorBias < 0.0f) params.colorBias += 1.0f; + + params.timeAcc += dt * params.speed; +} + +void draw() { + int w = PANEL_RES_W; + int h = PANEL_RES_H; + float t = params.timeAcc; + + float cx = w / 2.0f; + float cy = h / 2.0f; + float maxShift = params.split * 8.0f; + float densityCalc = params.density * 0.1f + 0.05f; + + for (int y = 0; y < h; y++) { + float dy = (float)y - cy; + for (int x = 0; x < w; x++) { + float dx = (float)x - cx; + + // 완벽한 원형을 위해 실제 sqrtf 사용 (ESP32-S3 FPU 하드웨어 연산 활용) + float dist = sqrtf(dx * dx + dy * dy); + float angle = atan2f(dy, dx); + + float dist005 = dist * 0.05f; + float shiftR = PFMath::fastSin(dist005 - t) * maxShift; + float shiftG = PFMath::fastSin(dist005 - t + 1.0f) * maxShift * 0.5f; + float shiftB = PFMath::fastSin(dist005 - t + 2.0f) * maxShift * -0.5f; + + // Red Channel Matrix + float rDist = dist + shiftR; + float rWave = PFMath::fastSin(rDist * densityCalc - t * 2.0f + angle); + float rInt = rWave * 0.5f + 0.5f; + if (rInt < 0.0f) rInt = 0.0f; + + // Green Channel Matrix + float gDist = dist + shiftG; + float gWave = PFMath::fastSin(gDist * densityCalc - t * 2.0f + angle + 1.0f); + float gInt = gWave * 0.5f + 0.5f; + if (gInt < 0.0f) gInt = 0.0f; + + // Blue Channel Matrix + float bDist = dist + shiftB; + float bWave = PFMath::fastSin(bDist * densityCalc - t * 2.0f + angle + 2.0f); + float bInt = bWave * 0.5f + 0.5f; + if (bInt < 0.0f) bInt = 0.0f; + + // Color Bias Processing + float rFloat = rInt * 255.0f * (params.colorBias * 0.5f + 0.5f); + float gFloat = gInt * 255.0f * (1.0f - params.colorBias * 0.3f); + float bFloat = bInt * 255.0f * (0.3f + params.colorBias * 0.7f); + + uint8_t r = (uint8_t)constrain(floorf(rFloat), 0.0f, 255.0f); + uint8_t g = (uint8_t)constrain(floorf(gFloat), 0.0f, 255.0f); + uint8_t b = (uint8_t)constrain(floorf(bFloat), 0.0f, 255.0f); + + // White Overlap Peak Correction + if (rInt > 0.85f && gInt > 0.85f && bInt > 0.85f) { + r = 255; + g = 255; + b = 255; + } + + PFCanvas::setPixel(x, y, r, g, b); + } + } + PFCanvas::present(); +} + +} // namespace ChromaticAberrationVortexPattern + +// ── Made with Patternflow · https://patternflow.work ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0629_2.h b/firmware/patternflow/presets/preset_0629_2.h new file mode 100644 index 0000000..a29a4e7 --- /dev/null +++ b/firmware/patternflow/presets/preset_0629_2.h @@ -0,0 +1,150 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: Vector Field Particle Flow +// Author: Seunghun LEE +// Date: 2026-06-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_color.h" + +namespace VectorFieldParticleFlowPattern { + +const char* NAME = "Vector Field Flow"; +const char* const KNOB_LABELS[4] = {"Turbulence", "Velocity", "Density", "Palette"}; + +const float VECTOR_FIELD_TURB_MIN = 0.0f; +const float VECTOR_FIELD_TURB_MAX = 1.0f; +const float VECTOR_FIELD_TURB_STEP = 0.05f; + +const float VECTOR_FIELD_SPEED_MIN = 0.1f; +const float VECTOR_FIELD_SPEED_MAX = 10.0f; +const float VECTOR_FIELD_SPEED_STEP = 0.10f; + +const float VECTOR_FIELD_DENSITY_MIN = 0.0f; +const float VECTOR_FIELD_DENSITY_MAX = 4.9f; +const float VECTOR_FIELD_DENSITY_STEP = 0.05f; + +const float VECTOR_FIELD_PALETTE_MIN = 0.0f; +const float VECTOR_FIELD_PALETTE_MAX = 1.0f; +const float VECTOR_FIELD_PALETTE_STEP = 0.05f; + +struct Params { + float turbulence; + float speed; + float density; + float palette; + float timeAcc; +}; + +Params params; + +float norm_x[PANEL_RES_W]; +float norm_y[PANEL_RES_H]; + +void setup() { + PFMath::buildSinLUT(); + + params.turbulence = 0.5f; + params.speed = 2.0f; + params.density = 2.5f; + params.palette = 0.1f; + params.timeAcc = 0.0f; + + for (int x = 0; x < PANEL_RES_W; x++) { + norm_x[x] = (float)x / PANEL_RES_W - 0.5f; + } + for (int y = 0; y < PANEL_RES_H; y++) { + norm_y[y] = (float)y / PANEL_RES_H - 0.5f; + } +} + +void update(float dt, const InputFrame& input) { + // Knob 1: Turbulence (Wrap) + params.turbulence += input.knobDeltas[0] * VECTOR_FIELD_TURB_STEP; + while (params.turbulence < VECTOR_FIELD_TURB_MIN) params.turbulence += (VECTOR_FIELD_TURB_MAX - VECTOR_FIELD_TURB_MIN); + while (params.turbulence > VECTOR_FIELD_TURB_MAX) params.turbulence -= (VECTOR_FIELD_TURB_MAX - VECTOR_FIELD_TURB_MIN); + + // Knob 2: Speed (Clamp) + params.speed += input.knobDeltas[1] * VECTOR_FIELD_SPEED_STEP; + params.speed = constrain(params.speed, VECTOR_FIELD_SPEED_MIN, VECTOR_FIELD_SPEED_MAX); + + // Knob 3: Density (Clamp) + params.density += input.knobDeltas[2] * VECTOR_FIELD_DENSITY_STEP; + params.density = constrain(params.density, VECTOR_FIELD_DENSITY_MIN, VECTOR_FIELD_DENSITY_MAX); + + // Knob 4: Palette (Wrap) + params.palette += input.knobDeltas[3] * VECTOR_FIELD_PALETTE_STEP; + while (params.palette < VECTOR_FIELD_PALETTE_MIN) params.palette += (VECTOR_FIELD_PALETTE_MAX - VECTOR_FIELD_PALETTE_MIN); + while (params.palette > VECTOR_FIELD_PALETTE_MAX) params.palette -= (VECTOR_FIELD_PALETTE_MAX - VECTOR_FIELD_PALETTE_MIN); + + params.timeAcc += dt * params.speed; +} + +void draw() { + float density_4 = params.density * 4.0f; + float t = params.timeAcc; + + float cos_ny_term[PANEL_RES_H]; + for (int y = 0; y < PANEL_RES_H; y++) { + cos_ny_term[y] = PFMath::fastCos(norm_y[y] * density_4 - t); + } + + float sin_nx_term[PANEL_RES_W]; + for (int x = 0; x < PANEL_RES_W; x++) { + sin_nx_term[x] = PFMath::fastSin(norm_x[x] * density_4 + t); + } + + for (int y = 0; y < PANEL_RES_H; y++) { + float cy = cos_ny_term[y]; + float ny = norm_y[y]; + + for (int x = 0; x < PANEL_RES_W; x++) { + float nx = norm_x[x]; + float angle = sin_nx_term[x] + cy; + + float turb_5 = angle * params.turbulence * 5.0f; + float forceX = PFMath::fastSin(turb_5); + float forceY = PFMath::fastCos(turb_5); + + float value = PFMath::fastSin((nx * forceX + ny * forceY) * 10.0f + t * 2.0f); + float intensity = value * 0.5f + 0.5f; + if (intensity < 0.0f) intensity = 0.0f; + + uint8_t r = 0, g = 0, b = 0; + if (intensity > 0.1f) { + float adjusted_intensity = constrain(0.05f + intensity * 1.1f, 0.0f, 1.0f); + float hue = params.palette + (angle * 0.159154943f) + (forceX * 0.2f); + + hue = fmodf(hue, 1.0f); + if (hue < 0.0f) hue += 1.0f; + + PFColor::hsvToRgb(hue, 0.85f, adjusted_intensity, r, g, b); + + if (adjusted_intensity > 0.88f) { + r = 255; + g = 255; + b = 255; + } + } + PFCanvas::setPixel(x, y, r, g, b); + } + } + PFCanvas::present(); +} + +} // namespace VectorFieldParticleFlowPattern + +// ── Made with Patternflow · https://patternflow.work ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0701.h b/firmware/patternflow/presets/preset_0701.h new file mode 100644 index 0000000..4c3cca2 --- /dev/null +++ b/firmware/patternflow/presets/preset_0701.h @@ -0,0 +1,132 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: Lissajous Weave +// Author: Seunghun LEE +// Date: 2026-07-01 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_color.h" + +namespace LissajousWeave { + + const char* NAME = "Lissajous Weave"; + const char* const KNOB_LABELS[4] = {"X Frequency", "Speed", "Y Frequency", "Phase Offset"}; + + // Knob state tracking initialized to the specified Pattern Lab current values + float knob1 = 1.586f; // min 0, max 2.8, step 0.05 + float knob2 = 0.358f; // min 0, max 2, step 0.1 + float knob3 = 1.437f; // min 0, max 7, step 0.05 + float knob4 = 2.713f; // min -3, max 3, step 0.05 + + // Derived pattern parameters + float freqX = 3.0f; + float speed = 2.0f; + float freqY = 4.0f; + float phase = 0.5f; + float timeAcc = 0.0f; + + void setup() { + PFMath::buildSinLUT(); + + // Initialize derived parameters from initial knob values + freqX = 1.0f + floorf(knob1 * 7.0f); + speed = knob2; + freqY = 1.0f + floorf(knob3 * 7.0f); + phase = knob4; + timeAcc = 0.0f; + } + + void update(float dt, const InputFrame& input) { + // Update Knob 1 + knob1 += input.knobDeltas[0] * 0.05f; + if (knob1 < 0.0f) knob1 = 0.0f; + if (knob1 > 2.8f) knob1 = 2.8f; + + // Update Knob 2 + knob2 += input.knobDeltas[1] * 0.1f; + if (knob2 < 0.0f) knob2 = 0.0f; + if (knob2 > 2.0f) knob2 = 2.0f; + + // Update Knob 3 + knob3 += input.knobDeltas[2] * 0.05f; + if (knob3 < 0.0f) knob3 = 0.0f; + if (knob3 > 7.0f) knob3 = 7.0f; + + // Update Knob 4 + knob4 += input.knobDeltas[3] * 0.05f; + if (knob4 < -3.0f) knob4 = -3.0f; + if (knob4 > 3.0f) knob4 = 3.0f; + + // Recompute parameters based on updated knob positions + freqX = 1.0f + floorf(knob1 * 7.0f); + speed = knob2; + freqY = 1.0f + floorf(knob3 * 7.0f); + phase = knob4; + + timeAcc += dt * speed; + } + + void draw() { + float t = timeAcc; + float fx = freqX; + float fy = freqY; + float phaseRad = phase * 3.14159265f * 2.0f; + + // Clear display with a vertical/horizontal background gradient + for (int y = 0; y < PANEL_RES_H; y++) { + for (int x = 0; x < PANEL_RES_W; x++) { + int bg = 10 + (x * 20) / PANEL_RES_W; + PFCanvas::setPixel(x, y, 0, bg, (bg * 7) / 10); + } + } + + // Draw multiple Lissajous curves + const int numCurves = 12; + const int points = 300; + const float centerW = PANEL_RES_W * 0.5f; + const char centerH = PANEL_RES_H * 0.5f; + const float radiusW = PANEL_RES_W * 0.4f; + const float radiusH = PANEL_RES_H * 0.4f; + + for (int curve = 0; curve < numCurves; curve++) { + float curvePhase = ((float)curve / numCurves) * 3.14159265f * 2.0f; + float hue = fmodf((float)curve / numCurves + t * 0.01f, 1.0f); + if (hue < 0.0f) hue += 1.0f; + + for (int i = 0; i < points; i++) { + float theta = ((float)i / points) * 3.14159265f * 8.0f + t * 0.2f; + + float cx = centerW + PFMath::fastSin(theta * fx + t * 0.1f + curvePhase) * radiusW; + float cy = centerH + PFMath::fastSin(theta * fy + t * 0.15f + curvePhase + phaseRad) * radiusH; + + int px = (int)cx; + int py = (int)cy; + + if (px >= 0 && px < PANEL_RES_W && py >= 0 && py < PANEL_RES_H) { + float brightness = 0.3f + 0.7f * (0.5f + 0.5f * PFMath::fastSin(theta * 3.0f)); + float saturation = 0.8f; + + uint8_t r, g, b; + PFColor::hsvToRgb(hue, saturation, brightness, r, g, b); + PFCanvas::setPixel(px, py, r, g, b); + } + } + } + + PFCanvas::present(); + } + +} // namespace LissajousWeave + +// ── Made with Patternflow · https://patternflow.work ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0707.h b/firmware/patternflow/presets/preset_0707.h new file mode 100644 index 0000000..4de5b8e --- /dev/null +++ b/firmware/patternflow/presets/preset_0707.h @@ -0,0 +1,149 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260707 +// Author: Seunghun LEE +// Date: 2026-07-07 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" + +namespace UntitledPattern { + +const char* NAME = "Untitled pattern"; +const char* const KNOB_LABELS[4] = {"Glitch", "Speed", "Freq", "Quantize"}; + +static const uint8_t RAMP_LUT[256][3] = { + {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,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,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},{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},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255},{43,0,255}, + {43,0,255},{43,0,255},{43,0,255},{43,0,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255}, + {0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255}, + {0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255}, + {0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255}, + {0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255}, + {0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{0,157,255},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, +}; + +static float glitch = 0.597f; +static float speed = 2.0f; +static float freq = 7.631555555555555f; +static float quantize = 10.0f; +static float timeAcc = 0.0f; + +static const float UNTITLED_GLITCH_STEP = 0.05f; +static const float UNTITLED_SPEED_STEP = 0.1f; +static const float UNTITLED_FREQ_STEP = 0.05f; +static const float UNTITLED_QUANTIZE_STEP = 0.05f; + +void setup() { + PFMath::buildSinLUT(); +} + +void update(float dt, const InputFrame& input) { + if (input.knobDeltas[0] != 0) { + glitch += input.knobDeltas[0] * UNTITLED_GLITCH_STEP; + if (glitch < 0.0f) glitch = 0.0f; + if (glitch > 2.0f) glitch = 2.0f; + } + if (input.knobDeltas[1] != 0) { + speed += input.knobDeltas[1] * UNTITLED_SPEED_STEP; + if (speed < 0.05f) speed = 0.05f; + if (speed > 2.0f) speed = 2.0f; + } + if (input.knobDeltas[2] != 0) { + freq += input.knobDeltas[2] * UNTITLED_FREQ_STEP; + if (freq < 2.0f) freq = 2.0f; + if (freq > 10.0f) freq = 10.0f; + } + if (input.knobDeltas[3] != 0) { + quantize += input.knobDeltas[3] * UNTITLED_QUANTIZE_STEP; + if (quantize < 2.0f) quantize = 2.0f; + if (quantize > 10.0f) quantize = 10.0f; + } + + timeAcc += dt * speed; + if (timeAcc > TWO_PI) timeAcc -= TWO_PI; +} + +void draw() { + const int w = PANEL_RES_W; + const int h = PANEL_RES_H; + const float t = timeAcc; + + const float halfW = w * 0.5f; + const float halfH = h * 0.5f; + const float invHalfW = 1.0f / halfW; + const float invHalfH = 1.0f / halfH; + + const float freqParam = freq; + const float glitchParam = glitch; + const int steps = (int)quantize; + const float stepsMinusOne = (float)(steps - 1); + + for (int y = 0; y < h; y++) { + float rowShift = 0.0f; + if (glitchParam > 0.02f) { + float ny0 = y * 0.5f; + float ny1 = y * 0.1f; + float noise = PFMath::fastSin(ny0 + t * 10.0f) * PFMath::fastCos(ny1 - t * 4.0f); + if (noise > 1.0f - glitchParam) { + rowShift = PFMath::fastSin(t * 30.0f) * glitchParam * 30.0f; + } + } + + for (int x = 0; x < w; x++) { + float nx = (x + rowShift - halfW) * invHalfW; + float ny = (y - halfH) * invHalfH; + float d = sqrtf(nx * nx + ny * ny); + + float waveValue = PFMath::fastSin(d * freqParam - t * 4.0f + PFMath::fastSin(nx * 4.0f + t) * glitchParam * 5.0f); + float rawV = (waveValue + 1.0f) * 0.5f; + + float v = floorf(rawV * steps) / stepsMinusOne; + if (v < 0.0f) v = 0.0f; + if (v > 1.0f) v = 1.0f; + + int li = (int)(v * 255.0f + 0.5f); + PFCanvas::setPixel(x, y, RAMP_LUT[li][0], RAMP_LUT[li][1], RAMP_LUT[li][2]); + } + } + + PFCanvas::present(); +} + +} // namespace UntitledPattern + +// ── Made with Patternflow Pattern Lab · https://patternflow.work/pattern-lab ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0710.h b/firmware/patternflow/presets/preset_0710.h new file mode 100644 index 0000000..178b9c3 --- /dev/null +++ b/firmware/patternflow/presets/preset_0710.h @@ -0,0 +1,216 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260710 +// Author: Seunghun LEE +// Date: 2026-07-10 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" + +namespace TileWaves { + +const char* NAME = "TileWaves"; +const char* const KNOB_LABELS[4] = {"Quantize", "Speed", "PhaseShift", "Sharpness"}; + +static const uint8_t RAMP_LUT[256][3] = { + {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,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0}, + {1,0,8},{3,0,17},{4,0,25},{6,0,34},{7,0,42},{9,0,50},{10,0,59},{11,0,67}, + {13,0,76},{14,0,84},{16,0,93},{17,0,101},{19,0,110},{20,0,118},{21,0,127},{23,0,135}, + {24,0,144},{26,0,152},{27,0,161},{29,0,169},{30,0,178},{31,0,186},{33,0,195},{34,0,203}, + {36,0,211},{37,0,220},{39,0,228},{40,0,237},{41,0,245},{43,0,254},{42,2,255},{42,4,255}, + {41,7,255},{41,9,255},{40,11,255},{39,13,255},{39,16,255},{38,18,255},{37,20,255},{37,22,255}, + {36,25,255},{36,27,255},{35,29,255},{34,32,255},{34,34,255},{33,36,255},{32,38,255},{32,41,255}, + {31,43,255},{31,45,255},{30,48,255},{29,50,255},{29,52,255},{28,54,255},{27,57,255},{27,59,255}, + {26,61,255},{26,64,255},{25,66,255},{24,68,255},{24,70,255},{23,73,255},{22,75,255},{22,77,255}, + {21,80,255},{21,82,255},{20,84,255},{19,86,255},{19,89,255},{18,91,255},{17,93,255},{17,95,255}, + {16,98,255},{16,100,255},{15,102,255},{14,105,255},{14,107,255},{13,109,255},{12,111,255},{12,114,255}, + {11,116,255},{11,118,255},{10,121,255},{9,123,255},{9,125,255},{8,127,255},{7,130,255},{7,132,255}, + {6,134,255},{6,137,255},{5,139,255},{4,141,255},{4,143,255},{3,146,255},{2,148,255},{2,150,255}, + {1,152,255},{1,155,255},{0,157,255},{7,153,248},{14,148,241},{21,144,234},{28,140,227},{35,135,220}, + {42,131,213},{49,127,206},{56,122,199},{63,118,192},{70,114,185},{77,110,178},{84,105,171},{91,101,164}, + {98,97,157},{105,92,150},{112,88,143},{119,84,136},{126,79,129},{133,75,122},{140,71,115},{147,67,108}, + {154,62,101},{161,58,94},{168,54,87},{175,49,80},{182,45,73},{189,41,66},{196,36,59},{203,32,52}, + {210,28,45},{217,23,38},{224,19,31},{231,15,24},{238,11,17},{245,6,10},{252,2,3},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, + {255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0},{255,0,0}, +}; + +// Knob state initialized to Pattern Lab current values +static float quantizeState = 6.533f; +static float speedState = 7.048f; +static float phaseShiftState = 3.0f; +static float sharpnessState = 1.0f; + +// Calibrated steps per detent +static const float TILE_QUANTIZE_STEP = 0.05f; +static const float TILE_SPEED_STEP = 0.1f; +static const float TILE_PHASESHIFT_STEP = 0.05f; +static const float TILE_SHARPNESS_STEP = 0.05f; + +// Range limits +static const float TILE_QUANTIZE_MIN = 1.0f; +static const float TILE_QUANTIZE_MAX = 12.0f; +static const float TILE_SPEED_MIN = 0.1f; +static const float TILE_SPEED_MAX = 10.0f; +static const float TILE_PHASESHIFT_MIN = 0.0f; +static const float TILE_PHASESHIFT_MAX = 5.0f; +static const float TILE_SHARPNESS_MIN = 1.0f; +static const float TILE_SHARPNESS_MAX = 10.0f; + +static float timeAcc = 0.0f; + +// tileSize is fixed at 8; precompute tile centers and distances for all grid cells. +// Grid dimensions: (PANEL_RES_W/8 + 1) x (PANEL_RES_H/8 + 1) — center of panel is (w/2, h/2) in pixel coords. +// We precompute dist for each grid cell once per frame since phaseShift can change, +// but the gridX/gridY → dist mapping is parameter-dependent only via phaseShift multiplier applied later. +// Actually dist itself is parameter-independent (pure geometry), so precompute it in setup. +// gridCols = ceil(PANEL_RES_W / 8.0f), gridRows = ceil(PANEL_RES_H / 8.0f). +// We store dist for each (gridX, gridY) so we don't recalc sqrtf per pixel. + +static const int TILE_SIZE = 8; +static int gridCols; +static int gridRows; +static float* gridDist; // size gridCols * gridRows; dist from tile center to panel center + +static float cx, cy; + +void setup() { + PFMath::buildSinLUT(); + + cx = PANEL_RES_W * 0.5f; + cy = PANEL_RES_H * 0.5f; + gridCols = (PANEL_RES_W + TILE_SIZE - 1) / TILE_SIZE; + gridRows = (PANEL_RES_H + TILE_SIZE - 1) / TILE_SIZE; + gridDist = new float[gridCols * gridRows]; + + float halfTile = TILE_SIZE * 0.5f; + for (int gy = 0; gy < gridRows; gy++) { + float ty = gy * TILE_SIZE + halfTile; + float dy = ty - cy; + int rowBase = gy * gridCols; + for (int gx = 0; gx < gridCols; gx++) { + float tx = gx * TILE_SIZE + halfTile; + float dx = tx - cx; + gridDist[rowBase + gx] = sqrtf(dx * dx + dy * dy); + } + } +} + +void update(float dt, const InputFrame& input) { + quantizeState += input.knobDeltas[0] * TILE_QUANTIZE_STEP; + if (quantizeState < TILE_QUANTIZE_MIN) quantizeState = TILE_QUANTIZE_MIN; + if (quantizeState > TILE_QUANTIZE_MAX) quantizeState = TILE_QUANTIZE_MAX; + + speedState += input.knobDeltas[1] * TILE_SPEED_STEP; + if (speedState < TILE_SPEED_MIN) speedState = TILE_SPEED_MIN; + if (speedState > TILE_SPEED_MAX) speedState = TILE_SPEED_MAX; + + phaseShiftState += input.knobDeltas[2] * TILE_PHASESHIFT_STEP; + if (phaseShiftState < TILE_PHASESHIFT_MIN) phaseShiftState = TILE_PHASESHIFT_MIN; + if (phaseShiftState > TILE_PHASESHIFT_MAX) phaseShiftState = TILE_PHASESHIFT_MAX; + + sharpnessState += input.knobDeltas[3] * TILE_SHARPNESS_STEP; + if (sharpnessState < TILE_SHARPNESS_MIN) sharpnessState = TILE_SHARPNESS_MIN; + if (sharpnessState > TILE_SHARPNESS_MAX) sharpnessState = TILE_SHARPNESS_MAX; + + timeAcc += dt * speedState; + // timeAcc feeds fastSin with integer multiplier (3.0) and floorf for quantization. + // Wrap at TWO_PI for the sin use; the quantization floorf operates on the wrapped value + // but produces the same floorf(localTime/step) since step is fractional and TWO_PI is the + // period of the underlying sine. The phase offset (dist * phaseShift * 0.08) drifts but is + // added before floorf. To keep floorf quantization stable we need to preserve the + // unbounded drift in the offset, but wrapping timeAcc at 2π is correct because + // sin(theta - 2π*k) == sin(theta) and floorf((theta - 2π*k)/step) differs from + // floorf(theta/step) by an integer, which sin eliminates. So wrap is safe. + if (timeAcc > TWO_PI) { + timeAcc -= TWO_PI; + } else if (timeAcc < -TWO_PI) { + timeAcc += TWO_PI; + } +} + +void draw() { + float t = timeAcc; + float quantize = quantizeState; + float phaseShift = phaseShiftState; + float sharpness = sharpnessState; + + int tileSize = TILE_SIZE; + int cols = gridCols; + int rows = gridRows; + + float quantizeStep = 1.0f; + float invQuantize = 1.0f; + if (quantize > 1.0f) { + quantizeStep = 1.0f / quantize; + invQuantize = quantize; + } + + for (int y = 0; y < PANEL_RES_H; y++) { + int gridY = y / tileSize; + int rowBase = gridY * cols; + + // tile border check: top edge of tile + bool borderY = (y % tileSize == 0); + + for (int x = 0; x < PANEL_RES_W; x++) { + int gridX = x / tileSize; + float dist = gridDist[rowBase + gridX]; + + // Phase-delayed, time-quantized motion + float localTime = t - dist * phaseShift * 0.08f; + if (quantize > 1.0f) { + localTime = floorf(localTime * invQuantize) * quantizeStep; + } + + // Concentric tile waves + float wave = PFMath::fastSin(dist * 0.25f - localTime * 3.0f); + float val = (wave + 1.0f) * 0.5f; + + // Sharpness power scaling + if (val > 0.0f) { + val = PFMath::fastPow(val, sharpness); + } + + // Tile frame borders + if (borderY || (x % tileSize == 0)) { + val = 0.0f; + } + + // Clamp + if (val < 0.0f) val = 0.0f; + if (val > 1.0f) val = 1.0f; + + int li = (int)(val * 255.0f + 0.5f); + PFCanvas::setPixel(x, y, RAMP_LUT[li][0], RAMP_LUT[li][1], RAMP_LUT[li][2]); + } + } + + PFCanvas::present(); +} + +} // namespace TileWaves + +// ── Made with Patternflow Pattern Lab · https://patternflow.work/pattern-lab ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0712.h b/firmware/patternflow/presets/preset_0712.h new file mode 100644 index 0000000..5f63625 --- /dev/null +++ b/firmware/patternflow/presets/preset_0712.h @@ -0,0 +1,202 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260712_Midsummer Sea +// Author: Seunghun LEE +// Date: 2026-07-12 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_noise.h" + +namespace MidsummerSea { + +const char* NAME = "Midsummer Sea"; +const char* const KNOB_LABELS[4] = {"Waves", "Speed", "Sun", "Glitter"}; + +static const uint8_t RAMP_LUT[256][3] = { + {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,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,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},{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,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,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},{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,0,7}, + {3,0,17},{5,0,27},{6,0,37},{8,0,47},{10,0,57},{11,0,67},{13,0,77},{15,0,87}, + {16,0,97},{18,0,107},{20,0,117},{21,0,127},{23,0,137},{25,0,147},{26,0,157},{28,0,167}, + {30,0,177},{32,0,187},{33,0,197},{35,0,207},{37,0,217},{38,0,227},{40,0,237},{42,0,247}, + {45,1,253},{53,3,244},{60,5,234},{68,8,225},{76,10,215},{84,12,206},{92,15,196},{100,17,187}, + {107,19,177},{115,22,168},{123,24,159},{131,27,149},{139,29,140},{147,31,130},{154,34,121},{162,36,111}, + {170,38,102},{178,41,93},{186,43,83},{194,45,74},{202,48,64},{209,50,55},{217,53,45},{225,55,36}, + {233,57,27},{241,60,17},{249,62,8},{255,65,2},{255,71,10},{255,78,18},{255,84,27},{255,90,35}, + {255,97,44},{255,103,52},{255,109,60},{255,116,69},{255,122,77},{255,128,86},{255,134,94},{255,141,102}, + {255,147,111},{255,153,119},{255,160,128},{255,166,136},{255,172,144},{255,178,153},{255,185,161},{255,191,170}, + {255,197,178},{255,204,186},{255,210,195},{255,216,203},{255,223,212},{255,229,220},{255,235,228},{255,241,237}, + {255,248,245},{255,254,254},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, +}; + +static float waves = 0.374f; +static float speed = 1.954f; +static float sun = 0.683f; +static float glit = 0.311f; +static float t = 0.0f; + +void setup() { + PFMath::buildSinLUT(); +} + +void update(float dt, const InputFrame& input) { + waves += input.knobDeltas[0] * 0.05f; + if (waves < 0.0f) waves = 0.0f; + if (waves > 1.0f) waves = 1.0f; + + speed += input.knobDeltas[1] * 0.1f; + if (speed < 0.1f) speed = 0.1f; + if (speed > 3.0f) speed = 3.0f; + + sun += input.knobDeltas[2] * 0.05f; + if (sun < 0.0f) sun = 0.0f; + if (sun > 1.0f) sun = 1.0f; + + glit += input.knobDeltas[3] * 0.05f; + if (glit < 0.0f) glit = 0.0f; + if (glit > 1.0f) glit = 1.0f; + + t += dt * speed; + // common period for all sine multipliers: 2000π + if (t > 2000.0f * TWO_PI) { + t -= 2000.0f * TWO_PI; + } +} + +void draw() { + const int W = PANEL_RES_W; // 128 + const int H = PANEL_RES_H; // 64 + const float vh = (float)(W); // portrait logic: vh = max(W,H) = 128 + const float vw = (float)(H); // 64 + const float horizon = floorf(vh * 0.34f); // 43 + const float beachTop = vh - floorf(vh * 0.14f); // 111 + const float sunU = vw * 0.5f; // 32.0 + const float sunV = horizon - 3.0f - sun * (horizon - 8.0f); // 40 - sun*35 + const float invHorizon = 1.0f / horizon; + const float beachRange = vh - beachTop; // 17 + + for (int y = 0; y < H; ++y) { + const float u = (float)y; // 0..63 + + for (int x = 0; x < W; ++x) { + // coordinate remap for vertical panel (landscape physical) + const float v = (float)(W - 1 - x); // 127..0 + + float val; + + if (v < horizon) { + // --- Sky --- + const float g = v * invHorizon; + val = 0.12f + 0.38f * g; + + const float du = u - sunU; + const float dv = (v - sunV) * 1.2f; + const float d = sqrtf(du * du + dv * dv); + if (d < 4.5f) { + val = 1.0f; + } else { + val += 0.7f * expf(-d * 0.18f); + } + + const float cl = PFMath::fastSin(u * 0.11f + v * 0.9f + t * 0.25f) + + PFMath::fastSin(u * 0.05f - v * 0.5f + 2.0f); + if (cl > 1.2f && v < horizon * 0.8f) { + val += 0.12f; + } + } + else if (v < beachTop) { + // --- Sea --- + const float depth = (v - horizon) / (beachTop - horizon); + const float persp = 1.0f / (depth + 0.09f); + const float wob = PFMath::fastSin(u * 0.25f + t * 1.3f) * 0.5f * waves; + const float band = PFMath::fastSin(persp * 2.6f + wob + t * (1.5f + depth * 2.0f)); + + val = 0.42f - 0.22f * depth + + band * (0.06f + 0.14f * waves) * (0.4f + depth); + + const float pathW = 2.5f + depth * 9.0f; + const float sway = PFMath::fastSin(v * 0.5f + t) * waves * 2.0f; + const bool inPath = fabsf(u - sunU + sway) < pathW; + if (inPath) { + val += 0.12f + 0.1f * (1.0f - depth); + } + + // glitter sparkle + const int gx = (int)(u * 7.3f); + const int gy = (int)(v * 13.1f); + const int seed = (int)(floorf(t * 7.0f) * 17.7f); + const float sp = PFNoise::cellHash(gx, gy, seed); + const float thr = 1.0f - glit * (inPath ? 0.10f : 0.03f); + if (sp > thr && band > 0.2f) { + val = 1.0f; + } + } + else { + // --- Beach --- + const float s = (v - beachTop) / beachRange; + const int bx = (int)(u * 3.7f); + const int by = (int)(v * 5.1f); + const float sandHash = PFNoise::cellHash(bx, by); + val = 0.5f + 0.15f * s + 0.06f * sandHash; + + const float surge = PFMath::fastSin(t * 1.8f) * 0.5f + 0.5f; + const float edge = beachTop + 2.0f + + surge * beachRange * 0.55f * (0.4f + waves) + + PFMath::fastSin(u * 0.35f + t * 2.2f) * 2.5f * waves; + + if (v < edge) { + const float waterDepth = (edge - v) / beachRange; + val = 0.34f - 0.1f * waterDepth; + if (edge - v < 1.5f) { + val = 0.95f; + } + } + } + + // clamp and map through baked ramp + val = fmaxf(0.0f, fminf(1.0f, val)); + int li = (int)(val * 255.0f + 0.5f); + if (li < 0) li = 0; + if (li > 255) li = 255; + PFCanvas::setPixel(x, y, + RAMP_LUT[li][0], + RAMP_LUT[li][1], + RAMP_LUT[li][2]); + } + } + + PFCanvas::present(); +} + +} // namespace MidsummerSea + +// ── Made with Patternflow Pattern Lab · https://patternflow.work/pattern-lab ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0712_2.h b/firmware/patternflow/presets/preset_0712_2.h new file mode 100644 index 0000000..2049332 --- /dev/null +++ b/firmware/patternflow/presets/preset_0712_2.h @@ -0,0 +1,506 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260713_Breakout Arcade +// Author: Seunghun LEE +// Date: 2026-07-12 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_mem.h" + +namespace BreakoutArcade { + +const char* NAME = "Breakout Arcade"; +const char* const KNOB_LABELS[4] = {"Paddle", "Speed", "Luck", "Glow"}; + +static const uint8_t RAMP_LUT[256][3] = { + {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,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,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},{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,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},{4,4,4}, + {8,8,9},{12,12,13},{16,16,17},{20,20,22},{24,23,26},{28,27,31},{31,30,35},{34,33,39}, + {38,36,44},{41,39,48},{44,42,53},{46,44,57},{49,47,61},{52,49,66},{54,51,70},{56,53,75}, + {59,55,79},{61,56,83},{63,58,88},{65,59,92},{66,60,97},{68,61,101},{69,62,105},{71,63,110}, + {72,63,114},{73,63,119},{74,64,123},{74,64,127},{75,64,132},{76,63,136},{76,63,141},{76,63,145}, + {77,62,150},{77,61,154},{77,60,158},{76,59,163},{76,58,167},{76,56,172},{75,55,176},{74,53,180}, + {73,51,185},{73,49,189},{71,47,194},{70,44,198},{69,42,202},{67,39,207},{66,36,211},{64,33,216}, + {62,30,220},{60,27,224},{58,23,229},{56,20,233},{54,16,238},{51,12,242},{48,8,246},{46,4,251}, + {44,0,255},{56,0,255},{68,0,255},{81,0,255},{93,0,255},{105,0,255},{118,0,255},{130,0,255}, + {142,0,255},{155,0,255},{167,0,255},{179,0,255},{192,0,255},{204,0,255},{216,0,255},{229,0,255}, + {241,0,255},{253,0,255},{255,0,245},{255,0,232},{255,0,220},{255,0,208},{255,0,195},{255,0,183}, + {255,0,171},{255,0,158},{255,0,146},{255,0,134},{255,0,121},{255,0,109},{255,0,97},{255,0,84}, + {255,0,72},{255,0,60},{255,0,47},{255,0,35},{255,0,23},{255,0,10},{255,2,0},{255,14,0}, + {255,27,0},{255,39,0},{255,51,0},{255,64,0},{255,67,4},{255,71,9},{255,74,14},{255,78,18}, + {255,81,23},{255,84,27},{255,88,32},{255,91,37},{255,95,41},{255,98,46},{255,102,50},{255,105,55}, + {255,109,59},{255,112,64},{255,115,69},{255,119,73},{255,122,78},{255,126,82},{255,129,87},{255,133,92}, + {255,136,96},{255,139,101},{255,143,105},{255,146,110},{255,150,114},{255,153,119},{255,157,124},{255,160,128}, + {255,164,133},{255,167,137},{255,170,142},{255,174,147},{255,177,151},{255,181,156},{255,184,160},{255,188,165}, + {255,191,170},{255,194,174},{255,198,179},{255,201,183},{255,205,188},{255,208,192},{255,212,197},{255,215,202}, + {255,218,206},{255,222,211},{255,225,215},{255,229,220},{255,232,225},{255,236,229},{255,239,234},{255,243,238}, + {255,246,243},{255,249,248},{255,253,252},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, +}; + +static constexpr int VW = 64; +static constexpr int VH = 128; +static constexpr int ROWS = 8; +static constexpr int BRICK_W = 8; +static constexpr int BRICK_H = 5; +static constexpr int BRICK_TOP = 10; +static constexpr float PAD_W = 10.0f; +static constexpr float PAD_W_WIDE = 16.0f; +static constexpr float ITEM_FALL = 20.0f; +static constexpr float AI_SPD = 115.0f; + +static uint8_t dead[64]; +static float balls[32]; +static uint8_t ballOn[8]; +static float items[18]; +static uint8_t itemOn[6]; +// VW*VH floats (32 KB) — as a static array this sat in internal DRAM from +// boot for every build; allocated from PSRAM in setup() instead (PFMem). +static float* glow = nullptr; + +static float knobPaddle = 46.116f; // raw knob value, only changed by encoder +static float paddlePos = 46.116f; // actual paddle position (manual or AI) +static float speed = 89.336f; +static float luck = 0.332f; +static float glowK = 0.355f; +static float tAccum = 0.0f; +static uint32_t rngState = 12345; +static float flash = 0.0f; +static int combo = 0; +static float idleT = 99.0f; +static float lastKnob = 46.116f; +static float aiTarget = 32.0f; +static float wideT = 0.0f; +static float fireT = 0.0f; +static float padW = PAD_W; +static bool fire = false; +static bool manual = false; + +static float rnd() { + rngState = (rngState * 48271u) % 2147483647u; + return (float)rngState / 2147483647.0f; +} + +static void depositGlow(float gx, float gy, float amt, float rad) { + int x0 = (int)fmaxf(0.0f, floorf(gx - rad)); + int x1 = (int)fminf(VW - 1.0f, ceilf(gx + rad)); + int y0 = (int)fmaxf(0.0f, floorf(gy - rad)); + int y1 = (int)fminf(VH - 1.0f, ceilf(gy + rad)); + for (int gy2 = y0; gy2 <= y1; ++gy2) { + for (int gx2 = x0; gx2 <= x1; ++gx2) { + float du = gx2 - gx; + float dv = gy2 - gy; + float f = 1.0f - sqrtf(du * du + dv * dv) / (rad + 0.001f); + if (f > 0.0f) { + int idx = gx2 + gy2 * VW; + float nv = glow[idx] + amt * f; + if (nv > 1.0f) nv = 1.0f; + glow[idx] = nv; + } + } + } +} + +static void multiball() { + for (int i = 0; i < 8; ++i) { + if (!ballOn[i]) continue; + for (int j = 0; j < 8; ++j) { + if (ballOn[j]) continue; + ballOn[j] = 1; + balls[j*4] = balls[i*4]; + balls[j*4+1] = balls[i*4+1]; + float a = 0.3f + rnd() * 0.4f; + balls[j*4+2] = -balls[i*4+2] * (0.7f + a); + balls[j*4+3] = balls[i*4+3]; + float dx = balls[j*4+2]; + float dy = balls[j*4+3]; + float len = sqrtf(dx*dx + dy*dy); + balls[j*4+2] = dx / len; + balls[j*4+3] = dy / len; + break; + } + } +} + +void setup() { + PFMath::buildSinLUT(); + memset(dead, 0, sizeof(dead)); + balls[0] = 32.0f; + balls[1] = 80.0f; + balls[2] = 0.55f; + balls[3] = -0.84f; + ballOn[0] = 1; + for (int i = 1; i < 8; ++i) ballOn[i] = 0; + memset(itemOn, 0, sizeof(itemOn)); + if (!glow) glow = PFMem::allocFloats(VW * VH); // zeroed by PFMem + else memset(glow, 0, sizeof(float) * VW * VH); + knobPaddle = 46.116f; + paddlePos = 46.116f; + lastKnob = 46.116f; + idleT = 99.0f; + aiTarget = 32.0f; + wideT = 0.0f; + fireT = 0.0f; + padW = PAD_W; + fire = false; + manual = false; + flash = 0.0f; + combo = 0; +} + +void update(float dt, const InputFrame& input) { + if (!glow) return; // allocation failed — degrade to a blank pattern + + // --- Knob Paddle (encoder driven, used only for manual mode and idle detection) --- + knobPaddle += input.knobDeltas[0] * 0.05f; + if (knobPaddle < 2.0f) knobPaddle = 2.0f; + if (knobPaddle > 62.0f) knobPaddle = 62.0f; + + speed += input.knobDeltas[1] * 0.1f; + if (speed < 25.0f) speed = 25.0f; + if (speed > 200.0f) speed = 200.0f; + + luck += input.knobDeltas[2] * 0.05f; + if (luck < 0.0f) luck = 0.0f; + if (luck > 1.0f) luck = 1.0f; + + glowK += input.knobDeltas[3] * 0.05f; + if (glowK < 0.0f) glowK = 0.0f; + if (glowK > 1.0f) glowK = 1.0f; + + if (input.btnPressed[0]) { + memset(dead, 0, sizeof(dead)); + } + if (input.btnPressed[1]) { + multiball(); + flash = 1.0f; + } + if (input.btnPressed[2]) { + wideT = 6.0f; + } + if (input.btnPressed[3]) { + fireT = 4.0f; + flash = 0.8f; + } + + // --- Idle detection (only from knob, never from AI movement) --- + if (lastKnob < -100.0f) lastKnob = knobPaddle; + if (fabsf(knobPaddle - lastKnob) > 0.15f) { + idleT = 0.0f; + } else { + idleT += dt; + } + lastKnob = knobPaddle; + manual = (idleT < 5.0f); + + const int vw = VW; + const int vh = VH; + const int brickTop = BRICK_TOP; + + float decay = expf(-dt * (8.0f - 7.0f * glowK)); + for (int i = 0; i < vw * vh; ++i) { + glow[i] *= decay; + } + + wideT = fmaxf(0.0f, wideT - dt); + fireT = fmaxf(0.0f, fireT - dt); + padW = (wideT > 0.0f) ? PAD_W_WIDE : PAD_W; + fire = (fireT > 0.0f); + float padV = vh - 7.0f; + + // --- Paddle movement --- + if (manual) { + paddlePos = knobPaddle; + } else { + float diff = aiTarget - paddlePos; + float step = fminf(fabsf(diff), AI_SPD * dt); + paddlePos += (diff > 0.0f ? step : -step); + } + float minPad = padW * 0.5f; + float maxPad = vw - padW * 0.5f; + if (paddlePos < minPad) paddlePos = minPad; + if (paddlePos > maxPad) paddlePos = maxPad; + + int aliveBalls = 0; + float ballTX = -1.0f; + float ballETA = 1e9f; + + for (int i = 0; i < 8; ++i) { + if (!ballOn[i]) continue; + int o = i * 4; + balls[o] += balls[o+2] * speed * dt; + balls[o+1] += balls[o+3] * speed * dt; + float bx = balls[o]; + float by = balls[o+1]; + + if (bx < 2.0f) { balls[o] = bx = 2.0f; balls[o+2] = fabsf(balls[o+2]); } + if (bx > vw - 2.0f) { balls[o] = bx = vw - 2.0f; balls[o+2] = -fabsf(balls[o+2]); } + if (by < 2.0f) { balls[o+1] = by = 2.0f; balls[o+3] = fabsf(balls[o+3]); } + + if (by >= brickTop && by < brickTop + ROWS * BRICK_H) { + int col = (int)(bx / BRICK_W); + if (col < 0) col = 0; + if (col > 7) col = 7; + int row = (int)((by - brickTop) / BRICK_H); + int idx = row * 8 + col; + if (row >= 0 && row < ROWS && !dead[idx]) { + dead[idx] = 1; + if (!fire) balls[o+3] = -balls[o+3]; + combo++; + flash = fminf(1.0f, 0.25f + combo * 0.1f); + depositGlow(col * BRICK_W + BRICK_W * 0.5f, + brickTop + row * BRICK_H + BRICK_H * 0.5f, + 0.9f, fire ? 5.0f : 3.0f); + + if (rnd() < luck * 0.4f) { + for (int s = 0; s < 6; ++s) { + if (itemOn[s]) continue; + itemOn[s] = 1; + items[s*3] = col * BRICK_W + BRICK_W * 0.5f; + items[s*3+1] = brickTop + row * BRICK_H; + items[s*3+2] = floorf(rnd() * 3.0f); + break; + } + } + } + } + + if (by >= padV - 1.0f && by <= padV + 2.0f && balls[o+3] > 0.0f + && fabsf(bx - paddlePos) < padW * 0.5f + 1.0f) { + balls[o+3] = -fabsf(balls[o+3]); + balls[o+2] += (bx - paddlePos) / (padW * 0.5f) * 0.7f; + float dx = balls[o+2]; + float dy = balls[o+3]; + float len = sqrtf(dx*dx + dy*dy); + dx /= len; + dy /= len; + if (dy > -0.35f) dy = -0.35f; + len = sqrtf(dx*dx + dy*dy); + balls[o+2] = dx / len; + balls[o+3] = dy / len; + combo = 0; + } + + if (by > vh + 4.0f) { + ballOn[i] = 0; + continue; + } + + aliveBalls++; + + if (balls[o+3] > 0.0f) { + float eta = (padV - by) / (balls[o+3] * speed); + if (eta >= 0.0f && eta < ballETA) { + ballETA = eta; + ballTX = bx; + } + } + + depositGlow(bx, by, fire ? 0.5f : 0.3f, fire ? 2.5f : 1.5f); + } + + if (aliveBalls == 0) { + ballOn[0] = 1; + balls[0] = vw * 0.5f; + balls[1] = vh * 0.55f; + balls[2] = rnd() > 0.5f ? 0.55f : -0.55f; + balls[3] = -0.84f; + flash = 1.0f; + combo = 0; + } + + int brickAlive = 0; + for (int r = 0; r < ROWS; ++r) { + for (int c = 0; c < 8; ++c) { + if (!dead[r * 8 + c]) brickAlive++; + } + } + if (brickAlive == 0) { + memset(dead, 0, sizeof(dead)); + flash = 1.0f; + depositGlow(vw * 0.5f, vh * 0.3f, 1.0f, 14.0f); + } + + float itemTX = -1.0f; + float itemETA = 1e9f; + for (int s = 0; s < 6; ++s) { + if (!itemOn[s]) continue; + items[s*3+1] += ITEM_FALL * dt; + float ix = items[s*3]; + float iy = items[s*3+1]; + + if (iy >= padV - 1.0f && iy <= padV + 3.0f && fabsf(ix - paddlePos) < padW * 0.5f + 1.5f) { + int ty = (int)items[s*3+2]; + if (ty == 0) multiball(); + else if (ty == 1) wideT = 6.0f; + else fireT = 4.0f; + flash = 1.0f; + depositGlow(ix, padV, 1.0f, 6.0f); + itemOn[s] = 0; + continue; + } + if (iy > vh + 3.0f) { + itemOn[s] = 0; + continue; + } + + float eta = (padV - iy) / ITEM_FALL; + if (eta >= 0.0f && eta < itemETA) { + itemETA = eta; + itemTX = ix; + } + } + + if (ballTX < 0.0f) { + aiTarget = (itemTX >= 0.0f) ? itemTX : vw * 0.5f; + } else if (itemTX >= 0.0f) { + float travelT = fabsf(itemTX - paddlePos) / AI_SPD; + if (itemETA + 0.35f < ballETA && travelT < itemETA + 0.2f) { + aiTarget = itemTX; + } else { + aiTarget = ballTX; + } + } else { + aiTarget = ballTX; + } + + flash = fmaxf(0.0f, flash - dt * 2.5f); + + tAccum += dt; + if (tAccum > TWO_PI) tAccum -= TWO_PI; +} + +void draw() { + if (!glow) { // allocation failed — show nothing rather than crash + PFCanvas::present(); + return; + } + const int W = PANEL_RES_W; // 128 + const int H = PANEL_RES_H; // 64 + const float vw = VW; + const float vh = VH; + const int brickTop = BRICK_TOP; + const float padV = vh - 7.0f; + const float t = tAccum; + + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + float u = (float)y; + float v = (float)(W - 1 - x); + + float val = 0.03f + 0.02f * fmodf(v, 2.0f) + flash * 0.15f; + + if (u < 1.0f || u > vw - 2.0f) val = 0.3f; + + if (v >= brickTop && v < brickTop + ROWS * BRICK_H) { + int col = (int)(u / BRICK_W); + int row = (int)((v - brickTop) / BRICK_H); + if (col >= 0 && col < 8 && row >= 0 && row < ROWS && !dead[row * 8 + col]) { + float lu = u - col * BRICK_W; + float lv = (v - brickTop) - row * BRICK_H; + bool border = (lu < 0.5f || lu > BRICK_W - 0.5f || lv < 0.5f || lv > BRICK_H - 0.5f); + val = border ? 0.30f : 0.80f - row * 0.05f; + if (fire) { + val += 0.12f * PFMath::fastSin(t * 10.0f + col * 2.0f + row); + } + } + } + + if (v >= padV && v <= padV + 1.0f && fabsf(u - paddlePos) < padW * 0.5f) { + val = 0.9f; + if (padW > PAD_W + 1.0f && fabsf(u - paddlePos) > padW * 0.5f - 2.0f) { + val = 0.6f + 0.4f * PFMath::fastSin(t * 12.0f); + } + if (manual && fabsf(u - paddlePos) < 1.0f && fabsf(v - padV) < 0.5f) { + val = 0.7f + 0.3f * PFMath::fastSin(t * 8.0f); + } + } + + int glowIdx = (int)u + (int)v * (int)vw; + if (glowIdx >= 0 && glowIdx < (int)(vw * vh)) { + val += glow[glowIdx] * 0.85f; + } + + val = fmaxf(0.0f, fminf(1.0f, val)); + int li = (int)(val * 255.0f + 0.5f); + if (li < 0) li = 0; + if (li > 255) li = 255; + PFCanvas::setPixel(x, y, RAMP_LUT[li][0], RAMP_LUT[li][1], RAMP_LUT[li][2]); + } + } + + // 람다 함수 수정 + auto plot = [&](float pu, float pv, float pval) { + int puInt = (int)roundf(pu); + int pvInt = (int)roundf(pv); + int px = W - 1 - pvInt; + int py = puInt; + if (px >= 0 && px < W && py >= 0 && py < H) { + pval = fmaxf(0.0f, fminf(1.0f, pval)); + int li = (int)(pval * 255.0f + 0.5f); + if (li < 0) li = 0; + if (li > 255) li = 255; + PFCanvas::setPixel(px, py, RAMP_LUT[li][0], RAMP_LUT[li][1], RAMP_LUT[li][2]); + } + }; + + for (int s = 0; s < 6; ++s) { + if (!itemOn[s]) continue; + float ix = items[s*3]; + float iy = items[s*3+1]; + int ty = (int)items[s*3+2]; + float blink = 0.6f + 0.4f * PFMath::fastSin(t * (6.0f + ty * 4.0f) + s); + plot(ix, iy, 1.0f); + plot(ix - 1, iy, blink); + plot(ix + 1, iy, blink); + plot(ix, iy - 1, blink); + plot(ix, iy + 1, blink); + if (ty == 0) { + plot(ix - 2, iy, blink * 0.5f); + plot(ix + 2, iy, blink * 0.5f); + } + } + + for (int i = 0; i < 8; ++i) { + if (!ballOn[i]) continue; + float bx = balls[i*4]; + float by = balls[i*4+1]; + plot(bx, by, 1.0f); + plot(bx - 1, by, 0.8f); + plot(bx + 1, by, 0.8f); + plot(bx, by - 1, 0.8f); + plot(bx, by + 1, 0.8f); + if (fire) { + plot(bx - 1, by - 1, 0.6f); + plot(bx + 1, by - 1, 0.6f); + plot(bx - 1, by + 1, 0.6f); + plot(bx + 1, by + 1, 0.6f); + } + } + + PFCanvas::present(); +} + +} // namespace BreakoutArcade + +// ── Made with Patternflow Pattern Lab · https://patternflow.work/pattern-lab ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. \ No newline at end of file diff --git a/firmware/patternflow/presets/preset_0713.h b/firmware/patternflow/presets/preset_0713.h new file mode 100644 index 0000000..eb24257 --- /dev/null +++ b/firmware/patternflow/presets/preset_0713.h @@ -0,0 +1,279 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260713_Firefly +// Author: Seunghun LEE +// Date: 2026-07-13 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_noise.h" + +namespace FireflyHollow { + +const char* NAME = "Firefly Hollow"; +const char* const KNOB_LABELS[4] = {"Flies", "Speed", "Glow", "Wind"}; + +// Ramp LUT – DO NOT EDIT (generated from user ramp, 256 entries) +static const uint8_t RAMP_LUT[256][3] = { + {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,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0}, + {11,10,10},{26,24,24},{41,36,36},{57,47,47},{72,56,56},{87,64,64},{103,70,70},{118,75,75}, + {133,79,79},{149,81,81},{164,82,82},{179,81,81},{195,79,79},{210,75,75},{225,70,70},{241,64,64}, + {254,58,56},{254,68,47},{254,80,38},{254,94,29},{255,109,20},{255,126,11},{255,144,2},{255,165,38}, + {255,185,87},{255,206,138},{255,227,187},{255,248,238},{255,250,227},{255,243,183},{255,236,140},{255,229,96}, + {255,222,53},{255,215,9},{255,207,0},{255,200,0},{255,193,0},{255,185,0},{255,178,0},{255,171,0}, + {255,163,0},{255,156,0},{255,149,0},{255,141,0},{255,134,0},{255,127,0},{255,119,0},{255,112,0}, + {255,105,0},{255,97,0},{255,90,0},{255,83,0},{255,75,0},{255,68,0},{255,61,0},{255,53,0}, + {255,46,0},{255,39,0},{255,31,0},{255,24,0},{255,17,0},{255,9,0},{255,2,0},{255,48,39}, + {255,109,93},{255,164,147},{255,213,201},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, + {255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235},{255,240,235}, +}; + +// Knob state (initial values from Pattern Lab) +static float knobFlies = 34.0f; // 2..34 +static float knobSpeed = 2.275f; // 0.2..5 +static float knobGlow = 1.32f; // 0..2 +static float knobWind = 0.409f; // 0..2 + +static constexpr int MAX_FLIES = 16; +static int numFlies = 16; // active count (capped at 16) + +// Firefly motion parameters (constant per firefly, set in setup) +static float fly_s1[MAX_FLIES]; +static float fly_s2[MAX_FLIES]; +static float fly_hb[MAX_FLIES]; + +// Phase accumulators (each wraps at TWO_PI) +static float phase_fu[MAX_FLIES]; // main x-osc +static float phase_fv_main[MAX_FLIES]; // main y-osc +static float phase_fv_extra[MAX_FLIES];// secondary y-osc (freq 0.9) +static float phase_b[MAX_FLIES]; // brightness oscillation + +// Output buffers used by draw() +static float fu[MAX_FLIES]; +static float fv[MAX_FLIES]; +static float fb[MAX_FLIES]; + +// Grass sway phases +static float grass_phase1 = 0.0f; // freq 1.8 +static float grass_phase2 = 0.0f; // freq 2.6 + +// Cloud drift accumulator (unwrapped – feeds noise coordinates, not sin) +static float cloudT = 0.0f; + +void setup() { + PFMath::buildSinLUT(); // idempotent + + for (int i = 0; i < MAX_FLIES; ++i) { + // deterministic per-firefly constants (replace sin‑based hash with cellHash) + fly_s1[i] = 0.13f + 0.11f * PFNoise::cellHash(i, 0, 7); + fly_s2[i] = 0.09f + 0.13f * PFNoise::cellHash(i, 0, 8); + fly_hb[i] = PFNoise::cellHash(i, 0, 9); + + // random initial phases + phase_fu[i] = PFNoise::cellHash(i, 0, 10) * TWO_PI; + phase_fv_main[i] = PFNoise::cellHash(i, 0, 11) * TWO_PI; + phase_fv_extra[i]= PFNoise::cellHash(i, 0, 12) * TWO_PI; + phase_b[i] = PFNoise::cellHash(i, 0, 13) * TWO_PI; + + // initial values so first frame is valid + fu[i] = 0.5f + 0.44f * PFMath::fastSin(phase_fu[i] + i * 2.39f); + fv[i] = 0.62f + + 0.30f * PFMath::fastSin(phase_fv_main[i] + i * 5.17f) + + 0.05f * PFMath::fastSin(phase_fv_extra[i] + (float)i); + float p = PFMath::fastSin(phase_b[i] + i * 1.7f); + float pp = fmaxf(0.0f, p); + fb[i] = pp * pp * pp; // x^3 + } + + grass_phase1 = 0.0f; + grass_phase2 = 0.0f; + cloudT = 0.0f; +} + +void update(float dt, const InputFrame& input) { + // ----- knobs ----- + knobFlies += input.knobDeltas[0] * 0.05f; + if (knobFlies < 2.0f) knobFlies = 2.0f; + if (knobFlies > 34.0f) knobFlies = 34.0f; + + knobSpeed += input.knobDeltas[1] * 0.1f; + if (knobSpeed < 0.2f) knobSpeed = 0.2f; + if (knobSpeed > 5.0f) knobSpeed = 5.0f; + + knobGlow += input.knobDeltas[2] * 0.05f; + if (knobGlow < 0.0f) knobGlow = 0.0f; + if (knobGlow > 2.0f) knobGlow = 2.0f; + + knobWind += input.knobDeltas[3] * 0.05f; + if (knobWind < 0.0f) knobWind = 0.0f; + if (knobWind > 2.0f) knobWind = 2.0f; + + int flies = (int)lroundf(knobFlies); + if (flies < 2) flies = 2; + if (flies > 16) flies = 16; + numFlies = flies; + + const float speed = knobSpeed; + + // ----- firefly phases & buffers ----- + for (int i = 0; i < MAX_FLIES; ++i) { + phase_fu[i] += dt * speed * fly_s1[i] * 2.0f; + phase_fv_main[i] += dt * speed * fly_s2[i] * 2.0f; + phase_fv_extra[i] += dt * speed * 0.9f; + float freqB = 0.8f + 0.5f * fly_hb[i]; + phase_b[i] += dt * speed * freqB; + + // wrap each phase at TWO_PI + while (phase_fu[i] > TWO_PI) phase_fu[i] -= TWO_PI; + while (phase_fv_main[i] > TWO_PI) phase_fv_main[i] -= TWO_PI; + while (phase_fv_extra[i] > TWO_PI) phase_fv_extra[i] -= TWO_PI; + while (phase_b[i] > TWO_PI) phase_b[i] -= TWO_PI; + + fu[i] = 0.5f + 0.44f * PFMath::fastSin(phase_fu[i] + i * 2.39f); + fv[i] = 0.62f + + 0.30f * PFMath::fastSin(phase_fv_main[i] + i * 5.17f) + + 0.05f * PFMath::fastSin(phase_fv_extra[i] + (float)i); + float p = PFMath::fastSin(phase_b[i] + i * 1.7f); + float pp = fmaxf(0.0f, p); + fb[i] = pp * pp * pp; + } + + // ----- grass phases ----- + grass_phase1 += dt * speed * 1.8f; + while (grass_phase1 > TWO_PI) grass_phase1 -= TWO_PI; + grass_phase2 += dt * speed * 2.6f; + while (grass_phase2 > TWO_PI) grass_phase2 -= TWO_PI; + + // ----- cloud drift (offset only, no sin → precision loss negligible) ----- + cloudT += dt * speed * (0.4f + knobWind * 0.8f); +} + +void draw() { + const int W = PANEL_RES_W; + const int H = PANEL_RES_H; + + const float vh = (float)W; // logical vertical extent + const float vw = (float)H; // logical horizontal extent + const float vh_half = vh * 0.5f; + + const float glowSq = knobGlow * knobGlow; + const float wind = knobWind; + const int n = numFlies; + + // ----- pre‑compute per‑row data (depends only on u = y) ----- + float hillTop[H]; + float grassGh[H]; + for (int u = 0; u < H; ++u) { + float hu = (float)u; + hillTop[u] = vh * 0.58f + + PFMath::fastSin(hu * 0.09f + 2.0f) * 5.0f + + PFMath::fastSin(hu * 0.023f) * 8.0f; + + int gx = u / 2; + float hh = PFNoise::cellHash(gx, 0, 91); // 0..1 + grassGh[u] = 10.0f + hh * 16.0f; + } + + const float cloudDrift = cloudT * (0.4f + wind * 0.8f); + + for (int y = 0; y < H; ++y) { + const int u = y; // logical x in JS + const float hill = hillTop[u]; + const float gh = grassGh[u]; + + for (int x = 0; x < W; ++x) { + const int v = W - 1 - x; // logical y in JS + const float g = (float)v / vh; // normalized 0..1 + + float val = 0.05f + 0.14f * g; + + // ----- clouds (only upper half of the view) ----- + if ((float)v < vh_half) { + float cn = PFNoise::valueNoise2D(u * 0.045f + cloudDrift * 0.12f, + v * 0.09f + cloudDrift * 0.015f) * 0.65f + + PFNoise::valueNoise2D(u * 0.11f - cloudDrift * 0.07f, + v * 0.22f + 40.0f) * 0.35f; + cn *= 1.0f - (g / 0.5f) * 0.7f; // g < 0.5 here + float cd = (cn - 0.38f) / 0.18f; + if (cd > 0.0f) { + float soft = (cd >= 1.0f) ? 1.0f + : cd * cd * (3.0f - 2.0f * cd); // smoothstep + val += soft * 0.13f * 1.5f; // lit = 1.5 + } + } + + // ----- distant hill silhouette ----- + if (v >= hill) { + val = 0.07f; + } + + // ----- foreground grass (swaying) ----- + float sway = PFMath::fastSin(u * 0.3f + grass_phase1) * wind * 4.0f + + PFMath::fastSin(u * 0.9f - grass_phase2) * wind * 2.0f; + float grassTop = vh - gh + sway; + if (v >= grassTop) { + val = 0.015f; + if (v - grassTop < 1.2f) val = 0.11f; // moonlit tips + } + + // ----- fireflies (additive glow) ----- + for (int i = 0; i < n; ++i) { + float du = (float)u - fu[i] * vw; + float dv = (float)v - fv[i] * vh; + float dd = du * du + dv * dv; + if (dd < glowSq * 9.0f) { + val += fb[i] * expf(-dd / glowSq); + } + } + + // ----- clamp and map through ramp ----- + if (val < 0.0f) val = 0.0f; + if (val > 1.0f) val = 1.0f; + int idx = (int)(val * 255.0f + 0.5f); + if (idx < 0) idx = 0; + if (idx > 255) idx = 255; + + PFCanvas::setPixel(x, y, + RAMP_LUT[idx][0], + RAMP_LUT[idx][1], + RAMP_LUT[idx][2]); + } + } + + PFCanvas::present(); +} + +} // namespace FireflyHollow + +// ── Made with Patternflow Pattern Lab · https://patternflow.work/pattern-lab ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0715.h b/firmware/patternflow/presets/preset_0715.h new file mode 100644 index 0000000..015cc6b --- /dev/null +++ b/firmware/patternflow/presets/preset_0715.h @@ -0,0 +1,106 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: Poincaré Sphere +// Author: Seunghun LEE +// Date: 2026-07-15 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" + +namespace PoincareSphere { + +const char* NAME = "Poincaré Sphere"; +const char* const KNOB_LABELS[4] = {"Color Tone", "Speed", "Grid Density", "Curvature"}; + +static float hueParam = 1.0f; // Knob 1, range 0..1 +static float speed = 8.62f; // Knob 2, range 0.1..10 +static float density = 2.50677966f; // Knob 3, range 0..5 +static float curve = 2.601f; // Knob 4, range 0..5 +static float t = 0.0f; + +void setup() { + PFMath::buildSinLUT(); +} + +void update(float dt, const InputFrame& input) { + // Knob 1: color tone + hueParam += input.knobDeltas[0] * 0.05f; + hueParam = fmaxf(0.0f, fminf(1.0f, hueParam)); + + // Knob 2: rotation speed + speed += input.knobDeltas[1] * 0.1f; + speed = fmaxf(0.1f, fminf(10.0f, speed)); + + // Knob 3: grid density + density += input.knobDeltas[2] * 0.05f; + density = fmaxf(0.0f, fminf(5.0f, density)); + + // Knob 4: projection curvature + curve += input.knobDeltas[3] * 0.05f; + curve = fmaxf(0.0f, fminf(5.0f, curve)); + + t += dt * speed; + // Wrap at common period 20π to avoid float precision loss over days. + const float period = 20.0f * PI; + if (t > period) t -= period; +} + +void draw() { + float gridCount = 2.0f + density * 3.0f; + float curvature = 0.1f + curve * 2.0f; + + float cx = PANEL_RES_W * 0.5f; + float cy = PANEL_RES_H * 0.5f; + + for (int y = 0; y < PANEL_RES_H; ++y) { + float dy = (y - cy) / cy; // -1 .. 1 + + for (int x = 0; x < PANEL_RES_W; ++x) { + float dx = (x - cx) / cy; // -W/H .. W/H + + float r2 = dx * dx + dy * dy; + float projScale = 1.0f / (1.0f + r2 * curvature); + float sphereX = dx * projScale; + float sphereY = dy * projScale; + + float waveU = PFMath::fastSin(sphereX * gridCount * TWO_PI + t); + float waveV = PFMath::fastSin(sphereY * gridCount * TWO_PI - t * 0.7f); + + float line = fabsf(waveU) * fabsf(waveV); + uint8_t r = 0, g = 0, b = 0; + + if (line < 0.15f) { + float intensity = 1.0f - line / 0.15f; + float fr = intensity * 128.0f * hueParam; + float fg = intensity * 255.0f * (1.0f - hueParam); + float fb = intensity * 255.0f; + + if (line < 0.03f) { + fr = 255.0f; fg = 255.0f; fb = 255.0f; + } + + r = (uint8_t)fminf(fmaxf(fr, 0.0f), 255.0f); + g = (uint8_t)fminf(fmaxf(fg, 0.0f), 255.0f); + b = (uint8_t)fminf(fmaxf(fb, 0.0f), 255.0f); + } + + PFCanvas::setPixel(x, y, r, g, b); + } + } + + PFCanvas::present(); +} + +} // namespace PoincareSphere + +// ── Made with Patternflow Live Editor · https://patternflow.work/pattern ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0716.h b/firmware/patternflow/presets/preset_0716.h new file mode 100644 index 0000000..6f45766 --- /dev/null +++ b/firmware/patternflow/presets/preset_0716.h @@ -0,0 +1,107 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260716_TriMarch +// Author: Seunghun LEE +// Date: 2026-07-16 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_color.h" + +namespace TriMarch { + +const char* NAME = "Tri March"; +const char* const KNOB_LABELS[4] = {"Triangle size", "Speed", "March angle", "Color palette"}; + +static float tsize = 0.0f; +static float speed = 3.931f; +static float angle = 3.122f; +static float palette = 2.607f; +static float t = 0.0f; + +void setup() { + PFMath::buildSinLUT(); +} + +void update(float dt, const InputFrame& input) { + tsize += input.knobDeltas[0] * 0.05f; + if (tsize < 0.0f) tsize = 0.0f; + if (tsize > 1.0f) tsize = 1.0f; + + speed += input.knobDeltas[1] * 0.1f; + if (speed < -3.0f) speed = -3.0f; + if (speed > 4.0f) speed = 4.0f; + + angle += input.knobDeltas[2] * 0.05f; + if (angle < -0.001f) angle = -0.001f; + if (angle > 5.0f) angle = 5.0f; + + palette += input.knobDeltas[3] * 0.05f; + if (palette < 0.0f) palette = 0.0f; + if (palette > 4.0f) palette = 4.0f; + + t += dt * speed; + // Period needed for multipliers 10, 2, 0.05 = 40π = 20*TWO_PI + const float kWrapPeriod = 20.0f * TWO_PI; + while (t > kWrapPeriod) t -= kWrapPeriod; + while (t < -kWrapPeriod) t += kWrapPeriod; +} + +void draw() { + const float triH = 6.0f + tsize * 25.0f; + const float triW = triH * 0.8660254f; // sqrt(3)/2 + + const float moveX = PFMath::fastCos(angle * PI) * t * 10.0f; + const float moveY = PFMath::fastSin(angle * PI) * t * 10.0f; + + for (int y = 0; y < PANEL_RES_H; ++y) { + for (int x = 0; x < PANEL_RES_W; ++x) { + float fx = ((float)x - moveX) / triW; + float fy = ((float)y - moveY) / triH; + int col = (int)floorf(fx); + int row = (int)floorf(fy); + float lx = fx - (float)col - 0.5f; + float ly = fy - (float)row - 0.5f; + int flip = (col + row) & 1; + + bool inTri = false; + if (flip == 0) { + inTri = (ly < 0.3f && fabsf(lx) * 1.8f + ly < 0.45f); + } else { + inTri = (ly > -0.3f && fabsf(lx) * 1.8f - ly < 0.45f); + } + + if (inTri) { + float seed = (float)(col * 11 + row * 17); + float brightness = PFMath::fastSin(seed * 0.5f + t * 2.0f) * 0.4f + 0.6f; + if (brightness < 0.0f) brightness = 0.0f; + if (brightness > 1.0f) brightness = 1.0f; + + float hue = seed * 0.03f * palette + brightness * 0.2f + t * 0.05f; + hue = PFMath::fract(hue); + + uint8_t r, g, b; + PFColor::hsvToRgb(hue, 1.0f, brightness, r, g, b); + PFCanvas::setPixel(x, y, r, g, b); + } else { + PFCanvas::setPixel(x, y, 0, 0, 0); + } + } + } + + PFCanvas::present(); +} + +} // namespace TriMarch + +// ── Made with Patternflow Live Editor · https://patternflow.work/pattern ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0718.h b/firmware/patternflow/presets/preset_0718.h new file mode 100644 index 0000000..4c81a53 --- /dev/null +++ b/firmware/patternflow/presets/preset_0718.h @@ -0,0 +1,118 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260718_Warped Wave +// Author: Seunghun LEE +// Date: 2026-07-18 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_math.h" +#include "../src/core_color.h" + +namespace WarpedWave { + +const char* NAME = "Warped Wave"; +const char* const KNOB_LABELS[4] = {"Hue", "Speed", "Warp Amp", "Warp Freq"}; + +static float s_hue = 0.0f; +static float s_speed = 2.0f; +static float s_warpAmp = 0.2f; +static float s_warpFreq = 0.5f; +static float s_timeAcc = 0.0f; + +void setup() { + PFMath::buildSinLUT(); + // no PFTables needed — warping uses moving-center / non-radial math +} + +void update(float dt, const InputFrame& input) { + // Accumulate knob deltas + s_hue += input.knobDeltas[0] * 0.05f; + s_speed += input.knobDeltas[1] * 0.1f; + s_warpAmp += input.knobDeltas[2] * 0.05f; + s_warpFreq += input.knobDeltas[3] * 0.05f; + + // Clamp to documented ranges + s_hue = fmaxf(0.0f, fminf(1.0f, s_hue)); + s_speed = fmaxf(-20.0f, fminf(20.0f, s_speed)); + s_warpAmp = fmaxf(0.1f, fminf(1.0f, s_warpAmp)); + s_warpFreq = fmaxf(0.0f, fminf(30.0f, s_warpFreq)); + + // Button resets (press, not long-press) + if (input.btnPressed[0]) s_hue = 0.0f; + if (input.btnPressed[1]) s_speed = 2.0f; + if (input.btnPressed[2]) s_warpAmp = 0.2f; + if (input.btnPressed[3]) s_warpFreq = 0.5f; + + // Map warpAmp from raw 0.1..1 to 0..1.225 (as in JS: v[2]*0.25) + // We store raw and convert on use, preserving the JS mapping. + + s_timeAcc += dt * s_speed; + // Wrap time accumulator. Multipliers: t*0.3, t*0.4, t*1.0. Common period: 20π wraps all seamlessly. + const float PERIOD = 20.0f * PI; + if (s_timeAcc > PERIOD) s_timeAcc -= PERIOD; + if (s_timeAcc < 0.0f) s_timeAcc += PERIOD; +} + +void draw() { + const float w = (float)PANEL_RES_W; + const float h = (float)PANEL_RES_H; + const float invW = 1.0f / w; + const float invH = 1.0f / h; + const float t = s_timeAcc; + + const float waveFreq = 15.0f; + // Map raw warpAmp (0.1..1) to JS scale (0..1.225) via v*0.25 + const float warpAmp = s_warpAmp * 0.25f; + const float warpFreqBase = 3.0f + s_warpFreq * 8.0f; + + // Precompute hue RGB once per frame + uint8_t hcR, hcG, hcB; + PFColor::hsvToRgb(s_hue, 1.0f, 1.0f, hcR, hcG, hcB); + + for (int y = 0; y < PANEL_RES_H; ++y) { + const float ny = ((float)y * invH - 0.5f) * 2.0f; // -1..1 + for (int x = 0; x < PANEL_RES_W; ++x) { + const float nx = ((float)x * invW - 0.5f) * 2.0f; + + // Warp coordinates + const float wx = nx + warpAmp * PFMath::fastSin(ny * warpFreqBase + t * 0.3f); + const float wy = ny + warpAmp * PFMath::fastCos(nx * warpFreqBase * 1.3f + t * 0.4f); + + const float dist = sqrtf(wx * wx + wy * wy); + const float wave = PFMath::fastSin(dist * waveFreq + t); + + float tt = (wave * 0.8f + 1.0f) * 0.5f; + tt = fmaxf(0.0f, fminf(1.0f, tt)); + + uint8_t r = 0, g = 0, b = 0; + if (tt >= 0.154f) { + r = 10; g = 10; b = 10; + } + if (tt >= 0.556f) { + r = (uint8_t)fmaxf(0, fminf(255, (int)(hcR * 1.5f))); + g = (uint8_t)fmaxf(0, fminf(255, (int)(hcG * 1.5f))); + b = (uint8_t)fmaxf(0, fminf(255, (int)(hcB * 1.5f))); + } + if (tt >= 0.816f) { + r = 255; g = 255; b = 255; + } + + PFCanvas::setPixel(x, y, r, g, b); + } + } + + PFCanvas::present(); +} + +} // namespace WarpedWave + +// ── Made with Patternflow Pattern Lab · https://patternflow.work/pattern-lab ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/presets/preset_0719.h b/firmware/patternflow/presets/preset_0719.h new file mode 100644 index 0000000..27b9e44 --- /dev/null +++ b/firmware/patternflow/presets/preset_0719.h @@ -0,0 +1,272 @@ +#pragma once + +// ===== Patternflow pattern ===== +// Title: 260719_MagVortex +// Author: Seunghun LEE +// Date: 2026-07-19 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// =============================== + +#include +#include "config.h" +#include "../src/core_display.h" +#include "../src/core_encoders.h" +#include "../src/core_canvas.h" +#include "../src/core_mem.h" + +namespace MagVortex { + +const char* NAME = "MagVortex"; +const char* const KNOB_LABELS[4] = { + "TrailDecay", + "Velocity", + "SpinPull", + "InflowPitch" +}; + +// Parameter state +static float trailDecay = 0.924f; +static float velocity = 2.013f; +static float spin = 0.746f; +static float pitch = -2.203f; + +// Density map (0..1 trails). 128×64 floats (32 KB) — as a static array this +// sat in internal DRAM from boot for every build; allocated from PSRAM in +// setup() instead (PFMem). +static float* densityMap = nullptr; + +// Particle structure +struct Charge { + float r; + float angle; + float speedProfile; + float brightness; + float prevX; + float prevY; +}; + +static const int NUM_CHARGES = 48; +static Charge charges[NUM_CHARGES]; + +// Helper for random float 0..1 +static float randf() { + return (float)random(0x7FFFFFFF) / (float)0x7FFFFFFF; +} + +// Pre-baked color ramp LUT (verbatim) +static const uint8_t RAMP_LUT[256][3] = { + {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,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,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},{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,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},{43,39,47},{80,61,100},{107,61,153},{123,40,205},{128,0,255},{132,0,255},{136,0,255}, + {141,0,255},{145,0,255},{149,0,255},{153,0,255},{157,0,255},{161,0,255},{165,0,255},{169,0,255}, + {174,0,255},{178,0,255},{182,0,255},{186,0,255},{190,0,255},{194,0,255},{198,0,255},{202,0,255}, + {206,0,255},{211,0,255},{215,0,255},{219,0,255},{223,0,255},{228,0,255},{232,0,255},{236,0,255}, + {240,0,255},{245,0,255},{249,0,255},{253,0,255},{255,0,253},{255,0,248},{255,0,244},{255,0,240}, + {255,0,236},{255,0,231},{255,0,227},{255,0,223},{255,0,219},{255,0,214},{255,0,210},{255,0,206}, + {255,0,202},{255,0,197},{255,0,193},{255,0,189},{255,0,185},{255,0,180},{255,0,176},{255,0,172}, + {255,0,168},{255,0,163},{255,0,159},{255,0,155},{255,0,151},{255,0,146},{255,0,142},{255,0,138}, + {255,0,134},{255,0,129},{255,0,125},{255,0,121},{255,0,117},{255,0,112},{255,0,108},{255,0,104}, + {255,0,100},{255,0,95},{255,0,91},{255,0,87},{255,0,83},{255,0,78},{255,0,74},{255,0,70}, + {255,0,66},{255,0,61},{255,0,57},{255,0,53},{255,0,49},{255,0,44},{255,0,40},{255,0,36}, + {255,0,32},{255,0,27},{255,0,23},{255,0,19},{255,0,15},{255,0,10},{255,0,6},{255,0,2}, + {255,13,13},{255,38,38},{255,63,63},{255,88,88},{255,113,113},{255,138,138},{255,163,163},{255,188,188}, + {255,213,213},{255,238,238},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, + {255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255},{255,255,255}, +}; + +void setup() { + // Seed randomness + randomSeed(esp_random()); + + // Density map lives in PSRAM; PFMem zeroes fresh allocations. + if (!densityMap) { + densityMap = PFMem::allocFloats(PANEL_RES_W * PANEL_RES_H); + } else { + for (int i = 0; i < PANEL_RES_W * PANEL_RES_H; i++) { + densityMap[i] = 0.0f; + } + } + + // Initialize particles + const float cx = PANEL_RES_W / 2.0f; + const float cy = PANEL_RES_H / 2.0f; + for (int i = 0; i < NUM_CHARGES; i++) { + float r = 10.0f + randf() * 60.0f; + float angle = randf() * TWO_PI; + float xPos = cx + cosf(angle) * r; + float yPos = cy + sinf(angle) * r; + + charges[i].r = r; + charges[i].angle = angle; + charges[i].speedProfile = 0.8f + randf() * 1.4f; + charges[i].brightness = 0.4f + randf() * 0.6f; + charges[i].prevX = xPos; + charges[i].prevY = yPos; + } +} + +void update(float dt, const InputFrame& input) { + if (!densityMap) return; // allocation failed — degrade to a blank pattern + + // Knob 0: TrailDecay + trailDecay += input.knobDeltas[0] * 0.05f; + if (trailDecay < 0.5f) trailDecay = 0.5f; + if (trailDecay > 0.98f) trailDecay = 0.98f; + + // Knob 1: Velocity + velocity += input.knobDeltas[1] * 0.1f; + if (velocity < -3.0f) velocity = -3.0f; + if (velocity > 5.0f) velocity = 5.0f; + + // Knob 2: SpinPull + spin += input.knobDeltas[2] * 0.05f; + if (spin < 0.0f) spin = 0.0f; + if (spin > 3.0f) spin = 3.0f; + + // Knob 3: InflowPitch + pitch += input.knobDeltas[3] * 0.05f; + if (pitch < -20.0f) pitch = -20.0f; + if (pitch > 20.0f) pitch = 20.0f; + + // Button resets + if (input.btnPressed[0]) trailDecay = 0.924f; + if (input.btnPressed[1]) velocity = 2.013f; + if (input.btnPressed[2]) spin = 0.746f; + if (input.btnPressed[3]) pitch = -2.203f; + + // Decay the density map + for (int i = 0; i < PANEL_RES_W * PANEL_RES_H; i++) { + densityMap[i] *= trailDecay; + } + + float tStep = velocity * dt * 4.0f; + const float cx = PANEL_RES_W / 2.0f; + const float cy = PANEL_RES_H / 2.0f; + + for (int i = 0; i < NUM_CHARGES; i++) { + Charge& c = charges[i]; + + // Save previous position + c.prevX = cx + cosf(c.angle) * c.r; + c.prevY = cy + sinf(c.angle) * c.r; + + // Update orbit + c.angle += spin * 0.4f * c.speedProfile * tStep; + c.r -= pitch * 6.0f * tStep; + + // Reset logic depending on flow direction + if (pitch > 0.01f) { + // Inward flow + if (c.r < 3.0f) { + c.r = 55.0f + randf() * 35.0f; + c.angle = randf() * TWO_PI; + c.prevX = cx + cosf(c.angle) * c.r; + c.prevY = cy + sinf(c.angle) * c.r; + } + } else if (pitch < -0.01f) { + // Outward flow + if (c.r > 90.0f) { + c.r = 2.0f + randf() * 5.0f; + c.angle = randf() * TWO_PI; + c.prevX = cx + cosf(c.angle) * c.r; + c.prevY = cy + sinf(c.angle) * c.r; + } + } else { + // Neutral + if (c.r < 3.0f || c.r > 90.0f) { + c.r = 30.0f + randf() * 40.0f; + c.angle = randf() * TWO_PI; + c.prevX = cx + cosf(c.angle) * c.r; + c.prevY = cy + sinf(c.angle) * c.r; + } + } + + // Current position + float curX = cx + cosf(c.angle) * c.r; + float curY = cy + sinf(c.angle) * c.r; + + // Interpolate trail between previous and current + float dx = curX - c.prevX; + float dy = curY - c.prevY; + float dist = sqrtf(dx * dx + dy * dy); + int steps = (int)ceilf(dist); + if (steps < 1) steps = 1; + + float addVal = c.brightness * 0.7f; + for (int s = 0; s <= steps; s++) { + float frac = (float)s / (float)steps; + float px = c.prevX + dx * frac; + float py = c.prevY + dy * frac; + int ix = (int)floorf(px); + int iy = (int)floorf(py); + + if (ix >= 0 && ix < PANEL_RES_W && iy >= 0 && iy < PANEL_RES_H) { + int idx = iy * PANEL_RES_W + ix; + float newVal = densityMap[idx] + addVal; + if (newVal > 1.0f) newVal = 1.0f; + densityMap[idx] = newVal; + } + } + } +} + +void draw() { + if (!densityMap) { // allocation failed — show nothing rather than crash + PFCanvas::present(); + return; + } + for (int y = 0; y < PANEL_RES_H; y++) { + for (int x = 0; x < PANEL_RES_W; x++) { + int idx = y * PANEL_RES_W + x; + float val = densityMap[idx]; + float v; + + if (val > 0.001f) { + if (val < 0.15f) { + v = 0.06f + val * 0.6f; + } else if (val < 0.45f) { + v = 0.2f + (val - 0.15f) * 1.3f; + } else if (val < 0.75f) { + v = 0.55f + (val - 0.45f) * 1.5f; + } else { + v = 0.85f + (val - 0.75f) * 2.0f; + } + if (v > 1.0f) v = 1.0f; + if (v < 0.0f) v = 0.0f; + } else { + v = 0.02f; + } + + int li = (int)(v * 255.0f + 0.5f); + if (li < 0) li = 0; + if (li > 255) li = 255; + PFCanvas::setPixel(x, y, + RAMP_LUT[li][0], + RAMP_LUT[li][1], + RAMP_LUT[li][2]); + } + } + PFCanvas::present(); +} + +} // namespace MagVortex + +// ── Made with Patternflow Pattern Lab · https://patternflow.work/pattern-lab ── +// Shared under CC-BY-SA-4.0. Attribution is part of this licence — +// please keep this notice and the author credit above when you reuse, +// remix, or redistribute this pattern. Do not delete it. diff --git a/firmware/patternflow/src/core_mem.h b/firmware/patternflow/src/core_mem.h new file mode 100644 index 0000000..557596c --- /dev/null +++ b/firmware/patternflow/src/core_mem.h @@ -0,0 +1,52 @@ +// ═══════════════════════════════════════════════════════════ +// PatternFlow - Big-buffer allocation for patterns +// +// Every pattern's namespace statics live in internal DRAM from the +// moment the chip boots, whether or not that pattern is active — all +// preset headers are compiled into one image. A single framebuffer- +// sized array (128×64 floats = 32 KB) is a fifth of the DRAM the +// Wi-Fi stack and the HUB75 DMA driver have to share, and two of +// them were enough to boot-loop the device. +// +// So: framebuffer-sized state (trail maps, glow accumulators, per- +// pixel scratch) must NOT be a static array. Declare a pointer and +// allocate it once from setup(): +// +// static float* trail = nullptr; +// void setup() { +// if (!trail) trail = PFMem::allocFloats(PANEL_RES_W * PANEL_RES_H); +// } +// +// Allocation prefers PSRAM (8 MB on the reference R16N8 board) and +// falls back to internal heap when PSRAM is absent. Returned memory +// is zeroed. Pattern setup() runs once at boot and never again, so +// there is no free() path — treat the buffer as owned for the life +// of the firmware. Guard update()/draw() with `if (!trail) return;` +// so an allocation failure degrades to a blank pattern, not a crash. +// +// Small fixed state (a few dozen particles, knob values) is fine as +// plain statics — this is only for the big per-pixel buffers. +// License: MIT +// ═══════════════════════════════════════════════════════════ +#pragma once + +#include +#include + +namespace PFMem { + +inline void* alloc(size_t bytes) { + void* p = nullptr; +#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32) + if (psramFound()) p = ps_malloc(bytes); +#endif + if (!p) p = malloc(bytes); + if (p) memset(p, 0, bytes); + return p; +} + +inline float* allocFloats(int count) { + return (float*)alloc((size_t)count * sizeof(float)); +} + +} // namespace PFMem diff --git a/firmware/patternflow/src/core_tables.h b/firmware/patternflow/src/core_tables.h index b96b9c3..7692508 100644 --- a/firmware/patternflow/src/core_tables.h +++ b/firmware/patternflow/src/core_tables.h @@ -40,6 +40,7 @@ #include #include #include "config.h" +#include "core_mem.h" namespace PFTables { @@ -51,20 +52,11 @@ inline int idx(int x, int y) { return y * PANEL_RES_W + x; } -inline float* allocFloats(int count) { - void* p = nullptr; -#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32) - if (psramFound()) p = ps_malloc((size_t)count * sizeof(float)); -#endif - if (!p) p = malloc((size_t)count * sizeof(float)); - return (float*)p; -} - inline void init() { if (ready) return; const int N = PANEL_RES_W * PANEL_RES_H; - rT = allocFloats(N); - thetaT = allocFloats(N); + rT = PFMem::allocFloats(N); + thetaT = PFMem::allocFloats(N); if (!rT || !thetaT) { // Allocation failed (no PSRAM and internal heap exhausted). Leave the // tables unavailable rather than half-initialized. diff --git a/hardware/case/README.md b/hardware/case/README.md index c140b4a..9db1abe 100644 --- a/hardware/case/README.md +++ b/hardware/case/README.md @@ -9,7 +9,7 @@ | Your printer bed | Print | Design | Status | |---|---|---|---| | **256 mm** (Bambu P1S / X1C / A1 class) | [`bed_256mm/encloser.stl`](bed_256mm/) | The snap-fit design split for a 256 mm bed — body frame, back panels, and LED-panel mount in one STL | ✅ Standard option, ~10 h total | -| **~330 mm+** (Bambu H2S-class) | [`bed_330mm/`](bed_330mm/) | One-piece snap-fit body + closing part | ✅ Main design ([#113](https://github.com/engmung/Patternflow/issues/113)); print verification of this exact STL pending | +| **~330 mm+** (Bambu H2S-class) | [`bed_330mm/encloser.stl`](bed_330mm/) | The one-piece original — body, closing part, and LED-panel mount in one STL, no bonding at all | ✅ Main design ([#113](https://github.com/engmung/Patternflow/issues/113)) | **Whichever body you print, also print [`knobs/knobs_20mm.stl`](knobs/) — required for every build.** Knobs go in **black**, everything else in **white**; run the knob plate as its own print job. (15 mm-shaft encoders → `knobs_15mm.stl`; 15 mm and 20 mm shafts are functionally identical, and the [BOM](../bom/)'s reference part, PEC11R-4220F-S0024, is 20 mm.) @@ -17,10 +17,7 @@ The original mass-production-oriented design: a single-piece body plus a snap-fit closing part, wall-mount hanger hole included. No bonding step at all. Does **not** fit a 256 mm bed. -| File | Color | -|---|---| -| `oneshot_v3_part1.stl` | White PLA | -| `oneshot_v3_part2.stl` | White PLA | +**`encloser.stl`** — everything in one STL (body, closing part, LED-panel mount), all **white** PLA; knobs from [`knobs/`](knobs/) in black. Same design as the print-&-assembly-verified 256 mm kit, just uncut. ## `bed_256mm/` — the standard build diff --git a/hardware/case/bed_330mm/encloser.stl b/hardware/case/bed_330mm/encloser.stl new file mode 100644 index 0000000..333c7f9 Binary files /dev/null and b/hardware/case/bed_330mm/encloser.stl differ diff --git a/hardware/case/bed_330mm/oneshot_v3_part1.stl b/hardware/case/bed_330mm/oneshot_v3_part1.stl deleted file mode 100644 index 3f858d4..0000000 Binary files a/hardware/case/bed_330mm/oneshot_v3_part1.stl and /dev/null differ diff --git a/hardware/case/bed_330mm/oneshot_v3_part2.stl b/hardware/case/bed_330mm/oneshot_v3_part2.stl deleted file mode 100644 index a4a607f..0000000 Binary files a/hardware/case/bed_330mm/oneshot_v3_part2.stl and /dev/null differ diff --git a/hardware/case/source/patternflow_case.blend b/hardware/case/source/patternflow_case.blend index f41066d..502d7d6 100644 --- a/hardware/case/source/patternflow_case.blend +++ b/hardware/case/source/patternflow_case.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6e8e099e97cb25a81dcbdfe2e0d54dfff185ca7ab069c2bddd8d88e1f3f9ab6 -size 49467529 +oid sha256:f3dff114846afb7d5431736186b8c37f67836a1366c946a15ef0e1fe18c7eb62 +size 49469617 diff --git a/web/public/flash/bin/v2/patternflow.ino.bin b/web/public/flash/bin/v2/patternflow.ino.bin index 1f9bfdf..48c3f66 100644 Binary files a/web/public/flash/bin/v2/patternflow.ino.bin and b/web/public/flash/bin/v2/patternflow.ino.bin differ diff --git a/web/src/app/pattern-lab/PatternLabClient.tsx b/web/src/app/pattern-lab/PatternLabClient.tsx index b81055c..608576f 100644 --- a/web/src/app/pattern-lab/PatternLabClient.tsx +++ b/web/src/app/pattern-lab/PatternLabClient.tsx @@ -1389,6 +1389,7 @@ Conditional includes — only when actually used in your code: #include "src/core_color.h" // PFColor:: hsvToRgb, buildPowLUT/buildPowLUTf, ColorStop, sampleRamp #include "src/core_noise.h" // PFNoise:: cellHash, valueNoise2D, perlin2D, fractal2D #include "src/core_tables.h" // PFTables:: init(), rT[], thetaT[] — per-pixel radius/angle from the panel center, precomputed + #include "src/core_mem.h" // PFMem:: allocFloats — PSRAM-first, zeroed allocation for framebuffer-sized buffers Helper signatures — these are the FULL argument lists. Call them exactly like this; do not add size arguments, reorder parameters, or invent overloads: @@ -1407,6 +1408,7 @@ Helper signatures — these are the FULL argument lists. Call them exactly like PFColor::hsvToRgb(h01, s01, v01, r8, g8, b8); // h/s/v floats 0..1; r8/g8/b8 are uint8_t& outputs static uint8_t plut[256]; PFColor::buildPowLUT(exponent, plut); // fills (i/255)^exp scaled to 0..255 static float plutf[256]; PFColor::buildPowLUTf(exponent, plutf); // fills (i/255)^exp as 0..1 floats + static float* buf = nullptr; buf = PFMem::allocFloats(count); // in setup(); PSRAM-first, returns zeroed memory (or nullptr) Other interface rules: - Use PANEL_RES_W and PANEL_RES_H. Never hardcode 128 or 64. @@ -1414,6 +1416,18 @@ Other interface rules: - The last line of draw() must be PFCanvas::present();. Without it nothing reaches the panel. - Macro collisions: Arduino.h and config.h define macros that will preprocessor-mangle same-named declarations into compile errors. Do NOT define your own variables, constants, or functions named PI, TWO_PI, HALF_PI, DEG_TO_RAD, RAD_TO_DEG, EULER, min, max, abs, sq, round, radians, degrees, constrain, MAX_HUE, MAX_SPEED, SPEED_STEP, MAX_FREQ, or FREQ_STEP. Use the existing PI / TWO_PI constants directly, use fminf/fmaxf/fabsf for your own helpers, and prefix pattern constants with the pattern name (e.g. CELLS_TWO_PI, CELLS_SPEED_STEP). +## Memory rules — big buffers must never be static arrays +All patterns compile into one firmware image, so every namespace static array occupies internal DRAM from boot even while the pattern is inactive — and internal DRAM is shared with the Wi-Fi stack and the HUB75 DMA driver. Two 32 KB static arrays were enough to boot-loop a real device. + +- Any per-pixel buffer (trail map, glow/density accumulator, feedback field — anything sized by PANEL_RES_W * PANEL_RES_H or similar) must be a POINTER allocated once in setup() with PFMem::allocFloats (include src/core_mem.h). PFMem allocates from PSRAM when available and returns zeroed memory: + + static float* trail = nullptr; + void setup() { if (!trail) trail = PFMem::allocFloats(PANEL_RES_W * PANEL_RES_H); } + +- Guard update() with \`if (!trail) return;\` and start draw() with \`if (!trail) { PFCanvas::present(); return; }\` so a failed allocation degrades to a blank pattern instead of crashing. +- Small fixed state (particle arrays, knob params, 256-entry LUTs — anything under ~2 KB) stays as plain statics; this rule is only for framebuffer-scale buffers. +- Call PFTables::init() ONLY if the code actually reads PFTables::rT or PFTables::thetaT. Never call it "just in case" — it allocates two 32 KB tables. + ## DO NOT reimplement existing helpers The firmware ships tested, optimized versions of these. Using your own breaks shared optimizations (color calibration, sin LUT sharing) and wastes ROM. If the JavaScript source contains an inline hsvToRgb or sin LUT, strip it and call the firmware helper instead. @@ -1485,9 +1499,10 @@ Before finalizing your code block, verify each of these. If any answer is wrong, 4. Are all pixel writes via PFCanvas::setPixel? Did I avoid touching dma_display? 5. Do my knob parameters consume input.knobDeltas (not input.knobValues), constrained to the documented range? 6. Does every time accumulator wrap at its period (TWO_PI or a common multiple — see Performance)? An unbounded accumulator is a bug even if the preview looks fine. -7. Is every line valid C++ that will compile — no stray tokens, no placeholder text, no truncated statements? Re-read the block once before finalizing.${ +7. Is every line valid C++ that will compile — no stray tokens, no placeholder text, no truncated statements? Re-read the block once before finalizing. +8. Did I declare any static array bigger than ~2 KB (e.g. float buf[PANEL_RES_W * PANEL_RES_H])? If yes, convert it to a PFMem::allocFloats pointer per the Memory rules. Did I call PFTables::init() without reading rT/thetaT anywhere? If yes, remove the call.${ usesValueField - ? "\n8. Did I paste the RAMP_LUT table verbatim (all 256 entries, unchanged), and does draw() get every color exclusively from RAMP_LUT with no other color code?" + ? "\n9. Did I paste the RAMP_LUT table verbatim (all 256 entries, unchanged), and does draw() get every color exclusively from RAMP_LUT with no other color code?" : "" } diff --git a/web/src/components/sections/PatternPanel.module.css b/web/src/components/sections/PatternPanel.module.css index e0b9658..a4acdff 100644 --- a/web/src/components/sections/PatternPanel.module.css +++ b/web/src/components/sections/PatternPanel.module.css @@ -276,6 +276,22 @@ color: var(--pf-cream); } +.toolItem { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + margin-top: 4px; +} + +.toolDesc { + margin: 0; + max-width: 560px; + color: var(--pf-ink-muted); + font-size: var(--pf-fs-body); + line-height: 1.5; +} + .unsupported { margin-top: 10px; color: var(--pf-ink-muted); diff --git a/web/src/components/sections/PatternPanel.tsx b/web/src/components/sections/PatternPanel.tsx index d137e1b..61b4a99 100644 --- a/web/src/components/sections/PatternPanel.tsx +++ b/web/src/components/sections/PatternPanel.tsx @@ -37,6 +37,7 @@ Required header structure: \`#include "src/core_math.h"\` // PFMath:: fastSin, fastCos, fract, lerp, approxLength, sin LUT \`#include "src/core_color.h"\` // PFColor:: hsvToRgb, ColorStop, sampleRamp \`#include "src/core_noise.h"\` // PFNoise:: perlin2D, fractal2D + \`#include "src/core_mem.h"\` // PFMem:: allocFloats — PSRAM-first, zeroed allocation for framebuffer-sized buffers - Do not use \`\`, \`\`, \`\`, \`std::clamp\`, \`std::round\`, \`std::vector\`, \`std::string\`, dynamic allocation, exceptions, file IO, external libraries, or placeholder declarations like \`extern Display*\` or mock \`InputFrame\`. - Use Arduino/math functions: \`constrain()\`, \`roundf()\`, \`floorf()\`, \`fmodf()\`, \`powf()\`. Prefer \`PFMath::fastSin()\` / \`PFMath::fastCos()\` over \`sinf()\` / \`cosf()\` inside the pixel loop. - Use \`PANEL_RES_W\` and \`PANEL_RES_H\`; never hardcode 128 or 64. @@ -54,6 +55,14 @@ ESP32 optimization: - Precompute row-only and column-only warp terms once per frame outside the inner pixel loop. - Preserve the visual structure, but prefer a faster approximation over a literal slow translation. +Memory rules (big buffers must never be static arrays): +- All patterns compile into one firmware image, so every namespace static array occupies internal DRAM from boot even while the pattern is inactive — and internal DRAM is shared with the Wi-Fi stack and the HUB75 DMA driver. Never declare a framebuffer-sized static array such as \`static float buf[PANEL_RES_W * PANEL_RES_H]\` (32 KB locked at boot). +- Any per-pixel buffer (trail map, glow/density accumulator, feedback field) must be a POINTER allocated once in \`setup()\` with \`PFMem::allocFloats\` from \`src/core_mem.h\` — it allocates from PSRAM when available and returns zeroed memory: + \`static float* trail = nullptr;\` + \`void setup() { if (!trail) trail = PFMem::allocFloats(PANEL_RES_W * PANEL_RES_H); }\` +- Guard \`update()\` with \`if (!trail) return;\` and start \`draw()\` with \`if (!trail) { PFCanvas::present(); return; }\` so a failed allocation degrades to a blank pattern instead of crashing. +- Small fixed state (particle arrays, knob params, 256-entry LUTs — anything under ~2 KB) stays as plain statics; this rule is only for framebuffer-scale buffers. + Required namespace interface: - Choose a stable PascalCase namespace ending in \`Pattern\`. - Inside the namespace, define: @@ -522,9 +531,14 @@ export default function PatternPanel({ content }: PatternPanelProps) {
Pattern tools - - Pattern Lab - +
+ + Pattern Lab + +

+ Use Pattern Lab if you want to build more sophisticated patterns. +

+