← sketches

julia set

Animated Julia set fractal with drifting c parameter.

p5.js

  import p5 from 'p5';
  import { hslToRgb } from '../../../utils/color';

  new p5((p: p5) => {
    let buf: p5.Graphics;
    let manualCr = 0;
    let manualCi = 0;
    let dirty = true;

    // Pan & zoom state
    let panX = 0;
    let panY = 0;
    let zoom = 1.5;
    let dragging = false;
    let dragStartX = 0;
    let dragStartY = 0;
    let panStartX = 0;
    let panStartY = 0;

    function rebuildBuf() {
      buf = p.createGraphics(Math.ceil(p.width / c.res), Math.ceil(p.height / c.res));
      buf.pixelDensity(1);
      dirty = true;
    }

    const c = controls({
      iter: { label: 'iterations', min: 20, max: 200, value: 80, onChange: () => { dirty = true; } },
      speed: { label: 'speed', min: 0, max: 30, rawValue: 8, value: 0.008, parse: (r) => parseInt(r) / 1000, format: (v) => v.toFixed(3) },
      res: { label: 'resolution', min: 1, max: 6, value: 3, onChange: () => rebuildBuf() },
      animate: {
        type: 'checkbox' as const,
        label: 'animate',
        checked: true,
        onChange: (checked: boolean) => {
          if (!checked) {
            const t = p.frameCount * c.speed;
            manualCr = -0.8 + Math.sin(t) * 0.15 + Math.sin(t * 0.6) * 0.05;
            manualCi = Math.cos(t * 0.7) * 0.25 + Math.sin(t * 0.3) * 0.05;
          }
          dirty = true;
        },
      },
    }, { onReset: () => { panX = 0; panY = 0; zoom = 1.5; rebuildBuf(); } });

    p.setup = () => {
      const el = document.getElementById('canvas')!.parentElement!;
      const cv = p.createCanvas(el.clientWidth, el.clientHeight);
      cv.parent(el);
      document.getElementById('canvas')?.remove();
      p.noSmooth();
      rebuildBuf();

      // Scroll to zoom (on the p5 canvas element)
      (p as any).canvas.addEventListener('wheel', (e: WheelEvent) => {
        e.preventDefault();
        // Mouse position in normalized coords (0-1)
        const rect = (p as any).canvas.getBoundingClientRect();
        const mx = (e.clientX - rect.left) / rect.width;
        const my = (e.clientY - rect.top) / rect.height;

        // Complex plane position under cursor before zoom
        const aspect = p.width / p.height;
        const cxBefore = (mx - 0.5) * 2 * zoom * aspect + panX;
        const cyBefore = (my - 0.5) * 2 * zoom + panY;

        // Apply zoom
        const factor = e.deltaY > 0 ? 1.03 : 0.97;
        zoom = Math.max(0.001, Math.min(20, zoom * factor));

        // Adjust pan so the point under cursor stays fixed
        panX = cxBefore - (mx - 0.5) * 2 * zoom * aspect;
        panY = cyBefore - (my - 0.5) * 2 * zoom;

        dirty = true;
      }, { passive: false });
    };

    p.mousePressed = (e: MouseEvent) => {
      const isCanvas = e.target === (p as any).canvas;
      if (isCanvas && p.mouseX >= 0 && p.mouseX <= p.width && p.mouseY >= 0 && p.mouseY <= p.height) {
        dragging = true;
        dragStartX = p.mouseX;
        dragStartY = p.mouseY;
        panStartX = panX;
        panStartY = panY;
      }
    };

    p.mouseDragged = () => {
      if (!dragging) return;
      const aspect = p.width / p.height;
      const dx = (p.mouseX - dragStartX) / p.width * 2 * zoom * aspect;
      const dy = (p.mouseY - dragStartY) / p.height * 2 * zoom;
      panX = panStartX - dx;
      panY = panStartY - dy;
      dirty = true;
    };

    p.mouseReleased = () => {
      dragging = false;
    };

    p.draw = () => {
      if (!c.animate && !dirty) return;

      const bw = buf.width;
      const bh = buf.height;
      const t = p.frameCount * c.speed;
      const aspect = p.width / p.height;

      let cr: number, ci: number;
      if (c.animate) {
        cr = -0.8 + Math.sin(t) * 0.15 + Math.sin(t * 0.6) * 0.05;
        ci = Math.cos(t * 0.7) * 0.25 + Math.sin(t * 0.3) * 0.05;
      } else {
        cr = manualCr;
        ci = manualCi;
      }

      buf.loadPixels();
      const px = buf.pixels;

      for (let by = 0; by < bh; by++) {
        for (let bx = 0; bx < bw; bx++) {
          let zr = (bx / bw - 0.5) * 2 * zoom * aspect + panX;
          let zi = (by / bh - 0.5) * 2 * zoom + panY;

          let iter = 0;
          let zr2 = zr * zr;
          let zi2 = zi * zi;

          while (zr2 + zi2 < 4 && iter < c.iter) {
            zi = 2 * zr * zi + ci;
            zr = zr2 - zi2 + cr;
            zr2 = zr * zr;
            zi2 = zi * zi;
            iter++;
          }

          const idx = (by * bw + bx) * 4;

          if (iter === c.iter) {
            px[idx] = 0;
            px[idx + 1] = 0;
            px[idx + 2] = 0;
          } else {
            const smooth = iter + 1 - Math.log(Math.log(Math.sqrt(zr2 + zi2))) / Math.log(2);
            const n = smooth / c.iter;

            const h = (n * 360 + t * 15) % 360;
            const s = 0.8;
            const l = 0.1 + n * 0.5;

            const [r, g, b] = hslToRgb(h / 360, s, l);
            px[idx] = r;
            px[idx + 1] = g;
            px[idx + 2] = b;
          }
          px[idx + 3] = 255;
        }
      }

      buf.updatePixels();
      p.image(buf, 0, 0, p.width, p.height);
      dirty = false;
    };


  });