// IFS Attractor — Chaos Game
//
// Pick a random affine transform from the set (weighted by probability),
// apply it to the current point, and plot the result. After millions of
// iterations the attractor emerges from the accumulated points.
//
// Each transform: x' = a*x + b*y + e, y' = c*x + d*y + f
export const PARAMS = {
preset: { value: "Barnsley Fern", options: ["Barnsley Fern", "Sierpinski Triangle", "Heighway Dragon", "Maple Leaf"], label: "IFS Preset", folder: "Structure", rebuildOnChange: true },
iterations: { value: 100000, min: 5000, max: 500000, step: 5000, label: "Iterations", folder: "Performance" },
hue: { value: 120, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
alpha: { value: 0.05, min: 0.01, max: 0.5, step: 0.01, label: "Point Alpha", folder: "Appearance" },
scale: { value: 0.7, min: 0.2, max: 1.2, step: 0.01, label: "Scale (% canvas)", folder: "Appearance" },
};
// Each transform: [prob, a, b, c, d, e, f]
const PRESETS = {
"Barnsley Fern": [
[0.01, 0, 0, 0, 0.16, 0, 0 ],
[0.85, 0.85, 0.04, -0.04, 0.85, 0, 1.60],
[0.07, 0.20, -0.26, 0.23, 0.22, 0, 1.60],
[0.07, -0.15, 0.28, 0.26, 0.24, 0, 0.44],
],
"Sierpinski Triangle": [
[0.333, 0.5, 0, 0, 0.5, 0, 0 ],
[0.333, 0.5, 0, 0, 0.5, 0.5, 0 ],
[0.334, 0.5, 0, 0, 0.5, 0.25, 0.5 ],
],
"Heighway Dragon": [
[0.5, 0.5, 0.5, -0.5, 0.5, 0, 0],
[0.5, 0.5, -0.5, 0.5, -0.5, 1, 0],
],
"Maple Leaf": [
[0.25, 0.14, 0.01, 0.00, 0.51, -0.08, -1.31],
[0.25, 0.43, 0.52,-0.45, 0.50, 1.49, -0.75],
[0.25, 0.45, -0.49, 0.47, 0.47, -1.62, -0.74],
[0.25, 0.49, 0.00, 0.00, 0.51, 0.02, 1.62],
],
};
// Bounding box info per preset (xmin, xmax, ymin, ymax) for scaling
const BOUNDS = {
"Barnsley Fern": [-2.18, 2.66, 0, 10 ],
"Sierpinski Triangle": [ 0, 1, 0, 1 ],
"Heighway Dragon": [-0.6, 1.4, -0.4, 1.0],
"Maple Leaf": [-2.5, 2.5, -2.5, 2.5],
};
export function sketchSetup(p, w, h) {
p.colorMode(p.HSB, 360, 100, 100, 1);
p.background(0);
p.noStroke();
return {};
}
export function sketchDraw(p, w, h, params) {
p.background(0);
const transforms = PRESETS[params.preset] ?? PRESETS["Barnsley Fern"];
const [xmin, xmax, ymin, ymax] = BOUNDS[params.preset] ?? [-2.5, 2.5, -2.5, 2.5];
const iters = params.iterations | 0;
// Build cumulative probability array
const cumProb = [];
let acc = 0;
for (const t of transforms) {
acc += t[0];
cumProb.push(acc);
}
// Fitting scale: map attractor bbox to canvas * scale param
const sceneW = xmax - xmin;
const sceneH = ymax - ymin;
const margin = 1 - params.scale;
const fitScale = Math.min(
(w * params.scale) / sceneW,
(h * params.scale) / sceneH,
);
const offX = w / 2 - (xmin + sceneW / 2) * fitScale;
const offY = h / 2 + (ymin + sceneH / 2) * fitScale;
let x = 0, y = 0;
// Warm-up iterations (not plotted)
for (let i = 0; i < 20; i++) {
const r = Math.random();
let t = transforms[0];
for (let j = 0; j < cumProb.length; j++) {
if (r < cumProb[j]) { t = transforms[j]; break; }
}
const nx = t[1] * x + t[2] * y + t[5];
const ny = t[3] * x + t[4] * y + t[6];
x = nx; y = ny;
}
// Plot points
for (let i = 0; i < iters; i++) {
const rnd = Math.random();
let t = transforms[0];
let tIdx = 0;
for (let j = 0; j < cumProb.length; j++) {
if (rnd < cumProb[j]) { t = transforms[j]; tIdx = j; break; }
}
const nx = t[1] * x + t[2] * y + t[5];
const ny = t[3] * x + t[4] * y + t[6];
x = nx; y = ny;
const sx = offX + x * fitScale;
const sy = offY - y * fitScale;
if (sx >= 0 && sx < w && sy >= 0 && sy < h) {
const hShift = (tIdx / transforms.length) * 60;
const h2 = ((params.hue + hShift) % 360 + 360) % 360;
p.fill(h2, 70, 90, params.alpha);
p.rect(sx, sy, 1, 1);
}
}
}