Schematik build

Generative Art Pendant

ESP32Beginner45 minutes
Photo of Generative Art Pendant

Schematik

Last updated August 26, 2026

This wearable pendant displays mesmerizing generative art patterns on a small color screen, powered by an ESP32 microcontroller. The animations respond to touch input, letting the wearer pause and shift through color variations by tapping the pendant.

The guide includes a complete parts list, wiring diagram for connecting the display and touch sensor to the ESP32, and ready-to-flash firmware that generates smooth, colorful animations. Assembly takes just minutes: verify the pendant board, insert the lightweight lithium battery, fit everything into a non-metal case, charge via USB, and wear as a unique conversation piece.

Wiring diagram

Gather all the parts

QtyComponent
1

Lithium Ion Polymer Battery - 3.7V 400mAh

3.7 V, 400 mAh

Lithium-ion polymer (also known as 'lipo' or 'lipoly') batteries are thin, light, and powerful. The output ranges from 4.2V when completely charged to 3.7V. This battery has a capacity of 400mAh for a total of about 1.9 Wh. If you need a larger (or smaller!) battery, we have a full range of LiPoly batteries.The batteries come pre-attached with a genuine 2-pin 25mm long JST-PH connector as shown and include the necessary protection circuitry. Because they have a genuine JST connector, not a knock-off, the cable won't snag or get stuck in a matching JST jack, they click in and out smoothly.

Assemble it in 4 steps

1. Check the pendant board

Place the Waveshare ESP32-S3-Touch-LCD-1.69 V2 on a clean table with the screen facing up. Leave the screen’s clear protective film in place until the case is ready, because scratches are very noticeable on a pendant.

  • The display and touch surface are already wired inside this board, so do not add jumper wires to their labeled connections.
  • Do not rest the powered board on metal, coins, or foil; they can touch two contacts together and damage it.

2. Plug in the thin battery

With the board unplugged, line up the battery’s tiny two-pin plug with the matching battery socket on the Waveshare board. Push it in gently by the plastic plug, not by pulling its wires. The LiPo battery in lipo_400mah powers the pendant.

  • The plug should slide in without force. If it does not, stop and turn it around rather than forcing it.
  • Make sure the battery plug matches the board socket’s positive and negative contacts — reversed battery wiring can damage the board or battery. Never use a swollen, punctured, or hot LiPo battery.

3. Fit it into a non-metal pendant case

Put the board and lipo_400mah into a roomy plastic, wood, or 3D-printed pendant case. Keep the battery flat and untwisted, keep the screen window clear, and add a small strain-relief loop so the battery wires cannot be pulled from their plug.

  • Leave a small opening for the USB-C connector so you can charge the pendant without opening the case.
  • Use thin foam tape only around the battery edges; do not press hard on the soft battery pouch.
  • Do not enclose the LiPo in a sealed case while charging. It needs to stay cool, and a damaged battery can swell. Avoid a metal necklace chain touching the board contacts.

4. Charge and wear it

Use a USB-C cable to power the board and charge lipo_400mah through the board’s built-in charging circuit. After disconnecting the cable, the animated artwork should keep running from the battery; tap the screen once to pause or resume it.

  • A 400 mAh battery is expected to run this bright animated pendant for roughly four hours; lowering screen brightness later can make it last longer.
  • Charge the pendant where you can keep an eye on it, on a non-flammable surface. Stop charging immediately if the battery or case becomes unusually hot.

Review all connections

1. Connections between "lipo_400mah" and "ESP32"

Functionlipo_400mahESP32
powerBAT+Waveshare board BAT+ contact on its built-in 2-pin battery connectorEXT
groundBAT-Waveshare board BAT- contact on its built-in 2-pin battery connectorEXT

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Arduino_GFX_Library.h>


// Forward declarations
uint16_t colorWheel(uint8_t wheel);
bool touchWasTapped();
void drawFrame(uint32_t t);

constexpr int LCD_DC = 4;
constexpr int LCD_CS = 5;
constexpr int LCD_SCK = 6;
constexpr int LCD_MOSI = 7;
constexpr int LCD_RST = 8;
constexpr int LCD_BL = 15;
constexpr int TOUCH_SCL = 10;
constexpr int TOUCH_SDA = 11;
constexpr int TOUCH_RST = 13;
constexpr int TOUCH_INT = 14;

constexpr int SCREEN_W = 240;
constexpr int SCREEN_H = 280;
constexpr uint32_t FRAME_INTERVAL_MS = 50;
constexpr uint8_t CST816T_ADDRESS = 0x15;

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI, GFX_NOT_DEFINED);
Arduino_GFX *display = new Arduino_ST7789(bus, LCD_RST, 1, true, SCREEN_W, SCREEN_H, 0, 20, 0, 0);

uint32_t lastFrameMs = 0;
uint32_t hueOffset = 0;
bool paused = false;

uint16_t colorWheel(uint8_t wheel) {
  wheel = 255 - wheel;
  if (wheel < 85) return display->color565(255 - wheel * 3, 0, wheel * 3);
  if (wheel < 170) {
    wheel -= 85;
    return display->color565(0, wheel * 3, 255 - wheel * 3);
  }
  wheel -= 170;
  return display->color565(wheel * 3, 255 - wheel * 3, 0);
}

bool touchWasTapped() {
  if (digitalRead(TOUCH_INT) == HIGH) return false;
  Wire.beginTransmission(CST816T_ADDRESS);
  Wire.write(0x02); // finger count register
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(CST816T_ADDRESS, static_cast<uint8_t>(1)) != 1) return false;
  return Wire.read() > 0;
}

void drawFrame(uint32_t t) {
  const int cx = SCREEN_W / 2;
  const int cy = SCREEN_H / 2;
  const float time = t * 0.001f;

  display->fillScreen(BLACK);
  for (int i = 0; i < 74; ++i) {
    float a = (i * 0.255f) + time * 0.72f;
    float b = (i * 0.137f) - time * 0.41f;
    float radius = 18.0f + (i % 19) * 5.2f + sinf(time * 1.5f + i * 0.31f) * 11.0f;
    int x1 = cx + static_cast<int>(cosf(a) * radius);
    int y1 = cy + static_cast<int>(sinf(b) * radius * 1.18f);
    int x2 = cx + static_cast<int>(cosf(a + 1.75f + sinf(time + i) * 0.35f) * (radius + 37));
    int y2 = cy + static_cast<int>(sinf(b + 1.75f) * (radius + 37) * 1.18f);
    display->drawLine(x1, y1, x2, y2, colorWheel((i * 9 + hueOffset) & 0xFF));
  }

  for (int ring = 0; ring < 7; ++ring) {
    int r = 25 + ring * 16 + static_cast<int>(sinf(time * 1.8f + ring) * 6.0f);
    display->drawCircle(cx, cy, r, colorWheel((ring * 31 + hueOffset + 80) & 0xFF));
  }

  display->fillCircle(cx, cy, 11 + static_cast<int>(sinf(time * 3.0f) * 3.0f), WHITE);
}

void setup() {
  pinMode(LCD_BL, OUTPUT);
  digitalWrite(LCD_BL, HIGH);
  pinMode(TOUCH_RST, OUTPUT);
  digitalWrite(TOUCH_RST, LOW);
  delay(10);
  digitalWrite(TOUCH_RST, HIGH);
  pinMode(TOUCH_INT, INPUT_PULLUP);
  Wire.begin(TOUCH_SDA, TOUCH_SCL);

  display->begin();
  display->fillScreen(BLACK);
  display->setTextColor(WHITE);
  display->setTextSize(2);
  display->setCursor(44, 126);
  display->println("Pocket bloom");
  display->setTextSize(1);
  display->setCursor(46, 154);
  display->println("tap to pause or play");
  delay(900);
}

void loop() {
  static bool touchHeld = false;
  bool touching = touchWasTapped();
  if (touching && !touchHeld) paused = !paused;
  touchHeld = touching;

  uint32_t now = millis();
  if (!paused && now - lastFrameMs >= FRAME_INTERVAL_MS) {
    lastFrameMs = now;
    hueOffset = (hueOffset + 2) & 0xFF;
    drawFrame(now);
  }
}

Download project files

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik