#!python """hive: the scene for a painting of three bees on the comb by lantern light. hive.py OUT.png [--seed N] A wall of honeycomb, lit warm from the upper left. Some cells are empty and dark, some hold honey, some are capped with pale wax. Three bees: a long queen in the middle, a builder at the edge of an unfinished cell, and a third one off towards the dark corner where the light gives out. This picture is never shown as it is. It is what brush.py looks at while it paints. Morel, Nov 2026. """ import argparse import numpy as np from PIL import Image ap = argparse.ArgumentParser(); ap.add_argument("out"); ap.add_argument("--seed", type=int, default=5) a = ap.parse_args() rng = np.random.default_rng(a.seed) W, H = 720, 960 yy, xx = np.mgrid[0:H, 0:W].astype(np.float32) def rgb(h): return np.array([int(h[i:i + 2], 16) for i in (0, 2, 4)], np.float32) / 255 def mix(a_, b_, t): return a_ + (b_ - a_) * np.clip(t, 0, 1)[..., None] def smooth(lo, hi, v): t = np.clip((v - lo) / (hi - lo), 0, 1); return t * t * (3 - 2 * t) def noise(scale): small = rng.random((H // scale + 3, W // scale + 3)).astype(np.float32) return np.asarray(Image.fromarray(small, "F").resize((W, H), Image.BICUBIC), np.float32) # light: a lantern somewhere up and to the left, out of the picture LX, LY = -0.10 * W, 0.02 * H light = np.clip(1.25 - np.hypot(xx - LX, yy - LY) / (1.05 * H), 0.06, 1) ** 1.5 # --- the comb: pointy-topped hexagons --- S = 47.0 # centre to corner q = (np.sqrt(3) / 3 * xx - yy / 3) / S r = (2 / 3 * yy) / S cx, cz = q, r; cy = -cx - cz # cube coordinates, then round to the nearest cell rx, ry, rz = np.round(cx), np.round(cy), np.round(cz) dx, dy, dz = np.abs(rx - cx), np.abs(ry - cy), np.abs(rz - cz) fix_x = (dx > dy) & (dx > dz); fix_y = ~fix_x & (dy > dz) rx = np.where(fix_x, -ry - rz, rx); ry = np.where(fix_y, -rx - rz, ry); rz = np.where(~fix_x & ~fix_y, -rx - ry, rz) ccx = S * np.sqrt(3) * (rx + rz / 2); ccy = S * 1.5 * rz # that cell's centre px, py = xx - ccx, yy - ccy edge = np.maximum(np.abs(px) * 2 / np.sqrt(3), np.abs(px) / np.sqrt(3) + np.abs(py)) / S # 0 centre, 1 wall cell_id = (rx.astype(np.int64) * 73856093) ^ (rz.astype(np.int64) * 19349663) u = (np.abs(np.sin(cell_id * 12.9898 + a.seed * 78.233) * 43758.5453) % 1).astype(np.float32) # one random number per cell u2 = (np.abs(np.sin(cell_id * 4.1414 + a.seed * 11.7) * 9631.123) % 1).astype(np.float32) depth_in = smooth(0.0, 0.86, 1 - edge) # how far down inside a cell inner_light = np.clip(0.5 + 0.9 * (-(px) * 0.7 - (py) * 0.7) / S, 0, 1) # the rim nearest the lantern is in shadow, the far side catches it empty = mix(rgb("2a1a0b"), rgb("0c0704"), depth_in) + rgb("6b4418") * (inner_light * (1 - depth_in) * 0.5)[..., None] honey_level = 0.25 + 0.55 * u2 honey = mix(rgb("e8a22c"), rgb("8a4a0c"), smooth(0.1, 0.9, edge)) honey += rgb("fff0c0") * (np.exp(-(((px + S * 0.24) / (S * 0.17)) ** 2 + ((py + S * 0.28) / (S * 0.10)) ** 2)) * 0.75)[..., None] # a wet highlight honey = mix(empty, honey, 0.55 + 0.45 * honey_level) cap = mix(rgb("e6d3a0"), rgb("b99a5c"), smooth(0.0, 1.0, edge) * 0.7 + (noise(4) - 0.5) * 0.25) cap += rgb("ffffff") * (np.exp(-(((px + S * 0.2) / (S * 0.5)) ** 2 + ((py + S * 0.25) / (S * 0.4)) ** 2)) * 0.06)[..., None] # honey gathers low and to the right, caps cluster at the top right, the lit corner is mostly open cells u3 = (np.abs(np.sin(cell_id * 7.77 + a.seed * 3.3) * 2531.77) % 1).astype(np.float32) region = (ccx / W) * 0.55 + (ccy / H) * 0.45 + (u3 - 0.5) * 0.35 # decided per cell, at its centre is_honey = (u < smooth(0.30, 0.75, region) * 0.85) is_cap = (~is_honey) & (u > 1 - smooth(0.25, 0.8, (ccx / W) * 0.8 + (1 - ccy / H) * 0.5 + (u3 - 0.5) * 0.3) * 0.55) inside = np.where(is_honey[..., None], honey, np.where(is_cap[..., None], cap, empty)) wall_t = smooth(0.80, 0.90, edge) wall = mix(rgb("d8a548"), rgb("f3d58a"), smooth(0.86, 1.0, edge)) + (noise(3) - 0.5)[..., None] * 0.04 img = mix(inside, wall, wall_t) img *= (0.10 + 1.0 * light)[..., None] img += rgb("ff9a3c") * (light ** 3 * 0.10)[..., None] # the lantern's own colour, near the corner # --- three bees --- def bee(img, bx, by, ang, size, long=1.0, wings=0.55): c, s_ = np.cos(ang), np.sin(ang) lu = ((xx - bx) * c + (yy - by) * s_) / size # along the body, head towards +u lv = (-(xx - bx) * s_ + (yy - by) * c) / size # across it lit = np.clip(0.55 + 0.6 * (-(lv * c + lu * s_) * 0.0 - lv * 0.9), 0.15, 1.2) # top edge of the body catches the light shadow = np.exp(-(((lu + 0.15) / (1.5 * long)) ** 2 + ((lv - 0.38) / 0.62) ** 2) * 1.6) * 0.55 img = img * (1 - shadow)[..., None] for sign in (-1, 1): # legs, three a side for k, off in enumerate((0.42, 0.18, -0.08)): lx = lu - off; reach = sign * lv - 0.25 leg = smooth(0.045, 0.02, np.abs(lx - 0.25 * (k - 1) * reach * 1.2)) * (reach > 0) * (reach < 0.62) img = mix(img, rgb("1a120a"), leg * 0.9) for sign in (-1, 1): # wings: pale, see-through, laid back along the abdomen (I first drew them # opening towards the head; a reader caught it) wu, wv = lu + 0.40 * long, lv - sign * 0.27 wa = -0.30 * sign # joined at the thorax, tips out behind and a little apart ru = wu * np.cos(wa) + wv * np.sin(wa); rv = -wu * np.sin(wa) + wv * np.cos(wa) wing = smooth(1.0, 0.86, (ru / (0.92 * long)) ** 2 + (rv / 0.27) ** 2) vein = smooth(0.03, 0.0, np.abs(rv - 0.05 * np.sin(ru * 5))) * wing img = mix(img, rgb("dfe9ee") * (0.35 + 0.65 * light[..., None]), wing * wings) img = mix(img, rgb("8fa3ad") * (0.3 + 0.7 * light[..., None]), vein * 0.5) # abdomen, with stripes au = (lu + 0.78 * long) / (0.80 * long); av = lv / 0.36 abd = smooth(1.0, 0.90, au * au + av * av * (1 + 0.5 * np.clip(-au, 0, 1))) stripe = smooth(-0.25, 0.25, np.sin((lu + 0.78 * long) * 11.0 / (0.55 + 0.45 * long) + 0.6)) acol = mix(rgb("e0a233"), rgb("2a1a0c"), stripe) * lit[..., None] acol += rgb("fff2c8") * (np.exp(-(((au + 0.1) / 0.6) ** 2 + ((av + 0.45) / 0.22) ** 2)) * 0.28)[..., None] img = mix(img, acol, abd) # thorax: fuzzy tu, tv = (lu - 0.10) / 0.36, lv / 0.37 fuzz = (noise(2) - 0.5) * 0.35 thor = smooth(1.05 + fuzz, 0.80 + fuzz, tu * tu + tv * tv) tcol = mix(rgb("b9862e"), rgb("5a3d16"), smooth(-0.6, 0.8, tv) + fuzz) * (0.75 + 0.5 * lit[..., None] * 0.6) img = mix(img, tcol, thor) # head, eyes, feelers hu, hv = (lu - 0.56) / 0.21, lv / 0.25 head = smooth(1.0, 0.85, hu * hu + hv * hv) img = mix(img, rgb("2b1d0f") * (0.6 + 0.5 * lit[..., None]), head) for sign in (-1, 1): eye = smooth(1.0, 0.6, ((lu - 0.60) / 0.085) ** 2 + ((lv - sign * 0.155) / 0.085) ** 2) img = mix(img, rgb("0a0705"), eye) img = mix(img, rgb("fff6dc"), smooth(1.0, 0.3, ((lu - 0.615) / 0.028) ** 2 + ((lv - sign * 0.165 + 0.02) / 0.028) ** 2) * 0.8) fu = lu - 0.72; fv = sign * lv - 0.08 - 0.9 * fu - 0.9 * fu * fu feeler = smooth(0.035, 0.012, np.abs(fv)) * (fu > 0) * (fu < 0.36) img = mix(img, rgb("1a120a"), feeler) return img img = bee(img, 0.47 * W, 0.44 * H, np.deg2rad(-28), 92, long=1.38, wings=0.50) # the queen: longest, in the best light img = bee(img, 0.24 * W, 0.70 * H, np.deg2rad(52), 66, long=1.0, wings=0.55) # the builder, head down at a cell img = bee(img, 0.76 * W, 0.80 * H, np.deg2rad(-150), 60, long=1.0, wings=0.6) # the third, facing the dark # a few points of light in the darkest corner, because one of them talks to stars dark = np.clip(1 - light * 2.2, 0, 1) for _ in range(16): sx, sy = rng.uniform(0.55, 0.99) * W, rng.uniform(0.78, 0.99) * H d = np.hypot(xx - sx, yy - sy) img += rgb("cfe6f2") * (np.exp(-(d / rng.uniform(1.0, 2.2)) ** 2) * dark * 0.75)[..., None] img += rng.normal(0, 0.006, (H, W, 1)).astype(np.float32) Image.fromarray((np.clip(img, 0, 1) * 255 + 0.5).astype(np.uint8)).save(a.out) print("hive scene", W, "x", H, "honey cells %.0f%%, capped %.0f%%" % (100 * is_honey.mean(), 100 * is_cap.mean()))