"""Evaporating basin -- blind hillclimb candidate, round 2 variant V.

Medium reconception of the round-1 champion (R): the shoreline is no longer
a smooth bezier curve. It is a faceted front -- five long straight segments
meeting at facet angles, as if the receding solution advanced facet by
facet, the way crystals grow. The deposition bands follow the facets, and
each facet joint catches a touch more deposit where the front changed
direction. Everything else -- saturation rule, five band offsets, mark
vocabulary (dots, dashes), heavy slash, corner mass -- is unchanged.

Rule: a faceted shoreline recedes across the square (top edge -> left edge).
The top-right region is the dark unsaturated basin: left quiet to do
compositional work. On the exposed side, precipitate is deposited along five
contour bands parallel to the tide line. Deposition follows a saturation rule:
density and mark size rise as a power of distance toward the bottom-left
corner (t = 1), sparse and fine where the solution was dilute (t = 0).

Vocabulary: small dots and short dashes only, exact Flexoki swatches,
no interpolation, no alpha. Seeded and deterministic.
"""

import math
import random
from PIL import Image, ImageDraw

SEED = 20260927

PALETTE = {
    "black": "#100F0F",
    "paper": "#FFFCF0",
    "base": {50: "#F2F0E5", 100: "#E6E4D9", 150: "#DAD8CE", 200: "#CECDC3", 300: "#B7B5AC", 400: "#9F9D96", 500: "#878580", 600: "#6F6E69", 700: "#575653", 800: "#403E3C", 850: "#343331", 900: "#282726", 950: "#1C1B1A"},
    "red": {50: "#FFE1D5", 100: "#FFCABB", 150: "#FDB2A2", 200: "#F89A8A", 300: "#E8705F", 400: "#D14D41", 500: "#C03E35", 600: "#AF3029", 700: "#942822", 800: "#6C201C", 850: "#551B18", 900: "#3E1715", 950: "#261312"},
    "orange": {50: "#FFE7CE", 100: "#FED3AF", 150: "#FCC192", 200: "#F9AE77", 300: "#EC8B49", 400: "#DA702C", 500: "#CB6120", 600: "#BC5215", 700: "#9D4310", 800: "#71320D", 850: "#59290D", 900: "#40200D", 950: "#27180E"},
    "yellow": {50: "#FAEEC6", 100: "#F6E2A0", 150: "#F1D67E", 200: "#ECCB60", 300: "#DFB431", 400: "#D0A215", 500: "#BE9207", 600: "#AD8301", 700: "#8E6B01", 800: "#664D01", 850: "#503D02", 900: "#3A2D04", 950: "#241E08"},
    "green": {50: "#EDEECF", 100: "#DDE2B2", 150: "#CDD597", 200: "#BEC97E", 300: "#A0AF54", 400: "#879A39", 500: "#768D21", 600: "#66800B", 700: "#536907", 800: "#3D4C07", 850: "#313D07", 900: "#252D09", 950: "#1A1E0C"},
    "cyan": {50: "#DDF1E4", 100: "#BFE8D9", 150: "#A2DECE", 200: "#87D3C3", 300: "#5ABDAC", 400: "#3AA99F", 500: "#2F968D", 600: "#24837B", 700: "#1C6C66", 800: "#164F4A", 850: "#143F3C", 900: "#122F2C", 950: "#101F1D"},
    "blue": {50: "#E1ECEB", 100: "#C6DDE8", 150: "#ABCFE2", 200: "#92BFDB", 300: "#66A0C8", 400: "#4385BE", 500: "#3171B2", 600: "#205EA6", 700: "#1A4F8C", 800: "#163B66", 850: "#133051", 900: "#12253B", 950: "#101A24"},
    "purple": {50: "#F0EAEC", 100: "#E2D9E9", 150: "#D3CAE6", 200: "#C4B9E0", 300: "#A699D0", 400: "#8B7EC8", 500: "#735EB5", 600: "#5E409D", 700: "#4F3685", 800: "#3C2A62", 850: "#31234E", 900: "#261C39", 950: "#1A1623"},
    "magenta": {50: "#FEE4E5", 100: "#FCCFDA", 150: "#F9B9CF", 200: "#F4A4C2", 300: "#E47DA8", 400: "#CE5D97", 500: "#B74583", 600: "#A02F6F", 700: "#87285E", 800: "#641F46", 850: "#4F1B39", 900: "#39172B", 950: "#24131D"},
}


def sw(family, step):
    return PALETTE[family][step]


def rgb(hexstr):
    return (int(hexstr[1:3], 16), int(hexstr[3:5], 16), int(hexstr[5:7], 16))


W = H = 1080

rng = random.Random(SEED)

# The unsaturated basin: deep blue-black, fills the frame.
img = Image.new("RGB", (W, H), rgb(sw("blue", 950)))
d = ImageDraw.Draw(img)

# Shoreline: faceted front, top edge -> left edge, off both edges.
# Five long straight segments; bearings sweep 118 -> 150 degrees
# (kinks of +9/+6/+10/+7), so the front bends one way like the
# champion's curve -- the overall diagonal is kept, no zigzag.
# t=0 at the top edge, t=1 at the left edge, parameterized by arc length.
VERTS = [
    (790.0, -70.0),
    (672.6, 150.7),
    (522.2, 350.4),
    (351.7, 533.2),
    (148.0, 686.7),
    (-72.8, 814.2),
]

SEG_LENS = [
    math.hypot(VERTS[i + 1][0] - VERTS[i][0], VERTS[i + 1][1] - VERTS[i][1])
    for i in range(len(VERTS) - 1)
]
CUM = [0.0]
for L in SEG_LENS:
    CUM.append(CUM[-1] + L)
TOTAL = CUM[-1]


def _locate(t):
    target = t * TOTAL
    for i in range(len(SEG_LENS)):
        if target <= CUM[i + 1] or i == len(SEG_LENS) - 1:
            return i, (target - CUM[i]) / SEG_LENS[i]
    return len(SEG_LENS) - 1, 1.0


def front(t):
    i, f = _locate(t)
    x0, y0 = VERTS[i]
    x1, y1 = VERTS[i + 1]
    return (x0 + (x1 - x0) * f, y0 + (y1 - y0) * f)


def fronttan(t):
    i, _ = _locate(t)
    x0, y0 = VERTS[i]
    x1, y1 = VERTS[i + 1]
    dx, dy = x1 - x0, y1 - y0
    m = math.hypot(dx, dy)
    return dx / m, dy / m


# Unit normal pointing at the exposed (bottom-left) side.
def normal_at(t):
    tx, ty = fronttan(t)
    nx, ny = -ty, tx
    if nx * (-1.0) + ny * (1.0) < 0:
        nx, ny = -nx, -ny
    return nx, ny


# Current tide line: thin mark along the faceted shoreline.
SEG = 720
shore = [front(i / (SEG - 1)) for i in range(SEG)]
d.line(shore, fill=rgb(sw("blue", 700)), width=5)
d.line(shore, fill=rgb(sw("blue", 200)), width=1)

# Saturation rule: 0 at the dilute end (t=0, top edge), 1 at the
# concentrated corner (t=1, bottom-left exit).
def sat(t):
    return t ** 2.4


# Five contour bands on the exposed side, widening gaps like old tide lines.
# (offset, palette choices weighted by position, base probability, accent color)
BANDS = [
    (30,  [("base", 100), ("base", 200), ("blue", 200)], None),
    (70,  [("base", 50), ("base", 150), ("base", 300)], None),
    (122, [("paper", None), ("base", 100), ("base", 200)], None),
    (192, [("base", 50), ("yellow", 150), ("yellow", 200)], ("orange", 300)),
    (286, [("yellow", 100), ("yellow", 200), ("yellow", 300)], ("orange", 300)),
]


def pick(color_spec):
    fam, step = color_spec
    if step is None:
        return rgb(PALETTE[fam])
    return rgb(sw(fam, step))


def dot(x, y, r, color):
    d.ellipse([x - r, y - r, x + r, y + r], fill=color)


def dash(x, y, tx, ty, length, w, color):
    d.line([(x - tx * length / 2, y - ty * length / 2),
            (x + tx * length / 2, y + ty * length / 2)],
           fill=color, width=w)


for bidx, (offset, palette_specs, accent) in enumerate(BANDS):
    band_w = 16 + bidx * 8  # band thickness grows with age
    n_steps = 3400
    for i in range(n_steps):
        t = i / (n_steps - 1)
        # edge taper so bands fade exactly at the frame exits
        taper = min(1.0, min(t, 1.0 - t) * 12.0)
        s = sat(t) * taper
        p = 0.05 + 0.80 * s  # deposition probability
        if rng.random() > p:
            continue
        px, py = front(t)
        nx, ny = normal_at(t)
        tx, ty = fronttan(t)
        # mark sits somewhere inside its band, jittered along shore
        j = rng.gauss(0.0, 1.0)
        ox = offset + j * band_w * 0.5
        x = px + nx * ox + tx * rng.uniform(-3, 3)
        y = py + ny * ox + ty * rng.uniform(-3, 3)
        if not (0 <= x < W and 0 <= y < H):
            continue
        size = 1.8 + 9.0 * s ** 1.7  # marks grow heavier toward saturation
        # rare warm accent only deep in the concentrated band(s)
        if accent is not None and s > 0.55 and rng.random() < 0.22 * s:
            color = pick(accent)
        else:
            color = pick(rng.choice(palette_specs))
        if rng.random() < 0.30:
            dash(x, y, tx, ty, size * 3.6, max(1, int(size * 0.9)), color)
        else:
            dot(x, y, size, color)

# Facet joints: the front changed direction at each interior vertex, and the
# solute caught up a little there. Same vocabulary, slightly denser, sized
# by the local saturation like every other mark.
for vi in range(1, len(VERTS) - 1):
    tv = CUM[vi] / TOTAL
    vx, vy = VERTS[vi]
    s = sat(tv)
    nx, ny = normal_at(tv)
    tx, ty = fronttan(tv)
    specs = BANDS[4][1] if s > 0.5 else BANDS[2][1]
    for _ in range(130):
        ox = 8 + abs(rng.gauss(0.0, 26.0))  # exposed side, hugging the joint
        along = rng.gauss(0.0, 34.0)
        x = vx + nx * ox + tx * along
        y = vy + ny * ox + ty * along
        if not (0 <= x < W and 0 <= y < H):
            continue
        size = 1.8 + 9.0 * s ** 1.7
        color = pick(rng.choice(specs))
        if rng.random() < 0.30:
            dash(x, y, tx, ty, size * 3.6, max(1, int(size * 0.9)), color)
        else:
            dot(x, y, size, color)

# Near the concentrated corner: one denser, more worked patch of deposit.
cx, cy = front(1.0)
for _ in range(900):
    s = rng.random() ** 0.5
    ang = rng.uniform(0, math.tau)
    rad = rng.gauss(0.0, 1.0) * (26 + 60 * s)
    nx, ny = normal_at(0.985)
    tx, ty = fronttan(0.985)
    x = cx + nx * (120 + rad) + tx * rng.uniform(-90, 90)
    y = cy + ny * (120 + rad) + ty * rng.uniform(-90, 90)
    if not (0 <= x < W and 0 <= y < H):
        continue
    size = 3.0 + 11.0 * s
    if rng.random() < 0.22:
        color = pick(("orange", 300))
    elif rng.random() < 0.5:
        color = pick(("paper", None))
    else:
        color = pick(rng.choice([("base", 100), ("base", 200)]))
    if rng.random() < 0.35:
        dash(x, y, tx, ty, size * 3.8, max(2, int(size * 0.9)), color)
    else:
        dot(x, y, size, color)

# One heavy gesture: a single worked slash seated in the saturated mass.
# Drawn deterministically (no rng), so the deposition sequence is untouched.
gt = 0.94
gpx, gpy = front(gt)
gnx, gny = normal_at(gt)
gtx, gty = fronttan(gt)
gcx = gpx + gnx * 210.0 + gtx * 30.0
gcy = gpy + gny * 210.0 + gty * 30.0
dash(gcx, gcy, gtx, gty, 175, 24, pick(("paper", None)))

# A few stray grains floating in the basin, far from shore: nearly nothing.
for _ in range(46):
    x = rng.uniform(560, W)
    y = rng.uniform(0, 560)
    s = rng.random() ** 2
    dot(x, y, 1.2 + 1.6 * s, pick(("base", 500)))

img.save("output.png")
