← sketches

supershape

3D superformula wireframe. Drag to rotate, scroll to zoom.

p5.js

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

  new p5((p: p5) => {
    let rotX = -0.4;
    let rotY = 0;
    let zoom = 1;
    let dragging = false;
    let lastMX = 0;
    let lastMY = 0;

    let shape: p5.Geometry;
    let dirty = true;

    const c = controls({
      m: { label: 'm', min: 0, max: 20, value: 7, onChange: () => { dirty = true; } },
      n1: { label: 'n1', min: 1, max: 100, rawValue: 20, value: 0.2, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2), onChange: () => { dirty = true; } },
      n2: { label: 'n2', min: 1, max: 200, rawValue: 170, value: 1.7, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2), onChange: () => { dirty = true; } },
      n3: { label: 'n3', min: 1, max: 200, rawValue: 170, value: 1.7, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2), onChange: () => { dirty = true; } },
      detail: { label: 'detail', min: 10, max: 60, value: 30, onChange: () => { dirty = true; } },
      spin: { label: 'spin', min: 0, max: 30, rawValue: 5, value: 0.005, parse: (r) => parseInt(r) / 1000, format: (v) => v.toFixed(3) },
    });

    function supershape(theta: number, m: number, n1: number, n2: number, n3: number): number {
      const t1 = Math.abs(Math.cos(m * theta / 4));
      const t2 = Math.abs(Math.sin(m * theta / 4));
      const r = Math.pow(Math.pow(t1, n2) + Math.pow(t2, n3), -1 / n1);
      return isFinite(r) ? r : 0;
    }

    function rebuild() {
      const res = c.detail;
      shape = p.buildGeometry(() => {
        for (let i = 0; i < res; i++) {
          const lat0 = -Math.PI / 2 + (i / res) * Math.PI;
          const lat1 = -Math.PI / 2 + ((i + 1) / res) * Math.PI;
          const r2a = supershape(lat0, c.m, c.n1, c.n2, c.n3);
          const r2b = supershape(lat1, c.m, c.n1, c.n2, c.n3);
          const cosLat0 = Math.cos(lat0), sinLat0 = Math.sin(lat0);
          const cosLat1 = Math.cos(lat1), sinLat1 = Math.sin(lat1);

          p.beginShape(p.LINES);
          for (let j = 0; j < res; j++) {
            const lon0 = -Math.PI + (j / res) * Math.PI * 2;
            const lon1 = -Math.PI + ((j + 1) / res) * Math.PI * 2;
            const r1a = supershape(lon0, c.m, c.n1, c.n2, c.n3);
            const r1b = supershape(lon1, c.m, c.n1, c.n2, c.n3);

            const x0 = r1a * Math.cos(lon0) * r2a * cosLat0;
            const y0 = r1a * Math.sin(lon0) * r2a * cosLat0;
            const z0 = r2a * sinLat0;

            const xR = r1b * Math.cos(lon1) * r2a * cosLat0;
            const yR = r1b * Math.sin(lon1) * r2a * cosLat0;
            const zR = r2a * sinLat0;

            const xD = r1a * Math.cos(lon0) * r2b * cosLat1;
            const yD = r1a * Math.sin(lon0) * r2b * cosLat1;
            const zD = r2b * sinLat1;

            p.vertex(x0, y0, z0);
            p.vertex(xR, yR, zR);
            p.vertex(x0, y0, z0);
            p.vertex(xD, yD, zD);
          }
          p.endShape();
        }
      });
      dirty = false;
    }

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

    p.mousePressed = () => {
      if (p.mouseX >= 0 && p.mouseX <= p.width && p.mouseY >= 0 && p.mouseY <= p.height) {
        dragging = true;
        lastMX = p.mouseX;
        lastMY = p.mouseY;
      }
    };

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

    p.mouseDragged = () => {
      if (!dragging) return;
      rotY += (p.mouseX - lastMX) * 0.01;
      rotX -= (p.mouseY - lastMY) * 0.01;
      lastMX = p.mouseX;
      lastMY = p.mouseY;
    };

    p.mouseWheel = (e: WheelEvent) => {
      if (p.mouseX >= 0 && p.mouseX <= p.width && p.mouseY >= 0 && p.mouseY <= p.height) {
        zoom *= e.deltaY > 0 ? 0.95 : 1.05;
        zoom = Math.max(0.2, Math.min(5, zoom));
        return false;
      }
    };

    p.draw = () => {
      if (dirty) rebuild();

      p.background(0);
      rotY += c.spin;
      p.rotateX(rotX);
      p.rotateY(rotY);

      const radius = Math.min(p.width, p.height) * 0.3 * zoom;
      const t = p.frameCount * c.spin;
      const h = (t * 40) % 360;
      const [r, g, b] = hsvToRgb(h / 360, 0.5, 0.9);

      p.stroke(r, g, b, 160);
      p.strokeWeight(0.8);
      p.scale(radius);
      p.model(shape);
    };


  });