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);
};
});