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