""" docs/make_spec_figures.py — regenerate the illustrations embedded in app_spec.md. These are rendered with the app's OWN engine (engine/mappings.py + renderer.py), so the figures are accurate to what the toy actually draws and reproducible: python docs/make_spec_figures.py Outputs PNGs into docs/figures/. The PAD (valence/arousal/dominance) values for the example sentences are the model-anchored few-shot values from prompts/prompt_core.md (so no model call is needed to regenerate). """ import os, sys import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.patches import Polygon # import the real engine sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from engine.mappings import affect_to_geometry, affect_to_color, hex_of from engine.renderer import geom_to_points, geom_to_points_segmented OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "figures") os.makedirs(OUT, exist_ok=True) INK = "#11131A" PAPER = "#ECE7DB" MIST = "#9AA3B2" GREY = "#8A8F9C" # the app's greyscale-during-play fill ACCENT = "#E8A33D" def _shape(ax, pts, fill, edge, lw=1.6, alpha=0.92): ax.add_patch(Polygon(pts, closed=True, facecolor=fill, edgecolor=edge, lw=lw, alpha=alpha, joinstyle="round")) ax.set_xlim(0.02, 0.98); ax.set_ylim(0.02, 0.98) ax.set_aspect("equal"); ax.axis("off") def fig_sentence_to_shape(): """Hero: four beats -> their shapes, greyscale (during play) then revealed.""" examples = [ ('"The blade flashed once\nin the dark."', 0.12, 0.88, 0.80), ('"Snow covered everything,\nsoft and still."', 0.82, 0.15, 0.40), ('"She waited by the phone\nthat never rang."', 0.25, 0.18, 0.30), ('"Fireworks burst over\nthe laughing crowd."', 0.85, 0.82, 0.62), ] fig, axes = plt.subplots(2, len(examples), figsize=(len(examples) * 2.5, 5.4)) fig.patch.set_facecolor(INK) for col, (sent, v, a, d) in enumerate(examples): g = affect_to_geometry(v, a, d) pts = geom_to_points(g, seed_key=sent) col_hex = hex_of(affect_to_color(v, a, d)) # row 0 — during play (greyscale, colour hidden) _shape(axes[0, col], pts, GREY, "#C9CDD6") axes[0, col].set_title(sent, color=PAPER, fontsize=10, pad=8) # row 1 — revealed (affect-driven colour) _shape(axes[1, col], pts, col_hex, "#FFFFFF") axes[1, col].text(0.5, -0.04, f"V {v:.2f} A {a:.2f} D {d:.2f}", color=MIST, fontsize=8, ha="center", va="top", family="monospace", transform=axes[1, col].transAxes) axes[0, 0].text(-0.12, 0.5, "during play", rotation=90, va="center", ha="center", color=MIST, fontsize=10, family="monospace", transform=axes[0, 0].transAxes) axes[1, 0].text(-0.12, 0.5, "revealed", rotation=90, va="center", ha="center", color=ACCENT, fontsize=10, family="monospace", transform=axes[1, 0].transAxes) fig.suptitle("Sentence → essence → shape (colour hidden until the reveal)", color=PAPER, fontsize=13, y=0.99) fig.tight_layout(rect=(0.02, 0, 1, 0.96)) _save(fig, "sentence_to_shape.png") def fig_color_corners(): """The four emotion-corner hue anchors, each as a shape in its colour.""" corners = [ ("calm · pleasant\n(low A, high V)", 0.85, 0.12, 0.45), # green ("joyful\n(high A, high V)", 0.88, 0.85, 0.60), # yellow-orange ("angry\n(high A, low V)", 0.12, 0.88, 0.80), # red ("sad\n(low A, low V)", 0.20, 0.16, 0.30), # blue ] fig, axes = plt.subplots(1, 4, figsize=(10, 3.0)) fig.patch.set_facecolor(INK) for ax, (label, v, a, d) in zip(axes, corners): g = affect_to_geometry(v, a, d) pts = geom_to_points(g, seed_key=label) _shape(ax, pts, hex_of(affect_to_color(v, a, d)), "#FFFFFF") ax.set_title(label, color=PAPER, fontsize=10, pad=8) fig.suptitle("Affect → colour: four hue anchors, interpolated circularly", color=PAPER, fontsize=13, y=1.02) fig.tight_layout() _save(fig, "color_corners.png") def fig_segmented(): """Per-segment essence: 'calm at first, then suddenly sharp' in one form.""" calm = affect_to_geometry(0.75, 0.15, 0.45) sharp = affect_to_geometry(0.15, 0.90, 0.75) pts = geom_to_points_segmented([calm, sharp], [1.0, 1.0], seed_key="calm-then-sharp") fig, ax = plt.subplots(figsize=(4.4, 4.4)) fig.patch.set_facecolor(INK) _shape(ax, pts, "#7F77DD", "#FFFFFF") ax.set_title('"Calm at first,\nthen suddenly sharp"', color=PAPER, fontsize=12, pad=10) fig.tight_layout() _save(fig, "segmented_essence.png") def fig_geometry_axes(): """Each scored geometric axis swept 0.0 -> 1.0 (one axis per row).""" baseline = {"spikiness": 0.15, "compactness": 0.7, "segmentability": 0.1, "symmetry": 0.9, "scale": 1.0} rows = ["spikiness", "compactness", "segmentability"] steps = np.round(np.arange(0.0, 1.01, 0.2), 1) fig, axes = plt.subplots(len(rows), len(steps), figsize=(len(steps) * 1.5, len(rows) * 1.6)) fig.patch.set_facecolor(INK) for r, axis in enumerate(rows): for c, val in enumerate(steps): g = dict(baseline); g[axis] = float(val) pts = geom_to_points(g, seed_key=f"{axis}{val}") _shape(axes[r, c], pts, "#7F77DD", "#26215C", lw=1.2) if r == 0: axes[r, c].set_title(f"{val:.1f}", color=MIST, fontsize=9) if c == 0: axes[r, c].text(-0.2, 0.5, axis, rotation=90, va="center", ha="center", color=PAPER, fontsize=10, weight="bold", transform=axes[r, c].transAxes) fig.suptitle("Scored geometric axes (Huang 2020) — each row varies ONE axis 0→1", color=PAPER, fontsize=12, y=1.0) fig.tight_layout(rect=(0.02, 0, 1, 0.95)) _save(fig, "geometry_axes.png") def _save(fig, name): path = os.path.join(OUT, name) fig.savefig(path, dpi=120, facecolor=fig.get_facecolor(), bbox_inches="tight") plt.close(fig) print("saved", os.path.relpath(path)) if __name__ == "__main__": fig_sentence_to_shape() fig_color_corners() fig_segmented() fig_geometry_axes() print("done")