commit d85fb2e6385971fa3e597087f3e105d19b1000d5 Author: The Dust Council Date: Mon Jun 1 21:59:36 2026 -0700 Excellent state. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..55cb29c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,29 @@ +{ + "permissions": { + "allow": [ + "Bash(pkg-config --list-all)", + "Read(//usr/include/GLFW/**)", + "Bash(ldconfig -p)", + "Bash(make *)", + "Bash(echo \"exit: $?\")", + "Bash(timeout 2 ./vectorgons)", + "Read(//tmp/**)", + "Bash(glxinfo)", + "Bash(grep -iE '^\\(scrot|grim|import|maim|spectacle\\)$')", + "Bash(convert vg_shot.png -crop 270x210+0+0 +repage -resize 300% vg_crop.png)", + "Bash(identify -format \"%wx%h\\\\n\" vg_multi.png)", + "Bash(convert vg_multi.png -crop 760x600+460+0 +repage vg_multi_solids.png)", + "Bash(convert vg2.png -crop 740x620+0+0 +repage vg2_crop.png)", + "Bash(convert vg_ast.png -resize 1000x vg_ast_full.png)", + "Bash(xwininfo -name Vectorgons -int)", + "Bash(timeout 3 ./vectorgons)", + "Bash(timeout 4 ./vectorgons)", + "Bash(cat)", + "Bash(cc -O2 -o /tmp/sim /tmp/sim.c -lm)", + "Bash(/tmp/sim)", + "Bash(cc -O2 -o /tmp/st2 /tmp/st2.c -lm)", + "Bash(/tmp/st2)", + "Bash(rm -f /tmp/st2 /tmp/st2.c)" + ] + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..669a6be --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +CC ?= gcc +CFLAGS ?= -O2 -Wall -Wextra -std=c11 +PKGS = glfw3 glu +CPPFLAGS += $(shell pkg-config --cflags $(PKGS)) +LDLIBS += $(shell pkg-config --libs $(PKGS)) -lGL -lm + +TARGET = vectorgons +SRC = vectorgons.c + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(SRC) $(LDLIBS) + +run: $(TARGET) + ./$(TARGET) + +clean: + rm -f $(TARGET) + +.PHONY: run clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..28c6f59 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Vectorgons + +A starfield simulator — except instead of stars, colorful **vector-drawn +platonic solids** tumble through space toward the camera. + +**~110 shape types** are rendered as glowing wireframes — each spawns far away +at a random size, tumbles on its own random axis, and streams past the camera +before being recycled at the render-distance sphere. Solids never overlap. + +The shape set includes: + +- **Platonic solids:** tetrahedron, cube, octahedron, dodecahedron, icosahedron +- **Other polyhedra:** cuboctahedron, truncated octahedron, stella octangula +- **Prisms / antiprisms / bipyramids** (various base polygons) +- **Many-faced solids named for their face count:** tridecahedron (13), + tetradecahedron (14), pentadecahedron (15), heptadecahedron (17), + octadecahedron (18), enneadecahedron (19), icosahedron (20), + icositetrahedron (24), triacontahedron (30), hexacontahedron (60), and + hecatohedron (100) — built from prism / bipyramid / trapezohedron families so + each lands on exactly the requested number of faces +- **Star polygons:** {5/2}, {6/2}, {7/2}, {7/3}, {8/3}, {9/2}, {9/4}, {12/5}, + plus the **unicursal hexagram** +- **Googie / atomic-age shapes**, most in both flat **2-D** and **3-D** forms: + jet-age bowling-alley twinkles (4- and 8-point), a layered double starburst, + an atomic burst with electron caps, a ray sunburst, boomerangs, kidney/amoeba + blobs, orbital atoms, concentric orbit rings, and a dense ray-star — plus the + naturally-3-D **Sputnik** satellite, a three-ring **gyroscope** cage, and a + spike-orb sea-urchin +- **Symbols & signs**, each in a flat **2-D** and an extruded **3-D** form: + smiley face, biohazard, peace sign, cross, question mark, exclamation point, + hash/pound (`#`), dollar sign (`$`), and pound sterling (`£`) +- **Random 3-D asteroids:** lumpy convex-hull rocks generated fresh each run, + with random vertex counts (≈7–26) giving each a different number of sides and + level of complexity +- **4-, 5- and 6-dimensional polytopes:** the 5-cell, **tesseract (hypercube)**, + penteract (5-cube), 6-cube, 16-cell, 5- and 6-orthoplexes, the 24-cell, and + the 5-simplex. These are rotated *in their own dimension* and + perspective-projected down to 3D every frame, so they morph (the classic + "cube-within-a-cube" unfolding) while also tumbling in 3D. + +## Build & run + +Requires a C compiler, GLFW3, and GLU (all detected via `pkg-config`). + +```sh +make +./vectorgons # or: make run +``` + +## Settings persistence + +Your settings are saved automatically on exit to `~/.vectorgons` (a plain +`key=value` text file) and reloaded on the next launch as the new defaults — so +the speed, tumble, render distance, density, colors, glow, fullscreen, etc. you +last used carry over. Delete the file to return to the built-in defaults; it's +safe to hand-edit (values are range-clamped on load). + +## On-screen display + +A vector-drawn OSD (top-left) lists every setting and its key binding. It +stays fully visible for **10 seconds** after your last keypress, then fades +out over a few seconds. Press any key to bring it back. + +## Controls + +| Setting | Keys | Notes | +|---------------------|-----------------|------------------------------------------| +| Move camera | `W`/`A`/`S`/`D` | Pan the camera up / left / down / right (hold to fly) | +| Rotate camera | `←`/`→`/`↑`/`↓` | Yaw / pitch the camera view (hold to turn) | +| Approach speed | `PgUp`/`PgDn` | How fast solids fly at you; `PgDn` past 0 reverses (fly backward) | +| Tumble rate | `Q` / `E` | Base rotation speed | +| Tumble variance | `T` / `Y` | Spread of tumble speeds: low = uniform, high = a very wide range (responds live) | +| Render distance | `Z` / `X` | Radius of the field sphere / far plane (40–1520; hold to ramp) | +| Density | `+` / `-` | Baseline number of solids (scales up with render distance; readout shows the live count) | +| Size min | `U` / `J` | Minimum random solid size | +| Size max | `I` / `K` | Maximum random solid size | +| Hue | `[` / `]` | Base color | +| Hue cycle | `C` / `V` | Continuously cycle through all hues (0 = off); all objects sweep the spectrum | +| Color mode | `M` | Toggle **single-hue** ⇄ **multicolor** | +| Glow | `O` / `L` | CRT glow / light bleed around vectors | +| Flicker | `G` / `H` | Vector flicker intensity | +| Shapes | `N` | Toggle random ⇄ cycling shape spawns | +| Fullscreen | `F` or `F11` | Toggle fullscreen | +| Pause | `Space` | | +| Quit | `Esc` | | + +## Color modes + +- **Single hue:** every solid uses the hue set with `[` / `]`. +- **Multicolor:** each solid gets its own hue (its base offset plus the + global hue, so `[` / `]` rotates the whole palette). +- **Hue cycle (`C` / `V`):** continuously advances the base hue over time (up + to ~120°/sec). In single-hue mode every object sweeps through the spectrum + together; in multicolor mode the whole palette rotates. Set to `0` to stop. + +## How it works + +- **Geometry** comes from an N-dimensional polytope engine (3–6 dimensions). + For regular figures, edges are derived automatically by connecting every + vertex pair at the shared minimum distance; parametric families (prisms, + stars, ...) set edges explicitly. +- **Higher-dimensional shapes** keep their full 4/5/6-D coordinates and are + rotated in their own dimension (driven by the tumble angle plus a per-body + phase), then perspective-projected down to 3D each frame and renormalized to + the unit sphere — which also keeps the no-overlap bound intact. +- **Asteroids** scatter random points over a sphere at jittered radii and wire + them up as a brute-force convex hull, so each rock is a unique faceted blob. +- **The field is a sphere** of radius *render distance* centered on the camera. + Bodies stream along −Z (or +Z when speed is negative) and, once they leave the + sphere, are recycled back to the **render-distance shell** on the incoming + side — so objects always appear far away and approach, never popping in close. + They ease in over a fraction of a second at the shell. The initial fill spreads + bodies through the volume so the field starts populated. Because culling is + purely radial, the camera can yaw/pitch a full 360° without revealing an edge. +- **Render distance** (`Z`/`X`, 40–1520) sets the sphere radius and the + perspective far plane. The **active body count scales with it**, so a deeper + field simply holds proportionally more shapes (constant near-field density) + instead of thinning out to empty space — the `DENSITY` readout shows the live + count, and `+`/`-` sets the baseline (the count at the default distance). +- **The camera** pans in world space via `WASD` and yaws/pitches via the arrow + keys (both polled per frame so movement is smooth and frame-rate + independent); the rest of the field streams past it. +- **No overlap:** each solid's bounding-sphere radius equals its size, and + spawn positions are rejection-sampled so no two spheres intersect (with a + margin). Because every solid translates by the same amount each frame, + non-overlap at spawn is preserved for the body's whole flight. +- **Tumble variance** is applied live as a log-uniform spread + (`spin = 4^(seed · variance)`), so changing it re-spreads every solid's + rotation rate instantly — `0` makes them all tumble alike, `100` gives a + very wide range. +- **CRT glow** wraps each sharp vector in a soft phosphor mist: many faint + additive width layers fade outward into a halo, and a few faint enlarged + ghost copies bloom that glow into a larger volume than the hardware line-width + cap (~10px) could reach on its own. The crisp core is drawn last so the vector + stays sharp; the glow setting scales the mist's spread and brightness. +- **Flicker** randomly dips each solid's brightness per frame, scaled by the + flicker setting. +- **Rendering** uses legacy OpenGL immediate mode (`GL_LINES`) with additive + blending, MSAA, and line smoothing; depth-based fading brightens and + thickens solids as they approach. The OSD uses a self-contained vector + stroke font (no font dependencies). diff --git a/vectorgons b/vectorgons new file mode 100755 index 0000000..15383f5 Binary files /dev/null and b/vectorgons differ diff --git a/vectorgons.c b/vectorgons.c new file mode 100644 index 0000000..11fa30a --- /dev/null +++ b/vectorgons.c @@ -0,0 +1,1607 @@ +/* + * Vectorgons — a starfield simulator of colorful vector-drawn platonic + * solids tumbling through space toward the camera. + * + * C + OpenGL (legacy immediate mode for true wireframe "vector" rendering) + * Windowing & input via GLFW3, perspective via GLU. + * + * Features: + * - 100+ shape types streaming at the camera: the 5 platonic solids, + * archimedeans, prisms, antiprisms, bipyramids, trapezohedra, the + * many-faced solids (tri/tetra/.../hecato-hedron), star polygons and the + * unicursal hexagram, googie / atomic-age starbursts, 2-D and 3-D symbols + * (smiley, biohazard, peace, cross, ? ! # $ £), 4/5/6-D polytopes + * (tesseract, penteract, 24-cell, ...) that morph as they tumble, plus + * randomly generated 3-D convex-hull asteroids + * - WASD pans the camera and the arrow keys rotate it + * - User-settable render distance (how far away solids spawn) + * - Random per-body sizes (user-settable min / max) + * - Guaranteed no overlap between solids (bounding-sphere rejection) + * - Adjustable tumble rate AND tumble-speed variance + * - Adjustable CRT-style glow / light bleed + * - Adjustable vector flicker + * - On-screen display (self-contained vector font) that fades after idle + * - Single-hue and multicolor modes; adjustable hue + continuous hue cycling + * - Fullscreen toggle + * - Settings auto-saved to ~/.vectorgons on exit, reloaded as defaults + * + * All controls are listed in the on-screen display (and printed at startup). + */ + +#define GL_SILENCE_DEPRECATION +#include +#include + +#include +#include +#include +#include +#include +#include + +/* ================================================================== */ +/* Geometry: an N-dimensional polytope engine (3..6 dimensions). */ +/* */ +/* Every shape stores vertices in up to MAXD dimensions plus an edge */ +/* list. 3-D shapes (platonic solids, archimedeans, prisms, stars) */ +/* are drawn directly; 4/5/6-D polytopes (tesseract, 24-cell, ...) */ +/* are rotated in their own dimension and perspective-projected down */ +/* to 3-D every frame, so they morph like true hyperobjects while */ +/* also tumbling in 3-D. */ +/* */ +/* For regular figures, edges are derived automatically: connect */ +/* every vertex pair at the shared minimum distance. Families with */ +/* non-uniform edges (prisms, stars, ...) set edges explicitly. */ +/* ================================================================== */ + +#define MAXD 6 +#define MAX_VERTS 128 +#define MAX_EDGES 512 +#define MAX_SHAPES 160 +#define TAU 6.283185307179586 + +typedef struct { + int dim; /* 3..6 */ + int nv; + float v[MAX_VERTS][MAXD]; + int ne; + int e[MAX_EDGES][2]; +} Solid; + +static Solid solids[MAX_SHAPES]; +static int num_shapes = 0; + +static float frand(void); /* uniform [0,1); defined below */ + +static Solid *new_solid(int dim) { + Solid *s = &solids[num_shapes++]; + s->dim = dim; s->nv = 0; s->ne = 0; + return s; +} +static void PV(Solid *s, double a,double b,double c,double d,double e,double f) { + if (s->nv >= MAX_VERTS) return; + float *p = s->v[s->nv++]; + p[0]=(float)a; p[1]=(float)b; p[2]=(float)c; p[3]=(float)d; p[4]=(float)e; p[5]=(float)f; +} +static void AE(Solid *s, int i, int j) { + if (s->ne >= MAX_EDGES) return; + s->e[s->ne][0]=i; s->e[s->ne][1]=j; s->ne++; +} +static void AE_unique(Solid *s, int i, int j) { + if (i > j) { int t = i; i = j; j = t; } + for (int k = 0; k < s->ne; k++) + if (s->e[k][0] == i && s->e[k][1] == j) return; + AE(s, i, j); +} + +/* Center on the centroid and scale so the farthest vertex sits on the + * unit sphere (radius == 1, in `dim` dimensions). */ +static void center_normalize(Solid *s) { + double cen[MAXD] = {0,0,0,0,0,0}; + for (int i = 0; i < s->nv; i++) + for (int k = 0; k < s->dim; k++) cen[k] += s->v[i][k]; + for (int k = 0; k < s->dim; k++) cen[k] /= (s->nv > 0 ? s->nv : 1); + + double maxr2 = 0; + for (int i = 0; i < s->nv; i++) { + double r2 = 0; + for (int k = 0; k < s->dim; k++) { s->v[i][k] -= cen[k]; r2 += (double)s->v[i][k]*s->v[i][k]; } + if (r2 > maxr2) maxr2 = r2; + } + double inv = maxr2 > 1e-12 ? 1.0/sqrt(maxr2) : 1.0; + for (int i = 0; i < s->nv; i++) + for (int k = 0; k < s->dim; k++) s->v[i][k] *= inv; +} + +/* Derive edges from the minimum pairwise distance (regular figures). */ +static void derive_edges(Solid *s) { + s->ne = 0; + double mind2 = 1e18; + for (int i = 0; i < s->nv; i++) + for (int j = i+1; j < s->nv; j++) { + double d2 = 0; + for (int k = 0; k < s->dim; k++) { double dd = s->v[i][k]-s->v[j][k]; d2 += dd*dd; } + if (d2 < mind2) mind2 = d2; + } + double tol2 = mind2 * 1.04; /* ~2% slack on length */ + for (int i = 0; i < s->nv; i++) + for (int j = i+1; j < s->nv; j++) { + double d2 = 0; + for (int k = 0; k < s->dim; k++) { double dd = s->v[i][k]-s->v[j][k]; d2 += dd*dd; } + if (d2 <= tol2) AE(s, i, j); + } +} + +/* ---- generic N-dimensional regular polytopes ---------------------- */ + +static void gen_hypercube(int d) { /* {4,3..} measure polytope */ + Solid *s = new_solid(d); + int N = 1 << d; + for (int m = 0; m < N; m++) { + double c[MAXD] = {0,0,0,0,0,0}; + for (int k = 0; k < d; k++) c[k] = (m >> k & 1) ? 1 : -1; + PV(s, c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); +} +static void gen_orthoplex(int d) { /* cross-polytope */ + Solid *s = new_solid(d); + for (int k = 0; k < d; k++) { + double c[MAXD] = {0,0,0,0,0,0}; + c[k] = 1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + c[k] = -1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); +} +static void gen_simplex(int nv) { /* regular (nv-1)-simplex */ + Solid *s = new_solid(nv); /* basis e_i in R^nv */ + for (int i = 0; i < nv; i++) { + double c[MAXD] = {0,0,0,0,0,0}; + c[i] = 1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); /* -> complete graph */ +} +static void gen_24cell(void) { /* perms of (+-1,+-1,0,0) */ + Solid *s = new_solid(4); + for (int a = 0; a < 4; a++) + for (int b = a+1; b < 4; b++) + for (int sa = -1; sa <= 1; sa += 2) + for (int sb = -1; sb <= 1; sb += 2) { + double c[MAXD] = {0,0,0,0,0,0}; + c[a]=sa; c[b]=sb; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); +} + +/* ---- curated 3-D solids ------------------------------------------- */ + +static void gen_cuboctahedron(void) { /* perms of (+-1,+-1,0) */ + Solid *s = new_solid(3); + for (int a = 0; a < 3; a++) + for (int b = a+1; b < 3; b++) + for (int sa = -1; sa <= 1; sa += 2) + for (int sb = -1; sb <= 1; sb += 2) { + double c[3] = {0,0,0}; + c[a]=sa; c[b]=sb; PV(s,c[0],c[1],c[2],0,0,0); + } + center_normalize(s); derive_edges(s); +} +static void gen_truncated_octahedron(void) { /* perms of (0,+-1,+-2) */ + Solid *s = new_solid(3); + int perm[6][3] = {{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}}; + for (int p = 0; p < 6; p++) + for (int s1 = -1; s1 <= 1; s1 += 2) + for (int s2 = -1; s2 <= 1; s2 += 2) { + double c[3] = {0,0,0}; + c[perm[p][1]] = s1 * 1.0; /* axis carrying value 1 */ + c[perm[p][2]] = s2 * 2.0; /* axis carrying value 2 */ + PV(s, c[0],c[1],c[2],0,0,0); + } + center_normalize(s); derive_edges(s); +} +static void gen_stella_octangula(void) { /* two interlocked tetrahedra */ + Solid *s = new_solid(3); + for (int m = 0; m < 8; m++) + PV(s, (m&1)?1:-1, (m&2)?1:-1, (m&4)?1:-1, 0,0,0); + center_normalize(s); + /* connect vertices whose minus-bit parity matches (each tetra) */ + for (int i = 0; i < 8; i++) + for (int j = i+1; j < 8; j++) { + int pi = __builtin_popcount(i & 7) & 1; + int pj = __builtin_popcount(j & 7) & 1; + if (pi == pj) AE(s, i, j); + } +} + +/* ---- parametric 3-D families -------------------------------------- */ + +static void gen_prism(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 1, 0,0,0); } + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), -1, 0,0,0); } + for (int i = 0; i < n; i++) { + AE(s, i, (i+1)%n); + AE(s, n+i, n+(i+1)%n); + AE(s, i, n+i); + } + center_normalize(s); +} +static void gen_antiprism(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 1, 0,0,0); } + for (int i = 0; i < n; i++) { double a = TAU*(i+0.5)/n; PV(s, cos(a), sin(a), -1, 0,0,0); } + for (int i = 0; i < n; i++) { + AE(s, i, (i+1)%n); + AE(s, n+i, n+(i+1)%n); + AE(s, i, n+i); + AE(s, i, n+(i+n-1)%n); + } + center_normalize(s); +} +static void gen_bipyramid(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0, 0,0,0); } + PV(s, 0,0, 1.25, 0,0,0); /* apex top = index n */ + PV(s, 0,0,-1.25, 0,0,0); /* apex bottom = index n+1 */ + for (int i = 0; i < n; i++) { + AE(s, i, (i+1)%n); + AE(s, n, i); + AE(s, n+1, i); + } + center_normalize(s); +} +static void gen_star(int n, int k) { /* star polygon {n/k} */ + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0, 0,0,0); } + for (int i = 0; i < n; i++) AE(s, i, (i+k)%n); + center_normalize(s); +} +/* n-gonal trapezohedron (dual of an antiprism): two staggered rings wired + * in a zig-zag equator and capped by two apexes -> 2n kite faces. Used for + * the larger many-faced solids (24-, 30-, 60-hedra). */ +static void gen_trapezohedron(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0.45, 0,0,0); } + for (int i = 0; i < n; i++) { double a = TAU*(i+0.5)/n; PV(s, cos(a), sin(a), -0.45, 0,0,0); } + int top = 2*n, bot = 2*n + 1; + PV(s, 0,0, 1.5, 0,0,0); /* top apex = 2n */ + PV(s, 0,0, -1.5, 0,0,0); /* bottom apex = 2n+1 */ + for (int i = 0; i < n; i++) { + AE(s, i, n + i); /* upper ring -> staggered lower ring */ + AE(s, n + i, (i + 1) % n); /* lower ring -> next upper-ring vertex */ + AE(s, top, i); /* top apex -> upper ring */ + AE(s, bot, n + i); /* bottom apex-> lower ring */ + } + center_normalize(s); +} +/* Unicursal hexagram: the six-pointed star drawn as a single closed stroke, + * the way the occult symbol actually is. Unlike the Star of David ({6/2}, two + * separate triangles), a regular hexagram cannot be traced in one pen-stroke, + * so this visits the six tips in the order 90 -> 210 -> 330 -> 270 -> 30 -> 150 + * degrees: the long 120-degree chords cross through the interior and weave the + * star, leaving the figure 2-fold symmetric like the genuine emblem. */ +static void gen_unicursal_hexagram(void) { + Solid *s = new_solid(3); + const int order[6] = { 90, 210, 330, 270, 30, 150 }; /* tip angles, path order */ + for (int i = 0; i < 6; i++) { + double a = order[i] * TAU / 360.0; + PV(s, cos(a), sin(a), 0, 0,0,0); + } + for (int i = 0; i < 6; i++) AE(s, i, (i + 1) % 6); /* one closed loop */ + center_normalize(s); +} + +/* ================================================================== */ +/* Symbols & signs: a small 2-D stroke builder that finalizes either */ +/* flat (depth 0) or extruded into a 3-D "wireframe prism" (depth > 0): */ +/* a front layer at +z, a back layer at -z, and rungs joining them. */ +/* ================================================================== */ + +typedef struct { + float px[MAX_VERTS], py[MAX_VERTS]; + int ea[MAX_EDGES], eb[MAX_EDGES]; + int np, ne; +} SymB; + +static void sym_init(SymB *b) { b->np = 0; b->ne = 0; } +static int sym_pt(SymB *b, double x, double y) { + if (b->np >= MAX_VERTS) return b->np - 1; + b->px[b->np] = (float)x; b->py[b->np] = (float)y; return b->np++; +} +static void sym_edge(SymB *b, int i, int j) { + if (b->ne >= MAX_EDGES || i < 0 || j < 0 || i == j) return; + b->ea[b->ne] = i; b->eb[b->ne] = j; b->ne++; +} +static void sym_line(SymB *b, double x0,double y0,double x1,double y1) { + sym_edge(b, sym_pt(b,x0,y0), sym_pt(b,x1,y1)); +} +static void sym_ring(SymB *b, double cx,double cy,double r,int seg) { /* closed circle */ + int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU * i / seg; + int idx = sym_pt(b, cx + r*cos(a), cy + r*sin(a)); + if (first < 0) first = idx; + sym_edge(b, prev, idx); + prev = idx; + } + sym_edge(b, prev, first); +} +static void sym_arc(SymB *b, double cx,double cy,double r,double a0,double a1,int seg) { + int prev = -1; /* open arc */ + for (int i = 0; i <= seg; i++) { + double a = a0 + (a1 - a0) * i / seg; + int idx = sym_pt(b, cx + r*cos(a), cy + r*sin(a)); + sym_edge(b, prev, idx); + prev = idx; + } +} +static void sym_poly(SymB *b, const double *xy, int n, int closed) { + int first = -1, prev = -1; + for (int i = 0; i < n; i++) { + int idx = sym_pt(b, xy[2*i], xy[2*i+1]); + if (first < 0) first = idx; + sym_edge(b, prev, idx); + prev = idx; + } + if (closed) sym_edge(b, prev, first); +} +static double sym_deg(double d) { return d * TAU / 360.0; } + +/* Turn the accumulated strokes into a Solid: flat if depth<=0, else extruded. */ +static void sym_finish(SymB *b, float depth) { + Solid *s = new_solid(3); + if (depth <= 0.0f) { + for (int k = 0; k < b->np; k++) PV(s, b->px[k], b->py[k], 0, 0,0,0); + for (int k = 0; k < b->ne; k++) AE(s, b->ea[k], b->eb[k]); + } else { + int n = b->np; + for (int k = 0; k < n; k++) PV(s, b->px[k], b->py[k], depth, 0,0,0); /* front */ + for (int k = 0; k < n; k++) PV(s, b->px[k], b->py[k], -depth, 0,0,0); /* back */ + for (int k = 0; k < b->ne; k++) { + AE(s, b->ea[k], b->eb[k]); + AE(s, n + b->ea[k], n + b->eb[k]); + } + for (int k = 0; k < n; k++) AE(s, k, n + k); /* rungs */ + } + center_normalize(s); +} + +/* ---- individual symbol strokes (drawn roughly within [-1,1]) ------- */ + +static void build_smiley(SymB *b) { + sym_ring(b, 0.0, 0.0, 1.0, 18); /* face */ + sym_ring(b, -0.35, 0.32, 0.13, 6); /* l eye */ + sym_ring(b, 0.35, 0.32, 0.13, 6); /* r eye */ + sym_arc (b, 0.0, 0.10, 0.55, sym_deg(200), sym_deg(340), 8); /* smile */ +} +static void build_biohazard(SymB *b) { + sym_ring(b, 0.0, 0.0, 0.24, 8); /* central circle */ + for (int k = 0; k < 3; k++) { /* three broken rings, gaps facing center */ + double th = sym_deg(90 + 120*k); + double cx = 0.58*cos(th), cy = 0.58*sin(th); + double g = sym_deg(90 + 120*k + 180); /* gap direction (toward center) */ + sym_arc(b, cx, cy, 0.44, g + sym_deg(38), g + sym_deg(322), 9); + } +} +static void build_peace(SymB *b) { + sym_ring(b, 0.0, 0.0, 1.0, 18); + int c = sym_pt(b, 0.0, 0.0); + const double ang[4] = {90, 270, 225, 315}; /* up, down, lower-left, lower-right */ + for (int i = 0; i < 4; i++) { + double a = sym_deg(ang[i]); + sym_edge(b, c, sym_pt(b, cos(a), sin(a))); + } +} +static void build_cross(SymB *b) { /* Latin cross outline */ + const double p[] = { + -0.22, 1.00, 0.22, 1.00, 0.22, 0.45, 0.60, 0.45, + 0.60, 0.00, 0.22, 0.00, 0.22,-1.00, -0.22,-1.00, + -0.22, 0.00, -0.60, 0.00, -0.60, 0.45, -0.22, 0.45 }; + sym_poly(b, p, 12, 1); +} +static void build_question(SymB *b) { + sym_arc(b, 0.0, 0.42, 0.42, sym_deg(200), sym_deg(-30), 10); /* top hook */ + int last = b->np - 1; + int t1 = sym_pt(b, 0.0, -0.05); sym_edge(b, last, t1); /* tail curls in */ + int t2 = sym_pt(b, 0.0, -0.35); sym_edge(b, t1, t2); + sym_ring(b, 0.0, -0.72, 0.12, 6); /* dot */ +} +static void build_exclaim(SymB *b) { + const double bar[] = { -0.12, 1.0, 0.12, 1.0, 0.06, -0.20, -0.06, -0.20 }; + sym_poly(b, bar, 4, 1); /* tapered stroke */ + sym_ring(b, 0.0, -0.62, 0.13, 6); /* dot */ +} +static void build_hash(SymB *b) { /* # — two verticals, two horizontals */ + sym_line(b, -0.32, 0.85, -0.20, -0.85); + sym_line(b, 0.20, 0.85, 0.32, -0.85); + sym_line(b, -0.85, 0.32, 0.85, 0.32); + sym_line(b, -0.85, -0.32, 0.85, -0.32); +} +static void build_dollar(SymB *b) { /* $ — S-curve with a vertical bar through it */ + const double S[] = { + 0.80, 0.64, 0.40, 0.96, -0.40, 0.96, -0.80, 0.64, + -0.40, 0.00, 0.40, 0.00, 0.80,-0.32, 0.40,-0.96, + -0.40,-0.96, -0.80,-0.64 }; + sym_poly(b, S, 10, 0); + sym_line(b, 0.0, 1.18, 0.0, -1.18); +} +static void build_sterling(SymB *b) { /* £ — hooked top, stem, crossbar, base */ + sym_arc(b, 0.05, 0.45, 0.42, sym_deg(8), sym_deg(200), 9); + int last = b->np - 1; + int s1 = sym_pt(b, -0.33, -0.58); sym_edge(b, last, s1); /* stem down */ + int base0 = sym_pt(b, -0.52, -0.62); + int base1 = sym_pt(b, 0.52, -0.62); + sym_edge(b, s1, base0); sym_edge(b, base0, base1); /* base */ + sym_line(b, -0.33, -0.05, 0.42, -0.05); /* crossbar */ +} + +/* ---- googie / atomic-age starbursts -------------------------------- */ + +/* Flat multi-point sparkle (the bowling-alley twinkle): sharp tips at radius + * r1 (or alternating r1/r2) joined through deep notches at radius `inner`. */ +static void build_sparkle(SymB *b, int tips, double r1, double r2, double inner) { + int first = -1, prev = -1; + for (int i = 0; i < tips; i++) { + double ta = TAU*0.25 + TAU*i/tips; /* first tip points up */ + double na = ta + TAU/(2*tips); + double tr = (r2 > 0 && (i & 1)) ? r2 : r1; + int t = sym_pt(b, tr*cos(ta), tr*sin(ta)); + if (first < 0) first = t; + sym_edge(b, prev, t); prev = t; + int n = sym_pt(b, inner*cos(na), inner*sin(na)); + sym_edge(b, prev, n); prev = n; + } + sym_edge(b, prev, first); +} +/* Flat atomic burst: spokes of alternating length from a hub, with little + * "electron" rings capping the long ones. */ +static void build_atomic(SymB *b) { + int c = sym_pt(b, 0.0, 0.0); + int n = 12; + for (int i = 0; i < n; i++) { + double a = TAU*i/n; + double r = (i & 1) ? 0.55 : 1.0; + sym_edge(b, c, sym_pt(b, r*cos(a), r*sin(a))); + if (!(i & 1)) sym_ring(b, cos(a), sin(a), 0.08, 5); + } +} +/* Flat sunburst: a dense fan of alternating-length rays from a hub. */ +static void build_sunburst(SymB *b) { + int c = sym_pt(b, 0.0, 0.0); + int n = 20; + for (int i = 0; i < n; i++) { + double a = TAU*i/n; + double r = (i & 1) ? 0.78 : 1.0; + sym_edge(b, c, sym_pt(b, r*cos(a), r*sin(a))); + } +} +/* True 3-D Sputnik: rods to the 12 icosahedral directions, each tipped with a + * small ring "ball" lying perpendicular to the rod. */ +static void gen_sputnik(void) { + Solid *s = new_solid(3); + const double P = 1.6180339887; + const double dir[12][3] = { + {0,1,P},{0,1,-P},{0,-1,P},{0,-1,-P},{1,P,0},{1,-P,0}, + {-1,P,0},{-1,-P,0},{P,0,1},{P,0,-1},{-P,0,1},{-P,0,-1} }; + int c = s->nv; PV(s, 0,0,0, 0,0,0); + for (int i = 0; i < 12; i++) { + double L = sqrt(dir[i][0]*dir[i][0] + dir[i][1]*dir[i][1] + dir[i][2]*dir[i][2]); + double ux = dir[i][0]/L, uy = dir[i][1]/L, uz = dir[i][2]/L; + int tip = s->nv; PV(s, ux, uy, uz, 0,0,0); + AE(s, c, tip); + double rx = uy, ry = -ux, rz = 0, rl = sqrt(rx*rx + ry*ry + rz*rz); + if (rl < 1e-6) { rx = 1; ry = 0; rz = 0; rl = 1; } + rx /= rl; ry /= rl; rz /= rl; + double sx = uy*rz - uz*ry, sy = uz*rx - ux*rz, sz = ux*ry - uy*rx; /* u x r */ + int ring0 = -1, prev = -1; double rr = 0.14; + for (int k = 0; k < 5; k++) { + double a = TAU*k/5; + double bx = ux + rr*(cos(a)*rx + sin(a)*sx); + double by = uy + rr*(cos(a)*ry + sin(a)*sy); + double bz = uz + rr*(cos(a)*rz + sin(a)*sz); + int idx = s->nv; PV(s, bx, by, bz, 0,0,0); + if (ring0 < 0) ring0 = idx; + if (prev >= 0) AE(s, prev, idx); + prev = idx; + } + AE(s, prev, ring0); + } + center_normalize(s); +} + +/* ---- more googie shapes: flat builders + naturally-3-D generators --- */ + +static void build_sparkle4(SymB *b) { build_sparkle(b, 4, 1.0, 0.0, 0.13); } +static void build_sparkle8(SymB *b) { build_sparkle(b, 8, 1.0, 0.62, 0.12); } + +static void sym_ellipse(SymB *b, double cx,double cy,double ra,double rb,double rot,int seg) { + int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg, ex = ra*cos(a), ey = rb*sin(a); + int idx = sym_pt(b, cx + ex*cos(rot) - ey*sin(rot), + cy + ex*sin(rot) + ey*cos(rot)); + if (first < 0) first = idx; + sym_edge(b, prev, idx); prev = idx; + } + sym_edge(b, prev, first); +} +static void build_atom2d(SymB *b) { /* flat atom: 3 ellipses + nucleus */ + sym_ring(b, 0, 0, 0.16, 7); + sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(0), 16); + sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(60), 16); + sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(120), 16); +} +static void build_boomerang(SymB *b) { /* crescent / boomerang band */ + double Ro = 1.0, Ri = 0.6, a0 = sym_deg(30), a1 = sym_deg(150); int seg = 8; + int first = -1, prev = -1; + for (int i = 0; i <= seg; i++) { double a = a0 + (a1-a0)*i/seg; + int idx = sym_pt(b, Ro*cos(a), Ro*sin(a)); if (first<0) first=idx; + sym_edge(b, prev, idx); prev = idx; } + for (int i = 0; i <= seg; i++) { double a = a1 + (a0-a1)*i/seg; + int idx = sym_pt(b, Ri*cos(a), Ri*sin(a)); sym_edge(b, prev, idx); prev = idx; } + sym_edge(b, prev, first); +} +static void build_amoeba(SymB *b) { /* googie kidney / amoeba blob */ + int seg = 18, first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg; + double r = 0.72 + 0.26*sin(3*a + 0.6) + 0.12*sin(2*a - 0.4); + int idx = sym_pt(b, r*cos(a), 0.82*r*sin(a)); + if (first < 0) first = idx; + sym_edge(b, prev, idx); prev = idx; + } + sym_edge(b, prev, first); +} +static void build_doublestar(SymB *b) { /* big 4-point twinkle + inner one at 45 */ + build_sparkle(b, 4, 1.0, 0.0, 0.12); + int first = -1, prev = -1, tips = 4; double R = 0.5, inner = 0.07, off = TAU/8; + for (int i = 0; i < tips; i++) { + double ta = TAU*0.25 + off + TAU*i/tips, na = ta + TAU/(2*tips); + int t = sym_pt(b, R*cos(ta), R*sin(ta)); if (first<0) first=t; + sym_edge(b, prev, t); prev = t; + int n = sym_pt(b, inner*cos(na), inner*sin(na)); sym_edge(b, prev, n); prev = n; + } + sym_edge(b, prev, first); +} +static void build_orbit2d(SymB *b) { /* concentric orbit rings + ticks */ + sym_ring(b, 0, 0, 1.00, 18); + sym_ring(b, 0, 0, 0.64, 14); + sym_ring(b, 0, 0, 0.30, 9); + for (int i = 0; i < 4; i++) { double a = sym_deg(45 + 90*i); + sym_line(b, 0.30*cos(a), 0.30*sin(a), cos(a), sin(a)); } +} +static void build_starorb2d(SymB *b) { /* dense ray star with a core ring */ + int c = sym_pt(b, 0, 0), n = 24; + for (int i = 0; i < n; i++) { double a = TAU*i/n; + sym_edge(b, c, sym_pt(b, cos(a), sin(a))); } + sym_ring(b, 0, 0, 0.30, 10); +} + +/* 3-D atom: three circular orbits in tilted planes around an octahedral core. */ +static void gen_atom3d(void) { + Solid *s = new_solid(3); int seg = 20; + const double tilt[3] = {0, 60, 120}; + for (int o = 0; o < 3; o++) { + double t = sym_deg(tilt[o]); int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg, x = cos(a), y = sin(a), z = 0; + double y2 = y*cos(t) - z*sin(t), z2 = y*sin(t) + z*cos(t); + int idx = s->nv; PV(s, x, y2, z2, 0,0,0); + if (first < 0) first = idx; + if (prev >= 0) AE(s, prev, idx); + prev = idx; + } + AE(s, prev, first); + } + int b0 = s->nv; double r = 0.13; + PV(s, r,0,0, 0,0,0); PV(s,-r,0,0, 0,0,0); PV(s, 0,r,0, 0,0,0); + PV(s, 0,-r,0, 0,0,0); PV(s, 0,0,r, 0,0,0); PV(s, 0,0,-r, 0,0,0); + const int oe[12][2] = {{0,2},{0,3},{0,4},{0,5},{1,2},{1,3},{1,4},{1,5},{2,4},{2,5},{3,4},{3,5}}; + for (int e = 0; e < 12; e++) AE(s, b0+oe[e][0], b0+oe[e][1]); + center_normalize(s); +} +/* 3-D gyroscope: three great circles in the coordinate planes. */ +static void gen_gyroscope3d(void) { + Solid *s = new_solid(3); int seg = 22; + for (int p = 0; p < 3; p++) { + int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg, c = cos(a), sn = sin(a), x, y, z; + if (p == 0) { x = c; y = sn; z = 0; } + else if (p == 1) { x = c; y = 0; z = sn; } + else { x = 0; y = c; z = sn; } + int idx = s->nv; PV(s, x, y, z, 0,0,0); + if (first < 0) first = idx; + if (prev >= 0) AE(s, prev, idx); + prev = idx; + } + AE(s, prev, first); + } + center_normalize(s); +} +/* 3-D spike orb / sea-urchin: rods to 20 symmetric directions from a hub. */ +static void gen_spikeorb3d(void) { + Solid *s = new_solid(3); + const double P = 1.6180339887; + const double d[20][3] = { + {0,1,P},{0,1,-P},{0,-1,P},{0,-1,-P},{1,P,0},{1,-P,0},{-1,P,0},{-1,-P,0}, + {P,0,1},{P,0,-1},{-P,0,1},{-P,0,-1}, + {1,1,1},{1,1,-1},{1,-1,1},{1,-1,-1},{-1,1,1},{-1,1,-1},{-1,-1,1},{-1,-1,-1} }; + int c = s->nv; PV(s, 0,0,0, 0,0,0); + for (int i = 0; i < 20; i++) { + double L = sqrt(d[i][0]*d[i][0] + d[i][1]*d[i][1] + d[i][2]*d[i][2]); + int tip = s->nv; PV(s, d[i][0]/L, d[i][1]/L, d[i][2]/L, 0,0,0); + AE(s, c, tip); + } + center_normalize(s); +} + +/* Build a symbol in both its flat and extruded-3-D forms. */ +static void add_symbol(void (*build)(SymB *)) { + SymB b; + sym_init(&b); build(&b); sym_finish(&b, 0.0f); /* 2-D */ + sym_init(&b); build(&b); sym_finish(&b, 0.5f); /* 3-D */ +} + +/* Random lumpy 3-D asteroid: n points scattered over a sphere at jittered + * radii, wired up as the convex hull (brute-force face enumeration). The + * vertex count `n` sets the number of sides / overall complexity, and the + * radial jitter gives each rock its irregular, faceted silhouette. */ +static void gen_asteroid(int n) { + if (n > MAX_VERTS) n = MAX_VERTS; + if (n < 6) n = 6; + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { + double z = 2.0*frand() - 1.0; /* uniform on the sphere */ + double t = TAU*frand(); + double rr = sqrt(1.0 - z*z); + double rad = 0.74 + 0.26*frand(); /* lumpy radius */ + PV(s, rr*cos(t)*rad, rr*sin(t)*rad, z*rad, 0,0,0); + } + /* a triangle (a,b,c) is a hull face iff every other vertex lies on one + * side of its plane; collect the edges of all such faces. */ + for (int a = 0; a < n; a++) + for (int b = a+1; b < n; b++) + for (int c = b+1; c < n; c++) { + double ux = s->v[b][0]-s->v[a][0], uy = s->v[b][1]-s->v[a][1], uz = s->v[b][2]-s->v[a][2]; + double vx = s->v[c][0]-s->v[a][0], vy = s->v[c][1]-s->v[a][1], vz = s->v[c][2]-s->v[a][2]; + double nx = uy*vz - uz*vy, ny = uz*vx - ux*vz, nz = ux*vy - uy*vx; + double nl = sqrt(nx*nx + ny*ny + nz*nz); + if (nl < 1e-9) continue; /* degenerate triple */ + int pos = 0, neg = 0; + for (int p = 0; p < n; p++) { + if (p == a || p == b || p == c) continue; + double dx = s->v[p][0]-s->v[a][0], dy = s->v[p][1]-s->v[a][1], dz = s->v[p][2]-s->v[a][2]; + double d = (nx*dx + ny*dy + nz*dz) / nl; + if (d > 1e-5) pos++; else if (d < -1e-5) neg++; + } + if (pos == 0 || neg == 0) { + AE_unique(s, a, b); AE_unique(s, b, c); AE_unique(s, c, a); + } + } + center_normalize(s); +} + +static void init_solids(void) { + const double P = 1.6180339887, I = 1.0/1.6180339887; + Solid *s; + + /* --- the five Platonic solids --- */ + s = new_solid(3); + PV(s,1,1,1,0,0,0); PV(s,1,-1,-1,0,0,0); PV(s,-1,1,-1,0,0,0); PV(s,-1,-1,1,0,0,0); + center_normalize(s); derive_edges(s); /* tetrahedron */ + gen_hypercube(3); /* cube */ + gen_orthoplex(3); /* octahedron */ + s = new_solid(3); /* icosahedron */ + PV(s,0,1,P,0,0,0); PV(s,0,1,-P,0,0,0); PV(s,0,-1,P,0,0,0); PV(s,0,-1,-P,0,0,0); + PV(s,1,P,0,0,0,0); PV(s,1,-P,0,0,0,0); PV(s,-1,P,0,0,0,0); PV(s,-1,-P,0,0,0,0); + PV(s,P,0,1,0,0,0); PV(s,P,0,-1,0,0,0); PV(s,-P,0,1,0,0,0); PV(s,-P,0,-1,0,0,0); + center_normalize(s); derive_edges(s); + s = new_solid(3); /* dodecahedron */ + PV(s,1,1,1,0,0,0); PV(s,1,1,-1,0,0,0); PV(s,1,-1,1,0,0,0); PV(s,1,-1,-1,0,0,0); + PV(s,-1,1,1,0,0,0); PV(s,-1,1,-1,0,0,0); PV(s,-1,-1,1,0,0,0); PV(s,-1,-1,-1,0,0,0); + PV(s,0,I,P,0,0,0); PV(s,0,I,-P,0,0,0); PV(s,0,-I,P,0,0,0); PV(s,0,-I,-P,0,0,0); + PV(s,I,P,0,0,0,0); PV(s,I,-P,0,0,0,0); PV(s,-I,P,0,0,0,0); PV(s,-I,-P,0,0,0,0); + PV(s,P,0,I,0,0,0); PV(s,P,0,-I,0,0,0); PV(s,-P,0,I,0,0,0); PV(s,-P,0,-I,0,0,0); + center_normalize(s); derive_edges(s); + + /* --- other 3-D solids --- */ + gen_cuboctahedron(); + gen_truncated_octahedron(); + gen_stella_octangula(); + + /* --- prisms, antiprisms, bipyramids --- */ + gen_prism(3); gen_prism(5); gen_prism(6); gen_prism(7); gen_prism(8); + gen_antiprism(3); gen_antiprism(4); gen_antiprism(5); gen_antiprism(6); gen_antiprism(8); + gen_bipyramid(3); gen_bipyramid(4); gen_bipyramid(5); gen_bipyramid(6); gen_bipyramid(8); + + /* --- star polygons --- */ + gen_star(5,2); gen_star(6,2); gen_star(7,2); gen_star(7,3); gen_star(8,3); + gen_star(9,2); gen_star(9,4); gen_star(12,5); + + /* --- many-faced solids (named for their face count) + a hexagram --- + * Built from prism / bipyramid / trapezohedron families so each lands on + * exactly the requested number of faces. (The 20-face icosahedron here is + * a decagonal bipyramid, distinct from the platonic icosahedron above.) */ + gen_prism(11); /* tridecahedron (13 faces) */ + gen_bipyramid(7); /* tetradecahedron (14 faces) */ + gen_prism(13); /* pentadecahedron (15 faces) */ + gen_prism(15); /* heptadecahedron (17 faces) */ + gen_bipyramid(9); /* octadecahedron (18 faces) */ + gen_prism(17); /* enneadecahedron (19 faces) */ + gen_bipyramid(10); /* icosahedron (20 faces) */ + gen_trapezohedron(12); /* icositetrahedron (24 faces) */ + gen_trapezohedron(15); /* triacontahedron (30 faces) */ + gen_trapezohedron(30); /* hexacontahedron (60 faces) */ + gen_bipyramid(50); /* hecatohedron (100 faces) */ + gen_unicursal_hexagram(); /* unicursal hexagram */ + + /* --- googie / atomic-age starbursts, each in 2-D and extruded 3-D --- */ + add_symbol(build_sparkle4); /* 4-point bowling-alley twinkle */ + add_symbol(build_sparkle8); /* 8-point twinkle */ + add_symbol(build_atomic); /* atomic burst with electrons */ + add_symbol(build_sunburst); /* ray sunburst */ + add_symbol(build_doublestar); /* layered double starburst */ + add_symbol(build_boomerang); /* boomerang / crescent */ + add_symbol(build_amoeba); /* kidney / amoeba blob */ + /* flat motifs paired with a naturally-3-D counterpart below */ + { SymB b; sym_init(&b); build_atom2d(&b); sym_finish(&b, 0.0f); } /* flat atom */ + { SymB b; sym_init(&b); build_orbit2d(&b); sym_finish(&b, 0.0f); } /* orbit rings */ + { SymB b; sym_init(&b); build_starorb2d(&b); sym_finish(&b, 0.0f); } /* dense star */ + gen_sputnik(); /* 3-D sputnik satellite */ + gen_atom3d(); /* 3-D orbital atom */ + gen_gyroscope3d(); /* 3-D gyroscope / orbit cage */ + gen_spikeorb3d(); /* 3-D spike orb / sea-urchin */ + + /* --- symbols & signs, each in a flat (2-D) and an extruded (3-D) form --- */ + add_symbol(build_smiley); /* smiley face */ + add_symbol(build_biohazard); /* biohazard */ + add_symbol(build_peace); /* peace sign */ + add_symbol(build_cross); /* cross */ + add_symbol(build_question); /* question mark */ + add_symbol(build_exclaim); /* exclamation point */ + add_symbol(build_hash); /* hash / pound (#) */ + add_symbol(build_dollar); /* dollar sign */ + add_symbol(build_sterling); /* pound sterling (£) */ + + /* --- 4/5/6-dimensional polytopes (projected & morphing) --- */ + gen_simplex(5); /* 5-cell (4-simplex) */ + gen_hypercube(4); /* tesseract / 8-cell */ + gen_hypercube(5); /* penteract / 5-cube */ + gen_hypercube(6); /* 6-cube */ + gen_orthoplex(4); /* 16-cell (4-orthoplex) */ + gen_orthoplex(5); /* 5-orthoplex */ + gen_orthoplex(6); /* 6-orthoplex */ + gen_24cell(); /* 24-cell */ + gen_simplex(6); /* 5-simplex */ + + /* --- random vector asteroids (each session generates a fresh set) --- */ + for (int k = 0; k < 18; k++) gen_asteroid(7 + (rand() % 20)); /* 7..26 verts */ +} + +/* ================================================================== */ +/* The field of tumbling solids. */ +/* ================================================================== */ + +#define MAX_BODIES 7200 +#define RENDER_REF 140.0f /* render distance at which density == body count */ +/* The field is a sphere around the camera whose radius is the user-settable + * render distance (cfg.render_dist). Bodies stream along -Z (forward) or +Z + * (when speed is negative) and, once they leave the sphere, are recycled back + * to the render-distance shell on the incoming side -- so objects always appear + * far away and approach, never popping in close. The active body count scales + * with render distance, so a deeper field simply holds proportionally more + * shapes. The camera can rotate a full 360 degrees around it. */ + +typedef struct { + int shape; + float x, y, z; /* world position; camera at origin, looking -Z */ + float size; /* == bounding-sphere radius (unit solid scaled) */ + float axis[3]; /* tumble axis (normalized) */ + float spin_seed; /* in [-1,1]; tumble spread applied live */ + float angle; /* current tumble angle (radians) */ + float hue_offset; /* used in multicolor mode */ + float spawn_fade; /* 0->1 ease-in after (re)spawn, kills pop-in */ +} Body; + +static Body bodies[MAX_BODIES]; + +/* ================================================================== */ +/* Settings (driven by keyboard). */ +/* ================================================================== */ + +typedef struct { + float speed; /* approach speed 0..100 */ + float tumble; /* tumble rate 0..100 */ + float tumble_var; /* tumble variance 0..100 */ + float render_dist; /* field sphere radius 40..1520 */ + int density; /* active body count */ + float size_min; /* min solid size */ + float size_max; /* max solid size */ + float hue; /* base hue 0..360 */ + float hue_cycle; /* auto hue-cycle rate 0..100 (0 = off) */ + int multicolor; /* 0 = single, 1 = multi */ + int cycle_shapes; /* 0 = random, 1 = cycle */ + float glow; /* CRT glow / bleed 0..100 */ + float flicker; /* vector flicker 0..100 */ + int fullscreen; + int paused; +} Settings; + +static Settings cfg = { + .speed = 35, .tumble = 40, .tumble_var = 50, .render_dist = 140, .density = 50, + .size_min = 1.4f, .size_max = 4.6f, .hue = 200, .hue_cycle = 0, + .multicolor = 0, .cycle_shapes = 0, + .glow = 45, .flicker = 18, .fullscreen = 0, .paused = 0 +}; + +static int spawn_counter = 0; +static float max_line_width = 10.0f; /* queried at runtime */ +static float cam_x = 0.0f, cam_y = 0.0f; /* camera pan (WASD) */ +static float cam_yaw = 0.0f, cam_pitch = 0.0f; /* camera rotate (arrow keys) */ + +static float frand(void) { return (float)rand() / (float)RAND_MAX; } + +/* Bounding-sphere overlap test against the first `check_count` bodies. */ +static int collides(const Body *c, int check_count, int self) { + for (int i = 0; i < check_count; i++) { + if (i == self) continue; + const Body *o = &bodies[i]; + float dx = c->x - o->x, dy = c->y - o->y, dz = c->z - o->z; + float rr = c->size + o->size + 0.6f; /* +margin so they never touch */ + if (dx*dx + dy*dy + dz*dz < rr*rr) return 1; + } + return 0; +} + +/* (Re)initialize one body. reset=0 (recycle) re-enters it ONLY at the render- + * distance shell, on the incoming side (which flips with the travel direction), + * so objects always appear far away and approach -- never popping in close. + * reset=1 (initial fill) spreads bodies through the volume instead, as if they + * had each spawned at the shell at a different past time, so the field starts + * populated rather than blank. Position is rejection-sampled to avoid overlap. */ +static void spawn_body(Body *b, int reset, int check_count, int self) { + b->shape = cfg.cycle_shapes ? (spawn_counter++ % num_shapes) + : (rand() % num_shapes); + + float smin = cfg.size_min, smax = cfg.size_max; + if (smax < smin) smax = smin; + b->size = smin + frand() * (smax - smin); + + float ax = frand()*2-1, ay = frand()*2-1, az = frand()*2-1; + float len = sqrtf(ax*ax + ay*ay + az*az); + if (len < 1e-4f) { ax = 1; ay = 0; az = 0; len = 1; } + b->axis[0] = ax/len; b->axis[1] = ay/len; b->axis[2] = az/len; + + b->spin_seed = frand()*2 - 1; + b->angle = frand() * 6.2831853f; + b->hue_offset = frand() * 360.0f; + b->spawn_fade = reset ? 1.0f : 0.0f; + + float R = cfg.render_dist; + float side = (cfg.speed >= 0.0f) ? 1.0f : -1.0f; /* +Z if flying forward */ + for (int t = 0; t < 80; t++) { + if (reset) { + /* initial fill: uniform through the volume of the sphere */ + float zc = 2.0f*frand() - 1.0f; + float th = 6.2831853f*frand(); + float rr = sqrtf(1.0f - zc*zc); + float rad = R * cbrtf(frand()); + b->x = rr*cosf(th)*rad; + b->y = rr*sinf(th)*rad; + b->z = zc*rad; + } else { + /* recycle: enter at the shell, on the incoming-side cap */ + float rho = 0.9f * R * sqrtf(frand()); + float phi = 6.2831853f*frand(); + b->x = rho*cosf(phi); + b->y = rho*sinf(phi); + b->z = side * sqrtf(R*R - rho*rho) * 0.999f; + } + if (!collides(b, check_count, self)) break; + } +} + +/* How many bodies are active right now: the density knob, scaled up with render + * distance so a larger sphere holds proportionally more shapes (constant near- + * field density), clamped to the hard array limit. */ +static int active_count(void) { + int n = (int)(cfg.density * (cfg.render_dist / RENDER_REF) + 0.5f); + if (n < 1) n = 1; + if (n > MAX_BODIES) n = MAX_BODIES; + return n; +} + +/* Make sure bodies[0..n) are initialized (spawns any not yet filled). */ +static int g_filled = 0; +static void ensure_filled(int n) { + if (n > MAX_BODIES) n = MAX_BODIES; + for (int i = g_filled; i < n; i++) + spawn_body(&bodies[i], 1, i, i); + if (n > g_filled) g_filled = n; +} + +static void rebuild_field(void) { + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + if (cfg.density < 1) cfg.density = 1; + ensure_filled(active_count()); +} + +/* ================================================================== */ +/* Color. */ +/* ================================================================== */ + +static void hsv_to_rgb(float h, float s, float v, float *r, float *g, float *b) { + h = fmodf(h, 360.0f); if (h < 0) h += 360.0f; + float c = v * s; + float x = c * (1 - fabsf(fmodf(h / 60.0f, 2) - 1)); + float m = v - c, rr, gg, bb; + if (h < 60) { rr=c; gg=x; bb=0; } + else if (h < 120) { rr=x; gg=c; bb=0; } + else if (h < 180) { rr=0; gg=c; bb=x; } + else if (h < 240) { rr=0; gg=x; bb=c; } + else if (h < 300) { rr=x; gg=0; bb=c; } + else { rr=c; gg=0; bb=x; } + *r = rr + m; *g = gg + m; *b = bb + m; +} + +/* ================================================================== */ +/* Self-contained vector stroke font (for the on-screen display). */ +/* Each glyph is a polyline list on a 0..4 (x) by 0..6 (y) grid. */ +/* PU lifts the pen (start a new stroke); EN ends the glyph. */ +/* ================================================================== */ + +#define PU 127 +#define EN (-128) +#define G_ static const signed char + +G_ g_A[] = {0,0, 2,6, 4,0, PU, 1,2, 3,2, EN}; +G_ g_B[] = {0,0, 0,6, 3,6, 4,5, 3,3, 0,3, PU, 3,3, 4,1, 3,0, 0,0, EN}; +G_ g_C[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, EN}; +G_ g_D[] = {0,0, 0,6, 2,6, 4,4, 4,2, 2,0, 0,0, EN}; +G_ g_E[] = {4,6, 0,6, 0,0, 4,0, PU, 0,3, 3,3, EN}; +G_ g_F[] = {4,6, 0,6, 0,0, PU, 0,3, 3,3, EN}; +G_ g_G[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, 4,3, 2,3, EN}; +G_ g_H[] = {0,0, 0,6, PU, 4,0, 4,6, PU, 0,3, 4,3, EN}; +G_ g_I[] = {1,0, 3,0, PU, 2,0, 2,6, PU, 1,6, 3,6, EN}; +G_ g_J[] = {3,6, 3,1, 2,0, 1,0, 0,1, EN}; +G_ g_K[] = {0,0, 0,6, PU, 4,6, 0,3, 4,0, EN}; +G_ g_L[] = {0,6, 0,0, 4,0, EN}; +G_ g_M[] = {0,0, 0,6, 2,3, 4,6, 4,0, EN}; +G_ g_N[] = {0,0, 0,6, 4,0, 4,6, EN}; +G_ g_O[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, EN}; +G_ g_P[] = {0,0, 0,6, 3,6, 4,5, 4,4, 3,3, 0,3, EN}; +G_ g_Q[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, PU, 2,2, 4,0, EN}; +G_ g_R[] = {0,0, 0,6, 3,6, 4,5, 4,4, 3,3, 0,3, PU, 2,3, 4,0, EN}; +G_ g_S[] = {4,5, 3,6, 1,6, 0,5, 1,3, 3,3, 4,2, 3,0, 1,0, 0,1, EN}; +G_ g_T[] = {0,6, 4,6, PU, 2,6, 2,0, EN}; +G_ g_U[] = {0,6, 0,1, 1,0, 3,0, 4,1, 4,6, EN}; +G_ g_V[] = {0,6, 2,0, 4,6, EN}; +G_ g_W[] = {0,6, 1,0, 2,3, 3,0, 4,6, EN}; +G_ g_X[] = {0,0, 4,6, PU, 0,6, 4,0, EN}; +G_ g_Y[] = {0,6, 2,3, 4,6, PU, 2,3, 2,0, EN}; +G_ g_Z[] = {0,6, 4,6, 0,0, 4,0, EN}; + +G_ g_0[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, PU, 1,1, 3,5, EN}; +G_ g_1[] = {1,5, 2,6, 2,0, PU, 1,0, 3,0, EN}; +G_ g_2[] = {0,5, 1,6, 3,6, 4,5, 4,4, 0,0, 4,0, EN}; +G_ g_3[] = {0,6, 4,6, 2,3, PU, 2,3, 4,2, 4,1, 3,0, 1,0, 0,1, EN}; +G_ g_4[] = {3,0, 3,6, 0,2, 4,2, EN}; +G_ g_5[] = {4,6, 0,6, 0,3, 3,3, 4,2, 4,1, 3,0, 1,0, 0,1, EN}; +G_ g_6[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, 4,2, 3,3, 0,3, EN}; +G_ g_7[] = {0,6, 4,6, 2,0, EN}; +G_ g_8[] = {1,3, 0,4, 0,5, 1,6, 3,6, 4,5, 4,4, 3,3, 1,3, 0,2, 0,1, 1,0, 3,0, 4,1, 4,2, 3,3, EN}; +G_ g_9[] = {0,1, 1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,4, 1,3, 4,3, EN}; + +G_ g_pct[] = {0,0, 4,6, PU, 0,5,1,5,1,6,0,6,0,5, PU, 3,0,4,0,4,1,3,1,3,0, EN}; +G_ g_slash[] = {0,0, 4,6, EN}; +G_ g_plus[] = {2,1, 2,5, PU, 0,3, 4,3, EN}; +G_ g_minus[] = {0,3, 4,3, EN}; +G_ g_dot[] = {2,0, 2,1, EN}; +G_ g_lbr[] = {3,6, 1,6, 1,0, 3,0, EN}; +G_ g_rbr[] = {1,6, 3,6, 3,0, 1,0, EN}; +G_ g_lpar[] = {3,6, 1,4, 1,2, 3,0, EN}; +G_ g_rpar[] = {1,6, 3,4, 3,2, 1,0, EN}; +G_ g_dash[] = {0,3, 4,3, EN}; /* same as minus, used by '/'-fallback */ + +static const signed char *glyph(char c) { + switch (toupper((unsigned char)c)) { + case 'A': return g_A; case 'B': return g_B; case 'C': return g_C; + case 'D': return g_D; case 'E': return g_E; case 'F': return g_F; + case 'G': return g_G; case 'H': return g_H; case 'I': return g_I; + case 'J': return g_J; case 'K': return g_K; case 'L': return g_L; + case 'M': return g_M; case 'N': return g_N; case 'O': return g_O; + case 'P': return g_P; case 'Q': return g_Q; case 'R': return g_R; + case 'S': return g_S; case 'T': return g_T; case 'U': return g_U; + case 'V': return g_V; case 'W': return g_W; case 'X': return g_X; + case 'Y': return g_Y; case 'Z': return g_Z; + case '0': return g_0; case '1': return g_1; case '2': return g_2; + case '3': return g_3; case '4': return g_4; case '5': return g_5; + case '6': return g_6; case '7': return g_7; case '8': return g_8; + case '9': return g_9; + case '%': return g_pct; case '/': return g_slash; + case '+': return g_plus; case '-': return g_minus; + case '.': return g_dot; case '[': return g_lbr; + case ']': return g_rbr; case '(': return g_lpar; + case ')': return g_rpar; case '_': return g_dash; + default: return NULL; /* space and unknowns advance only */ + } +} + +/* Draw a string with its bottom-left at (x,y); glyph height = h px. */ +static float stroke_text(const char *str, float x, float y, float h) { + float sx = h / 6.0f, sy = h / 6.0f; + float advance = sx * 6.0f; /* 4 wide + 2 spacing */ + float cx = x; + for (const char *p = str; *p; p++) { + const signed char *g = glyph(*p); + if (g) { + glBegin(GL_LINE_STRIP); + for (int i = 0;;) { + signed char a = g[i++]; + if (a == EN) break; + if (a == PU) { glEnd(); glBegin(GL_LINE_STRIP); continue; } + signed char b = g[i++]; + glVertex2f(cx + a * sx, y + b * sy); + } + glEnd(); + } + cx += advance; + } + return cx; +} + +/* ================================================================== */ +/* On-screen display. */ +/* ================================================================== */ + +static void render_osd(int fbw, int fbh, float alpha) { + if (alpha <= 0.001f) return; + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + gluOrtho2D(0, fbw, 0, fbh); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + + const float h = 13.0f; + const float left = 22.0f; + const float lineStep = h + 9.0f; + const int nlines = 19; + const float panelW = 384.0f; + float panelTop = fbh - 18.0f; + float panelH = nlines * lineStep + 16.0f; + + /* dim backdrop for readability (normal alpha blend) */ + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColor4f(0.02f, 0.03f, 0.06f, 0.55f * alpha); + glBegin(GL_QUADS); + glVertex2f(8, panelTop + 10); + glVertex2f(panelW, panelTop + 10); + glVertex2f(panelW, panelTop + 10 - panelH); + glVertex2f(8, panelTop + 10 - panelH); + glEnd(); + /* subtle border */ + glColor4f(0.3f, 0.6f, 0.9f, 0.35f * alpha); + glBegin(GL_LINE_LOOP); + glVertex2f(8, panelTop + 10); + glVertex2f(panelW, panelTop + 10); + glVertex2f(panelW, panelTop + 10 - panelH); + glVertex2f(8, panelTop + 10 - panelH); + glEnd(); + + /* additive, theme-tinted text with a faint glow pass */ + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + float tr, tg, tb; + hsv_to_rgb(cfg.hue, 0.35f, 1.0f, &tr, &tg, &tb); + + char buf[80]; + float y = panelTop - h; + + /* title */ + glColor4f(tr*alpha, tg*alpha, tb*alpha, alpha); + glLineWidth(2.4f); stroke_text("- VECTORGONS -", left, y, h + 3); + glLineWidth(1.6f); + y -= lineStep + 6; + + #define LINE(...) do { snprintf(buf,sizeof buf,__VA_ARGS__); \ + /* glow pass */ \ + glColor4f(tr*alpha*0.6f,tg*alpha*0.6f,tb*alpha*0.6f, alpha*0.5f); \ + glLineWidth(3.0f); stroke_text(buf,left,y,h); \ + /* crisp pass */ \ + glColor4f(tr*alpha,tg*alpha,tb*alpha,alpha); \ + glLineWidth(1.5f); stroke_text(buf,left,y,h); \ + y -= lineStep; } while(0) + + LINE("SPEED PGUP/DN %.0f%%%s", cfg.speed, cfg.speed < 0 ? " REV" : ""); + LINE("TUMBLE Q/E %.0f%%", cfg.tumble); + LINE("TUMBLE VAR T/Y %.0f%%", cfg.tumble_var); + LINE("RENDER DST Z/X %.0f", cfg.render_dist); + LINE("DENSITY +/- %d", active_count()); + LINE("SIZE MIN U/J %.1f", cfg.size_min); + LINE("SIZE MAX I/K %.1f", cfg.size_max); + LINE("HUE [ / ] %.0f", cfg.hue); + if (cfg.hue_cycle > 0.001f) LINE("HUE CYCLE C/V %.0f%%", cfg.hue_cycle); + else LINE("%s", "HUE CYCLE C/V OFF"); + LINE("COLOR M %s", cfg.multicolor ? "MULTICOLOR" : "SINGLE"); + LINE("GLOW O/L %.0f%%", cfg.glow); + LINE("FLICKER G/H %.0f%%", cfg.flicker); + LINE("SHAPES N %s", cfg.cycle_shapes ? "CYCLE" : "RANDOM"); + LINE("MOVE CAM WASD %+.0f %+.0f", cam_x, cam_y); + LINE("ROTATE CAM ARROWS %+.0f %+.0f", cam_yaw, cam_pitch); + LINE("FULLSCREEN F %s", cfg.fullscreen ? "ON" : "OFF"); + LINE("PAUSE SPACE %s", cfg.paused ? "PAUSED" : "RUNNING"); + LINE("%s", "QUIT ESC"); + #undef LINE + + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); + glPopMatrix(); + /* leave additive blending on for the next 3-D frame */ +} + +/* ================================================================== */ +/* Input. */ +/* ================================================================== */ + +static double last_input_time = 0.0; + +static void clampf(float *v, float lo, float hi) { + if (*v < lo) *v = lo; + if (*v > hi) *v = hi; +} + +static void normalize_sizes(void) { + if (cfg.size_min < 0.4f) cfg.size_min = 0.4f; + if (cfg.size_max < cfg.size_min) cfg.size_max = cfg.size_min; + if (cfg.size_max > 54.0f) cfg.size_max = 54.0f; +} + +/* ================================================================== */ +/* Settings persistence: a plain key=value file under $HOME so the */ +/* user's last-used settings become the defaults next launch. */ +/* ================================================================== */ + +static void settings_path(char *buf, size_t n) { + const char *home = getenv("HOME"); + if (home && *home) snprintf(buf, n, "%s/.vectorgons", home); + else snprintf(buf, n, ".vectorgons"); +} + +static void save_settings(void) { + char path[1024]; + settings_path(path, sizeof path); + FILE *f = fopen(path, "w"); + if (!f) return; + fprintf(f, "# Vectorgons settings (auto-saved on exit)\n"); + fprintf(f, "speed=%g\n", cfg.speed); + fprintf(f, "tumble=%g\n", cfg.tumble); + fprintf(f, "tumble_var=%g\n", cfg.tumble_var); + fprintf(f, "render_dist=%g\n", cfg.render_dist); + fprintf(f, "density=%d\n", cfg.density); + fprintf(f, "size_min=%g\n", cfg.size_min); + fprintf(f, "size_max=%g\n", cfg.size_max); + fprintf(f, "hue=%g\n", cfg.hue); + fprintf(f, "hue_cycle=%g\n", cfg.hue_cycle); + fprintf(f, "multicolor=%d\n", cfg.multicolor); + fprintf(f, "cycle_shapes=%d\n", cfg.cycle_shapes); + fprintf(f, "glow=%g\n", cfg.glow); + fprintf(f, "flicker=%g\n", cfg.flicker); + fprintf(f, "fullscreen=%d\n", cfg.fullscreen); + fclose(f); +} + +static void load_settings(void) { + char path[1024]; + settings_path(path, sizeof path); + FILE *f = fopen(path, "r"); + if (!f) return; /* first run: keep built-in defaults */ + char line[256]; + while (fgets(line, sizeof line, f)) { + if (line[0] == '#' || line[0] == '\n') continue; + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + const char *key = line, *val = eq + 1; + double d = atof(val); + if (!strcmp(key, "speed")) cfg.speed = (float)d; + else if (!strcmp(key, "tumble")) cfg.tumble = (float)d; + else if (!strcmp(key, "tumble_var")) cfg.tumble_var = (float)d; + else if (!strcmp(key, "render_dist")) cfg.render_dist = (float)d; + else if (!strcmp(key, "density")) cfg.density = (int)d; + else if (!strcmp(key, "size_min")) cfg.size_min = (float)d; + else if (!strcmp(key, "size_max")) cfg.size_max = (float)d; + else if (!strcmp(key, "hue")) cfg.hue = (float)d; + else if (!strcmp(key, "hue_cycle")) cfg.hue_cycle = (float)d; + else if (!strcmp(key, "multicolor")) cfg.multicolor = (int)d; + else if (!strcmp(key, "cycle_shapes")) cfg.cycle_shapes = (int)d; + else if (!strcmp(key, "glow")) cfg.glow = (float)d; + else if (!strcmp(key, "flicker")) cfg.flicker = (float)d; + else if (!strcmp(key, "fullscreen")) cfg.fullscreen = (int)d; + } + fclose(f); + /* defend against a hand-edited or stale file by clamping into range */ + clampf(&cfg.speed, -100, 100); + clampf(&cfg.tumble, 0, 100); + clampf(&cfg.tumble_var, 0, 100); + clampf(&cfg.render_dist, 40, 1520); + clampf(&cfg.hue_cycle, 0, 100); + clampf(&cfg.glow, 0, 100); + clampf(&cfg.flicker, 0, 100); + cfg.hue = fmodf(cfg.hue, 360.0f); if (cfg.hue < 0) cfg.hue += 360.0f; + if (cfg.density < 1) cfg.density = 1; + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + normalize_sizes(); + cfg.multicolor = cfg.multicolor ? 1 : 0; + cfg.cycle_shapes = cfg.cycle_shapes ? 1 : 0; + cfg.fullscreen = cfg.fullscreen ? 1 : 0; +} + +static void toggle_fullscreen(GLFWwindow *win) { + static int wx = 100, wy = 100, ww = 1100, wh = 760; + cfg.fullscreen = !cfg.fullscreen; + if (cfg.fullscreen) { + glfwGetWindowPos(win, &wx, &wy); + glfwGetWindowSize(win, &ww, &wh); + GLFWmonitor *mon = glfwGetPrimaryMonitor(); + const GLFWvidmode *m = glfwGetVideoMode(mon); + glfwSetWindowMonitor(win, mon, 0, 0, m->width, m->height, m->refreshRate); + } else { + glfwSetWindowMonitor(win, NULL, wx, wy, ww, wh, 0); + } +} + +static void key_cb(GLFWwindow *win, int key, int sc, int action, int mods) { + (void)sc; (void)mods; + if (action != GLFW_PRESS && action != GLFW_REPEAT) return; + last_input_time = glfwGetTime(); /* keeps the OSD awake */ + + switch (key) { + case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(win, 1); break; + + /* WASD pans the camera and the arrow keys rotate it; both are polled + * per-frame in the main loop for smooth, frame-rate-independent motion. */ + case GLFW_KEY_PAGE_UP: cfg.speed += 2.5f; clampf(&cfg.speed, -100,100); break; + case GLFW_KEY_PAGE_DOWN: cfg.speed -= 2.5f; clampf(&cfg.speed, -100,100); break; + + case GLFW_KEY_E: cfg.tumble += 2.5f; clampf(&cfg.tumble,0,100); break; + case GLFW_KEY_Q: cfg.tumble -= 2.5f; clampf(&cfg.tumble,0,100); break; + + case GLFW_KEY_T: cfg.tumble_var += 5; clampf(&cfg.tumble_var,0,100); break; + case GLFW_KEY_Y: cfg.tumble_var -= 5; clampf(&cfg.tumble_var,0,100); break; + + case GLFW_KEY_X: cfg.render_dist += 10; clampf(&cfg.render_dist,40,1520); break; + case GLFW_KEY_Z: cfg.render_dist -= 10; clampf(&cfg.render_dist,40,1520); break; + + case GLFW_KEY_U: cfg.size_min += 0.2f; normalize_sizes(); break; + case GLFW_KEY_J: cfg.size_min -= 0.2f; normalize_sizes(); break; + case GLFW_KEY_I: cfg.size_max += 0.2f; normalize_sizes(); break; + case GLFW_KEY_K: cfg.size_max -= 0.2f; normalize_sizes(); break; + + case GLFW_KEY_LEFT_BRACKET: cfg.hue -= 6; if (cfg.hue < 0) cfg.hue += 360; break; + case GLFW_KEY_RIGHT_BRACKET: cfg.hue += 6; if (cfg.hue >= 360) cfg.hue -= 360; break; + + case GLFW_KEY_C: cfg.hue_cycle += 5; clampf(&cfg.hue_cycle,0,100); break; + case GLFW_KEY_V: cfg.hue_cycle -= 5; clampf(&cfg.hue_cycle,0,100); break; + + case GLFW_KEY_M: cfg.multicolor = !cfg.multicolor; break; + case GLFW_KEY_N: cfg.cycle_shapes = !cfg.cycle_shapes; spawn_counter = 0; break; + + case GLFW_KEY_O: cfg.glow += 5; clampf(&cfg.glow,0,100); break; + case GLFW_KEY_L: cfg.glow -= 5; clampf(&cfg.glow,0,100); break; + case GLFW_KEY_G: cfg.flicker += 5; clampf(&cfg.flicker,0,100); break; + case GLFW_KEY_H: cfg.flicker -= 5; clampf(&cfg.flicker,0,100); break; + + /* +/- adjust the live on-screen count and back-solve the baseline + * density, so a press always moves the count even when render-distance + * scaling has the active total pinned at MAX_BODIES. */ + case GLFW_KEY_EQUAL: { + int a = active_count() + 10; if (a > MAX_BODIES) a = MAX_BODIES; + cfg.density = (int)(a * (RENDER_REF / cfg.render_dist) + 0.5f); + if (cfg.density < 1) cfg.density = 1; + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + rebuild_field(); break; + } + case GLFW_KEY_MINUS: { + int a = active_count() - 10; if (a < 1) a = 1; + cfg.density = (int)(a * (RENDER_REF / cfg.render_dist) + 0.5f); + if (cfg.density < 1) cfg.density = 1; + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + break; + } + + case GLFW_KEY_F: + case GLFW_KEY_F11: toggle_fullscreen(win); break; + + case GLFW_KEY_SPACE: cfg.paused = !cfg.paused; break; + default: break; + } +} + +/* ================================================================== */ +/* Projection + body wireframe (modelview transform must be set). */ +/* ================================================================== */ + +static void rot2(double *c, int a, int b, double th) { + double ca = cos(th), sa = sin(th), x = c[a], y = c[b]; + c[a] = x*ca - y*sa; + c[b] = x*sa + y*ca; +} + +/* Produce 3-D vertices for a body. 3-D shapes are copied through; + * higher-D shapes are rotated in their own dimension (driven by the + * tumble angle + a per-body phase) and perspective-projected down to + * 3-D, then renormalized to the unit sphere so the bounding radius + * stays <= 1 (which keeps the no-overlap guarantee intact). */ +static void project_body(const Solid *s, float angf, float phasef, float out[][3]) { + int d = s->dim; + if (d == 3) { + for (int i = 0; i < s->nv; i++) { + out[i][0] = s->v[i][0]; out[i][1] = s->v[i][1]; out[i][2] = s->v[i][2]; + } + return; + } + double ang = angf, ph = phasef; + for (int i = 0; i < s->nv; i++) { + double c[MAXD]; + for (int k = 0; k < MAXD; k++) c[k] = (k < d) ? s->v[i][k] : 0.0; + + rot2(c, 0, 1, ang*0.5); + if (d >= 4) { rot2(c, 2, 3, ang*0.9 + ph); rot2(c, 0, 3, ang*0.6); } + if (d >= 5) { rot2(c, 1, 4, ang*0.7 + ph*0.5); rot2(c, 3, 4, ang*0.45); } + if (d >= 6) { rot2(c, 2, 5, ang*0.8); rot2(c, 4, 5, ang*0.4 + ph*0.3); } + + for (int k = d-1; k >= 3; k--) { /* project k-D -> (k-1)-D */ + double den = 4.0 - c[k]; + if (den < 0.5) den = 0.5; + double f = 4.0 / den; + for (int j = 0; j < k; j++) c[j] *= f; + } + out[i][0] = (float)c[0]; out[i][1] = (float)c[1]; out[i][2] = (float)c[2]; + } + double mr2 = 0; + for (int i = 0; i < s->nv; i++) { + double r2 = (double)out[i][0]*out[i][0] + (double)out[i][1]*out[i][1] + (double)out[i][2]*out[i][2]; + if (r2 > mr2) mr2 = r2; + } + if (mr2 > 1e-9) { + float inv = (float)(1.0/sqrt(mr2)); + for (int i = 0; i < s->nv; i++) { out[i][0]*=inv; out[i][1]*=inv; out[i][2]*=inv; } + } +} + +static void draw_edges(const Solid *s, float p[][3]) { + glBegin(GL_LINES); + for (int e = 0; e < s->ne; e++) { + glVertex3fv(p[s->e[e][0]]); + glVertex3fv(p[s->e[e][1]]); + } + glEnd(); +} + +/* ================================================================== */ +/* Help / main. */ +/* ================================================================== */ + +static void print_help(void) { + printf( + "\nVECTORGONS — vector solids & asteroids tumbling through space\n" + "------------------------------------------------------------\n" + " W/A/S/D move (pan) camera Arrows rotate camera\n" + " PgUp/PgDn approach speed (PgDn past 0 = reverse)\n" + " Q / E tumble rate\n" + " T / Y tumble variance Z / X render distance\n" + " U / J size min I / K size max\n" + " [ / ] hue C / V hue-cycle rate\n" + " O / L glow G / H flicker\n" + " + / - density M color N shapes\n" + " F / F11 fullscreen Space pause Esc quit\n\n" + " (all settings are also shown in the on-screen display)\n\n"); + fflush(stdout); +} + +int main(void) { + srand((unsigned)time(NULL)); + init_solids(); + load_settings(); /* user's last-used settings become this run's defaults */ + printf("Vectorgons: %d shape types loaded.\n", num_shapes); + + if (!glfwInit()) { fprintf(stderr, "Failed to init GLFW\n"); return 1; } + glfwWindowHint(GLFW_SAMPLES, 4); + + GLFWwindow *win = glfwCreateWindow(1100, 760, "Vectorgons", NULL, NULL); + if (!win) { fprintf(stderr, "Failed to create window\n"); glfwTerminate(); return 1; } + glfwMakeContextCurrent(win); + glfwSwapInterval(1); + glfwSetKeyCallback(win, key_cb); + + GLfloat lwr[2] = {1, 10}; + glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, lwr); + max_line_width = lwr[1] > 1 ? lwr[1] : 10.0f; + + if (cfg.fullscreen) { cfg.fullscreen = 0; toggle_fullscreen(win); } /* restore saved */ + + rebuild_field(); + print_help(); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); /* additive glow */ + glEnable(GL_LINE_SMOOTH); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glEnable(GL_MULTISAMPLE); + + last_input_time = glfwGetTime(); + double last = glfwGetTime(); + + while (!glfwWindowShouldClose(win)) { + double now = glfwGetTime(); + float dt = (float)(now - last); + last = now; + if (dt > 0.05f) dt = 0.05f; + + int fbw, fbh; + glfwGetFramebufferSize(win, &fbw, &fbh); + if (fbh < 1) fbh = 1; + glViewport(0, 0, fbw, fbh); + + /* --- WASD pan + arrow-key rotate (held keys, frame-rate independent) --- */ + { + float pan = 42.0f * dt; + float rot = 60.0f * dt; /* degrees / second */ + int moved = 0; + if (glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) { cam_y += pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) { cam_y -= pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) { cam_x += pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) { cam_x -= pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_LEFT) == GLFW_PRESS) { cam_yaw -= rot; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_RIGHT) == GLFW_PRESS) { cam_yaw += rot; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_UP) == GLFW_PRESS) { cam_pitch += rot; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_DOWN) == GLFW_PRESS) { cam_pitch -= rot; moved = 1; } + if (cam_x < -180) cam_x = -180; + if (cam_x > 180) cam_x = 180; + if (cam_y < -180) cam_y = -180; + if (cam_y > 180) cam_y = 180; + cam_yaw = fmodf(cam_yaw, 360.0f); /* free 360 rotation, both axes */ + cam_pitch = fmodf(cam_pitch, 360.0f); + if (moved) last_input_time = now; /* keep the OSD awake */ + } + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluPerspective(60.0, (double)fbw / (double)fbh, 0.5, cfg.render_dist + 80.0); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glRotatef(cam_pitch, 1.0f, 0.0f, 0.0f); /* apply camera rotation (arrows) */ + glRotatef(cam_yaw, 0.0f, 1.0f, 0.0f); + glTranslatef(-cam_x, -cam_y, 0.0f); /* apply camera pan (WASD) */ + + glClearColor(0.006f, 0.010f, 0.035f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + /* continuous hue cycling (advances even while paused) */ + if (cfg.hue_cycle > 0.001f) { + cfg.hue += (cfg.hue_cycle / 100.0f) * 120.0f * dt; /* up to 120 deg/s */ + cfg.hue = fmodf(cfg.hue, 360.0f); + if (cfg.hue < 0) cfg.hue += 360.0f; + } + + /* signed approach: negative speed flies the field backward (Page Down) */ + float sp = cfg.speed / 100.0f; + float approach = copysignf(powf(fabsf(sp), 1.6f), sp) * 120.0f; + float tumble_rate = powf(cfg.tumble / 100.0f, 1.4f) * 3.2f; + float var = cfg.tumble_var / 100.0f; + float glow = cfg.glow / 100.0f; + float fl = cfg.flicker / 100.0f; + + int active = active_count(); + ensure_filled(active); + + for (int i = 0; i < active; i++) { + Body *b = &bodies[i]; + + if (!cfg.paused) { + float spin = powf(4.0f, b->spin_seed * var); /* live variance */ + b->z -= approach * dt; + b->angle += tumble_rate * spin * dt; + } + if (b->spawn_fade < 1.0f) { /* ease in after spawn */ + b->spawn_fade += dt * 2.2f; + if (b->spawn_fade > 1.0f) b->spawn_fade = 1.0f; + } + /* recycle once the body leaves the render-distance sphere (in any + * direction), so culling is purely radial and rotation-agnostic */ + float dist2 = b->x*b->x + b->y*b->y + b->z*b->z; + float R = cfg.render_dist; + if (dist2 > R*R) { spawn_body(b, 0, active, i); continue; } + + float depth = sqrtf(dist2) / R; /* 0 at camera .. 1 at far sphere */ + if (depth < 0) depth = 0; + if (depth > 1) depth = 1; + + float bright = 0.25f + (1.0f - depth) * 0.75f; + if (fl > 0.001f) bright *= 1.0f - fl * 0.6f * frand(); /* flicker */ + + float alpha = b->spawn_fade; + if (depth > 0.85f) alpha *= (1.0f - depth) / 0.15f; /* fade in far */ + if (alpha < 0) alpha = 0; + + float hue = cfg.multicolor ? (cfg.hue + b->hue_offset) : cfg.hue; + float r, g, bl; + hsv_to_rgb(hue, 0.9f, 1.0f, &r, &g, &bl); + + float lw = 1.0f + (1.0f - depth) * 2.0f; + const Solid *s = &solids[b->shape]; + + /* project once (4/5/6-D shapes morph; 3-D shapes pass through) */ + static float p3[MAX_VERTS][3]; + project_body(s, b->angle, b->hue_offset * 0.01745329f, p3); + + glPushMatrix(); + glTranslatef(b->x, b->y, -b->z); + glRotatef(b->angle * 57.2957795f, b->axis[0], b->axis[1], b->axis[2]); + glScalef(b->size, b->size, b->size); + + /* CRT phosphor glow: a sharp core wrapped in a soft, dim mist. + * Hardware caps line width (~10px), so the near halo is built from + * many faint antialiased width layers fading outward (a smooth + * gradient, not one fat blurry line), and a few faint scaled-up + * ghost copies bloom the glow into a larger volume than the line + * width alone could ever reach. */ + if (glow > 0.001f) { + float maxw = max_line_width; + + /* (a) soft halo hugging each vector, fading out into mist */ + for (int p = 1; p <= 5; p++) { + float t = p / 5.0f; /* 0..1 outward */ + float w = lw + (maxw - lw) * t; + if (w < 1.0f) w = 1.0f; + float fade = (1.0f - t) * (1.0f - t); /* dim, fades out */ + glColor4f(r*bright, g*bright, bl*bright, alpha * glow * 0.16f * fade); + glLineWidth(w); + draw_edges(s, p3); + } + + /* (b) volumetric bloom: a few faint, widely-spaced enlarged + * ghosts drawn at the maximum (blurriest) width, so they smear + * into an outer haze that fills a larger volume than the line- + * width-capped halo can reach -- without reading as crisp rings */ + glLineWidth(maxw); + for (int p = 1; p <= 3; p++) { + float t = p / 3.0f; + float sc = 1.0f + glow * (0.22f + 0.55f * t); + glColor4f(r*bright, g*bright, bl*bright, + alpha * glow * 0.035f * (1.0f - 0.6f * t)); + glPushMatrix(); + glScalef(sc, sc, sc); + draw_edges(s, p3); + glPopMatrix(); + } + } + + /* crisp core, drawn on top so the vector stays sharp */ + glColor4f(r*bright, g*bright, bl*bright, alpha); + glLineWidth(lw); + draw_edges(s, p3); + + glPopMatrix(); + } + + /* OSD: full for 10 s after last keypress, then fades over 4 s */ + float idle = (float)(now - last_input_time); + float osd_alpha = 1.0f; + if (idle > 10.0f) osd_alpha = 1.0f - (idle - 10.0f) / 4.0f; + if (osd_alpha < 0) osd_alpha = 0; + render_osd(fbw, fbh, osd_alpha); + + glfwSwapBuffers(win); + glfwPollEvents(); + } + + save_settings(); /* persist current settings as next run's defaults */ + glfwDestroyWindow(win); + glfwTerminate(); + return 0; +}