patterns — thaw

generator verdict no votes yet
source — color/auto/e005_thaw.g55_thaw
def g55_thaw(rng):
    """Thaw — a field-weighted, multi-seed melt front; each cell's ramp step is its arrival time.

    A handful of frost seeds retreat outward together, like a thaw line eating a
    frozen mosaic. Unlike a uniform flood, every frontier cell's chance of melting
    next is weighted by an underlying field, so the front races through the
    field's warm regions and lingers in its cold ones. Cells are ranked by
    arrival order and cut into equal-sized ramp bands, then a sparse scatter of
    rotated squares — tilted toward the cell each one melted from — marks the
    seams between bands.
    """
    N = rng.choice([25, 30, 40, 50])
    steps = rng.randint(5, 9)
    c = Canvas(N, rng.choice(PALETTE_NAMES), steps, "thaw", "squares")

    f = Field(rng, rng.choice(["mesh", "linear", "radial", "weave", "bilinear"]),
              warp=rng.uniform(0.15, 0.6), steps=steps)
    f.calibrate(N)

    warmth = [[0.08 + f.raw((i + 0.5) / N, (j + 0.5) / N) for j in range(N)]
              for i in range(N)]

    moore = rng.random() < 0.5
    deltas = ([(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
              if moore else [(-1, 0), (1, 0), (0, -1), (0, 1)])

    nseeds = rng.randint(2, 5)
    seeds = set()
    while len(seeds) < nseeds:
        seeds.add((rng.randrange(N), rng.randrange(N)))
    seeds = list(seeds)

    arrival = [[-1] * N for _ in range(N)]
    frm = [[(0, 0)] * N for _ in range(N)]
    order = 0
    for (si, sj) in seeds:
        arrival[si][sj] = order
        order += 1

    frontier = []       # [i, j, from_i, from_j]
    infront = {}         # (i, j) -> index into frontier

    def push(i, j, fi, fj):
        if arrival[i][j] != -1 or (i, j) in infront:
            return
        infront[(i, j)] = len(frontier)
        frontier.append([i, j, fi, fj])

    for (si, sj) in seeds:
        for (di, dj) in deltas:
            ni, nj = si + di, sj + dj
            if 0 <= ni < N and 0 <= nj < N:
                push(ni, nj, si, sj)

    while frontier:
        weights = [warmth[i][j] for (i, j, _, _) in frontier]
        pick = rng.choices(range(len(frontier)), weights=weights, k=1)[0]
        last = len(frontier) - 1
        frontier[pick], frontier[last] = frontier[last], frontier[pick]
        i, j, fi, fj = frontier.pop()
        del infront[(i, j)]
        if pick != last:
            infront[(frontier[pick][0], frontier[pick][1])] = pick

        arrival[i][j] = order
        frm[i][j] = (fi, fj)
        order += 1
        for (di, dj) in deltas:
            ni, nj = i + di, j + dj
            if 0 <= ni < N and 0 <= nj < N:
                push(ni, nj, i, j)

    total = N * N
    ranked = sorted((i, j) for j in range(N) for i in range(N))
    ranked.sort(key=lambda p: arrival[p[0]][p[1]])
    band = [[0] * N for _ in range(N)]
    for rank, (i, j) in enumerate(ranked):
        band[i][j] = min(steps - 1, (rank * steps) // total)

    c.ground(lambda i, j: band[i][j])

    seam_p = rng.uniform(0.25, 0.55)
    w = c.C * rng.uniform(0.3, 0.5)
    for j in range(N):
        for i in range(N):
            here = band[i][j]
            lit = False
            for (di, dj) in ((1, 0), (0, 1)):
                ni, nj = i + di, j + dj
                if ni < N and nj < N and band[ni][nj] != here:
                    lit = True
                    break
            if not lit or rng.random() >= seam_p:
                continue
            if c.room() < 4:
                break
            fi, fj = frm[i][j]
            ang = math.degrees(math.atan2(j - fj, i - fi)) if (fi, fj) != (i, j) else 0.0
            cx, cy = c.center(i, j)
            k = min(steps - 1, here + 1)
            c.sq(cx - w / 2, cy - w / 2, w, w, k, rot=ang)

    return c
thaw-squares-25-5caa
thaw-squares-25-5caa
thaw-squares-40-1d32
thaw-squares-40-1d32

generator