DEV Community

John Samuel
John Samuel

Posted on

Building a Fractal Explorer to Test‑Drive My Homegrown Language

I’ve been building a small programming language and needed a project that was visual, demanding, and unforgiving about performance. Fractals turned out to be the perfect testbed: they’re embarrassingly parallel, heavy on floating‑point work, and any bug shows up immediately as a glitch on the screen.

In this post I’ll walk through how I wired a Fractal explorer in the browser using:

  • A homegrown language as the “authoring layer”
  • WebAssembly as the execution target
  • A minimal JavaScript shell for input and rendering

Why Fractals Make Great Language Benchmarks

From a distance, a fractal is just an image. From the runtime’s point of view, it’s a brutal little benchmark:

  • Millions of pixels, same loop shape, different data
  • Lots of floating‑point operations
  • Clear performance feedback: do users see results in milliseconds or seconds?

That combination makes fractals a nice stress test for:

  • Code generation quality (are we emitting tight loops?)
  • Numeric semantics (are we handling edge cases and precision consistently?)
  • Runtime model (do we block the UI thread, do we stream, can we refine progressively?)

Unlike a synthetic benchmark, you also get something beautiful at the end, which keeps motivation high.

Architecture: Language → WASM → Canvas

The overall architecture looks like this:

  1. Write fractal logic in my language
  2. Compile to WebAssembly.
  3. Call the exported functions from JavaScript.
  4. Stream results into an ImageData buffer on a <canvas>.

In more detail:

  • Language layer

    Describes the per‑pixel computation: given coordinates and parameters, return an “escape value” that we’ll later map to color.

  • WebAssembly module

    Exposes functions like:

    • render_region(centerX, centerY, scale, width, height, maxIterations)
    • Writes raw iteration counts into a linear memory buffer.
  • JavaScript shell

    • Handles input (mouse, touch, wheel).
    • Translates viewport changes into calls to render_region.
    • Maps iteration counts → RGBA and blits to the canvas.

The nice thing about this layering: I can iterate on the language and the fractal formulas without touching the UI.

Authoring Fractals in a Custom Language

My language’s job here is: “describe what one pixel should do.” The core concept is a pure‑ish function that takes a coordinate and some parameters and returns a scalar:

  • Input: coordinate in the complex plane, global parameters (max iterations, escape radius, etc.).
  • Output: a number representing how “quickly” the point escapes or stabilizes.

Conceptually, the code you’d write in a conventional language looks like this:

// PSEUDOCODE – the real syntax is my language
fn iterate(x, y, params) -> int {
  // map (x, y) to complex plane
  // apply small iterative rule until escape or max iterations
  // return how long it took
}
Enter fullscreen mode Exit fullscreen mode

In the language, I deliberately kept:

  • No classes, minimal state
  • Simple control flow
  • Explicit numeric types

Because the target is WebAssembly, avoiding hidden allocations and dynamic dispatch makes code generation much simpler.

WebAssembly: Trade‑offs and Lessons

Compiling to WebAssembly gives you:

  • Near‑native inner loops in the browser
  • A simple, linear memory model
  • Tooling that’s getting better every year

For this project, a few practical lessons:

  • Linear memory as a frame buffer I allocate a chunk of WASM memory as a raw u32 buffer. Each pixel gets one slot, which I fill with an integer “escape value.” JavaScript reads that buffer and maps it to colors.
  • Progressive refinement

    Instead of rendering full quality in one go, I can render in passes: coarse grid first, then fill in gaps. This pattern gives users immediate feedback while the WASM loops keep working.

  • Responsiveness vs. throughput

    You quickly learn to balance batch sizes. Large chunks make WASM efficient but can freeze the main thread; smaller chunks keep the UI snappy but add overhead. A simple “time budget per frame” works well enough.

If you want to go further, WebAssembly threads and SharedArrayBuffer open the door to true multi‑core rendering, but I kept this explorer single‑threaded to focus on the language itself.

The JavaScript Shell: Minimal but Important

JavaScript is the glue layer:

  • Sets up the <canvas> and a 2D context.
  • Manages the viewport (center, zoom).
  • Handles user input (drag, wheel, touch gestures).
  • Calls into WASM with the current viewport and reads back the buffer. A typical render cycle looks like:
  1. User pans or zooms.
  2. JS updates centerX, centerY, scale.
  3. JS calls render_region on the WASM module.
  4. WASM fills its output buffer with iteration counts.
  5. JS maps those values to colors and updates ImageData.

This separation keeps the UI code tiny and lets the language + WASM do the heavy lifting.

Using Fractals as a Language Design Tool

Beyond having a cool demo, fractals turned out to be surprisingly useful for language design:

  • Semantics become visible

    Off‑by‑one errors, numeric issues, or small semantic changes show up as visible artifacts. Great for debugging semantics and codegen.

  • Performance work is rewarding

    Tightening a loop or changing a data layout translates directly into smoother zooms or higher iteration counts.

  • New features can be validated interactively

    Adding a new numeric type, a control‑flow construct, or a standard library function can be exercised immediately by writing a new fractal “kernel” and seeing if it behaves.

It’s an ongoing feedback loop: improvements in the language make the explorer better, and new ideas from the explorer push the language forward.

Top comments (0)