string art
Lines stretched between two hidden parametric curves.
p5.js
import p5 from 'p5';
import { hsvToRgb } from '../../../utils/color';
new p5((p: p5) => {
const c = controls({
length: { label: 'length', min: 1, max: 100, value: 100, format: (v) => `${Math.round(v)}%` },
offset: { label: 'offset', min: 1, max: 200, rawValue: 40, value: 0.02, parse: (r) => parseInt(r) / 1000, format: (v) => v.toFixed(3) },
weight: { label: 'weight', min: 1, max: 50, rawValue: 8, value: 0.8, parse: (r) => parseInt(r) / 10, format: (v) => v.toFixed(1) },
opacity: { label: 'opacity', min: 1, max: 255, value: 50 },
speed: { label: 'speed', min: 1, max: 30, rawValue: 6, value: 0.006, parse: (r) => parseInt(r) / 1000, format: (v) => v.toFixed(3) },
freqa: { label: 'curve A', min: 1, max: 8, value: 3 },
freqb: { label: 'curve B', min: 1, max: 8, value: 5 },
curvea: { type: 'checkbox', label: 'show curve A', checked: false },
curveb: { type: 'checkbox', label: 'show curve B', checked: false },
});
function curveA(t: number, time: number): [number, number] {
const cx = p.width / 2;
const cy = p.height / 2;
const r = Math.min(p.width, p.height) * 0.42;
const x = cx + Math.sin(c.freqa * t + time) * r * Math.cos(t + time * 0.3);
const y = cy + Math.cos(c.freqa * t - time * 0.7) * r * Math.sin(t - time * 0.2);
return [x, y];
}
function curveB(t: number, time: number): [number, number] {
const cx = p.width / 2;
const cy = p.height / 2;
const r = Math.min(p.width, p.height) * 0.38;
const x = cx + Math.cos(c.freqb * t - time * 0.5) * r * Math.sin(t * 0.7 + time * 0.4);
const y = cy + Math.sin(c.freqb * t + time * 0.3) * r * Math.cos(t * 0.7 - time * 0.6);
return [x, y];
}
p.setup = () => {
const el = document.getElementById('canvas')!.parentElement!;
const canvas = p.createCanvas(el.clientWidth, el.clientHeight);
canvas.parent(el);
document.getElementById('canvas')?.remove();
};
p.draw = () => {
p.background(0);
const time = p.frameCount * c.speed;
const arcLen = (c.length / 100) * Math.PI * 2;
const lineCount = Math.max(1, Math.floor(arcLen / c.offset));
for (let i = 0; i < lineCount; i++) {
const t = i * c.offset;
const [ax, ay] = curveA(t, time);
const [bx, by] = curveB(t + c.offset, time);
const h = ((i / lineCount) * 360 + p.frameCount * 0.5) % 360;
const [r, g, b] = hsvToRgb(h / 360, 0.6, 0.85);
p.stroke(r, g, b, c.opacity);
p.strokeWeight(c.weight);
p.line(ax, ay, bx, by);
}
const fullArc = Math.PI * 2;
const steps = 200;
if (c.curvea) {
p.noFill();
p.strokeWeight(1.5);
p.stroke(255, 80, 80);
p.beginShape();
for (let i = 0; i <= steps; i++) {
const [x, y] = curveA((i / steps) * fullArc, time);
p.vertex(x, y);
}
p.endShape();
}
if (c.curveb) {
p.noFill();
p.strokeWeight(1.5);
p.stroke(80, 140, 255);
p.beginShape();
for (let i = 0; i <= steps; i++) {
const [x, y] = curveB((i / steps) * fullArc + c.offset, time);
p.vertex(x, y);
}
p.endShape();
}
};
});