/* * 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: * - 160+ 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, a big set of googie / atomic-age 3-D objects * (starbursts, sea-urchins, gyroscopes, orbital atoms, ringed planets, * molecules, diabolos, sputniks), 2-D and 3-D symbols (smiley, frowny, * angry, biohazard, peace, cross, ? ! # $ £ @), object iconography * (space invaders, UFOs, pac-man, alien heads, pipes, umbrellas, hands) * and live analog/digital clocks showing the real current time, 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 220 #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 int clock_idx[4] = {-1,-1,-1,-1}; /* analog2d, analog3d, digital2d, digital3d */ 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; } /* Fill an existing Solid from the strokes: flat if depth<=0, else extruded. * (Used both for one-shot shapes and for the live clocks, which rewrite their * solid in place every frame.) */ static void sym_fill_solid(Solid *s, SymB *b, float depth) { s->dim = 3; s->nv = 0; s->ne = 0; 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); } static void sym_finish(SymB *b, float depth) { sym_fill_solid(new_solid(3), b, depth); } /* ---- 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); } /* ================================================================== */ /* More googie / atomic-age 3-D objects, plus faces and the '@' sign. */ /* ================================================================== */ /* Symmetric unit-direction sets for radial bursts. Returns the count. * 0:octahedron(6) 1:cube(8) 2:icosahedron(12) 3:dodecahedron(20) * 4:icosa+dodeca(32). */ static int dir_set(int which, double out[][3]) { const double P = 1.6180339887, Q = 0.6180339887; double t[40][3]; int n = 0; if (which == 0) { double d[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}; for (int i=0;i<6;i++){ t[n][0]=d[i][0];t[n][1]=d[i][1];t[n][2]=d[i][2];n++; } } else if (which == 1) { for (int x=-1;x<=1;x+=2) for (int y=-1;y<=1;y+=2) for (int z=-1;z<=1;z+=2) { t[n][0]=x;t[n][1]=y;t[n][2]=z;n++; } } else { if (which == 2 || which == 4) { double d[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}}; for (int i=0;i<12;i++){ t[n][0]=d[i][0];t[n][1]=d[i][1];t[n][2]=d[i][2];n++; } } if (which == 3 || which == 4) { for (int x=-1;x<=1;x+=2) for (int y=-1;y<=1;y+=2) for (int z=-1;z<=1;z+=2) { t[n][0]=x;t[n][1]=y;t[n][2]=z;n++; } double e[12][3]={{0,Q,P},{0,Q,-P},{0,-Q,P},{0,-Q,-P},{Q,P,0},{Q,-P,0}, {-Q,P,0},{-Q,-P,0},{P,0,Q},{P,0,-Q},{-P,0,Q},{-P,0,-Q}}; for (int i=0;i<12;i++){ t[n][0]=e[i][0];t[n][1]=e[i][1];t[n][2]=e[i][2];n++; } } } for (int i=0;inv; PV(s, px+rad*(cos(a)*rx+sin(a)*sx), py+rad*(cos(a)*ry+sin(a)*sy), pz+rad*(cos(a)*rz+sin(a)*sz), 0,0,0); if(first<0)first=idx; if(prev>=0)AE(s,prev,idx); prev=idx; } if(first>=0&&prev>=0) AE(s,prev,first); } /* radial spike burst (starburst): a spoke to each direction, optionally * alternating long/short, optionally capped with a little ball */ static void gen_burst3d(int which, double longr, double shortr, int balls) { Solid *s=new_solid(3); double d[40][3]; int nd=dir_set(which,d); int c=s->nv; PV(s,0,0,0,0,0,0); for (int i=0;i0 && (i&1))?shortr:longr; double tx=d[i][0]*r, ty=d[i][1]*r, tz=d[i][2]*r; int tip=s->nv; PV(s,tx,ty,tz,0,0,0); AE(s,c,tip); if (balls) add_ball(s,tx,ty,tz,d[i][0],d[i][1],d[i][2],0.12,5); } center_normalize(s); } /* great circles at spread orientations (gyroscope / orbit cage) */ static void gen_rings3d(int nrings, int seg) { Solid *s=new_solid(3); for (int r=0;rnv; 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); } /* atomic orbital cluster: tilted elliptical orbits around an octahedral nucleus */ static void gen_atomorbits(int norbits, int seg) { Solid *s=new_solid(3); for (int o=0;onv; 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); } int b0=s->nv; double r=0.12; 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); 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); } /* ringed planet (Saturn): a wireframe sphere with tilted equatorial ring(s) */ static void gen_saturn3d(int nrings, double tilt) { Solid *s=new_solid(3); const double pi=TAU*0.5; int lats=3, seg=14; for (int la=1;la<=lats;la++){ double phi=pi*la/(lats+1)-pi*0.5, y=sin(phi)*0.6, rr=cos(phi)*0.6; int first=-1, prev=-1; for (int i=0;inv; PV(s,rr*cos(a),y,rr*sin(a),0,0,0); if(first<0)first=idx; if(prev>=0)AE(s,prev,idx); prev=idx; } AE(s,prev,first); } for (int m=0;m<2;m++){ double mo=TAU*m/4; int first=-1, prev=-1; for (int i=0;inv; PV(s,x1,y,z1,0,0,0); if(first<0)first=idx; if(prev>=0)AE(s,prev,idx); prev=idx; } AE(s,prev,first); } for (int rg=0;rgnv; PV(s,x,y1,z1,0,0,0); if(first<0)first=idx; if(prev>=0)AE(s,prev,idx); prev=idx; } AE(s,prev,first); } center_normalize(s); } /* ball-and-stick molecule: a central ball bonded to balls in each direction */ static void gen_molecule3d(int which) { Solid *s=new_solid(3); double d[40][3]; int nd=dir_set(which,d); int c=s->nv; PV(s,0,0,0,0,0,0); add_ball(s,0,0,0, 0,0,1, 0.2, 6); for (int i=0;inv; PV(s,tx,ty,tz,0,0,0); AE(s,c,tip); add_ball(s,tx,ty,tz,d[i][0],d[i][1],d[i][2],0.17,6); } center_normalize(s); } /* diabolo / hourglass burst: two cones tip-to-tip with a waisted lattice */ static void gen_diabolo3d(int nspokes) { Solid *s=new_solid(3); int top=s->nv; PV(s,0,0,1.0,0,0,0); int bot=s->nv; PV(s,0,0,-1.0,0,0,0); int rt=s->nv; for(int i=0;inv; for(int i=0;inv; PV(s,0,0,0,0,0,0); for (int i=0;inv; PV(s,r*cos(a),r*sin(a),0,0,0,0); AE(s,c,tip); } int z1=s->nv; PV(s,0,0,0.7,0,0,0); AE(s,c,z1); int z2=s->nv; PV(s,0,0,-0.7,0,0,0); AE(s,c,z2); center_normalize(s); } /* ---- expressive faces and the '@' sign (flat + extruded) ---------- */ static void build_frowny(SymB *b) { sym_ring(b, 0,0, 1.0, 18); sym_ring(b, -0.35, 0.32, 0.12, 6); sym_ring(b, 0.35, 0.32, 0.12, 6); sym_arc (b, 0.0, -0.55, 0.5, sym_deg(20), sym_deg(160), 8); /* downturned mouth */ } static void build_angry(SymB *b) { sym_ring(b, 0,0, 1.0, 18); sym_ring(b, -0.33, 0.26, 0.11, 6); sym_ring(b, 0.33, 0.26, 0.11, 6); sym_line(b, -0.58, 0.62, -0.14, 0.42); /* angry V brows: inner ends low */ sym_line(b, 0.58, 0.62, 0.14, 0.42); sym_arc (b, 0.0, -0.58, 0.48, sym_deg(20), sym_deg(160), 8); /* frown */ } static void build_at(SymB *b) { sym_arc (b, 0.0, 0.0, 1.0, sym_deg(-25), sym_deg(295), 20); /* outer ring, open at right */ sym_ring(b, -0.05, 0.0, 0.40, 12); /* inner 'a' bowl */ sym_line(b, 0.35, 0.42, 0.35, -0.55); /* 'a' stem / tail */ } /* ---- pop-culture / object iconography (flat 2-D + extruded 3-D) ---- */ static void build_invader(SymB *b) { /* blocky pixel-alien (Space Invader) */ const double body[] = { -0.7, 0.2, -0.7,-0.2, -0.5,-0.2, -0.5,-0.5, -0.25,-0.5, -0.25,-0.25, 0.25,-0.25, 0.25,-0.5, 0.5,-0.5, 0.5,-0.2, 0.7,-0.2, 0.7, 0.2, 0.45,0.5, 0.2,0.5, 0.2,0.78, 0.05,0.78, 0.05,0.5, -0.05,0.5, -0.05,0.78, -0.2,0.78, -0.2,0.5, -0.45,0.5 }; sym_poly(b, body, 22, 1); sym_ring(b, -0.25, 0.12, 0.08, 5); sym_ring(b, 0.25, 0.12, 0.08, 5); } static void build_ufo(SymB *b) { /* flying saucer */ sym_ellipse(b, 0,0, 1.0, 0.30, 0, 22); /* saucer disc */ sym_arc(b, 0.0, 0.06, 0.46, sym_deg(0), sym_deg(180), 11); /* dome */ for (int i=-2;i<=2;i++) sym_ring(b, i*0.32, -0.22, 0.05, 4); /* lights */ } static void build_pacman(SymB *b) { /* wedge-mouth + pellet */ sym_arc(b, 0,0, 1.0, sym_deg(36), sym_deg(324), 24); int last = b->np - 1, c = sym_pt(b, 0, 0); sym_edge(b, last, c); sym_edge(b, c, 0); /* close the mouth wedge */ sym_ring(b, 0.05, 0.45, 0.09, 5); /* eye */ sym_ring(b, 1.35, 0.0, 0.12, 6); /* pellet */ } static void build_alien(SymB *b) { /* grey alien head */ int seg=22, first=-1, prev=-1; for (int i=0;i0 ? y*0.95 : y*1.28); if(first<0)first=idx; if(prev>=0)sym_edge(b,prev,idx); prev=idx; } sym_edge(b, prev, first); sym_ellipse(b, -0.26, 0.05, 0.22, 0.10, sym_deg( 28), 9); /* almond eyes */ sym_ellipse(b, 0.26, 0.05, 0.22, 0.10, sym_deg(-28), 9); } static void build_pipe(SymB *b) { /* smoking pipe */ sym_arc(b, -0.55, 0.0, 0.34, sym_deg(18), sym_deg(342), 14); /* bowl, open at top */ const double stem[] = { -0.28,-0.22, 0.2,-0.32, 0.7,-0.3, 0.98,-0.18 }; sym_poly(b, stem, 4, 0); sym_line(b, 0.98,-0.18, 1.04,-0.04); /* mouthpiece */ } static void build_umbrella(SymB *b) { /* canopy + ribs + pole + J-handle */ double R = 0.95; int n = 4; sym_arc(b, 0, 0.0, R, sym_deg(0), sym_deg(180), 14); /* canopy dome */ for (int i=0;itm_hour%12, m=lt->tm_min, s=lt->tm_sec; double ha=TAU*0.25 - TAU*((h + m/60.0)/12.0); /* 12 at top, clockwise */ double ma=TAU*0.25 - TAU*(m/60.0); double sa=TAU*0.25 - TAU*(s/60.0); int c=sym_pt(b,0,0); sym_edge(b, c, sym_pt(b, 0.50*cos(ha), 0.50*sin(ha))); /* hour */ sym_edge(b, c, sym_pt(b, 0.74*cos(ma), 0.74*sin(ma))); /* minute */ sym_edge(b, c, sym_pt(b, 0.84*cos(sa), 0.84*sin(sa))); /* second */ } static void build_digital(SymB *b, const struct tm *lt) { int hh=lt->tm_hour, mm=lt->tm_min, d[4]={hh/10,hh%10,mm/10,mm%10}; double w=0.30, h=0.30, cw=0.42, oy=-0.3; double x[4]={-0.92,-0.92+cw, 0.18, 0.18+cw}; for (int i=0;i<4;i++) sym_7seg(b, d[i], x[i], oy, w, h); sym_ring(b, -0.02, oy+0.42*h, 0.035, 4); /* colon dots */ sym_ring(b, -0.02, oy+1.4*h, 0.035, 4); const double fr[]={-1.12,-0.55, 1.12,-0.55, 1.12,0.55, -1.12,0.55}; sym_poly(b, fr, 4, 1); /* bezel keeps size constant */ } /* Rewrite the four clock solids in place from the current local time. */ static void update_clocks(void) { if (clock_idx[0] < 0) return; time_t t = time(NULL); struct tm *lt = localtime(&t); SymB b; sym_init(&b); build_analog (&b, lt); sym_fill_solid(&solids[clock_idx[0]], &b, 0.0f); sym_init(&b); build_analog (&b, lt); sym_fill_solid(&solids[clock_idx[1]], &b, 0.5f); sym_init(&b); build_digital(&b, lt); sym_fill_solid(&solids[clock_idx[2]], &b, 0.0f); sym_init(&b); build_digital(&b, lt); sym_fill_solid(&solids[clock_idx[3]], &b, 0.5f); } /* 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 (£) */ /* --- 30+ more googie / atomic-age 3-D objects --- */ gen_burst3d(0, 1.0, 0.0, 0); /* 6-spike asterisk star */ gen_burst3d(1, 1.0, 0.0, 0); /* 8-spike cube burst */ gen_burst3d(2, 1.0, 0.55, 0); /* 12-spike spiky star */ gen_burst3d(3, 1.0, 0.0, 0); /* 20-spike sea urchin */ gen_burst3d(4, 1.0, 0.6, 0); /* 32-spike dense urchin */ gen_burst3d(4, 1.0, 0.0, 0); /* 32-spike uniform urchin */ gen_burst3d(2, 1.0, 0.0, 1); /* 12 ball-tipped sputnik */ gen_burst3d(0, 1.0, 0.0, 1); /* 6 ball-tipped jacks */ gen_burst3d(1, 1.0, 0.0, 1); /* 8 ball-tipped burst */ gen_burst3d(3, 1.0, 0.0, 1); /* 20 ball-tipped burst */ gen_burst3d(2, 1.0, 0.4, 1); /* 12 alt ball-tipped burst */ gen_rings3d(2, 24); /* crossed rings */ gen_rings3d(3, 22); /* gyroscope */ gen_rings3d(4, 20); /* 4-ring cage */ gen_rings3d(5, 20); /* 5-ring cage */ gen_rings3d(6, 20); /* 6-ring orb cage */ gen_atomorbits(2, 24); /* 2-orbit atom */ gen_atomorbits(4, 20); /* 4-orbit atom */ gen_atomorbits(5, 18); /* 5-orbit atom */ gen_atomorbits(6, 16); /* 6-orbit atom */ gen_saturn3d(1, 0.0); /* ringed planet */ gen_saturn3d(2, 0.35); /* double-ringed planet */ gen_saturn3d(1, 0.6); /* tilted-ring planet */ gen_molecule3d(0); /* octahedral molecule */ gen_molecule3d(1); /* cubic molecule */ gen_molecule3d(2); /* icosahedral molecule */ gen_diabolo3d(6); /* hex diabolo */ gen_diabolo3d(10); /* diabolo / hourglass */ gen_diabolo3d(16); /* fine diabolo */ gen_raystar3d(12); /* 12-ray starburst */ gen_raystar3d(16); /* 16-ray starburst */ gen_raystar3d(24); /* 24-ray sunburst */ /* --- expressive faces + the @ sign (flat 2-D and extruded 3-D) --- */ add_symbol(build_angry); /* angry face */ add_symbol(build_frowny); /* frowny face */ add_symbol(build_at); /* at sign (@) */ /* --- object iconography (flat 2-D and extruded 3-D each) --- */ add_symbol(build_invader); /* space invader */ add_symbol(build_ufo); /* flying saucer */ add_symbol(build_pacman); /* pac-man */ add_symbol(build_alien); /* alien head */ add_symbol(build_pipe); /* smoking pipe */ add_symbol(build_umbrella); /* umbrella */ add_symbol(build_hand); /* hand */ /* --- live clocks: reserve 4 slots, then fill from the current time * (refreshed every frame by update_clocks() in the main loop) --- */ for (int i = 0; i < 4; i++) { clock_idx[i] = num_shapes; new_solid(3); } update_clocks(); /* --- 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); update_clocks(); /* keep the clock shapes showing the current time */ 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; }