ring-layout-visualizer.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>InterBrain Ring Layout Algorithm - Interactive Visualizer</title> 5 <style> 6 body { 7 font-family: Arial, sans-serif; 8 margin: 20px; 9 background: #1a1a1a; 10 color: white; 11 } 12 canvas { 13 border: 1px solid #444; 14 background: #000; 15 margin: 10px 0; 16 } 17 .controls { 18 margin: 20px 0; 19 padding: 20px; 20 background: #333; 21 border-radius: 8px; 22 } 23 input[type="range"] { 24 width: 200px; 25 margin: 0 10px; 26 } 27 .info { 28 background: #444; 29 padding: 15px; 30 border-radius: 8px; 31 margin: 10px 0; 32 font-family: monospace; 33 } 34 </style> 35 </head> 36 <body> 37 <h1>Ring Layout Algorithm Visualizer</h1> 38 39 <div style="background: #2d4a22; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #4caf50;"> 40 <strong>Documentation Reference</strong><br> 41 This interactive tool demonstrates the mathematical precision of InterBrain's honeycomb ring layout algorithm. 42 It shows how 1-36 nodes are positioned using a 42-coordinate system with intelligent selection patterns. 43 <br><em>This is for learning and reference only - not part of the active application.</em> 44 </div> 45 46 <div class="controls"> 47 <label>Node Count: <input type="range" id="nodeCount" min="1" max="36" value="6"> <span id="nodeCountValue">6</span></label> 48 <br><br> 49 <button onclick="setNodeCount(1)">1 Node</button> 50 <button onclick="setNodeCount(6)">6 Nodes</button> 51 <button onclick="setNodeCount(7)">7 Nodes</button> 52 <button onclick="setNodeCount(12)">12 Nodes</button> 53 <button onclick="setNodeCount(18)">18 Nodes</button> 54 <button onclick="setNodeCount(24)">24 Nodes</button> 55 <button onclick="setNodeCount(36)">36 Nodes</button> 56 <br><br> 57 <button onclick="previousNode()">Previous</button> 58 <button onclick="nextNode()">Next</button> 59 <button onclick="visualizeProgression()">Auto Progression (1-36)</button> 60 </div> 61 62 <div class="info" id="info"> 63 Ring 1: 0 nodes | Ring 2: 0 nodes | Ring 3: 0 nodes 64 </div> 65 66 <div style="display: flex; gap: 20px;"> 67 <div> 68 <h3>Dynamic Visualization</h3> 69 <canvas id="canvas" width="800" height="800"></canvas> 70 </div> 71 <div> 72 <h3>Static 42-Node Coordinate System</h3> 73 <canvas id="staticCanvas" width="800" height="800"></canvas> 74 <div style="margin-top: 10px; font-family: monospace; font-size: 12px;"> 75 <strong>42 Possible Coordinates:</strong><br> 76 <strong>Ring 1:</strong> Nodes 1-6 (center hexagon)<br> 77 <strong>Ring 2:</strong> Nodes 7-18 (12 nodes: 6 corners + 6 edge midpoints)<br> 78 <strong>Ring 3:</strong> Nodes 19-42 (24 nodes: 6 corners + 18 edge positions)<br> 79 <em>Up to 36 nodes displayed using intelligent coordinate selection</em> 80 </div> 81 </div> 82 </div> 83 84 <script> 85 // Improved hexagonal ring positioning algorithm for optimal geometric balance 86 function calculateHexagonalRingPositions(maxNodes, radius, zDistance) { 87 if (maxNodes === 0) return []; 88 89 const positions = []; 90 91 if (maxNodes <= 6) { 92 // Ring 1: Equidistant spacing on circle (dynamic repositioning) 93 let startAngle = -Math.PI / 2; // Default: start at top (point up) 94 95 // For exactly 6 nodes, rotate by 30° so flat edge is at top 96 if (maxNodes === 6) { 97 startAngle = -Math.PI / 2 + Math.PI / 6; // Rotate by 30° (π/6 radians) 98 } 99 100 for (let i = 0; i < Math.min(maxNodes, 6); i++) { 101 const angle = (i / maxNodes) * 2 * Math.PI + startAngle; 102 const x = radius * Math.cos(angle); 103 const y = radius * Math.sin(angle); 104 positions.push([x, y, zDistance]); 105 } 106 } else if (maxNodes <= 12) { 107 // Ring 2: Fixed positions - midpoints between inner rays first (more circular) 108 109 // Define all 12 fixed positions for Ring 2 110 const allRing2Positions = []; 111 112 // First 6 positions: specific angles for proper rectangle formation 113 // Position 0: -60° (top-right for rectangle) 114 // Position 1: +60° (top-left for rectangle) 115 // Position 2: -120° (bottom-right for rectangle) 116 // Position 3: +120° (bottom-left for rectangle) 117 // Position 4: 0° (top center) 118 // Position 5: 180° (bottom center) 119 const specificAngles = [ 120 -Math.PI / 3, // -60° (top-right) 121 Math.PI / 3, // +60° (top-left) 122 -2 * Math.PI / 3, // -120° (bottom-right) 123 2 * Math.PI / 3, // +120° (bottom-left) 124 0, // 0° (top center) 125 Math.PI // 180° (bottom center) 126 ]; 127 128 for (let i = 0; i < 6; i++) { 129 const angle = specificAngles[i]; 130 const x = radius * Math.cos(angle); 131 const y = radius * Math.sin(angle); 132 allRing2Positions.push([x, y, zDistance]); 133 } 134 135 // Next 6 positions: aligned with inner rays 136 // Same angles as inner ring: -90°, -30°, 30°, 90°, 150°, 210° 137 for (let i = 0; i < 6; i++) { 138 const angle = (i / 6) * 2 * Math.PI - Math.PI / 2; // Same as Ring 1 139 const x = radius * Math.cos(angle); 140 const y = radius * Math.sin(angle); 141 allRing2Positions.push([x, y, zDistance]); 142 } 143 144 // Optimal placement order for rectangle formation 145 const placementOrder = [ 146 5, // 1st: Bottom center (180°) 147 4, // 2nd: Top center (0°) - vertical line 148 2, // 3rd: Bottom-right (-120°) - triangle 149 0, // 4th: Top-right (-60°) - rectangle! 150 3, 1, // 5th, 6th: Bottom-left (+120°) and Top-left (+60°) 151 10, 7, // 7th, 8th: Bottom and top corners (aligned with inner) 152 11, 8, // 9th, 10th: Bottom-right and top-left corners 153 6, 9 // 11th, 12th: Top-right and bottom-left corners 154 ]; 155 156 // Place nodes according to optimal order (fixed positions) 157 for (let i = 0; i < Math.min(maxNodes - 6, 12); i++) { 158 const posIndex = placementOrder[i]; 159 positions.push(allRing2Positions[posIndex]); 160 } 161 } else { 162 // Ring 3: Single coherent hexagon - fixed positions at same radius 163 // 18 total positions: 6 corners + 12 edge positions (2 between each corner pair) 164 165 const allRing3Positions = []; 166 167 // Generate all 18 evenly distributed positions around the circle 168 for (let i = 0; i < 18; i++) { 169 const angle = (i / 18) * 2 * Math.PI; // Every 20 degrees 170 const x = radius * Math.cos(angle); 171 const y = radius * Math.sin(angle); 172 allRing3Positions.push({pos: [x, y, zDistance], angle: angle}); 173 } 174 175 // Optimal placement order for maximum symmetry at each step 176 // Start with maximally separated positions, then fill in systematically 177 const placementOrder = [ 178 0, 9, // Opposite positions (0°, 180°) 179 3, 12, // 60°, 240° (perpendicular) 180 6, 15, // 120°, 300° (complete 6-fold symmetry) 181 1, 10, // 20°, 200° (fill between) 182 4, 13, // 80°, 260° 183 7, 16, // 140°, 320° 184 2, 11, // 40°, 220° 185 5, 14, // 100°, 280° 186 8, 17 // 160°, 340° (final positions) 187 ]; 188 189 // Place nodes according to optimal symmetric order (fixed positions) 190 for (let i = 0; i < Math.min(maxNodes - 18, 18); i++) { 191 const posIndex = placementOrder[i]; 192 positions.push(allRing3Positions[posIndex].pos); 193 } 194 } 195 196 return positions; 197 } 198 199 // Generate all 42 static node positions using the same logic as the static coordinate system 200 function generateAll36StaticPositions() { // Keep function name for compatibility 201 const allPositions = []; 202 const scale = 1; // Use unscaled world coordinates 203 204 // Ring 1: Nodes 1-6 (same as existing logic) 205 const ring1StartAngle = -Math.PI / 2 + Math.PI / 6; // 30° rotation for flat edge at top 206 for (let i = 0; i < 6; i++) { 207 const angle = (i / 6) * 2 * Math.PI + ring1StartAngle; 208 const x = RING_RADII[0] * Math.cos(angle); 209 const y = RING_RADII[0] * Math.sin(angle); 210 allPositions.push([x, y, 0]); 211 } 212 213 // Ring 2: Nodes 7-18 (6 edge positions + 6 corner positions) 214 const ring2EdgeRadius = RING_RADII[1] * Math.cos(Math.PI / 6); 215 // First 6 nodes (7-12): edge positions (reduced radius) 216 for (let i = 0; i < 6; i++) { 217 const angle = (i / 6) * 2 * Math.PI - Math.PI / 2; 218 const x = ring2EdgeRadius * Math.cos(angle); 219 const y = ring2EdgeRadius * Math.sin(angle); 220 allPositions.push([x, y, 0]); 221 } 222 // Next 6 nodes (13-18): corner positions (full radius) 223 for (let i = 0; i < 6; i++) { 224 const angle = (i / 6) * 2 * Math.PI - Math.PI / 2 + Math.PI / 6; 225 const x = RING_RADII[1] * Math.cos(angle); 226 const y = RING_RADII[1] * Math.sin(angle); 227 allPositions.push([x, y, 0]); 228 } 229 230 // Ring 3: Nodes 19-36 (6 vertices + 12 edge nodes using path parameterization) 231 const hexagonAngles = [30, 90, 150, 210, 270, 330]; 232 const baseRadius = RING_RADII[2]; 233 const vertexPositions = []; 234 235 // Calculate vertex positions (19-24) 236 for (let i = 0; i < 6; i++) { 237 const angleDegrees = hexagonAngles[i]; 238 const angleRadians = (angleDegrees - 90) * Math.PI / 180; 239 const x = baseRadius * Math.cos(angleRadians); 240 const y = baseRadius * Math.sin(angleRadians); 241 vertexPositions.push([x, y]); 242 allPositions.push([x, y, 0]); // Add vertex positions (19-24) 243 } 244 245 // Path parameterization for edge nodes (25-36) 246 function lerpPath(pointA, pointB, t) { 247 return [ 248 pointA[0] + t * (pointB[0] - pointA[0]), 249 pointA[1] + t * (pointB[1] - pointA[1]) 250 ]; 251 } 252 253 // Add edge nodes using path parameterization (25-36) 254 for (let edgeIndex = 0; edgeIndex < 6; edgeIndex++) { 255 const startVertex = vertexPositions[edgeIndex]; 256 const endVertex = vertexPositions[(edgeIndex + 1) % 6]; 257 258 for (let nodeOnEdge = 0; nodeOnEdge < 2; nodeOnEdge++) { 259 const t = (nodeOnEdge + 1) / 3; // t = 1/3, 2/3 260 const [worldX, worldY] = lerpPath(startVertex, endVertex, t); 261 allPositions.push([worldX, worldY, 0]); 262 } 263 } 264 265 // Add 6 additional edge midpoint nodes (37-42) at t=0.5 266 for (let edgeIndex = 0; edgeIndex < 6; edgeIndex++) { 267 const startVertex = vertexPositions[edgeIndex]; 268 const endVertex = vertexPositions[(edgeIndex + 1) % 6]; 269 270 const t = 0.5; // Exact midpoint 271 const [worldX, worldY] = lerpPath(startVertex, endVertex, t); 272 allPositions.push([worldX, worldY, 0]); // Nodes 37-42 273 } 274 275 return allPositions; 276 } 277 278 // Helper function to activate Ring 2 nodes (always full for node counts > 18) 279 function activateRing2(mask) { 280 for (let i = 6; i < 18; i++) { 281 mask[i] = true; // Nodes 7-18 (indices 6-17) 282 } 283 } 284 285 // Helper function to activate specific Ring 3 node sets 286 function activateRing3Nodes(mask, nodeNumbers) { 287 nodeNumbers.forEach(nodeNum => { 288 mask[nodeNum - 1] = true; // Convert 1-based to 0-based indexing 289 }); 290 } 291 292 // Define which nodes are active for each total node count (boolean mask approach) 293 function getActiveMask(totalNodes) { 294 const mask = new Array(42).fill(false); 295 296 // Always activate Ring 1 (nodes 1-6) for counts > 0 297 for (let i = 0; i < Math.min(totalNodes, 6); i++) { 298 mask[i] = true; // Nodes 1-6 (indices 0-5) 299 } 300 301 // If totalNodes <= 6, we're done 302 if (totalNodes <= 6) return mask; 303 304 // Ring 2 specific patterns (nodes 7-18, indices 6-17) 305 if (totalNodes === 7) { 306 mask[6] = true; // Node 7 307 } 308 else if (totalNodes === 8) { 309 mask[6] = true; // Node 7 310 mask[9] = true; // Node 10 311 } 312 else if (totalNodes === 9) { 313 mask[6] = true; // Node 7 314 mask[8] = true; // Node 9 315 mask[10] = true; // Node 11 316 } 317 else if (totalNodes === 10) { 318 mask[6] = true; // Node 7 (already in your description but adding for completeness) 319 mask[7] = true; // Node 8 320 mask[8] = true; // Node 9 321 mask[10] = true; // Node 11 322 mask[11] = true; // Node 12 323 // Wait, let me re-read: you want 12, 8, 9, 11 for 10 nodes 324 mask[6] = false; mask[7] = false; mask[8] = false; mask[10] = false; mask[11] = false; // Clear first 325 mask[11] = true; // Node 12 326 mask[7] = true; // Node 8 327 mask[8] = true; // Node 9 328 mask[10] = true; // Node 11 329 } 330 else if (totalNodes === 11) { 331 mask[6] = true; // Node 7 332 mask[7] = true; // Node 8 333 mask[8] = true; // Node 9 334 mask[10] = true; // Node 11 335 mask[11] = true; // Node 12 336 } 337 else if (totalNodes === 12) { 338 mask[6] = true; // Node 7 339 mask[7] = true; // Node 8 340 mask[8] = true; // Node 9 341 mask[9] = true; // Node 10 342 mask[10] = true; // Node 11 343 mask[11] = true; // Node 12 344 } 345 else if (totalNodes === 13) { 346 mask[6] = true; // Node 7 347 mask[7] = true; // Node 8 348 mask[8] = true; // Node 9 349 mask[14] = true; // Node 15 350 mask[15] = true; // Node 16 351 mask[10] = true; // Node 11 352 mask[11] = true; // Node 12 353 } 354 else if (totalNodes === 14) { 355 mask[17] = true; // Node 18 356 mask[12] = true; // Node 13 357 mask[7] = true; // Node 8 358 mask[8] = true; // Node 9 359 mask[14] = true; // Node 15 360 mask[15] = true; // Node 16 361 mask[10] = true; // Node 11 362 mask[11] = true; // Node 12 363 } 364 else if (totalNodes === 15) { 365 // All nodes from 14-node setup + Node 7 366 mask[17] = true; // Node 18 367 mask[12] = true; // Node 13 368 mask[7] = true; // Node 8 369 mask[8] = true; // Node 9 370 mask[14] = true; // Node 15 371 mask[15] = true; // Node 16 372 mask[10] = true; // Node 11 373 mask[11] = true; // Node 12 374 mask[6] = true; // Node 7 (additional) 375 } 376 else if (totalNodes === 16) { 377 // All nodes from 15-node setup + Node 10 378 mask[17] = true; // Node 18 379 mask[12] = true; // Node 13 380 mask[7] = true; // Node 8 381 mask[8] = true; // Node 9 382 mask[14] = true; // Node 15 383 mask[15] = true; // Node 16 384 mask[10] = true; // Node 11 385 mask[11] = true; // Node 12 386 mask[6] = true; // Node 7 387 mask[9] = true; // Node 10 (additional) 388 } 389 else if (totalNodes === 17) { 390 // 16-node setup + Node 17, Node 14, - Node 10 391 mask[17] = true; // Node 18 392 mask[12] = true; // Node 13 393 mask[7] = true; // Node 8 394 mask[8] = true; // Node 9 395 mask[14] = true; // Node 15 396 mask[15] = true; // Node 16 397 mask[10] = true; // Node 11 398 mask[11] = true; // Node 12 399 mask[6] = true; // Node 7 400 // mask[9] = false; // Node 10 (turn off) - already false by default 401 mask[16] = true; // Node 17 (additional) 402 mask[13] = true; // Node 14 (additional) 403 } 404 else if (totalNodes === 18) { 405 // All Ring 2 nodes (nodes 7-18) active 406 for (let i = 6; i < 18; i++) { 407 mask[i] = true; // Nodes 7-18 (indices 6-17) 408 } 409 } 410 411 // Ring 3 specific patterns (Ring 1 + Ring 2 always fully active for 19+) 412 else if (totalNodes === 19) { 413 activateRing2(mask); 414 activateRing3Nodes(mask, [42]); 415 } 416 else if (totalNodes === 20) { 417 activateRing2(mask); 418 activateRing3Nodes(mask, [42, 39]); 419 } 420 else if (totalNodes === 21) { 421 activateRing2(mask); 422 activateRing3Nodes(mask, [42, 38, 40]); 423 } 424 else if (totalNodes === 22) { 425 activateRing2(mask); 426 activateRing3Nodes(mask, [37, 38, 40, 41]); 427 } 428 else if (totalNodes === 23) { 429 activateRing2(mask); 430 activateRing3Nodes(mask, [37, 38, 40, 41, 42]); // 22-node setup + 42 431 } 432 else if (totalNodes === 24) { 433 activateRing2(mask); 434 activateRing3Nodes(mask, [37, 38, 39, 40, 41, 42]); // 23-node setup + 39 435 } 436 else if (totalNodes === 25) { 437 activateRing2(mask); 438 activateRing3Nodes(mask, [37, 38, 40, 41, 42, 29, 30]); // 24-node setup - 39 + 29,30 439 } 440 else if (totalNodes === 26) { 441 activateRing2(mask); 442 activateRing3Nodes(mask, [37, 38, 40, 41, 29, 30, 35, 36]); // 25-node setup - 42 + 35,36 443 } 444 else if (totalNodes === 27) { 445 // All Ring 2 nodes (7-18) active 446 for (let i = 6; i < 18; i++) { 447 mask[i] = true; 448 } 449 // Ring 3: Complex modifications from 26-node setup 450 mask[36] = true; // Node 37 (index 36) 451 // mask[37] = false; // Node 38 turned off (already false by default) 452 // mask[39] = false; // Node 40 turned off (already false by default) 453 mask[40] = true; // Node 41 (index 40) 454 // mask[28] = false; // Node 29 turned off (already false by default) 455 // mask[29] = false; // Node 30 turned off (already false by default) 456 mask[34] = true; // Node 35 (index 34) 457 mask[35] = true; // Node 36 (index 35) 458 mask[38] = true; // Node 39 (index 38) - turn back on 459 mask[26] = true; // Node 27 (index 26) - additional 460 mask[27] = true; // Node 28 (index 27) - additional 461 mask[30] = true; // Node 31 (index 30) - additional 462 mask[31] = true; // Node 32 (index 31) - additional 463 } 464 else if (totalNodes === 28) { 465 // All Ring 2 nodes (7-18) active 466 for (let i = 6; i < 18; i++) { 467 mask[i] = true; 468 } 469 // Ring 3: 27-node setup - Node 39 + Nodes 29, 30 470 mask[36] = true; // Node 37 (index 36) 471 mask[40] = true; // Node 41 (index 40) 472 mask[34] = true; // Node 35 (index 34) 473 mask[35] = true; // Node 36 (index 35) 474 // mask[38] = false; // Node 39 turned off (already false by default) 475 mask[26] = true; // Node 27 (index 26) 476 mask[27] = true; // Node 28 (index 27) 477 mask[30] = true; // Node 31 (index 30) 478 mask[31] = true; // Node 32 (index 31) 479 mask[28] = true; // Node 29 (index 28) - additional 480 mask[29] = true; // Node 30 (index 29) - additional 481 } 482 else if (totalNodes === 29) { 483 // All Ring 2 nodes (7-18) active 484 for (let i = 6; i < 18; i++) { 485 mask[i] = true; 486 } 487 // Ring 3: 28-node setup with complex modifications 488 // mask[36] = false; // Node 37 turned off (already false by default) 489 // mask[40] = false; // Node 41 turned off (already false by default) 490 mask[34] = true; // Node 35 (index 34) 491 mask[35] = true; // Node 36 (index 35) 492 mask[26] = true; // Node 27 (index 26) 493 mask[27] = true; // Node 28 (index 27) 494 mask[30] = true; // Node 31 (index 30) 495 mask[31] = true; // Node 32 (index 31) 496 // mask[28] = false; // Node 29 turned off (already false by default) 497 // mask[29] = false; // Node 30 turned off (already false by default) 498 mask[38] = true; // Node 39 (index 38) - additional 499 mask[32] = true; // Node 33 (index 32) - additional 500 mask[33] = true; // Node 34 (index 33) - additional 501 mask[24] = true; // Node 25 (index 24) - additional 502 mask[25] = true; // Node 26 (index 25) - additional 503 } 504 else if (totalNodes === 30) { 505 // All Ring 2 nodes (7-18) active 506 for (let i = 6; i < 18; i++) { 507 mask[i] = true; 508 } 509 // Ring 3: 29-node setup - Node 39 + Nodes 29, 30 510 mask[34] = true; // Node 35 (index 34) 511 mask[35] = true; // Node 36 (index 35) 512 mask[26] = true; // Node 27 (index 26) 513 mask[27] = true; // Node 28 (index 27) 514 mask[30] = true; // Node 31 (index 30) 515 mask[31] = true; // Node 32 (index 31) 516 // mask[38] = false; // Node 39 turned off (already false by default) 517 mask[32] = true; // Node 33 (index 32) 518 mask[33] = true; // Node 34 (index 33) 519 mask[24] = true; // Node 25 (index 24) 520 mask[25] = true; // Node 26 (index 25) 521 mask[28] = true; // Node 29 (index 28) - additional 522 mask[29] = true; // Node 30 (index 29) - additional 523 } 524 else if (totalNodes === 31) { 525 // All Ring 2 nodes (7-18) active 526 for (let i = 6; i < 18; i++) { 527 mask[i] = true; 528 } 529 // Ring 3: 30-node setup - Nodes 35, 36 + Nodes 24, 42, 19 530 // mask[34] = false; // Node 35 turned off (already false by default) 531 // mask[35] = false; // Node 36 turned off (already false by default) 532 mask[26] = true; // Node 27 (index 26) 533 mask[27] = true; // Node 28 (index 27) 534 mask[30] = true; // Node 31 (index 30) 535 mask[31] = true; // Node 32 (index 31) 536 mask[32] = true; // Node 33 (index 32) 537 mask[33] = true; // Node 34 (index 33) 538 mask[24] = true; // Node 25 (index 24) 539 mask[25] = true; // Node 26 (index 25) 540 mask[28] = true; // Node 29 (index 28) 541 mask[29] = true; // Node 30 (index 29) 542 mask[23] = true; // Node 24 (index 23) - additional 543 mask[41] = true; // Node 42 (index 41) - additional 544 mask[18] = true; // Node 19 (index 18) - additional 545 } 546 // For 32+ nodes, use existing logic 547 else if (totalNodes > 31 && totalNodes < 32) { 548 for (let i = 6; i < Math.min(totalNodes, 42); i++) { 549 mask[i] = true; 550 } 551 } 552 else if (totalNodes === 32) { 553 // All Ring 2 nodes (7-18) active 554 for (let i = 6; i < 18; i++) { 555 mask[i] = true; 556 } 557 // Ring 3: 31-node setup - Nodes 29, 30 + Nodes 21, 39, 20 558 mask[26] = true; // Node 27 (index 26) 559 mask[27] = true; // Node 28 (index 27) 560 mask[30] = true; // Node 31 (index 30) 561 mask[31] = true; // Node 32 (index 31) 562 mask[32] = true; // Node 33 (index 32) 563 mask[33] = true; // Node 34 (index 33) 564 mask[24] = true; // Node 25 (index 24) 565 mask[25] = true; // Node 26 (index 25) 566 // mask[28] = false; // Node 29 turned off (already false by default) 567 // mask[29] = false; // Node 30 turned off (already false by default) 568 mask[23] = true; // Node 24 (index 23) 569 mask[41] = true; // Node 42 (index 41) 570 mask[18] = true; // Node 19 (index 18) 571 mask[20] = true; // Node 21 (index 20) - additional 572 mask[38] = true; // Node 39 (index 38) - additional 573 mask[21] = true; // Node 22 (index 21) - additional 574 } 575 else if (totalNodes === 33) { 576 // All Ring 2 nodes (7-18) active 577 for (let i = 6; i < 18; i++) { 578 mask[i] = true; 579 } 580 // Ring 3: 32-node setup - Node 42 + Nodes 35, 36 581 mask[26] = true; // Node 27 (index 26) 582 mask[27] = true; // Node 28 (index 27) 583 mask[30] = true; // Node 31 (index 30) 584 mask[31] = true; // Node 32 (index 31) 585 mask[32] = true; // Node 33 (index 32) 586 mask[33] = true; // Node 34 (index 33) 587 mask[24] = true; // Node 25 (index 24) 588 mask[25] = true; // Node 26 (index 25) 589 mask[23] = true; // Node 24 (index 23) 590 // mask[41] = false; // Node 42 turned off (already false by default) 591 mask[18] = true; // Node 19 (index 18) 592 mask[20] = true; // Node 21 (index 20) 593 mask[38] = true; // Node 39 (index 38) 594 mask[21] = true; // Node 22 (index 21) 595 mask[34] = true; // Node 35 (index 34) - additional 596 mask[35] = true; // Node 36 (index 35) - additional 597 } 598 else if (totalNodes === 34) { 599 // All Ring 2 nodes (7-18) active 600 for (let i = 6; i < 18; i++) { 601 mask[i] = true; 602 } 603 // Ring 3: 33-node setup - Node 39 + Nodes 30, 29 604 mask[26] = true; // Node 27 (index 26) 605 mask[27] = true; // Node 28 (index 27) 606 mask[30] = true; // Node 31 (index 30) 607 mask[31] = true; // Node 32 (index 31) 608 mask[32] = true; // Node 33 (index 32) 609 mask[33] = true; // Node 34 (index 33) 610 mask[24] = true; // Node 25 (index 24) 611 mask[25] = true; // Node 26 (index 25) 612 mask[23] = true; // Node 24 (index 23) 613 mask[18] = true; // Node 19 (index 18) 614 mask[20] = true; // Node 21 (index 20) 615 // mask[38] = false; // Node 39 turned off (already false by default) 616 mask[21] = true; // Node 22 (index 21) 617 mask[34] = true; // Node 35 (index 34) 618 mask[35] = true; // Node 36 (index 35) 619 mask[29] = true; // Node 30 (index 29) - additional 620 mask[28] = true; // Node 29 (index 28) - additional 621 } 622 else if (totalNodes === 35) { 623 // All Ring 2 nodes (7-18) active 624 for (let i = 6; i < 18; i++) { 625 mask[i] = true; 626 } 627 // Ring 3: 34-node setup - Nodes 30, 29 + Nodes 39, 23, 20 628 mask[26] = true; // Node 27 (index 26) 629 mask[27] = true; // Node 28 (index 27) 630 mask[30] = true; // Node 31 (index 30) 631 mask[31] = true; // Node 32 (index 31) 632 mask[32] = true; // Node 33 (index 32) 633 mask[33] = true; // Node 34 (index 33) 634 mask[24] = true; // Node 25 (index 24) 635 mask[25] = true; // Node 26 (index 25) 636 mask[23] = true; // Node 24 (index 23) 637 mask[18] = true; // Node 19 (index 18) 638 mask[20] = true; // Node 21 (index 20) 639 mask[21] = true; // Node 22 (index 21) 640 mask[34] = true; // Node 35 (index 34) 641 mask[35] = true; // Node 36 (index 35) 642 // mask[29] = false; // Node 30 turned off (already false by default) 643 // mask[28] = false; // Node 29 turned off (already false by default) 644 mask[38] = true; // Node 39 (index 38) - additional 645 mask[22] = true; // Node 23 (index 22) - additional 646 mask[19] = true; // Node 20 (index 19) - additional 647 } 648 else if (totalNodes === 36) { 649 // All Ring 2 nodes (7-18) active 650 for (let i = 6; i < 18; i++) { 651 mask[i] = true; 652 } 653 // Ring 3: 35-node setup - Node 39 + Nodes 30, 29 654 mask[26] = true; // Node 27 (index 26) 655 mask[27] = true; // Node 28 (index 27) 656 mask[30] = true; // Node 31 (index 30) 657 mask[31] = true; // Node 32 (index 31) 658 mask[32] = true; // Node 33 (index 32) 659 mask[33] = true; // Node 34 (index 33) 660 mask[24] = true; // Node 25 (index 24) 661 mask[25] = true; // Node 26 (index 25) 662 mask[23] = true; // Node 24 (index 23) 663 mask[18] = true; // Node 19 (index 18) 664 mask[20] = true; // Node 21 (index 20) 665 mask[21] = true; // Node 22 (index 21) 666 mask[34] = true; // Node 35 (index 34) 667 mask[35] = true; // Node 36 (index 35) 668 // mask[38] = false; // Node 39 turned off (already false by default) 669 mask[22] = true; // Node 23 (index 22) 670 mask[19] = true; // Node 20 (index 19) 671 mask[29] = true; // Node 30 (index 29) - additional 672 mask[28] = true; // Node 29 (index 28) - additional 673 } 674 // For other counts > 36, use sequential logic for now 675 else if (totalNodes > 36) { 676 for (let i = 6; i < Math.min(totalNodes, 42); i++) { 677 mask[i] = true; 678 } 679 } 680 681 return mask; 682 } 683 684 // Raw values from actual algorithm 685 const RING_RADII = [40, 125, 335]; // From RingLayout.ts 686 687 function visualizeLayout(totalNodes) { 688 const canvas = document.getElementById('canvas'); 689 const ctx = canvas.getContext('2d'); 690 const centerX = canvas.width / 2; 691 const centerY = canvas.height / 2; 692 const scale = 0.8; // Scale factor to fit in canvas 693 694 // Clear canvas 695 ctx.fillStyle = '#000'; 696 ctx.fillRect(0, 0, canvas.width, canvas.height); 697 698 // Draw center point 699 ctx.fillStyle = '#ff6b6b'; 700 ctx.beginPath(); 701 ctx.arc(centerX, centerY, 8, 0, 2 * Math.PI); 702 ctx.fill(); 703 704 // Add center label 705 ctx.fillStyle = '#fff'; 706 ctx.font = '12px Arial'; 707 ctx.textAlign = 'center'; 708 ctx.fillText('CENTER', centerX, centerY - 15); 709 710 // Hybrid approach: Original Ring 1 logic + masking for Ring 2/3 711 const colors = ['#4ecdc4', '#45b7d1', '#96ceb4']; 712 713 // Draw ring circles 714 for (let ringIndex = 0; ringIndex < 3; ringIndex++) { 715 const radius = RING_RADII[ringIndex] * scale; 716 ctx.strokeStyle = '#333'; 717 ctx.lineWidth = 1; 718 ctx.beginPath(); 719 ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); 720 ctx.stroke(); 721 } 722 723 let nodeDisplayNumber = 1; 724 let ring1Count = 0, ring2Count = 0, ring3Count = 0; 725 726 // Ring 1: Use original dynamic equidistant spacing (nodes 1-6) 727 if (totalNodes >= 1) { 728 const ring1Nodes = Math.min(totalNodes, 6); 729 ring1Count = ring1Nodes; 730 731 // Original Ring 1 logic with proper rotation 732 let startAngle = -Math.PI / 2; // Default: start at top (point up) 733 if (ring1Nodes === 6) { 734 startAngle = -Math.PI / 2 + Math.PI / 6; // Rotate by 30° (flat edge at top) 735 } 736 737 ctx.fillStyle = colors[0]; // Ring 1 color 738 for (let i = 0; i < ring1Nodes; i++) { 739 const angle = (i / ring1Nodes) * 2 * Math.PI + startAngle; 740 const x = centerX + RING_RADII[0] * scale * Math.cos(angle); 741 const y = centerY + RING_RADII[0] * scale * Math.sin(angle); 742 743 ctx.beginPath(); 744 ctx.arc(x, y, 6, 0, 2 * Math.PI); 745 ctx.fill(); 746 747 ctx.fillStyle = '#fff'; 748 ctx.font = '10px Arial'; 749 ctx.textAlign = 'center'; 750 ctx.fillText(nodeDisplayNumber.toString(), x, y + 3); 751 ctx.fillStyle = colors[0]; 752 753 nodeDisplayNumber++; 754 } 755 } 756 757 // Ring 2 & Ring 3: Use masking approach for nodes 7+ 758 if (totalNodes > 6) { 759 const all42Positions = generateAll36StaticPositions(); // Now returns 42 positions 760 const activeMask = getActiveMask(totalNodes); 761 762 // Draw Ring 2 and Ring 3 nodes using mask (skip Ring 1 indices 0-5) 763 for (let i = 6; i < 42; i++) { 764 if (!activeMask[i]) continue; 765 766 const pos = all42Positions[i]; 767 const x = centerX + pos[0] * scale; 768 const y = centerY + pos[1] * scale; 769 770 // Determine color based on position range 771 let color; 772 if (i < 18) { 773 color = colors[1]; // Ring 2 774 ring2Count++; 775 } else { 776 color = colors[2]; // Ring 3 777 ring3Count++; 778 } 779 780 ctx.fillStyle = color; 781 ctx.beginPath(); 782 ctx.arc(x, y, 6, 0, 2 * Math.PI); 783 ctx.fill(); 784 785 // Add node number 786 ctx.fillStyle = '#fff'; 787 ctx.font = '10px Arial'; 788 ctx.textAlign = 'center'; 789 ctx.fillText(nodeDisplayNumber.toString(), x, y + 3); 790 791 nodeDisplayNumber++; 792 } 793 } 794 795 // Add ring labels 796 ctx.fillStyle = '#fff'; 797 ctx.font = '14px Arial'; 798 ctx.textAlign = 'left'; 799 ctx.fillText(`Ring 1: ${ring1Count} nodes`, 20, 30); 800 ctx.fillText(`Ring 2: ${ring2Count} nodes`, 20, 50); 801 ctx.fillText(`Ring 3: ${ring3Count} nodes`, 20, 70); 802 803 // Update info 804 document.getElementById('info').textContent = 805 `Ring 1: ${ring1Count} nodes | Ring 2: ${ring2Count} nodes | Ring 3: ${ring3Count} nodes`; 806 } 807 808 function setNodeCount(count) { 809 document.getElementById('nodeCount').value = count; 810 document.getElementById('nodeCountValue').textContent = count; 811 visualizeLayout(count); 812 } 813 814 function nextNode() { 815 const current = parseInt(document.getElementById('nodeCount').value); 816 if (current < 36) { 817 setNodeCount(current + 1); 818 } 819 } 820 821 function previousNode() { 822 const current = parseInt(document.getElementById('nodeCount').value); 823 if (current > 1) { 824 setNodeCount(current - 1); 825 } 826 } 827 828 function visualizeProgression() { 829 let count = 1; 830 const interval = setInterval(() => { 831 setNodeCount(count); 832 count++; 833 if (count > 36) { 834 clearInterval(interval); 835 } 836 }, 300); // 300ms between frames for better visibility 837 } 838 839 // Event listeners 840 document.getElementById('nodeCount').addEventListener('input', (e) => { 841 const value = parseInt(e.target.value); 842 document.getElementById('nodeCountValue').textContent = value; 843 visualizeLayout(value); 844 }); 845 846 // Static 36-node coordinate system 847 function createStaticCoordinateSystem() { 848 const canvas = document.getElementById('staticCanvas'); 849 const ctx = canvas.getContext('2d'); 850 const centerX = canvas.width / 2; 851 const centerY = canvas.height / 2; 852 const scale = 0.8; 853 854 // Clear canvas 855 ctx.fillStyle = '#000'; 856 ctx.fillRect(0, 0, canvas.width, canvas.height); 857 858 // Draw center point 859 ctx.fillStyle = '#ff6b6b'; 860 ctx.beginPath(); 861 ctx.arc(centerX, centerY, 8, 0, 2 * Math.PI); 862 ctx.fill(); 863 ctx.fillStyle = '#fff'; 864 ctx.font = '12px Arial'; 865 ctx.textAlign = 'center'; 866 ctx.fillText('CENTER', centerX, centerY - 15); 867 868 let nodeNumber = 1; 869 const colors = ['#4ecdc4', '#45b7d1', '#96ceb4']; // Teal, blue, green 870 871 // Ring 1: 6 nodes (nodes 1-6) - Simple hexagon with flat edge at top 872 const ring1Radius = RING_RADII[0] * scale; 873 ctx.strokeStyle = '#333'; 874 ctx.lineWidth = 1; 875 ctx.beginPath(); 876 ctx.arc(centerX, centerY, ring1Radius, 0, 2 * Math.PI); 877 ctx.stroke(); 878 879 ctx.fillStyle = colors[0]; 880 const ring1StartAngle = -Math.PI / 2 + Math.PI / 6; // 30° rotation for flat edge at top 881 for (let i = 0; i < 6; i++) { 882 const angle = (i / 6) * 2 * Math.PI + ring1StartAngle; 883 const x = centerX + ring1Radius * Math.cos(angle); 884 const y = centerY + ring1Radius * Math.sin(angle); 885 886 ctx.beginPath(); 887 ctx.arc(x, y, 8, 0, 2 * Math.PI); 888 ctx.fill(); 889 890 ctx.fillStyle = '#fff'; 891 ctx.font = 'bold 12px Arial'; 892 ctx.textAlign = 'center'; 893 ctx.fillText(nodeNumber.toString(), x, y + 4); 894 ctx.fillStyle = colors[0]; 895 896 nodeNumber++; 897 } 898 899 // Ring 2: 12 nodes (nodes 7-18) - Hexagon corners + edge midpoints 900 const ring2Radius = RING_RADII[1] * scale; 901 ctx.strokeStyle = '#333'; 902 ctx.beginPath(); 903 ctx.arc(centerX, centerY, ring2Radius, 0, 2 * Math.PI); 904 ctx.stroke(); 905 906 ctx.fillStyle = colors[1]; 907 908 // First 6 nodes: Hexagon corners (aligned with Ring 1 but no rotation) - reduced radius to lie on hexagon edges 909 const ring2EdgeRadius = ring2Radius * Math.cos(Math.PI / 6); // cos(30°) = √3/2 ≈ 0.866 910 for (let i = 0; i < 6; i++) { 911 const angle = (i / 6) * 2 * Math.PI - Math.PI / 2; // Start at top 912 const x = centerX + ring2EdgeRadius * Math.cos(angle); 913 const y = centerY + ring2EdgeRadius * Math.sin(angle); 914 915 ctx.beginPath(); 916 ctx.arc(x, y, 8, 0, 2 * Math.PI); 917 ctx.fill(); 918 919 ctx.fillStyle = '#fff'; 920 ctx.font = 'bold 12px Arial'; 921 ctx.textAlign = 'center'; 922 ctx.fillText(nodeNumber.toString(), x, y + 4); 923 ctx.fillStyle = colors[1]; 924 925 nodeNumber++; 926 } 927 928 // Next 6 nodes: Edge midpoints (30° offset) - full radius (these are the actual hexagon corners) 929 for (let i = 0; i < 6; i++) { 930 const angle = (i / 6) * 2 * Math.PI - Math.PI / 2 + Math.PI / 6; // 30° offset 931 const x = centerX + ring2Radius * Math.cos(angle); 932 const y = centerY + ring2Radius * Math.sin(angle); 933 934 ctx.beginPath(); 935 ctx.arc(x, y, 8, 0, 2 * Math.PI); 936 ctx.fill(); 937 938 ctx.fillStyle = '#fff'; 939 ctx.font = 'bold 12px Arial'; 940 ctx.textAlign = 'center'; 941 ctx.fillText(nodeNumber.toString(), x, y + 4); 942 ctx.fillStyle = colors[1]; 943 944 nodeNumber++; 945 } 946 947 // Ring 3: Only nodes 19-25 at hexagon vertices (step-by-step approach) 948 const ring3Radius = RING_RADII[2] * scale; 949 ctx.strokeStyle = '#333'; 950 ctx.beginPath(); 951 ctx.arc(centerX, centerY, ring3Radius, 0, 2 * Math.PI); 952 ctx.stroke(); 953 954 ctx.fillStyle = colors[2]; 955 956 // Nodes 19-24: Hexagon vertices starting at 30° (0° = top) 957 const hexagonAngles = [ 958 30, // Node 19 at 30° 959 90, // Node 20 at 90° 960 150, // Node 21 at 150° 961 210, // Node 22 at 210° 962 270, // Node 23 at 270° 963 330, // Node 24 at 330° 964 30 // Node 25 at 30° (back to start - this creates 7 nodes with one overlap) 965 ]; 966 967 // Place nodes 19-24 at specified angles 968 for (let i = 0; i < 6; i++) { // 6 nodes (19-24) 969 const angleDegrees = hexagonAngles[i]; 970 const angleRadians = (angleDegrees - 90) * Math.PI / 180; // Convert to radians, adjust for 0° = top 971 const x = centerX + ring3Radius * Math.cos(angleRadians); 972 const y = centerY + ring3Radius * Math.sin(angleRadians); 973 974 ctx.beginPath(); 975 ctx.arc(x, y, 8, 0, 2 * Math.PI); 976 ctx.fill(); 977 978 ctx.fillStyle = '#fff'; 979 ctx.font = 'bold 10px Arial'; 980 ctx.textAlign = 'center'; 981 ctx.fillText((19 + i).toString(), x, y + 3); 982 ctx.fillStyle = colors[2]; 983 } 984 985 // Now add edge nodes (25-36) using path parameterization between vertices 986 const vertexPositions = []; 987 988 // Calculate world coordinates for the 6 vertices (nodes 19-24) - unscaled 989 const baseRadius = RING_RADII[2]; // Use base radius before scaling 990 for (let i = 0; i < 6; i++) { 991 const angleDegrees = hexagonAngles[i]; 992 const angleRadians = (angleDegrees - 90) * Math.PI / 180; 993 const x = baseRadius * Math.cos(angleRadians); // True world coordinates 994 const y = baseRadius * Math.sin(angleRadians); 995 vertexPositions.push([x, y]); 996 } 997 998 // Path parameterization function: lerp between two points 999 function lerpPath(pointA, pointB, t) { 1000 return [ 1001 pointA[0] + t * (pointB[0] - pointA[0]), 1002 pointA[1] + t * (pointB[1] - pointA[1]) 1003 ]; 1004 } 1005 1006 let edgeNodeNumber = 25; // Start with node 25 1007 1008 // For each edge (6 edges total), place 2 nodes at t=1/3 and t=2/3 1009 for (let edgeIndex = 0; edgeIndex < 6; edgeIndex++) { 1010 const startVertex = vertexPositions[edgeIndex]; 1011 const endVertex = vertexPositions[(edgeIndex + 1) % 6]; // Wrap around for last edge 1012 1013 // Place 2 nodes on this edge: at t=1/3 and t=2/3 1014 for (let nodeOnEdge = 0; nodeOnEdge < 2; nodeOnEdge++) { 1015 const t = (nodeOnEdge + 1) / 3; // t = 1/3 for first node, 2/3 for second 1016 const [worldX, worldY] = lerpPath(startVertex, endVertex, t); 1017 1018 // Convert to canvas coordinates - use the lerp result directly (no radius adjustment!) 1019 const x = centerX + worldX * scale; 1020 const y = centerY + worldY * scale; 1021 1022 ctx.beginPath(); 1023 ctx.arc(x, y, 8, 0, 2 * Math.PI); 1024 ctx.fill(); 1025 1026 ctx.fillStyle = '#fff'; 1027 ctx.font = 'bold 10px Arial'; 1028 ctx.textAlign = 'center'; 1029 ctx.fillText(edgeNodeNumber.toString(), x, y + 3); 1030 ctx.fillStyle = colors[2]; 1031 1032 edgeNodeNumber++; 1033 } 1034 } 1035 1036 // Add 6 additional edge midpoint nodes (37-42) at t=0.5 1037 let additionalNodeNumber = 37; 1038 for (let edgeIndex = 0; edgeIndex < 6; edgeIndex++) { 1039 const startVertex = vertexPositions[edgeIndex]; 1040 const endVertex = vertexPositions[(edgeIndex + 1) % 6]; 1041 1042 const t = 0.5; // Exact midpoint 1043 const [worldX, worldY] = lerpPath(startVertex, endVertex, t); 1044 1045 // Convert to canvas coordinates 1046 const x = centerX + worldX * scale; 1047 const y = centerY + worldY * scale; 1048 1049 ctx.beginPath(); 1050 ctx.arc(x, y, 8, 0, 2 * Math.PI); 1051 ctx.fill(); 1052 1053 ctx.fillStyle = '#fff'; 1054 ctx.font = 'bold 10px Arial'; 1055 ctx.textAlign = 'center'; 1056 ctx.fillText(additionalNodeNumber.toString(), x, y + 3); 1057 ctx.fillStyle = colors[2]; 1058 1059 additionalNodeNumber++; 1060 } 1061 1062 // Add ring labels 1063 ctx.fillStyle = '#fff'; 1064 ctx.font = '14px Arial'; 1065 ctx.textAlign = 'left'; 1066 ctx.fillText('Ring 1: Nodes 1-6', 20, 30); 1067 ctx.fillText('Ring 2: Nodes 7-18', 20, 50); 1068 ctx.fillText('Ring 3: Nodes 19-42', 20, 70); 1069 } 1070 1071 // Initial visualizations 1072 visualizeLayout(6); 1073 createStaticCoordinateSystem(); 1074 </script> 1075 </body> 1076 </html>