Created
February 28, 2026 21:19
-
-
Save AndyMoreland/686ed7c8742169a9a85d88b24c556ae1 to your computer and use it in GitHub Desktop.
Chalk DAG with sparkles flowing through the graph
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>DAG – Sparkles</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: #0d1117; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| #frame { | |
| background: #1a2a1f; | |
| border: 2px solid #2d4a35; | |
| border-radius: 8px; | |
| box-shadow: 0 0 60px rgba(74, 160, 96, 0.06); | |
| padding: 24px 32px; | |
| } | |
| pre { | |
| font-family: 'Menlo', 'Consolas', monospace; | |
| font-size: 15px; | |
| line-height: 1.35; | |
| color: #4a6b52; | |
| white-space: pre; | |
| letter-spacing: 0.5px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="frame"> | |
| <pre id="canvas"></pre> | |
| </div> | |
| <script> | |
| // ── DAG layout (row, col) ────────────────────────────────────── | |
| // Each node has a label, position (row, col in character grid), and children. | |
| // Edges flow downward through explicit waypoints. | |
| const GRAPH = ` | |
| ┌──────────┐ | |
| │ Source │ | |
| └────┬─────┘ | |
| │ | |
| ┌───────┴───────┐ | |
| │ │ | |
| ┌────┴─────┐ ┌─────┴────┐ | |
| │ Parse │ │ Fetch │ | |
| └────┬─────┘ └─────┬────┘ | |
| │ │ | |
| └───────┬───────┘ | |
| │ | |
| ┌─────┴─────┐ | |
| │ Transform │ | |
| └─────┬─────┘ | |
| │ | |
| ┌──────┴──────┐ | |
| │ │ | |
| ┌────┴─────┐ ┌─────┴────┐ | |
| │ Score │ │ Enrich │ | |
| └────┬─────┘ └─────┬────┘ | |
| │ │ | |
| └──────┬──────┘ | |
| │ | |
| ┌─────┴─────┐ | |
| │ Serve │ | |
| └───────────┘ | |
| `; | |
| // Parse the graph into a char grid | |
| const lines = GRAPH.split('\n').slice(1); // drop leading empty line | |
| const H = lines.length; | |
| const W = Math.max(...lines.map(l => l.length)); | |
| const grid = lines.map(l => l.padEnd(W, ' ')); | |
| // Build a mutable buffer for rendering | |
| function baseBuffer() { | |
| return grid.map(row => [...row]); | |
| } | |
| // ── Find edge characters that sparkles can travel along ───────── | |
| // Edge chars: │ ┬ ┴ ┐ ┘ ┌ └ ─ ┤ ├ | |
| const VERT = new Set('│┬┴┐┘┌└┤├'); | |
| const HORIZ = new Set('─┬┴┐┘┌└┤├'); | |
| const EDGE = new Set('│─┬┴┐┘┌└┤├'); | |
| // ── Trace all paths from top to bottom ────────────────────────── | |
| // We find all connected edge segments as paths (sequences of [r,c]). | |
| // Sparkles will follow these paths. | |
| function neighbors(r, c) { | |
| const ch = grid[r]?.[c]; | |
| if (!ch) return []; | |
| const result = []; | |
| // Can go down? | |
| if (VERT.has(ch) && r + 1 < H && VERT.has(grid[r+1][c])) result.push([r+1, c]); | |
| // Can go up? | |
| if (VERT.has(ch) && r - 1 >= 0 && VERT.has(grid[r-1][c])) result.push([r-1, c]); | |
| // Can go right? | |
| if (HORIZ.has(ch) && c + 1 < W && HORIZ.has(grid[r][c+1])) result.push([r, c+1]); | |
| // Can go left? | |
| if (HORIZ.has(ch) && c - 1 >= 0 && HORIZ.has(grid[r][c-1])) result.push([r, c-1]); | |
| return result; | |
| } | |
| // Find all edge cells and trace paths downward from the topmost edge cells. | |
| // We want paths that flow top-to-bottom for sparkle direction. | |
| function traceAllPaths() { | |
| // Find all edge cells | |
| const edgeCells = []; | |
| for (let r = 0; r < H; r++) { | |
| for (let c = 0; c < W; c++) { | |
| if (EDGE.has(grid[r][c])) edgeCells.push([r, c]); | |
| } | |
| } | |
| // Find topmost edge cells (entry points) — cells with no upward neighbor | |
| const starts = edgeCells.filter(([r, c]) => { | |
| const ch = grid[r][c]; | |
| return !(VERT.has(ch) && r - 1 >= 0 && VERT.has(grid[r-1][c])) && | |
| !(HORIZ.has(ch) && c - 1 >= 0 && HORIZ.has(grid[r][c-1]) && | |
| grid[r][c-1] !== '┐' && grid[r][c-1] !== '┘'); | |
| }); | |
| // BFS/DFS to trace paths from each start, always preferring downward flow | |
| const paths = []; | |
| function dfs(r, c, path, visited) { | |
| path.push([r, c]); | |
| visited.add(`${r},${c}`); | |
| const nexts = neighbors(r, c).filter(([nr, nc]) => !visited.has(`${nr},${nc}`)); | |
| if (nexts.length === 0) { | |
| paths.push([...path]); | |
| } else { | |
| for (const [nr, nc] of nexts) { | |
| dfs(nr, nc, path, new Set(visited)); | |
| } | |
| } | |
| path.pop(); | |
| } | |
| for (const [r, c] of starts) { | |
| dfs(r, c, [], new Set()); | |
| } | |
| return paths; | |
| } | |
| const allPaths = traceAllPaths(); | |
| // Filter to only substantial paths (more than 3 cells) | |
| const paths = allPaths.filter(p => p.length > 3); | |
| // ── Sparkle system ────────────────────────────────────────────── | |
| const SPARKLE_CHARS = ['✦', '✧', '·', '⋅', '˙']; | |
| const SPARKLE_COLORS = [ | |
| '#5ce882', '#4ade6a', '#3dcc5c', '#7aed9e', | |
| '#45d470', '#6be894', '#52db78', '#8af5ab', | |
| ]; | |
| const TRAIL_COLORS = [ | |
| 'rgba(92,232,130,0.65)', | |
| 'rgba(74,222,106,0.5)', | |
| 'rgba(61,204,92,0.38)', | |
| 'rgba(122,237,158,0.25)', | |
| 'rgba(69,212,112,0.15)', | |
| ]; | |
| class Sparkle { | |
| constructor() { | |
| this.reset(); | |
| } | |
| reset() { | |
| this.path = paths[Math.floor(Math.random() * paths.length)]; | |
| this.pos = 0; | |
| this.speed = 0.15 + Math.random() * 0.25; | |
| this.char = SPARKLE_CHARS[0]; | |
| this.colorIdx = Math.floor(Math.random() * SPARKLE_COLORS.length); | |
| this.trailLen = 3 + Math.floor(Math.random() * 5); | |
| } | |
| update(dt) { | |
| this.pos += this.speed * dt * 60; | |
| if (this.pos >= this.path.length) { | |
| this.reset(); | |
| } | |
| } | |
| getTrail() { | |
| const points = []; | |
| const headIdx = Math.floor(this.pos); | |
| for (let i = 0; i <= this.trailLen; i++) { | |
| const idx = headIdx - i; | |
| if (idx >= 0 && idx < this.path.length) { | |
| points.push({ r: this.path[idx][0], c: this.path[idx][1], age: i }); | |
| } | |
| } | |
| return points; | |
| } | |
| } | |
| const NUM_SPARKLES = 12; | |
| const sparkles = []; | |
| for (let i = 0; i < NUM_SPARKLES; i++) { | |
| const s = new Sparkle(); | |
| s.pos = Math.random() * (s.path.length - 1); // stagger starts | |
| sparkles.push(s); | |
| } | |
| // ── Box glow tracking ─────────────────────────────────────────── | |
| // Find box interiors to glow when a sparkle passes through adjacent edges | |
| const BOX_CHARS = new Set('┌┐└┘─│'); | |
| const LABEL_RE = /│\s+\w/; | |
| function findBoxes() { | |
| const boxes = []; | |
| for (let r = 0; r < H - 2; r++) { | |
| for (let c = 0; c < W - 5; c++) { | |
| if (grid[r][c] === '┌') { | |
| // Find matching ┐ | |
| let ec = c + 1; | |
| while (ec < W && grid[r][ec] !== '┐') ec++; | |
| if (ec >= W) continue; | |
| // Find matching └ | |
| let er = r + 1; | |
| while (er < H && grid[er][c] !== '└') er++; | |
| if (er >= H) continue; | |
| boxes.push({ r1: r, c1: c, r2: er, c2: ec }); | |
| } | |
| } | |
| } | |
| return boxes; | |
| } | |
| const boxes = findBoxes(); | |
| // ── Rendering ─────────────────────────────────────────────────── | |
| const canvasEl = document.getElementById('canvas'); | |
| // Color a character with an inline span | |
| function colorChar(ch, color) { | |
| return `<span style="color:${color}">${ch}</span>`; | |
| } | |
| function glowChar(ch, color, glow) { | |
| return `<span style="color:${color};text-shadow:0 0 ${glow}px ${color}">${ch}</span>`; | |
| } | |
| let lastTime = performance.now(); | |
| function render(now) { | |
| const dt = Math.min((now - lastTime) / 1000, 0.05); | |
| lastTime = now; | |
| // Update sparkles | |
| for (const s of sparkles) s.update(dt); | |
| // Collect all lit cells: map "r,c" -> { char, color, glow } | |
| const litCells = new Map(); | |
| for (const s of sparkles) { | |
| const trail = s.getTrail(); | |
| for (const { r, c, age } of trail) { | |
| const key = `${r},${c}`; | |
| if (age === 0) { | |
| // Head | |
| litCells.set(key, { | |
| char: SPARKLE_CHARS[0], | |
| color: SPARKLE_COLORS[s.colorIdx], | |
| glow: 8, | |
| }); | |
| } else { | |
| const existing = litCells.get(key); | |
| const intensity = Math.max(0, 1 - age / s.trailLen); | |
| if (!existing || existing.glow < intensity * 6) { | |
| const charIdx = Math.min(age, SPARKLE_CHARS.length - 1); | |
| litCells.set(key, { | |
| char: SPARKLE_CHARS[charIdx], | |
| color: TRAIL_COLORS[Math.min(age, TRAIL_COLORS.length - 1)], | |
| glow: intensity * 6, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| // Check which boxes are near sparkle heads for glow effect | |
| const boxGlow = boxes.map(box => { | |
| let maxGlow = 0; | |
| for (const s of sparkles) { | |
| const headIdx = Math.floor(s.pos); | |
| if (headIdx < 0 || headIdx >= s.path.length) continue; | |
| const [hr, hc] = s.path[headIdx]; | |
| // Check if sparkle head is on or adjacent to this box | |
| if (hr >= box.r1 - 1 && hr <= box.r2 + 1 && hc >= box.c1 - 1 && hc <= box.c2 + 1) { | |
| maxGlow = Math.max(maxGlow, 1.0); | |
| } else { | |
| // Proximity fade | |
| const dr = Math.max(0, Math.max(box.r1 - hr, hr - box.r2)); | |
| const dc = Math.max(0, Math.max(box.c1 - hc, hc - box.c2)); | |
| const dist = Math.sqrt(dr * dr + dc * dc); | |
| if (dist < 5) { | |
| maxGlow = Math.max(maxGlow, 1 - dist / 5); | |
| } | |
| } | |
| } | |
| return maxGlow; | |
| }); | |
| // Build output | |
| const output = []; | |
| for (let r = 0; r < H; r++) { | |
| let line = ''; | |
| for (let c = 0; c < W; c++) { | |
| const key = `${r},${c}`; | |
| const lit = litCells.get(key); | |
| const ch = grid[r][c]; | |
| if (lit && EDGE.has(ch)) { | |
| // Sparkle on an edge | |
| line += glowChar(lit.char, lit.color, lit.glow); | |
| } else { | |
| // Check if inside a glowing box | |
| let inBox = -1; | |
| for (let bi = 0; bi < boxes.length; bi++) { | |
| const b = boxes[bi]; | |
| if (r >= b.r1 && r <= b.r2 && c >= b.c1 && c <= b.c2) { | |
| inBox = bi; | |
| break; | |
| } | |
| } | |
| if (inBox >= 0 && boxGlow[inBox] > 0.1) { | |
| const g = boxGlow[inBox]; | |
| const r_ = Math.floor(50 + g * 60); | |
| const gn = Math.floor(90 + g * 140); | |
| const b_ = Math.floor(60 + g * 70); | |
| const color = `rgb(${r_},${gn},${b_})`; | |
| const glow = g * 6; | |
| if (EDGE.has(ch) || ch === '┌' || ch === '┐' || ch === '└' || ch === '┘') { | |
| line += glowChar(ch, color, glow); | |
| } else if (ch !== ' ') { | |
| line += colorChar(ch, color); | |
| } else { | |
| line += ch; | |
| } | |
| } else if (EDGE.has(ch)) { | |
| line += colorChar(ch, '#2d4a35'); | |
| } else if (ch !== ' ' && ch !== '\n') { | |
| // Label text | |
| line += colorChar(ch, '#4a6b52'); | |
| } else { | |
| line += ch; | |
| } | |
| } | |
| } | |
| output.push(line); | |
| } | |
| canvasEl.innerHTML = output.join('\n'); | |
| requestAnimationFrame(render); | |
| } | |
| requestAnimationFrame(render); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment