Walldye

Kumiko light

A shoji panel in the asanoha pattern, which joins each lattice triangle’s center to its corners.

Made with Claude Opus 5.5

Technique
tiling
Inspired by
Japanese art
Shape
Any screen
Added
27 September 2026

Export

Shape
cropped from 16:9
Crop
Size
Format

Colors

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Notes

Kumiko is Japanese lattice joinery: thin wooden laths are notched and fitted together without nails, mostly for shoji doors and screens. Asanoha is named after the hemp leaf, and its lines split each triangle into three cells.

Sources

  1. Kumiko (woodworking).
  2. Shoji.

Source code

wallpapers/kumiko-light/design.py, 90 lines

"""A shoji panel of asanoha kumiko, lit from behind."""

import math
from collections.abc import Iterator

from walldye import (
    ACCENT,
    ACCENT_1,
    ACCENT_3,
    BG,
    BG_ALT,
    BG_DEEP,
    UI,
    Canvas,
    P,
    Vec,
    clamp,
    design,
    ramp,
)

WIDTH, RAIL, BAR = 380, 24, 14  # panel width, stile width, crossbar height
SIDE = 70
HT = SIDE * math.sqrt(3) / 2
SECTION = 12 * HT  # crossbar spacing; the lamp sits mid-section on a lattice joint
LAMP_LAND = (1370 / 1920, 6 * HT / 1080)  # the 16:9 panel spans x 1180-1560
LAMP_PORT = (0.64, 0.38)
GLOW = (ACCENT, ACCENT_1, ACCENT_3)
BANDS = (40, 70, 100)  # lamp distance of a leaf cell's centroid for each glow step
FADE = 4  # frame tone steps from lamp-lit wood down to the quiet lattice
# Wood by step: 0 is the backlit silhouette, 1..FADE step from lamp-lit frames out to BG_ALT.
FRAME = (BG, *ramp(UI, BG_ALT, FADE))
LEAF = (BG, *[BG_ALT] * FADE)
FRAME_W = (2.6, *[2.2] * FADE)
LEAF_W = (1.6, *[1.2] * FADE)


def triangles(lamp: Vec, x0: float, x1: float, h: float) -> Iterator[tuple[Vec, Vec, Vec]]:
    """The lattice triangles overlapping the strip `x0`..`x1`, 0..`h`, with a joint on `lamp`.

    Rows run `HT` apart from the lamp's row, each odd row shifted half a side."""
    for j in range(math.floor(-lamp.y / HT), math.ceil((h - lamp.y) / HT)):
        y = lamp.y + j * HT
        for i in range(math.floor((x0 - lamp.x) / SIDE) - 1, math.ceil((x1 - lamp.x) / SIDE) + 1):
            a = Vec(lamp.x + i * SIDE + (j % 2) * SIDE / 2, y)
            b, c, d = a + (SIDE, 0), a + (SIDE / 2, HT), a + (-SIDE / 2, HT)
            for tri in ((a, b, c), (a, c, d)):
                if max(p.x for p in tri) > x0 and min(p.x for p in tri) < x1:
                    yield tri


@design(aspects="any")
def draw(s: Canvas) -> None:
    at = s.pick(landscape=LAMP_LAND, portrait=LAMP_PORT)
    lamp = Vec(round(at.x), at.y)  # whole-pixel stiles
    x0 = lamp.x - WIDTH / 2
    x1 = x0 + WIDTH
    inner = P().rect(x0 + RAIL, 0, WIDTH - 2 * RAIL, s.h)
    with s.clip() as pane:
        pane.add(inner)
    s.fill(inner, BG_DEEP)
    wood = [(P(), P()) for _ in FRAME]  # (frame, leaf) per step
    with s.group(clip_path=pane.ref):
        with s.buckets(GLOW, "fill") as lit:
            for tri in triangles(lamp, x0 + RAIL, x1 - RAIL, s.h):
                c = (tri[0] + tri[1] + tri[2]) / 3
                any_lit = False
                for k in range(3):
                    a, b = tri[k], tri[(k + 1) % 3]
                    lvl = sum(abs((a + b + c) / 3 - lamp) > t for t in BANDS)
                    if lvl < len(GLOW):
                        lit[lvl].poly([c, a, b], closed=True)
                        any_lit = True
                far = clamp((abs(c - lamp) - BANDS[-1]) / (2.2 * HT))
                frame, leaf = wood[0 if any_lit else 1 + min(FADE - 1, int(far * FADE))]
                frame.poly(tri, closed=True)
                for p in tri:
                    leaf.M(c).L(p)
        # far to near, so shared edges take the tone of the wood nearer the lamp
        for step in reversed(range(len(FRAME))):
            frame, leaf = wood[step]
            s.stroke(leaf, LEAF[step], LEAF_W[step])
            s.stroke(frame, FRAME[step], FRAME_W[step], join="round")
    rails = P().rect(x0, 0, RAIL, s.h).rect(x1 - RAIL, 0, RAIL, s.h)
    # crossbars halfway between lamp sections, on lattice rows, only where wholly on screen
    for k in range(math.floor(-lamp.y / SECTION), math.ceil((s.h - lamp.y) / SECTION)):
        y = lamp.y + (k + 0.5) * SECTION
        if HT < y < s.h - HT:
            rails.rect(x0, round(y - BAR / 2), WIDTH, BAR)
    s.fill(rails, BG_ALT)

Run it yourself

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