# PixelJam — Frequency UI

Radar-based music player interface designed for the **ESP32-S3 Sense** microcontroller.
Drag the dial with mouse or finger to rotate the character. Select tracks to morph the radar polygon.

---

## File structure

```
pixeljam-ui/
├── index.html   ← Semantic HTML shell, no logic or styles inline
├── style.css    ← All layout, theming, and component styles
├── script.js    ← All rendering, interaction, and state logic
└── README.md    ← This file
```

---

## index.html — Structure

The HTML is a **pure structural shell**. Two main sections inside `.app`:

| Element | Role |
|---|---|
| `.app` | CSS Grid wrapper — two equal columns |
| `.left-panel` | Contains the radar canvas + axis labels + titles |
| `#radar-canvas` | 420×420 `<canvas>` — all drawing done in JS |
| `.axis-lbl` × 5 | Positioned labels (raw, deep, bright, soft, dark) |
| `.right-panel` | Contains now-playing info, controls, playlist |
| `.now-playing-block` | Track number, name, artist, description |
| `#vibe-tags` | Injected pill tags per track |
| `.controls` | Prev / Play-Pause / Next buttons |
| `.dial-hint` | Dial rotation progress bar + degree readout |
| `#playlist` | Track list built by `buildPlaylist()` |
| `.esp-status` | Pulsing green dot with "ESP32-S3 · ready" label |

---

## style.css — Visual design

### Layout
- `.app` uses `display: grid; grid-template-columns: 1fr 1fr` — equal halves.
- Left panel is vertically centred with `flexbox`.
- Right panel scrolls independently (`overflow-y: auto`).

### Design tokens (CSS variables on `:root`)
```css
--bg        #eeeae3   warm off-white page background
--surface   #f5f2ec   slightly lighter surface
--border    #d0cdc7   subtle dividers
--text-hi   #111111   primary text
--text-mid  #555550   secondary text
--text-lo   #999990   muted labels / metadata
--accent    #111111   buttons, active states
--pill-bg   #e2dfd8   vibe tag background
```

### Key components

**Dot-grid texture** — `.left-panel::before` uses `radial-gradient` tiled at 24 px to create the paper-like texture.

**Radar canvas cursor** — `cursor: grab` at rest, `cursor: grabbing` during drag. `touch-action: none` disables browser scroll so JS can intercept all touch events.

**Track card** — `.track-row.active` adds bold name + dark dot. Hover shifts `padding-left` for a subtle slide effect.

**Dial bar** — a thin 2 px bar with a dark fill whose width is updated by JS to reflect current rotation (0–360° → 0–100%).

**ESP status dot** — 7 px circle in `#5a9a5a` green with a CSS `@keyframes pulse` opacity animation.

**Responsive** — at `max-width: 760px` the grid stacks to a single column. Canvas shrinks to 320×320.

---

## script.js — Logic

### Asset embedding
```js
const DISK_JPG  = "...";   // data:image/jpeg;base64,...
const WEBP_B64  = "...";   // data:image/webp;base64,...
```
Both files are fully inlined as base-64 strings, so the project works from `file://` — no server required. This also works when loading from ESP32 SPIFFS.

### TRACKS array
```js
{
  num, name, artist, desc,
  tags: ["dark","deep","slow"],        // shown as pill chips
  vibes: { raw, deep, bright, soft, dark },  // 0.0–1.0 per axis
  charCol: 0                           // column in 5×4 sprite sheet
}
```

### State variables
| Name | Type | Description |
|---|---|---|
| `current` | number | Index of active track |
| `isPlaying` | boolean | Whether transport is playing |
| `displayed` | object | Current interpolated vibe values (morphing) |
| `target` | object | Target vibe values after track switch |
| `dialAngle` | number | Character rotation in radians (drag-controlled) |
| `isDragging` | boolean | True while pointer/touch is held |
| `dragStartAngle` | number | Angle at drag start (from canvas centre) |
| `dragStartDial` | number | `dialAngle` value at drag start |

### drawFrame()
Called every time state changes. Paints **7 layers** onto `#radar-canvas`:

1. **Disk JPG** — clipped to a circle of radius 170 px. Uses `ctx.drawImage`.
2. **Grid circles** — three concentric arcs at 33 %, 66 %, 100 % of `MAX_R`. Inner two are dashed.
3. **Axis lines** — five lines from centre to `MAX_R` at pentagon angles.
4. **Character inside polygon** — `ctx.clip(path)` restricts drawing to the polygon shape, then the sprite is drawn rotated by `dialAngle` around the canvas centre.
5. **Polygon outline** — 1.5 px dark stroke on the Path2D.
6. **Vertex dots** — 3.5 px filled circles at each polygon corner.
7. **Spindle hole** — small white circle at centre.

### Polygon morph — startMorph()
```js
displayed[ax] = lerp(displayed[ax], target[ax], 0.09);
```
Runs via `requestAnimationFrame`. Interpolates each axis value at 9 % per frame toward `target`, stopping when all values are within 0.003. Calls `drawFrame()` each step.

### Drag-to-rotate
```
onDragStart → record dragStartAngle (atan2 from canvas centre) and dragStartDial
onDragMove  → delta = currentAngle − dragStartAngle → dialAngle = dragStartDial + delta
onDragEnd   → isDragging = false
```
The **same handlers** run for both mouse and touch events, making it work identically on desktop and mobile (or an ESP32 touch display):
- Mouse: `mousedown / mousemove / mouseup / mouseleave`
- Touch: `touchstart / touchmove / touchend` (all with `{ passive: false }` + `e.preventDefault()` to block scroll)

### updateDialBar()
Converts `dialAngle` (radians, unbounded) to a 0–360 ° display value and maps to 0–100 % bar width.

### selectTrack(i)
1. Updates `current`
2. Writes track metadata to DOM (`np-num`, `np-name`, `np-artist`, `np-desc`)
3. Rebuilds vibe pill tags
4. Sets `target = { ...t.vibes }` then calls `startMorph()`
5. Toggles `.active` class on playlist rows

### togglePlay()
Swaps the SVG `<path>` inside `#play-icon` between ▶ and ⏸ by setting `innerHTML` and toggling `fill`/`stroke` attributes.

---

## Running

### Locally (any browser)
```bash
# Option A — direct file open (works because assets are inlined)
open index.html

# Option B — local server
python3 -m http.server 8080   # visit http://localhost:8080
npx serve .
```

### On ESP32-S3 (SPIFFS)
1. Upload `index.html`, `style.css`, `script.js` to SPIFFS using the Arduino ESP32 filesystem uploader.
2. Serve them with `ESPAsyncWebServer`:
```cpp
server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
```
3. Connect to the ESP32 AP or local WiFi and open the IP in a mobile browser.
4. Touch drag works natively via the same touch event listeners.

---

## Customising tracks

Edit the `TRACKS` array in `script.js`:
```js
{ num:"006", name:"Your Track", artist:"Artist Name",
  desc:"your · description · here",
  tags:["tag1","tag2"],
  vibes:{ raw:0.5, deep:0.5, bright:0.5, soft:0.5, dark:0.5 },
  charCol: 0  // 0–4: which column in the sprite sheet
}
```

For a 6th track, also add `.track-card:nth-child(6)` positioning rules to `style.css` if using the card-stack layout variant.
