Walldye

One-bit moon

The Moon’s dark maria are plains of basalt left by ancient lava flows.

Made with Claude Opus 5.5

Technique
dithering
Shape
Any screen
Added
27 September 2026

Versions

Export

Shape
cropped from 16:9
Across
Down
Scale
Crop
Size
Format

Colors

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Sources

  1. Robert Ulichney, Void-and-cluster method for dither array generation, 1993.

Source code

wallpapers/dither-moon/design.py, 97 lines

"""A moon in one-bit void-and-cluster dither."""

import math

import numpy as np
from numpy.typing import NDArray

from walldye import ACCENT, Canvas, Params, design, knob
from walldye.field import gauss, noise_grid
from walldye.pixel import dither, grid_runs


class Moon(Params):
    phase: float = knob(
        default=58,
        lo=-150,
        hi=150,
        unit="deg",
        doc="sun angle from the viewer; above 0 lights the right limb",
    )


VARIANTS = {"crescent": Moon(phase=120)}

R, CELL = 270, 3  # moon radius and dither cell
MAX_TONE = 0.75  # keep the background showing through even the brightest limb
# Albedo blobs: dark maria across the upper half, like the near side (x, y, radius, weight).
MARIA = ((-0.1, -0.35, 0.22, 0.5), (0.3, -0.05, 0.16, 0.45), (-0.35, 0.05, 0.14, 0.35))
TYCHO = (0.2, 0.6)  # the ray crater, low on the disc

type Field = NDArray[np.float64]


def blobs(u: Field, v: Field, spots: list[tuple[float, float, float, float]]) -> Field:
    """Sum of Gaussian bumps at disc coordinates (x, y) with radius r and weight a."""
    out = np.zeros_like(u)
    for x, y, r, a in spots:
        out += a * gauss(np.hypot(u - x, v - y), r)
    return out


@design(aspects="any", variants=VARIANTS)
def draw(s: Canvas[Moon]) -> None:
    # right of center on a landscape screen (the left stays free for windows), the upper third
    # on a portrait one (below the clock, above the dock); snapped to whole cells, since a
    # fractional grid origin blurs every cell edge
    c = s.pick(landscape=(0.68, 0.46), portrait=(0.56, 0.34), snap=CELL)
    n = int(R * 1.04 / CELL)
    size = 2 * n
    ys, xs = np.mgrid[0:size, 0:size]
    # Disc coordinates: u, v in [-1, 1] across the moon, z the sphere's height toward the viewer.
    u = (xs + 0.5 - n) * CELL / R
    v = (ys + 0.5 - n) * CELL / R
    rr = np.hypot(u, v)
    inside = rr < 1
    z = np.sqrt(np.clip(1 - rr * rr, 0, 1))

    # Albedo carries the texture: dark maria and bright ejecta.
    mare = 1.6 * noise_grid(size, size, 60, s.np_rng(4), octaves=3) + 0.3 * (-v) - 0.1
    mare += blobs(u, v, list(MARIA))
    albedo = (
        0.95
        - 0.62 * np.clip((mare - 0.02) * 2.8, 0, 1)
        + 0.08 * noise_grid(size, size, 6, s.np_rng(11), octaves=2)
    )
    rng = s.np_rng(3)
    ejecta = [(*rng.uniform(-0.8, 0.8, 2), rng.uniform(0.015, 0.04), 0.4) for _ in range(18)]
    albedo += blobs(u, v, ejecta)

    # Tycho: a bright halo plus thin rays.
    tx, ty = TYCHO
    ang = np.arctan2(v - ty, u - tx)
    dist = np.hypot(u - tx, v - ty)
    rays = sum(
        np.exp(-((((ang - a + np.pi) % (2 * np.pi) - np.pi) * dist / 0.014) ** 2))
        * np.exp(-dist / w)
        for a, w in zip(rng.uniform(-np.pi, np.pi, 18), rng.uniform(0.15, 0.45, 18), strict=True)
    )
    albedo += 0.3 * np.clip(rays, 0, 1) + 0.45 * gauss(dist, 0.06)

    sun = math.radians(s.params.phase)
    lit = np.clip((u * math.sin(sun) + z * math.cos(sun)) * 3 + 0.2, 0, 1) ** 0.8
    limb = 1 - 0.35 * np.clip((rr - 0.88) / 0.12, 0, 1)
    tone = np.clip(lit * albedo * limb * 0.62, 0, MAX_TONE)
    tone = np.maximum(tone, 0.07)  # faint earthshine on the dark side
    if s.light:
        # on paper, ink the shadows instead: a pencil moon, not a negative
        tone = 0.8 * (MAX_TONE - tone)
    tone = np.where(inside, tone, 0)

    grid = dither(tone, 2, method="bluenoise", rng=s.np_rng(5))
    grid[~inside] = 0
    # A sparse one-cell limb ring keeps the silhouette round where the dark side fades out.
    ring = (np.abs(rr - 1 + 0.5 * CELL / R) < 0.5 * CELL / R) & ((xs + ys) % 2 == 0)
    grid[ring] = 1
    with s.motif(c, n * CELL, cell=CELL):
        grid_runs(s, grid, [None, ACCENT], CELL, (-n * CELL, -n * CELL))

Run it yourself

$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render dither-moon --theme fireproof -o dither-moon-fireproof-16x9.svg