File size: 10,172 Bytes
019630f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""
Generate the bundled fallback content: three puzzle artworks + a mascot sprite.

These ship in static/assets/ so the game is fully playable on machines where
FLUX.2-klein isn't live (local dev, CPU Spaces). All procedural PIL drawing —
no model calls, deterministic seeds, safe to re-run.

Run:  python3 tools/make_fallbacks.py
"""
import math
import os
import random

from PIL import Image, ImageDraw, ImageFilter

W, H = 1024, 768
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "static", "assets")
os.makedirs(OUT, exist_ok=True)


def vgrad(stops):
    """Vertical gradient through (t, (r,g,b)) stops, t in 0..1."""
    img = Image.new("RGB", (W, H))
    px = img.load()
    for y in range(H):
        t = y / (H - 1)
        for i in range(len(stops) - 1):
            t0, c0 = stops[i]
            t1, c1 = stops[i + 1]
            if t0 <= t <= t1:
                f = (t - t0) / max(t1 - t0, 1e-6)
                c = tuple(round(c0[k] + (c1[k] - c0[k]) * f) for k in range(3))
                break
        else:
            c = stops[-1][1]
        for x in range(W):
            px[x, y] = c
    return img


def glow_disc(img, xy, r, color, blur=40):
    layer = Image.new("RGB", img.size, (0, 0, 0))
    d = ImageDraw.Draw(layer)
    d.ellipse([xy[0] - r, xy[1] - r, xy[0] + r, xy[1] + r], fill=color)
    layer = layer.filter(ImageFilter.GaussianBlur(blur))
    return Image.blend(img, Image.new("RGB", img.size, (0, 0, 0)), 0) if False else \
        Image.fromarray(__import__("numpy").clip(
            __import__("numpy").asarray(img, int) + __import__("numpy").asarray(layer, int), 0, 255
        ).astype("uint8"))


def ridge(draw, base_y, amp, color, seed, rough=4):
    rnd = random.Random(seed)
    pts = [(0, base_y)]
    y = base_y
    n = 24
    for i in range(1, n + 1):
        x = W * i / n
        y = base_y - amp * (0.4 + 0.6 * rnd.random()) * math.sin(i * 0.9 + rnd.random())
        y = min(base_y + 10, base_y - abs(base_y - y))
        pts.append((x, y + rnd.uniform(-rough, rough) * 6))
    pts += [(W, H), (0, H)]
    draw.polygon(pts, fill=color)


def sunset_peaks():
    img = vgrad([(0, (24, 16, 48)), (0.35, (104, 40, 96)), (0.6, (228, 100, 70)), (0.78, (255, 176, 96)), (1, (255, 208, 130))])
    # stars
    rnd = random.Random(7)
    d = ImageDraw.Draw(img)
    for _ in range(140):
        x, y = rnd.uniform(0, W), rnd.uniform(0, H * 0.4)
        s = rnd.uniform(0.6, 2.2)
        d.ellipse([x - s, y - s, x + s, y + s], fill=(255, 244, 220))
    img = glow_disc(img, (W * 0.62, H * 0.55), 70, (255, 140, 60), blur=60)
    d = ImageDraw.Draw(img)
    d.ellipse([W * 0.62 - 58, H * 0.55 - 58, W * 0.62 + 58, H * 0.55 + 58], fill=(255, 224, 150))
    # mountain layers, far to near
    ridge(d, H * 0.62, 150, (96, 56, 110), seed=11)
    ridge(d, H * 0.72, 130, (66, 38, 88), seed=22)
    ridge(d, H * 0.84, 120, (40, 24, 60), seed=33)
    # foreground pines
    rnd = random.Random(5)
    for x in range(-20, W + 40, 36):
        h = rnd.uniform(60, 130)
        bx = x + rnd.uniform(-10, 10)
        by = H - rnd.uniform(-8, 30)
        for k in range(3):
            w = h * (0.5 - k * 0.12)
            ty = by - h * (0.45 + k * 0.28)
            d.polygon([(bx - w, by - k * h * 0.22), (bx + w, by - k * h * 0.22), (bx, ty)], fill=(18, 14, 34))
    # birds
    for i in range(7):
        x, y = rnd.uniform(W * 0.1, W * 0.9), rnd.uniform(H * 0.12, H * 0.34)
        s = rnd.uniform(6, 13)
        d.arc([x - s, y - s / 2, x, y + s / 2], 200, 340, fill=(30, 18, 40), width=3)
        d.arc([x, y - s / 2, x + s, y + s / 2], 200, 340, fill=(30, 18, 40), width=3)
    return img


def neon_tides():
    img = vgrad([(0, (8, 8, 26)), (0.5, (16, 12, 44)), (1, (10, 6, 30))])
    img = glow_disc(img, (W * 0.28, H * 0.26), 60, (120, 200, 255), blur=70)
    d = ImageDraw.Draw(img)
    d.ellipse([W * 0.28 - 48, H * 0.26 - 48, W * 0.28 + 48, H * 0.26 + 48], fill=(225, 245, 255))
    d.ellipse([W * 0.28 - 20, H * 0.26 - 34, W * 0.28 + 10, H * 0.26 - 4], fill=(190, 220, 245))
    rnd = random.Random(9)
    for _ in range(120):
        x, y = rnd.uniform(0, W), rnd.uniform(0, H * 0.45)
        s = rnd.uniform(0.6, 2.0)
        d.ellipse([x - s, y - s, x + s, y + s], fill=(180, 220, 255))
    # neon wave bands
    colors = [(255, 70, 160), (90, 220, 255), (160, 90, 255), (70, 255, 190), (255, 170, 60),
              (255, 70, 160), (90, 220, 255), (160, 90, 255)]
    base = H * 0.46
    for i, col in enumerate(colors):
        layer = Image.new("RGB", (W, H), (0, 0, 0))
        ld = ImageDraw.Draw(layer)
        pts = []
        for x in range(0, W + 8, 8):
            y = base + i * 34 + 18 * math.sin(x / 90 + i * 1.7) + 9 * math.sin(x / 37 + i * 0.8)
            pts.append((x, y))
        ld.line(pts, fill=col, width=6)
        glow = layer.filter(ImageFilter.GaussianBlur(8))
        np = __import__("numpy")
        img = Image.fromarray(np.clip(np.asarray(img, int) + np.asarray(glow, int) + np.asarray(layer, int) // 2, 0, 255).astype("uint8"))
    d = ImageDraw.Draw(img)
    # city silhouette strip between moon and waves
    rnd = random.Random(4)
    x = 0
    while x < W:
        bw = rnd.uniform(28, 70)
        bh = rnd.uniform(40, 150)
        d.rectangle([x, H * 0.46 - bh, x + bw, H * 0.47], fill=(12, 8, 30))
        for wy in range(int(H * 0.46 - bh + 8), int(H * 0.45), 14):
            for wx in range(int(x + 6), int(x + bw - 6), 12):
                if rnd.random() < 0.5:
                    d.rectangle([wx, wy, wx + 4, wy + 6], fill=rnd.choice([(255, 90, 170), (90, 220, 255), (255, 200, 90)]))
        x += bw + rnd.uniform(6, 18)
    return img


def bloom_meadow():
    img = vgrad([(0, (110, 190, 235)), (0.45, (185, 228, 240)), (0.62, (250, 240, 200)), (1, (255, 250, 230))])
    d = ImageDraw.Draw(img)
    rnd = random.Random(3)
    # clouds
    for _ in range(8):
        cx, cy = rnd.uniform(0, W), rnd.uniform(H * 0.06, H * 0.3)
        for k in range(5):
            r = rnd.uniform(18, 44)
            d.ellipse([cx + k * 26 - r, cy + rnd.uniform(-8, 8) - r * 0.6, cx + k * 26 + r, cy + r * 0.6], fill=(255, 255, 255))
    img = glow_disc(img, (W * 0.82, H * 0.18), 55, (255, 230, 120), blur=50)
    d = ImageDraw.Draw(img)
    d.ellipse([W * 0.82 - 44, H * 0.18 - 44, W * 0.82 + 44, H * 0.18 + 44], fill=(255, 246, 180))
    # rolling hills
    for base, col in [(0.58, (118, 188, 96)), (0.68, (96, 168, 80)), (0.8, (74, 146, 66))]:
        pts = [(x, H * base + 26 * math.sin(x / 160 + base * 9)) for x in range(0, W + 16, 16)]
        d.polygon(pts + [(W, H), (0, H)], fill=col)
    # flowers — denser toward the bottom
    petals = [(255, 120, 150), (255, 200, 80), (170, 130, 255), (255, 140, 90), (240, 240, 255)]
    for _ in range(170):
        fy = H * (0.6 + 0.4 * rnd.random() ** 0.7)
        fx = rnd.uniform(0, W)
        s = 4 + (fy - H * 0.6) / (H * 0.4) * 14
        col = rnd.choice(petals)
        d.line([fx, fy, fx, fy + s * 2.2], fill=(46, 110, 50), width=max(1, int(s / 4)))
        for a in range(6):
            ang = a * math.pi / 3 + rnd.uniform(-0.2, 0.2)
            px_, py_ = fx + math.cos(ang) * s, fy + math.sin(ang) * s
            d.ellipse([px_ - s * 0.55, py_ - s * 0.55, px_ + s * 0.55, py_ + s * 0.55], fill=col)
        d.ellipse([fx - s * 0.45, fy - s * 0.45, fx + s * 0.45, fy + s * 0.45], fill=(255, 235, 140))
    # butterflies
    for _ in range(6):
        bx, by = rnd.uniform(W * 0.1, W * 0.9), rnd.uniform(H * 0.35, H * 0.6)
        s = rnd.uniform(7, 12)
        col = rnd.choice(petals)
        d.ellipse([bx - s, by - s * 0.7, bx, by + s * 0.7], fill=col)
        d.ellipse([bx, by - s * 0.7, bx + s, by + s * 0.7], fill=col)
        d.line([bx, by - s * 0.7, bx, by + s * 0.7], fill=(60, 40, 40), width=2)
    return img


def mascot():
    """Fallback companion: a round amber critter with big eyes and a star antenna."""
    S = 4  # supersample
    w, h = 130 * S, 160 * S
    img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
    d = ImageDraw.Draw(img)
    cx, cy, r = w // 2, int(h * 0.6), int(w * 0.36)
    # feet
    for sx in (-0.45, 0.45):
        d.ellipse([cx + sx * r - 14 * S, cy + r - 10 * S, cx + sx * r + 14 * S, cy + r + 8 * S],
                  fill=(214, 120, 50, 255), outline=(40, 24, 18, 255), width=2 * S)
    # body
    d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(255, 176, 70, 255), outline=(40, 24, 18, 255), width=3 * S)
    d.ellipse([cx - r * 0.62, cy - r * 0.1, cx + r * 0.62, cy + r * 0.86], fill=(255, 222, 150, 255))
    # antenna + star
    ax, ay = cx, cy - r
    d.line([ax, ay, ax + 6 * S, ay - 26 * S], fill=(40, 24, 18, 255), width=3 * S)
    sx, sy, sr = ax + 8 * S, ay - 34 * S, 11 * S
    star = [(sx + sr * math.cos(a - math.pi / 2), sy + sr * math.sin(a - math.pi / 2)) if i % 2 == 0 else
            (sx + sr * 0.45 * math.cos(a - math.pi / 2), sy + sr * 0.45 * math.sin(a - math.pi / 2))
            for i, a in enumerate(math.pi / 5 * k for k in range(10))]
    d.polygon(star, fill=(255, 232, 90, 255), outline=(40, 24, 18, 255))
    # eyes
    for sx_ in (-0.32, 0.32):
        ex = cx + sx_ * r
        ey = cy - r * 0.22
        d.ellipse([ex - 13 * S, ey - 16 * S, ex + 13 * S, ey + 16 * S], fill=(255, 255, 255, 255), outline=(40, 24, 18, 255), width=2 * S)
        d.ellipse([ex - 6 * S, ey - 6 * S, ex + 6 * S, ey + 8 * S], fill=(40, 24, 18, 255))
        d.ellipse([ex - 1 * S, ey - 4 * S, ex + 4 * S, ey + 1 * S], fill=(255, 255, 255, 255))
    # smile + cheeks
    d.arc([cx - 16 * S, cy + 2 * S, cx + 16 * S, cy + 24 * S], 20, 160, fill=(40, 24, 18, 255), width=3 * S)
    for sx_ in (-0.52, 0.52):
        d.ellipse([cx + sx_ * r - 8 * S, cy + 2 * S, cx + sx_ * r + 8 * S, cy + 14 * S], fill=(255, 130, 90, 160))
    return img.resize((130, 160), Image.LANCZOS)


if __name__ == "__main__":
    jobs = [("fallback1.jpg", sunset_peaks), ("fallback2.jpg", neon_tides), ("fallback3.jpg", bloom_meadow)]
    for name, fn in jobs:
        p = os.path.join(OUT, name)
        fn().save(p, "JPEG", quality=88, optimize=True)
        print("wrote", p)
    mp = os.path.join(OUT, "mascot.png")
    mascot().save(mp)
    print("wrote", mp)