Mandelbrot-Menge — interaktiver Zoom
Bellards QuickJS-Mandelbrot-Demo, von ANSI-Terminal-Blöcken auf einen echten ImageHandle portiert. Smooth-Iteration-Count-Färbung gibt dem Rand sein typisches Regenbogenleuchten. Pan und Zoom über die CENTER / SCALE / MAX_ITER Slider; im Header-Kommentar stehen ein paar Sweet-Spot-Koordinaten zum Probieren.
Mandelbrot render
JavaScript
// Mandelbrot set rendered into an ImageHandle — interactive zoom +
// pan via PARAMs. Iteration-count colouring uses a smooth HSV
// gradient (vivid for the "escape band" near the boundary, deep
// inside-the-set black).
//
// Based on Fabrice Bellard's QuickJS mandelbrot demo (MIT) — the
// inner loop is the textbook escape-time algorithm, just rewritten
// to paint pixels via setPixel instead of ANSI-coloured upper-half
// blocks (▀) in a terminal.
//
// demo_mandelbrot.js
//!NOIMAGE
//!OUTPUT: OUTPUT
//!PARAM: WIDTH:integer=800,min=200,max=2400
//!PARAM: HEIGHT:integer=600,min=200,max=2400
//!PARAM: CENTER_X:number=-0.75,min=-2,max=1,step=0.001
//!PARAM: CENTER_Y:number=0.0,min=-1.5,max=1.5,step=0.001
//!PARAM: SCALE:number=2.5,min=0.0001,max=4,step=0.001
//!PARAM: MAX_ITER:integer=180,min=20,max=2000
//!SAMPLE Seahorse-Valley(CENTER_X=-0.745, CENTER_Y=0.105, SCALE=0.025, MAX_ITER=260)
//!SAMPLE Triple-Spiral(CENTER_X=-0.088, CENTER_Y=0.654, SCALE=0.012, MAX_ITER=340)
//!SAMPLE Mini-Mandelbrot(CENTER_X=-1.7693, CENTER_Y=0.0042, SCALE=0.0001, MAX_ITER=600)
//!SAMPLE Elephant-Valley(CENTER_X=0.275, CENTER_Y=0.006, SCALE=0.012, MAX_ITER=300)
// The presets above jump straight to four classic Mandelbrot
// landmarks — pick one from the preset selector, or pan / zoom
// manually with the CENTER_X / CENTER_Y / SCALE sliders. Each preset
// also dials MAX_ITER up to match the zoom depth.
const img = Engine.createImage(WIDTH, HEIGHT);
// One-pixel-wide aspect-corrected mapping from image space to the
// complex plane. fx scales so the SCALE fits the shorter axis.
const fx = SCALE / Math.min(WIDTH, HEIGHT);
const fy = fx;
const cx0 = CENTER_X - (WIDTH * 0.5) * fx;
const cy0 = CENTER_Y - (HEIGHT * 0.5) * fy;
// Pre-bind the Pixel constructor — avoids hot-loop allocation churn.
const Pix = Pixel;
const t0 = Date.now();
for (let py = 0; py < HEIGHT; py++) {
const cy = cy0 + py * fy;
for (let px_ = 0; px_ < WIDTH; px_++) {
const cx = cx0 + px_ * fx;
// Escape-time: iterate z ← z² + c until |z| > 2 or budget hits.
let x = 0, y = 0, i = 0;
let xx = 0, yy = 0;
while (i < MAX_ITER && xx + yy < 4) {
y = 2 * x * y + cy;
x = xx - yy + cx;
xx = x * x;
yy = y * y;
i++;
}
if (i >= MAX_ITER) {
// Inside the set — solid black.
img.setPixel(px(px_, py), new Pix(0, 0, 0, 1));
} else {
// Outside — colour by smooth iteration count. The standard
// continuous-colouring trick: subtract log-log of the escape
// radius so colour bands across the boundary go away.
const log_zn = Math.log(xx + yy) * 0.5;
const nu = Math.log(log_zn / Math.log(2)) / Math.log(2);
const smooth = i + 1 - nu;
// Map smooth iteration count → HSL. Hue cycles through 0.8
// turns per `MAX_ITER`, lightness ramps up toward the
// boundary so the inside of bright bands stays vivid.
const hue = (smooth * 0.02) % 1;
const lightness = 0.5 + 0.45 * Math.cos(smooth * 0.2);
img.setPixel(px(px_, py), Pix.fromHSL(hue * 360, 0.9, Math.max(0.05, Math.min(0.85, lightness)), 1));
}
}
}
const elapsed = ((Date.now() - t0) / 1000).toFixed(2);
// Caption with the current view parameters, for those zooming around.
img.drawText(
`Mandelbrot center=${CENTER_X.toFixed(4)}, ${CENTER_Y.toFixed(4)} scale=${SCALE} iter=${MAX_ITER}`,
WIDTH / 2,
HEIGHT - 14,
{ size: 12, color: "#ffffff", anchor: "middle", font: "JetBrains Mono" }
);
img.save(OUTPUT);
`Rendered ${WIDTH}×${HEIGHT} in ${elapsed}s (${MAX_ITER} max iterations)`;
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0