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
Mandelbrot render — Mandelbrot-Menge — interaktiver Zoom
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