"""The Asheron's Call sigil geometry, rebuilt from the retail wordmark. Measured off the logo rather than guessed: * the ring is a **thin, complete** circle -- roughly 1/16 of its diameter in thickness -- not a thick band with a gap. It passes *behind* the glyph. * both ring and glyph are chiselled relief with a raised centre ridge, which is why every stroke shows a bright spine and two darker chamfers. * the glyph is a crescent opening to the right. Its upper terminal extends into a barbed hook that crosses over the ring; its lower terminal tapers to a fine point. Everything here produces masks; chisel.shade() turns them into metal. """ import numpy as np from PIL import Image, ImageDraw, ImageFilter SS = 4 # supersample factor for all mask drawing def _canvas(size): return Image.new("L", (size * SS, size * SS), 0) def _down(img, size): return np.asarray(img.resize((size, size), Image.LANCZOS), dtype=np.float32) / 255.0 def _px(size, v): """normalised (-1..1) -> pixel on the supersampled canvas""" return (v * 0.5 + 0.5) * size * SS def thin_ring(size=1024, R=0.86, thick=0.055, wobble=0.0, seed=3): """Complete slender circle, the AC ring proportion.""" W = size * SS y, x = np.mgrid[0:W, 0:W].astype(np.float32) x = (x / (W - 1)) * 2 - 1 y = (y / (W - 1)) * 2 - 1 r = np.sqrt(x * x + y * y) if wobble > 0: th = np.arctan2(y, x) rng = np.random.default_rng(seed) r = r - wobble * (np.sin(6 * th + rng.uniform(0, 6)) * 0.6 + np.sin(11 * th + rng.uniform(0, 6)) * 0.4) m = (np.abs(r - R) < thick / 2).astype(np.float32) im = Image.fromarray((m * 255).astype(np.uint8), "L") return _down(im, size) def crescent(size=1024, R=0.66, inner_r=0.60, offset=0.30, cy=0.0, rot_deg=-18.0): """Classic two-circle crescent: outer disc minus an offset inner disc. The two intersection points give naturally sharp cusps -- exactly the terminals the AC glyph has, before the barb is added on top. """ W = size * SS y, x = np.mgrid[0:W, 0:W].astype(np.float32) x = (x / (W - 1)) * 2 - 1 y = (y / (W - 1)) * 2 - 1 a = np.radians(rot_deg) xr = x * np.cos(a) - y * np.sin(a) yr = x * np.sin(a) + y * np.cos(a) outer = (xr * xr + (yr - cy) ** 2) < R * R inner = ((xr - offset) ** 2 + (yr - cy) ** 2) < inner_r * inner_r m = (outer & ~inner).astype(np.float32) return _down(Image.fromarray((m * 255).astype(np.uint8), "L"), size) def _tapered_arc(draw, size, cx, cy, r0, r1, a0_deg, a1_deg, w0, w1, steps=90, curl=0.0): """Draw a tapering curved stroke as a polygon strip.""" t = np.linspace(0, 1, steps) ang = np.radians(a0_deg + (a1_deg - a0_deg) * t) rad = r0 + (r1 - r0) * t + curl * np.sin(np.pi * t) cxs = cx + np.cos(ang) * rad cys = cy + np.sin(ang) * rad w = w0 + (w1 - w0) * (t ** 1.35) dx = np.gradient(cxs); dy = np.gradient(cys) ln = np.sqrt(dx * dx + dy * dy); ln = np.where(ln < 1e-9, 1, ln) nx, ny = -dy / ln, dx / ln left = [( _px(size, cxs[i] + nx[i] * w[i]), _px(size, cys[i] + ny[i] * w[i]) ) for i in range(steps)] right = [( _px(size, cxs[i] - nx[i] * w[i]), _px(size, cys[i] - ny[i] * w[i]) ) for i in range(steps)][::-1] draw.polygon(left + right, fill=255) def cusp_angles(R, inner_r, offset): """Where the two circles meet -- the crescent's two sharp terminals. Solving the circle intersection gives the exact attachment points, so the barb and tail grow out of the cusps instead of floating near them. """ x = (R * R - inner_r * inner_r + offset * offset) / (2 * offset) y2 = R * R - x * x if y2 <= 0: return None y = np.sqrt(y2) return np.degrees(np.arctan2(-y, x)), np.degrees(np.arctan2(y, x)) def ac_glyph(size=1024, R=0.66, inner_r=0.60, offset=0.30, rot_deg=-18.0, barb=True, tail=True, fork=True): """Crescent + barbed upper hook + tapering lower tail, as one mask.""" # Build crescent and spikes in ONE unrotated frame, then rotate the combined # mask once. Rotating them separately mixed two sign conventions and left the # tail floating clear of the cusp it is supposed to grow out of. m = crescent(size, R, inner_r, offset, 0.0, 0.0) cu = cusp_angles(R, inner_r, offset) if cu is None: return m upper, lower = cu img = _canvas(size) d = ImageDraw.Draw(img) if barb: # main spike: leaves the cusp thin, swells, then needles out past the ring _tapered_arc(d, size, 0.0, 0.0, R, R * 1.34, upper, upper - 62.0, 0.030, 0.004, curl=0.085) if fork: # the second, shorter prong that makes the terminal read as barbed _tapered_arc(d, size, 0.0, 0.0, R * 1.02, R * 1.16, upper - 26.0, upper - 54.0, 0.022, 0.003, curl=-0.055) if tail: _tapered_arc(d, size, 0.0, 0.0, R, R * 1.14, lower, lower + 34.0, 0.028, 0.003, curl=0.030) spikes = _down(img, size) return np.clip(_rot(np.maximum(m, spikes), rot_deg, size), 0, 1) def _rot(mask, deg, size): im = Image.fromarray((mask * 255).astype(np.uint8), "L") im = im.rotate(-deg, resample=Image.BICUBIC, center=(size / 2, size / 2)) return np.asarray(im, dtype=np.float32) / 255.0 def soften(mask, px=1.0): im = Image.fromarray((np.clip(mask, 0, 1) * 255).astype(np.uint8), "L") return np.asarray(im.filter(ImageFilter.GaussianBlur(px)), dtype=np.float32) / 255.0