// Julia Set
//
// Iterate z = z² + c where c is a fixed complex constant and z starts at the
// pixel's complex coordinate. Same escape-time logic as Mandelbrot but a very
// different shape for each choice of c.
export const PARAMS = {
cReal: { value: -0.7, min: -2, max: 2, step: 0.001, label: "c (real)", folder: "Constant c" },
cImag: { value: 0.27015, min: -2, max: 2, step: 0.001, label: "c (imaginary)", folder: "Constant c" },
preset: { value: "Custom", options: ["Custom", "Dragon", "Dendrite", "Rabbit", "San Marco"], label: "Preset c", folder: "Constant c" },
iterations: { value: 200, min: 50, max: 1000, step: 10, label: "Max Iterations", folder: "Performance" },
step: { value: 2, options: ["1", "2", "3", "4"], label: "Pixel Step", folder: "Performance", rebuildOnChange: true },
hue: { value: 30, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
hueSpread: { value: 180, min: 0, max: 360, step: 1, label: "Hue Spread", folder: "Appearance" },
};
// Preset c values
const PRESETS = {
"Dragon": [-0.74543, 0.11301],
"Dendrite": [0, 1.0],
"Rabbit": [-0.123, 0.745],
"San Marco": [-0.75, 0.0],
};
// HSL to RGB (h: 0-360, s/l: 0-1) → [r,g,b] 0-255
function hslToRgb(h, s, l) {
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255),
];
}
export function sketchSetup(p, w, h) {
p.pixelDensity(1);
return {};
}
export function sketchDraw(p, w, h, params) {
const step = parseInt(params.step, 10) || 2;
const maxIter = params.iterations | 0;
const { hue, hueSpread } = params;
// Resolve c — preset overrides sliders (unless "Custom")
let cr, ci;
if (params.preset !== "Custom" && PRESETS[params.preset]) {
[cr, ci] = PRESETS[params.preset];
} else {
cr = params.cReal;
ci = params.cImag;
}
// Scale: at zoom=1 the full width spans about 3.5 units (same as Mandelbrot)
const scale = 3.5 / w;
p.pixelDensity(1);
p.loadPixels();
for (let py = 0; py < h; py += step) {
for (let px = 0; px < w; px += step) {
// Map pixel to initial z in complex plane
let zr = (px - w / 2) * scale;
let zi = (py - h / 2) * scale;
let n = 0;
while (n < maxIter) {
const zr2 = zr * zr;
const zi2 = zi * zi;
if (zr2 + zi2 > 4.0) break;
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
n++;
}
let r, g, b;
if (n === maxIter) {
r = 0; g = 0; b = 0;
} else {
const log2 = Math.log(2);
const absZ = Math.sqrt(zr * zr + zi * zi);
const smooth = n + 1 - Math.log(Math.log(Math.max(1.001, absZ)) / log2) / log2;
const t = (smooth / maxIter) * hueSpread;
const h2 = ((hue + t * 3) % 360 + 360) % 360;
const lit = 0.45 + 0.2 * Math.sin(smooth * 0.3);
[r, g, b] = hslToRgb(h2, 0.75, Math.min(0.85, Math.max(0.05, lit)));
}
for (let dy = 0; dy < step && py + dy < h; dy++) {
for (let dx = 0; dx < step && px + dx < w; dx++) {
const idx = ((py + dy) * w + (px + dx)) * 4;
p.pixels[idx] = r;
p.pixels[idx + 1] = g;
p.pixels[idx + 2] = b;
p.pixels[idx + 3] = 255;
}
}
}
}
p.updatePixels();
}