Sierpinski Flow

Interactive visualization of the bitwise art formula (x & y) % (y - x) % 37, producing fractal interference patterns from Sierpinski-like structure modulated by diagonal banding.

---
format: typebulb/v1
name: Sierpinski Flow
---

**code.tsx**

```tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createRoot } from 'react-dom/client';

const CONFIG = {
  DEFAULT_SIZE: 512,
  MIN_SIZE: 128,
  MAX_SIZE: 1024,
  DEFAULT_SPEED: 1,
  MAX_SPEED: 10,
  DEFAULT_MODULUS: 37,
  MIN_MODULUS: 2,
  MAX_MODULUS: 100,
} as const;

type PaletteMode = 'hue' | 'fire' | 'grayscale' | 'neon';

const PALETTES: Record<PaletteMode, (v: number, max: number) => [number, number, number]> = {
  hue: (v, max) => {
    const h = (v / max) * 360;
    return hslToRgb(h, 0.9, 0.5);
  },
  fire: (v, max) => {
    const t = v / max;
    return [
      Math.min(255, t * 3 * 255) | 0,
      Math.min(255, Math.max(0, (t - 0.33) * 3 * 255)) | 0,
      Math.min(255, Math.max(0, (t - 0.66) * 3 * 255)) | 0,
    ];
  },
  grayscale: (v, max) => {
    const g = ((v / max) * 255) | 0;
    return [g, g, g];
  },
  neon: (v, max) => {
    const h = (v / max) * 360;
    return hslToRgb(h, 1.0, 0.6);
  },
};

function hslToRgb(h: number, s: number, l: number): [number, number, number] {
  const c = (1 - Math.abs(2 * l - 1)) * s;
  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
  const m = l - c / 2;
  let r = 0, g = 0, b = 0;
  if (h < 60) { r = c; g = x; }
  else if (h < 120) { r = x; g = c; }
  else if (h < 180) { g = c; b = x; }
  else if (h < 240) { g = x; b = c; }
  else if (h < 300) { r = x; b = c; }
  else { r = c; b = x; }
  return [((r + m) * 255) | 0, ((g + m) * 255) | 0, ((b + m) * 255) | 0];
}

function computePixel(x: number, y: number, t: number, modulus: number): number {
  const xt = (x + t) | 0;
  const yt = (y + t) | 0;
  const band = xt & yt;
  const diff = yt - xt;
  if (diff === 0) return 0;
  return ((band % diff) % modulus + modulus) % modulus; // ensure non-negative
}

function renderFrame(
  ctx: CanvasRenderingContext2D,
  width: number,
  height: number,
  t: number,
  modulus: number,
  palette: PaletteMode,
  zoom: number,
  panX: number,
  panY: number,
) {
  const imageData = ctx.createImageData(width, height);
  const data = imageData.data;
  const colorFn = PALETTES[palette];
  const invZoom = 1 / zoom;

  for (let py = 0; py < height; py++) {
    for (let px = 0; px < width; px++) {
      const x = ((px - width / 2) * invZoom + panX) | 0;
      const y = ((py - height / 2) * invZoom + panY) | 0;
      const v = computePixel(x, y, t, modulus);
      const [r, g, b] = colorFn(v, modulus);
      const idx = (py * width + px) * 4;
      data[idx] = r;
      data[idx + 1] = g;
      data[idx + 2] = b;
      data[idx + 3] = 255;
    }
  }
  ctx.putImageData(imageData, 0, 0);
}

function App() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const animRef = useRef<number>(0);
  const tRef = useRef(0);
  const [isRunning, setIsRunning] = useState(true);
  const [palette, setPalette] = useState<PaletteMode>('hue');
  const [speed, setSpeed] = useState<number>(CONFIG.DEFAULT_SPEED);
  const [modulus, setModulus] = useState<number>(CONFIG.DEFAULT_MODULUS);
  const [zoom, setZoom] = useState<number>(1);
  const [size, setSize] = useState<number>(CONFIG.DEFAULT_SIZE);
  const [panX, setPanX] = useState<number>(256);
  const [panY, setPanY] = useState<number>(256);
  const [showInfo, setShowInfo] = useState(false);
  const [currentT, setCurrentT] = useState<number>(0);

  const stateRef = useRef({ palette, speed, modulus, zoom, panX, panY, size });
  useEffect(() => {
    stateRef.current = { palette, speed, modulus, zoom, panX, panY, size };
  }, [palette, speed, modulus, zoom, panX, panY, size]);

  const draw = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    const s = stateRef.current;
    renderFrame(ctx, s.size, s.size, tRef.current, s.modulus, s.palette, s.zoom, s.panX, s.panY);
  }, []);

  const loop = useCallback(() => {
    const s = stateRef.current;
    tRef.current += s.speed;
    setCurrentT(tRef.current);
    draw();
    animRef.current = requestAnimationFrame(loop);
  }, [draw]);

  useEffect(() => {
    draw();
  }, [palette, modulus, zoom, panX, panY, size]);

  useEffect(() => {
    if (isRunning) {
      animRef.current = requestAnimationFrame(loop);
    } else {
      cancelAnimationFrame(animRef.current);
    }
    return () => cancelAnimationFrame(animRef.current);
  }, [isRunning, loop]);

  const step = () => {
    tRef.current += 1;
    setCurrentT(tRef.current);
    draw();
  };

  const reset = () => {
    setIsRunning(false);
    tRef.current = 0;
    setCurrentT(0);
    setZoom(1);
    setPanX(256);
    setPanY(256);
    setTimeout(draw, 0);
  };

  const handleWheel = (e: React.WheelEvent) => {
    e.preventDefault();
    const factor = e.deltaY < 0 ? 1.1 : 0.9;
    setZoom(z => Math.max(0.1, Math.min(20, z * factor)));
  };

  const dragRef = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);

  const handlePointerDown = (e: React.PointerEvent) => {
    dragRef.current = { startX: e.clientX, startY: e.clientY, startPanX: panX, startPanY: panY };
    (e.target as HTMLElement).setPointerCapture(e.pointerId);
  };

  const handlePointerMove = (e: React.PointerEvent) => {
    if (!dragRef.current) return;
    const dx = (e.clientX - dragRef.current.startX) / zoom;
    const dy = (e.clientY - dragRef.current.startY) / zoom;
    setPanX(dragRef.current.startPanX - dx);
    setPanY(dragRef.current.startPanY - dy);
  };

  const handlePointerUp = () => {
    dragRef.current = null;
  };

  return (
    <div className="app">
      <header className="header">
        <h1>Sierpinski Flow</h1>
        <button className="info-btn" onClick={() => setShowInfo(!showInfo)}>
          {showInfo ? '▼' : '▶'} What is this?
        </button>
        {showInfo && (
          <p className="info-text">
            Each pixel at (x, y) is colored by <code>(x &amp; y) % (y - x) % {modulus}</code>.
            The bitwise AND produces Sierpinski-triangle-like fractal structure, while the
            modular arithmetic by the diagonal distance <code>(y - x)</code> and then by the
            selected number (default <code>{CONFIG.DEFAULT_MODULUS}</code>) creates banded
            interference patterns. Press play to animate by shifting the coordinate origin
            over time. Drag to pan, scroll to zoom.
          </p>
        )}
      </header>

      <div className="control-bar">
        <div className="controls">
          <button onClick={() => setIsRunning(!isRunning)} className="btn-icon">
            {isRunning ? '⏸' : '▶'}
          </button>
          <button onClick={step} disabled={isRunning} className="btn-icon">⏭</button>
          <button onClick={reset} className="btn-icon">↻</button>
          <div className="info-stack">
            <span className="info-text">t = <strong>{currentT}</strong></span>
            <span className="info-text">zoom: <strong>{zoom.toFixed(1)}×</strong></span>
          </div>
        </div>

        <div className="config-panel">
          <div className="config-group">
            <label>Speed</label>
            <input type="range" min="1" max={CONFIG.MAX_SPEED} step="1" value={speed}
              onChange={e => setSpeed(Number(e.target.value))} />
            <span className="config-value">{speed}</span>
          </div>
          <div className="config-group">
            <label>Number</label>
            <input
              type="range"
              min={CONFIG.MIN_MODULUS}
              max={CONFIG.MAX_MODULUS}
              step="1"
              value={modulus}
              onChange={e => setModulus(Number(e.target.value))}
            />
            <span className="config-value">{modulus}</span>
          </div>
          <div className="config-group">
            <label>Resolution</label>
            <input type="range" min={CONFIG.MIN_SIZE} max={CONFIG.MAX_SIZE} step="64" value={size}
              onChange={e => setSize(Number(e.target.value))} />
            <span className="config-value">{size}×{size}</span>
          </div>
          <div className="config-group">
            <label>Palette</label>
            <div className="palette-buttons">
              {(Object.keys(PALETTES) as PaletteMode[]).map(p => (
                <button key={p} className={`palette-btn ${p === palette ? 'active' : ''}`}
                  onClick={() => setPalette(p)}>
                  {p}
                </button>
              ))}
            </div>
          </div>
        </div>
      </div>

      <main className="content">
        <div className="canvas-container"
          onWheel={handleWheel}
          onPointerDown={handlePointerDown}
          onPointerMove={handlePointerMove}
          onPointerUp={handlePointerUp}
        >
          <canvas
            ref={canvasRef}
            width={size}
            height={size}
            className="art-canvas"
          />
        </div>
      </main>

      <footer className="footer-credit">
        <span>Idea by Non-Euclidean Dreamer</span>
        <span>@NonEuclideanDr1</span>
      </footer>
    </div>
  );
}

const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);
```
**styles.css**

```css
:root {
  --color-bg: #0a0a0a;
  --color-surface: #1a1a1a;
  --color-border: #333;
  --color-text: #e0e0e0;
  --color-text-secondary: #9e9e9e;
  --color-primary: #7c4dff;
  --color-primary-hover: #9e77ff;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.6;
}

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.header {
  background: var(--color-surface);
  border-bottom: 2px solid var(--color-border);
  padding: 1rem 2rem;
  text-align: center;
}

.header h1 {
  font-size: 1.4rem;
  font-weight: 600;
}

.header h1 code {
  font-size: 1.1rem;
  color: var(--color-primary);
}

.info-btn {
  background: transparent;
  border: none;
  color: var(--color-text-secondary);
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 600;
  padding: 0.5rem 0;
  margin-top: 0.5rem;
}

.info-btn:hover { color: var(--color-text); }

.info-text {
  max-width: 700px;
  margin: 0.75rem auto 0;
  text-align: left;
  font-size: 0.9rem;
  color: var(--color-text-secondary);
  line-height: 1.7;
}

.info-text code {
  background: var(--color-border);
  padding: 0.15rem 0.4rem;
  border-radius: 3px;
  font-size: 0.85rem;
}

.control-bar {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: 1rem 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.controls {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
}

.btn-icon {
  padding: 0.75rem 1.25rem;
  border: none;
  border-radius: 6px;
  font-size: 1.25rem;
  cursor: pointer;
  background: var(--color-primary);
  color: white;
  transition: all 0.2s;
  min-width: 60px;
}

.btn-icon:hover {
  background: var(--color-primary-hover);
  transform: scale(1.05);
}

.btn-icon:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none;
}

.info-stack {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  margin-left: 0.5rem;
}

.info-stack .info-text {
  margin: 0;
  font-size: 0.9rem;
  white-space: nowrap;
}

.info-stack .info-text strong {
  font-weight: 700;
  color: var(--color-text);
}

.config-panel {
  display: flex;
  gap: 2rem;
  flex-wrap: wrap;
  justify-content: center;
}

.config-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
}

.config-group label {
  font-size: 0.85rem;
  color: var(--color-text-secondary);
  font-weight: 600;
}

.config-value {
  color: var(--color-primary);
  font-weight: 700;
  font-size: 0.9rem;
}

.palette-buttons {
  display: flex;
  gap: 0.25rem;
}

.palette-btn {
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: transparent;
  color: var(--color-text-secondary);
  font-size: 0.8rem;
  cursor: pointer;
  transition: all 0.2s;
}

.palette-btn:hover {
  border-color: var(--color-primary);
  color: var(--color-text);
}

.palette-btn.active {
  background: var(--color-primary);
  color: white;
  border-color: var(--color-primary);
}

.content {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 1.5rem 1.5rem 0.9rem;
}

.canvas-container {
  cursor: grab;
  touch-action: none;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  overflow: hidden;
  line-height: 0;
}

.canvas-container:active {
  cursor: grabbing;
}

.art-canvas {
  display: block;
  max-width: 90vw;
  max-height: 75vh;
  width: auto;
  height: auto;
  image-rendering: pixelated;
}

.footer-credit {
  padding: 0.35rem 1rem 0.85rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  font-size: 0.8rem;
  color: var(--color-text-secondary);
  text-align: center;
}

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  background: var(--color-border);
  outline: none;
  border-radius: 4px;
  height: 6px;
  width: 120px;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  background: var(--color-primary);
  cursor: pointer;
  border-radius: 50%;
}

input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  background: var(--color-primary);
  cursor: pointer;
  border-radius: 50%;
  border: none;
}

@media (max-width: 640px) {
  .header {
    padding: 0.85rem 1rem;
  }

  .control-bar {
    padding: 0.75rem 0.6rem;
    gap: 0.75rem;
  }

  .controls {
    gap: 0.35rem;
  }

  .btn-icon {
    padding: 0.6rem 0.9rem;
    min-width: 48px;
  }

  .info-stack {
    margin-left: 0.25rem;
  }

  .config-panel {
    width: 100%;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: 0.55rem;
    row-gap: 0.7rem;
    align-items: start;
  }

  .config-panel > .config-group {
    min-width: 0;
  }

  .config-panel > .config-group:nth-child(-n + 3) {
    align-items: stretch;
    gap: 0.2rem;
  }

  .config-panel > .config-group:nth-child(-n + 3) label,
  .config-panel > .config-group:nth-child(-n + 3) .config-value {
    text-align: center;
  }

  .config-panel > .config-group:nth-child(-n + 3) label {
    font-size: 0.75rem;
  }

  .config-panel > .config-group:nth-child(-n + 3) .config-value {
    font-size: 0.8rem;
  }

  .config-panel > .config-group:nth-child(-n + 3) input[type="range"] {
    width: 100%;
  }

  .config-panel > .config-group:nth-child(4) {
    grid-column: 1 / -1;
    align-items: center;
  }

  .palette-buttons {
    flex-wrap: wrap;
    justify-content: center;
  }
}

```
**index.html**

```html
<div id="root"></div>

```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.3",
    "react-dom": "^19.2.3"
  },
  "description": "Interactive visualization of the bitwise art formula (x & y) % (y - x) % 37, producing fractal interference patterns from Sierpinski-like structure modulated by diagonal banding."
}

```

Markdown source · More bulbs by antypica · Typebulb home