Walldye

Seigaiha

Seigaiha wave scales stack up from the bottom and shed their inner rings at a ragged surf line.

Made with Claude Opus 5.5

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

Colours

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Export

Format
Shape
Size

Notes

Seigaiha is a Japanese wave pattern of concentric arcs, each row covering the lower half of the one behind it. In Japan it goes back to a haniwa figure from Gunma and was worn on Heian court robes.

Here the rows are laid from the bottom up, and a scale stays only if both scales it rests on stayed, so no crest floats. Scales near the surf keep fewer rings.

Sources

  1. Seigaiha.

Source code

wallpapers/seigaiha/design.py, 89 lines

"""Seigaiha wave scales laid bottom-up in painter's order, shedding rings towards a noise-ragged surf line, with one pyramid of three scales lit."""

from walldye import (
    ACCENT,
    ACCENT_2,
    ACCENT_3,
    ACCENT_5,
    ACCENT_6,
    ACCENT_7,
    BG,
    BG_ALT,
    UI,
    Canvas,
    P,
    design,
    ladder,
    mix,
    smoothstep,
)

R = 60  # scale radius; rows sit R / 2 apart, alternate rows shifted by R
SLOPE = 0.14  # the surf line's climb per unit to the right, eased on screens wider than 16:9
# Ring tones of the lit scales, outer to inner: the apex, then the two it rests on.
LIT = (ACCENT, ACCENT_2, ACCENT_3, ACCENT_5)
DIM = (ACCENT_2, ACCENT_3, ACCENT_5, ACCENT_6)
# Scales fade from UI at the surf line to the deep tone 150 units below it.
TONES = ladder((UI, mix(BG_ALT, BG, 0.17)), 6)


def radii(rings: int) -> list[float]:
    """Radii of a scale's outer `rings` rings, inset by half the widest stroke."""
    return [R * (1 - 0.2 * m) - 1.5 for m in range(rings)]


@design(aspects="any")
def draw(s: Canvas) -> None:
    n, rnd = s.noise(5), s.rng(5)
    # where the surf line crosses the middle of the screen
    base = s.h - 440 if s.landscape else 0.6 * s.h
    slope = SLOPE * min(1.0, 1920 / s.w)
    # Two rows start below the frame so the bottom edge is a mid-pattern crop.
    rows = [
        (s.h - k * R / 2, [i * 2 * R + (R if k % 2 else 0) for i in range(-1, s.w // (2 * R) + 2)])
        for k in range(-2, int((s.h - base + 320) / (R / 2)))
    ]

    # Bottom-up: a scale survives only if both scales it rests on did, so crests never float.
    kept: dict[tuple[float, float], float] = {}
    for k, (y, xs) in enumerate(rows):
        below = set(rows[k - 1][1]) if k else set()
        for x in xs:
            u = x - s.w / 2  # centred, so every screen shape shows the middle of one surf line
            surf = base - slope * u + 150 * n.fbm((u + 960) / 520, 0.5, 2) + rnd.uniform(-20, 20)
            if y >= surf and all(
                xb not in below or (xb, y + R / 2) in kept for xb in (x - R, x + R)
            ):
                kept[(x, y)] = y - surf

    # The lit apex sits two scales below the crest of a column right of centre.
    px = R * round((0.75 if s.landscape else 0.7) * s.w / R)
    py = min(y for x, y in kept if x == px) + 2 * R
    lit = {(px, py): LIT, (px - R, py + R / 2): DIM, (px + R, py + R / 2): DIM}

    # Painter's order: upper rows first, so each row hides the lower half of the one above.
    # Scales in one row never overlap, so a row is one fill and a few stroke buckets.
    for y, xs in reversed(rows):
        row = [(x, kept[(x, y)]) for x in xs if (x, y) in kept]
        s.fill(P().dots([(x, y) for x, _ in row if (x, y) not in lit], R - 1.5), BG)
        with (
            s.buckets(TONES, "stroke", stroke_width=2.2) as crest,
            s.buckets(TONES, "stroke", stroke_width=2) as deep,
        ):
            for x, depth in row:
                if (x, y) in lit:
                    continue
                # Crest scales keep only their outer rings: the foam thins out as it breaks.
                rings = min(4, 1 + int((depth + rnd.uniform(-12, 12)) / 45))
                d = (crest if depth < 60 else deep)[TONES.rung(smoothstep(0, 150, depth))]
                for r in radii(rings):
                    d.circle((x, y), r)
        for x, depth in row:
            tones = lit.get((x, y))
            if tones is None:
                continue
            w = 2.2 if depth < 60 else 2
            outer, *inner = radii(4)
            s.path(P().circle((x, y), outer), fill=ACCENT_7, stroke=tones[0], stroke_width=w)
            for r, tone in zip(inner, tones[1:], strict=True):
                s.stroke(P().circle((x, y), r), tone, w)

Run it yourself

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