/*
ESP32-S3 + ILI9341 (240x320) — Animated Spiral Generator
10 color palettes | 5 synced gradient backgrounds
Auto-advance every 1400ms | manual advance via BOOT button (GPIO0)
Arvind Patil / AI Centre Nandurbar
Libraries required (Arduino Library Manager):
- Adafruit GFX Library
- Adafruit ILI9341
Wiring (ESP32-S3 dev board):
TFT_CS -> GPIO10
TFT_DC -> GPIO11
TFT_RST -> GPIO12
TFT_SCLK -> GPIO13
TFT_MOSI -> GPIO14
TFT_MISO -> GPIO21 (unused by the display, wired for HW-SPI compatibility)
BOOT btn -> GPIO0 (built-in, active LOW)
*/
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// ---------- Pin configuration ----------
#define TFT_CS 10
#define TFT_DC 11
#define TFT_RST 12
#define TFT_SCLK 13
#define TFT_MOSI 14
#define TFT_MISO 21
#define BOOT_BTN 0
Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_DC, TFT_CS, TFT_RST);
// ---------- Screen geometry ----------
const int16_t SCR_W = 240;
const int16_t SCR_H = 320;
const int16_t HEADER_H = 28;
const int16_t CX = SCR_W / 2;
const int16_t CY = HEADER_H + (SCR_H - HEADER_H) / 2;
const float MAX_R = 138.0f;
// ---------- Timing (non-blocking, no delay()) ----------
const uint32_t MODE_INTERVAL_MS = 1400;
const uint16_t FRAME_MS = 16; // ~87 draw ticks per 1400ms window
uint32_t lastModeSwitch = 0;
uint32_t lastFrame = 0;
// ---------- Mode state ----------
uint8_t spiralMode = 0; // 0..9 -> 10 palettes
const uint8_t NUM_MODES = 10;
const uint8_t NUM_BG = 5; // synced gradient backgrounds
float theta = 0.0f;
const float thetaStep = 0.35f; // angle increment per frame
const float THETA_MAX = 6.0f * PI; // 3 full turns per cycle
// ---------- Trig lookup table (360 steps) — avoids repeated sinf/cosf ----------
float sinTab[360];
float cosTab[360];
void buildTrigTable() {
for (int i = 0; i < 360; i++) {
float a = i * (PI / 180.0f);
sinTab[i] = sinf(a);
cosTab[i] = cosf(a);
}
}
inline float fastSin(float rad) {
int idx = ((int)(rad * (180.0f / PI))) % 360;
if (idx < 0) idx += 360;
return sinTab[idx];
}
inline float fastCos(float rad) {
int idx = ((int)(rad * (180.0f / PI))) % 360;
if (idx < 0) idx += 360;
return cosTab[idx];
}
// ---------- Palettes: 5-stop RGB565 gradient per spiral mode ----------
const uint16_t palettes[10][5] PROGMEM = {
{0xF800,0xFD20,0xFFE0,0x07E0,0x001F}, // 0 Rainbow classic
{0x780F,0x901F,0xA81F,0xC01F,0xE01F}, // 1 Violet fade
{0x07FF,0x03FF,0x021F,0x011F,0x000F}, // 2 Cyan -> Blue
{0xFFE0,0xFDA0,0xFB60,0xF920,0xF800}, // 3 Yellow -> Red
{0x07E0,0x2FE0,0x57E0,0x7FE0,0xA7E0}, // 4 Green shift
{0xFD20,0xFB00,0xF8C0,0xF640,0xF400}, // 5 Orange burn
{0xF81F,0xC01F,0x901F,0x601F,0x301F}, // 6 Magenta -> Purple
{0xFFFF,0xBDF7,0x7BEF,0x39E7,0x0000}, // 7 White -> Black
{0x001F,0x041F,0x081F,0x0C1F,0x101F}, // 8 Deep blue steps
{0x07E0,0xFFE0,0xF800,0xF81F,0x07FF} // 9 Multi-hue
};
void setup() {
Serial.begin(115200);
pinMode(BOOT_BTN, INPUT_PULLUP);
buildTrigTable();
SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI, TFT_CS);
tft.begin();
tft.setRotation(0); // portrait, 240x320
tft.fillScreen(ILI9341_BLACK);
resetModeVisual();
lastModeSwitch = millis();
lastFrame = millis();
}
void loop() {
uint32_t now = millis();
// ---- BOOT button: manual advance, debounced, active LOW ----
static bool lastBtn = HIGH;
static uint32_t lastBtnMs = 0;
bool btn = digitalRead(BOOT_BTN);
if (btn == LOW && lastBtn == HIGH && (now - lastBtnMs) > 180) {
advanceMode();
lastBtnMs = now;
}
lastBtn = btn;
// ---- Auto-advance every 1400ms ----
if (now - lastModeSwitch >= MODE_INTERVAL_MS) {
advanceMode();
}
// ---- Frame-paced incremental spiral drawing (no full-screen redraw per frame) ----
if (now - lastFrame >= FRAME_MS) {
lastFrame = now;
if (theta < THETA_MAX) {
drawSpiralPoint(theta);
theta += thetaStep;
}
}
}
void advanceMode() {
spiralMode = (spiralMode + 1) % NUM_MODES;
lastModeSwitch = millis();
resetModeVisual();
}
void resetModeVisual() {
theta = 0.0f;
uint8_t bgMode = spiralMode % NUM_BG; // sync background to spiral mode
drawGradientBackground(bgMode);
drawHeader();
}
// ---------- Background: fast horizontal-strip gradients (cheap vs per-pixel fill) ----------
void drawGradientBackground(uint8_t bgMode) {
const uint8_t STRIPS = 40;
int16_t bodyY = HEADER_H;
int16_t bodyH = SCR_H - HEADER_H;
int16_t stripH = bodyH / STRIPS;
uint8_t r0=0,g0=0,b0=0,r1=0,g1=0,b1=0;
switch (bgMode) {
case 0: r0=10; g0=10; b0=60; r1=90; g1=20; b1=140; break; // blue -> purple
case 1: r0=10; g0=60; b0=20; r1=200; g1=210; b1=30; break; // green -> yellow
case 2: r0=90; g0=10; b0=10; r1=230; g1=110; b1=10; break; // red -> orange
case 3: r0=10; g0=60; b0=90; r1=20; g1=40; b1=200; break; // cyan -> blue
default: break; // case 4 = rainbow wave, handled per-strip below
}
for (uint8_t i = 0; i < STRIPS; i++) {
uint16_t color;
if (bgMode == 4) {
float hue = (float)i / STRIPS * 360.0f;
color = hueToRGB565(hue);
} else {
float t = (float)i / (STRIPS - 1);
uint8_t r = r0 + (uint8_t)((r1 - r0) * t);
uint8_t g = g0 + (uint8_t)((g1 - g0) * t);
uint8_t b = b0 + (uint8_t)((b1 - b0) * t);
color = tft.color565(r, g, b);
}
tft.fillRect(0, bodyY + i * stripH, SCR_W, stripH + 1, color);
}
}
uint16_t hueToRGB565(float hue) {
float c = 1.0f, m = 0.0f;
float hp = hue / 60.0f;
float x = c * (1.0f - fabsf(fmodf(hp, 2.0f) - 1.0f));
float r, g, b;
if (hp < 1) { r = c; g = x; b = 0; }
else if (hp < 2) { r = x; g = c; b = 0; }
else if (hp < 3) { r = 0; g = c; b = x; }
else if (hp < 4) { r = 0; g = x; b = c; }
else if (hp < 5) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
// darkened a bit so header/spiral stay readable against it
return tft.color565((uint8_t)((r + m) * 170), (uint8_t)((g + m) * 170), (uint8_t)((b + m) * 170));
}
// ---------- Header: spiral index + hardware info ----------
void drawHeader() {
tft.fillRect(0, 0, SCR_W, HEADER_H, ILI9341_BLACK);
tft.drawFastHLine(0, HEADER_H - 1, SCR_W, ILI9341_DARKGREY);
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(4, 4);
tft.printf("Spiral %d/%d", spiralMode + 1, NUM_MODES);
tft.setCursor(4, 16);
tft.setTextColor(ILI9341_YELLOW);
tft.print("ESP32-S3 | ILI9341 240x320");
}
// ---------- Palette color interpolation (5 stops -> smooth gradient) ----------
uint16_t paletteColor(uint8_t mode, float t) {
if (t < 0) t = 0;
if (t > 1) t = 1;
float scaled = t * 4.0f;
int seg = (int)scaled;
if (seg > 3) seg = 3;
float f = scaled - seg;
uint16_t c0 = pgm_read_word(&palettes[mode][seg]);
uint16_t c1 = pgm_read_word(&palettes[mode][seg + 1]);
uint8_t r0 = (c0 >> 11) & 0x1F, g0 = (c0 >> 5) & 0x3F, b0 = c0 & 0x1F;
uint8_t r1 = (c1 >> 11) & 0x1F, g1 = (c1 >> 5) & 0x3F, b1 = c1 & 0x1F;
uint8_t r = r0 + (uint8_t)((r1 - r0) * f);
uint8_t g = g0 + (uint8_t)((g1 - g0) * f);
uint8_t b = b0 + (uint8_t)((b1 - b0) * f);
return (r << 11) | (g << 5) | b;
}
// ---------- Spiral point drawing (incremental — one small draw call per arm per frame) ----------
void drawSpiralPoint(float th) {
float t = th / THETA_MAX; // 0..1 progress -> drives color + radius
float r = MAX_R * t; // Archimedean growth
// 3 interleaved arms, phase-offset 120 degrees, for a fuller spiral silhouette
for (uint8_t arm = 0; arm < 3; arm++) {
float phase = th + arm * (2.0f * PI / 3.0f);
float x = CX + r * fastCos(phase);
float y = CY + r * fastSin(phase);
if (x < 0 || x >= SCR_W || y < HEADER_H || y >= SCR_H) continue;
uint16_t col = paletteColor(spiralMode, t);
tft.fillCircle((int16_t)x, (int16_t)y, 2, col);
}
}
Loading
esp32-s3-devkitc-1
esp32-s3-devkitc-1
https://wokwi.com/projects/473033788735931393