// Command cubegif generates an animated GIF of a cube rotating through // its 6 canonical S_3 viewing angles. At each vertex, the projection // glyph is drawn using the dendrite 8-color scheme. // // Usage: go run ./cmd/cubegif [-o output.gif] package main import ( "flag" "fmt" "image" "image/color" "image/gif" "math" "os" ) const ( width = 480 height = 480 cx = width / 2 // center x cy = height / 2 // center y scale = 120.0 // cube half-size in pixels ) // 8-color palette: black, red, orange, yellow, green, blue, purple, white. // Plus background (dark navy) and wireframe gray. var palette = []color.Color{ color.RGBA{15, 23, 42, 255}, // 0: background color.RGBA{26, 26, 26, 255}, // 1: black (vertex 000) color.RGBA{239, 68, 68, 255}, // 2: red (vertex 001) color.RGBA{249, 115, 22, 255}, // 3: orange (vertex 010) color.RGBA{234, 179, 8, 255}, // 4: yellow (vertex 011) color.RGBA{34, 197, 94, 255}, // 5: green (vertex 100) color.RGBA{59, 130, 246, 255}, // 6: blue (vertex 101) color.RGBA{168, 85, 247, 255}, // 7: purple (vertex 110) color.RGBA{240, 240, 240, 255}, // 8: white (vertex 111) color.RGBA{71, 85, 105, 255}, // 9: wireframe gray color.RGBA{148, 163, 184, 255}, // 10: label gray color.RGBA{226, 232, 240, 255}, // 11: bright label } const ( colBG = 0 colBlack = 1 colRed = 2 colOrange = 3 colYellow = 4 colGreen = 5 colBlue = 6 colPurple = 7 colWhite = 8 colWireframe = 9 colLabel = 10 colBright = 11 ) // vertexColor maps vertex index (0-7) to palette index. var vertexColor = [8]uint8{ colBlack, colRed, colOrange, colYellow, colGreen, colBlue, colPurple, colWhite, } // Bit-to-color: bit₀ = red, bit₁ = orange, bit₂ = green. var bitColor = [3]uint8{colRed, colOrange, colGreen} // 3D point. type vec3 struct{ x, y, z float64 } // 8 cube vertices at (±1, ±1, ±1), indexed by 3-bit address. // Bit 0 → x axis (bonding), bit 1 → y axis (constraint), bit 2 → z axis (energy). var cubeVerts [8]vec3 func init() { for i := range 8 { cubeVerts[i] = vec3{ x: btof(i & 1), y: btof((i >> 1) & 1), z: btof((i >> 2) & 1), } } } // btof maps 0 → -1, 1 → +1. func btof(b int) float64 { return float64(b)*2 - 1 } // 12 cube edges: pairs of vertices differing by exactly 1 bit. var cubeEdges = [12][2]int{ {0, 1}, {0, 2}, {0, 4}, {1, 3}, {1, 5}, {2, 3}, {2, 6}, {3, 7}, {4, 5}, {4, 6}, {5, 7}, {6, 7}, } // Rotation matrix: rotate by angle around a unit axis (ax, ay, az). func rotateAxis(v vec3, ax, ay, az, angle float64) vec3 { c := math.Cos(angle) s := math.Sin(angle) t := 1 - c return vec3{ x: (t*ax*ax + c)*v.x + (t*ax*ay - s*az)*v.y + (t*ax*az + s*ay)*v.z, y: (t*ax*ay + s*az)*v.x + (t*ay*ay + c)*v.y + (t*ay*az - s*ax)*v.z, z: (t*ax*az - s*ay)*v.x + (t*ay*az + s*ax)*v.y + (t*az*az + c)*v.z, } } // Composite rotation: first around Y by yaw, then around X by pitch. func rotate(v vec3, yaw, pitch float64) vec3 { // Yaw around Y axis. v = rotateAxis(v, 0, 1, 0, yaw) // Pitch around X axis. v = rotateAxis(v, 1, 0, 0, pitch) return v } // project maps a 3D point to 2D screen coordinates (orthographic). func project(v vec3) (int, int) { return int(cx + v.x*scale), int(cy - v.y*scale) } // viewAngle defines a viewing direction for one frame. type viewAngle struct { yaw, pitch float64 name string permName string } // tilt adds a small offset so vertices never perfectly overlap. // This ensures all 8 vertices are distinguishable in every frame, // making all 48 = 2^n × n! configurations visible across 6 frames. const tilt = 0.21 // ~12 degrees // 6 canonical viewing angles, one per S_3 permutation. // Each has a small tilt so back-face vertices are displaced from front ones. var views = []viewAngle{ // Face-on views: tilted slightly so all 8 vertices separate. {tilt, tilt, "Face XY", "Identity"}, // near -Z axis {math.Pi/2 + tilt, tilt, "Face XZ", "Swap(C,E)"}, // near -Y axis {tilt, math.Pi/2 + tilt, "Face YZ", "Swap(B,E)"}, // near -X axis {math.Pi/4 + tilt/2, tilt, "Edge-on", "Swap(B,C)"}, // edge view {math.Pi / 4, math.Atan(1/math.Sqrt2) + tilt/3, "Vertex A", "Cycle(012)"}, // isometric A {-math.Pi / 4, math.Atan(1/math.Sqrt2) + tilt/3, "Vertex B", "Cycle(021)"}, // isometric B } func main() { outPath := flag.String("o", "projection_cube.gif", "output file path") flag.Parse() g := &gif.GIF{} for _, view := range views { img := image.NewPaletted(image.Rect(0, 0, width, height), palette) // Fill background. for y := range height { for x := range width { img.SetColorIndex(x, y, colBG) } } // Transform all vertices. var projected [8]vec3 var screenX, screenY [8]int for i := range 8 { projected[i] = rotate(cubeVerts[i], view.yaw, view.pitch) screenX[i], screenY[i] = project(projected[i]) } // Draw edges (sorted by average depth — back edges first). type edgeDepth struct { i, j int depth float64 } var edges []edgeDepth for _, e := range cubeEdges { avgZ := (projected[e[0]].z + projected[e[1]].z) / 2 edges = append(edges, edgeDepth{e[0], e[1], avgZ}) } // Sort: deeper edges first (they get drawn underneath). for i := range len(edges) { for j := i + 1; j < len(edges); j++ { if edges[j].depth < edges[i].depth { edges[i], edges[j] = edges[j], edges[i] } } } for _, e := range edges { drawLine(img, screenX[e.i], screenY[e.i], screenX[e.j], screenY[e.j], colWireframe) } // Sort vertices by depth (back to front) for drawing. type vertDepth struct { idx int depth float64 } var verts []vertDepth for i := range 8 { verts = append(verts, vertDepth{i, projected[i].z}) } for i := range len(verts) { for j := i + 1; j < len(verts); j++ { if verts[j].depth < verts[i].depth { verts[i], verts[j] = verts[j], verts[i] } } } // Draw glyphs and vertices (back to front). for _, vd := range verts { v := vd.idx sx, sy := screenX[v], screenY[v] // Draw glyph arms: for each set bit, draw a short line in that bit's color. armLen := 18.0 for bit := range 3 { if v&(1<= x1 { sx = -1 } if y0 >= y1 { sy = -1 } err := dx + dy for { if x0 >= 0 && x0 < width && y0 >= 0 && y0 < height { img.SetColorIndex(x0, y0, col) } if x0 == x1 && y0 == y1 { break } e2 := 2 * err if e2 >= dy { err += dy x0 += sx } if e2 <= dx { err += dx y0 += sy } } } // drawThickLine draws a line with thickness (by offsetting perpendicular). func drawThickLine(img *image.Paletted, x0, y0, x1, y1 int, col uint8, thickness int) { if thickness <= 1 { drawLine(img, x0, y0, x1, y1, col) return } dx := float64(x1 - x0) dy := float64(y1 - y0) length := math.Sqrt(dx*dx + dy*dy) if length < 0.5 { return } // Perpendicular unit vector. px := -dy / length py := dx / length half := float64(thickness) / 2 for t := -half; t <= half; t += 0.5 { ox := int(math.Round(px * t)) oy := int(math.Round(py * t)) drawLine(img, x0+ox, y0+oy, x1+ox, y1+oy, col) } } // fillCircle draws a filled circle. func fillCircle(img *image.Paletted, cx, cy, r int, col uint8) { for dy := -r; dy <= r; dy++ { for dx := -r; dx <= r; dx++ { if dx*dx+dy*dy <= r*r { px, py := cx+dx, cy+dy if px >= 0 && px < width && py >= 0 && py < height { img.SetColorIndex(px, py, col) } } } } } // drawString renders text using a simple built-in 5x7 bitmap font. // Only supports ASCII printable characters. func drawString(img *image.Paletted, x, y int, s string, col uint8) { for _, ch := range s { if ch < 32 || ch > 126 { ch = '?' } glyph := font5x7[ch-32] for row := range 7 { for col_bit := range 5 { if glyph[row]&(1<<(4-col_bit)) != 0 { px, py := x+col_bit, y+row if px >= 0 && px < width && py >= 0 && py < height { img.SetColorIndex(px, py, col) } } } } x += 6 // 5 pixels + 1 space } } func abs(x int) int { if x < 0 { return -x } return x } // font5x7 is a minimal 5×7 bitmap font for ASCII 32-126. // Each character is 7 rows of 5-bit bitmaps (MSB = leftmost pixel). var font5x7 = [95][7]uint8{ // ' ' (32) {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // '!' (33) {0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04}, // '"' (34) {0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00}, // '#' (35) {0x0A, 0x1F, 0x0A, 0x0A, 0x1F, 0x0A, 0x00}, // '$' (36) {0x04, 0x0F, 0x14, 0x0E, 0x05, 0x1E, 0x04}, // '%' (37) {0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03}, // '&' (38) {0x08, 0x14, 0x14, 0x08, 0x15, 0x12, 0x0D}, // '\'' (39) {0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}, // '(' (40) {0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02}, // ')' (41) {0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08}, // '*' (42) {0x00, 0x0A, 0x04, 0x1F, 0x04, 0x0A, 0x00}, // '+' (43) {0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00}, // ',' (44) {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08}, // '-' (45) {0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00}, // '.' (46) {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04}, // '/' (47) {0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10}, // '0' (48) {0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E}, // '1' (49) {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}, // '2' (50) {0x0E, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1F}, // '3' (51) {0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E}, // '4' (52) {0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02}, // '5' (53) {0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E}, // '6' (54) {0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E}, // '7' (55) {0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, // '8' (56) {0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E}, // '9' (57) {0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C}, // ':' (58) {0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00}, // ';' (59) {0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x08}, // '<' (60) {0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02}, // '=' (61) {0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00}, // '>' (62) {0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08}, // '?' (63) {0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04}, // '@' (64) {0x0E, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0E}, // 'A' (65) {0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // 'B' (66) {0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E}, // 'C' (67) {0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E}, // 'D' (68) {0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E}, // 'E' (69) {0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F}, // 'F' (70) {0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10}, // 'G' (71) {0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F}, // 'H' (72) {0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // 'I' (73) {0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E}, // 'J' (74) {0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E}, // 'K' (75) {0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}, // 'L' (76) {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F}, // 'M' (77) {0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11}, // 'N' (78) {0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11}, // 'O' (79) {0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, // 'P' (80) {0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10}, // 'Q' (81) {0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D}, // 'R' (82) {0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11}, // 'S' (83) {0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E}, // 'T' (84) {0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, // 'U' (85) {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, // 'V' (86) {0x11, 0x11, 0x11, 0x11, 0x0A, 0x0A, 0x04}, // 'W' (87) {0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11}, // 'X' (88) {0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11}, // 'Y' (89) {0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04}, // 'Z' (90) {0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F}, // '[' (91) {0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E}, // '\' (92) {0x10, 0x08, 0x08, 0x04, 0x02, 0x02, 0x01}, // ']' (93) {0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E}, // '^' (94) {0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00}, // '_' (95) {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F}, // '`' (96) {0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}, // 'a' (97) {0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F}, // 'b' (98) {0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E}, // 'c' (99) {0x00, 0x00, 0x0E, 0x11, 0x10, 0x11, 0x0E}, // 'd' (100) {0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F}, // 'e' (101) {0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E}, // 'f' (102) {0x06, 0x08, 0x1E, 0x08, 0x08, 0x08, 0x08}, // 'g' (103) {0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x0E}, // 'h' (104) {0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x11}, // 'i' (105) {0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E}, // 'j' (106) {0x02, 0x00, 0x02, 0x02, 0x02, 0x12, 0x0C}, // 'k' (107) {0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12}, // 'l' (108) {0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E}, // 'm' (109) {0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11}, // 'n' (110) {0x00, 0x00, 0x1E, 0x11, 0x11, 0x11, 0x11}, // 'o' (111) {0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E}, // 'p' (112) {0x00, 0x00, 0x1E, 0x11, 0x1E, 0x10, 0x10}, // 'q' (113) {0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x01}, // 'r' (114) {0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10}, // 's' (115) {0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E}, // 't' (116) {0x08, 0x08, 0x1E, 0x08, 0x08, 0x09, 0x06}, // 'u' (117) {0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D}, // 'v' (118) {0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04}, // 'w' (119) {0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A}, // 'x' (120) {0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11}, // 'y' (121) {0x00, 0x00, 0x11, 0x11, 0x0F, 0x01, 0x0E}, // 'z' (122) {0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F}, // '{' (123) {0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02}, // '|' (124) {0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, // '}' (125) {0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08}, // '~' (126) {0x00, 0x00, 0x08, 0x15, 0x02, 0x00, 0x00}, }