Skip to content

Instantly share code, notes, and snippets.

@AndyMoreland
Created February 28, 2026 21:19
Show Gist options
  • Select an option

  • Save AndyMoreland/686ed7c8742169a9a85d88b24c556ae1 to your computer and use it in GitHub Desktop.

Select an option

Save AndyMoreland/686ed7c8742169a9a85d88b24c556ae1 to your computer and use it in GitHub Desktop.
Chalk DAG with sparkles flowing through the graph
<!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