// Flexagon Simulation // This implementation uses a functional programming approach with immutable data structures // and clear separation between the mathematical model, rendering, and interaction systems. // ===== Core Mathematical Model ===== // The flexagon is modeled as a series of connected triangles in 3D space that can be folded // A hexahexaflexagon is made from a strip of 19 equilateral triangles /** * Represents a point in 3D space * @typedef {Object} Point3D * @property {number} x - X coordinate * @property {number} y - Y coordinate * @property {number} z - Z coordinate */ /** * Represents a 3x3 transformation matrix * @typedef {Object} Matrix3D * @property {number[]} values - 3x3 matrix values in row-major order */ /** * Represents a triangle face of the flexagon * @typedef {Object} Face * @property {Point3D[]} vertices - Three vertices of the triangle * @property {string} color - Color of the face * @property {number} layer - Layer depth of the face * @property {number[]} connectedFaces - Indices of connected faces * @property {number[]} sharedEdges - Indices of shared edges with connected faces */ /** * Represents the state of the flexagon * @typedef {Object} FlexagonState * @property {Face[]} faces - All faces of the flexagon * @property {number} currentFaceIndex - Index of the currently visible face * @property {Matrix3D} transform - Current 3D transformation * @property {boolean} isAnimating - Whether an animation is in progress * @property {number} animationProgress - Progress of current animation (0-1) */ // Constants for the hexaflexagon const FLEXAGON_CONFIG = { triangleCount: 19, // Number of triangles in the strip colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD', '#D4A5A5'], animationDuration: 500, // ms foldAngle: Math.PI / 3, // 60 degrees sideLength: 100, // Length of triangle sides // Pat notation for the hexahexaflexagon // This represents the folding pattern as described in the Flexagon paper patPattern: [1, 2, 3, 1, 2, 3, 1, 2, 3, 4, 5, 6, 4, 5, 6, 4, 5, 6, 1] }; // ===== 3D Transformation Utilities ===== /** * Creates an identity matrix * @returns {Matrix3D} Identity matrix */ const createIdentityMatrix = () => ({ values: [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ] }); /** * Creates a rotation matrix around the X axis * @param {number} angle - Rotation angle in radians * @returns {Matrix3D} Rotation matrix */ const createRotationX = (angle) => ({ values: [ 1, 0, 0, 0, Math.cos(angle), -Math.sin(angle), 0, Math.sin(angle), Math.cos(angle) ] }); /** * Creates a rotation matrix around the Y axis * @param {number} angle - Rotation angle in radians * @returns {Matrix3D} Rotation matrix */ const createRotationY = (angle) => ({ values: [ Math.cos(angle), 0, Math.sin(angle), 0, 1, 0, -Math.sin(angle), 0, Math.cos(angle) ] }); /** * Multiplies two matrices * @param {Matrix3D} a - First matrix * @param {Matrix3D} b - Second matrix * @returns {Matrix3D} Resulting matrix */ const multiplyMatrices = (a, b) => { const result = createIdentityMatrix(); for (let row = 0; row < 3; row++) { for (let col = 0; col < 3; col++) { let sum = 0; for (let i = 0; i < 3; i++) { sum += a.values[row * 3 + i] * b.values[i * 3 + col]; } result.values[row * 3 + col] = sum; } } return result; }; /** * Applies a transformation matrix to a point * @param {Point3D} point - Point to transform * @param {Matrix3D} matrix - Transformation matrix * @returns {Point3D} Transformed point */ const transformPoint = (point, matrix) => ({ x: point.x * matrix.values[0] + point.y * matrix.values[1] + point.z * matrix.values[2], y: point.x * matrix.values[3] + point.y * matrix.values[4] + point.z * matrix.values[5], z: point.x * matrix.values[6] + point.y * matrix.values[7] + point.z * matrix.values[8] }); // ===== Animation System ===== /** * Easing function for smooth animations * @param {number} t - Time progress (0-1) * @returns {number} Eased progress */ const easeInOutCubic = (t) => { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }; /** * Creates an equilateral triangle in 3D space * @param {number} centerX - Center X coordinate * @param {number} centerY - Center Y coordinate * @param {number} centerZ - Center Z coordinate * @param {number} angle - Rotation angle * @param {number} size - Size of the triangle * @returns {Point3D[]} Array of three vertices */ const createTriangle = (centerX, centerY, centerZ, angle, size) => { const height = size * Math.sqrt(3) / 2; return [ { x: centerX, y: centerY, z: centerZ }, { x: centerX + size * Math.cos(angle), y: centerY + size * Math.sin(angle), z: centerZ }, { x: centerX + size * Math.cos(angle + Math.PI / 3), y: centerY + size * Math.sin(angle + Math.PI / 3), z: centerZ } ]; }; /** * Creates the initial strip of triangles for the hexaflexagon * @returns {Face[]} Array of connected triangles */ const createInitialStrip = () => { const faces = []; const size = FLEXAGON_CONFIG.sideLength; // Create the strip of triangles following the pat pattern for (let i = 0; i < FLEXAGON_CONFIG.triangleCount; i++) { const centerX = (i * size * 1.5); const centerY = 0; const centerZ = 0; const angle = (i % 2) * Math.PI; // Alternate triangle orientations faces.push({ vertices: createTriangle(centerX, centerY, centerZ, angle, size), color: FLEXAGON_CONFIG.colors[FLEXAGON_CONFIG.patPattern[i] - 1], layer: Math.floor(i / 6), // Group triangles into layers connectedFaces: [ (i + 1) % FLEXAGON_CONFIG.triangleCount, (i - 1 + FLEXAGON_CONFIG.triangleCount) % FLEXAGON_CONFIG.triangleCount ], sharedEdges: [1, 2] // Indices of shared edges with next and previous triangles }); } return faces; }; /** * Determines if a face is visible * @param {Face} face - Face to check * @param {Matrix3D} transform - Current transformation * @returns {boolean} Whether the face is visible */ const isFaceVisible = (face) => { // During development, show all faces return true; }; /** * Creates the initial state * @returns {FlexagonState} Initial state of the flexagon */ const createInitialState = () => { const faces = createInitialStrip(); // Apply initial transformations to make faces visible const initialTransform = multiplyMatrices( createRotationX(Math.PI / 6), // Tilt forward createRotationY(Math.PI / 6) // Rotate slightly right ); return { faces: faces, currentFaceIndex: 0, transform: initialTransform, isAnimating: false, animationProgress: 0 }; }; /** * Folds the strip of triangles into a hexagonal shape * @param {Face[]} faces - Array of faces in the strip * @returns {Face[]} Folded faces */ const foldStrip = (faces) => { // Implementation of the folding algorithm // This follows the Tuckerman traverse pattern const foldedFaces = [...faces]; const foldAngles = [ Math.PI / 3, -Math.PI / 3, // First fold Math.PI / 3, -Math.PI / 3, // Second fold Math.PI / 3, -Math.PI / 3 // Third fold ]; // Apply the folds for (let i = 0; i < foldAngles.length; i++) { const foldIndex = i * 3; const foldAngle = foldAngles[i]; // Transform all vertices after the fold point for (let j = foldIndex + 1; j < foldedFaces.length; j++) { foldedFaces[j].vertices = foldedFaces[j].vertices.map(vertex => transformPoint(vertex, createRotationY(foldAngle)) ); } } return foldedFaces; }; /** * Performs a flex operation on the flexagon * @param {FlexagonState} state - Current state * @returns {FlexagonState} New state after flexing */ const flex = (state) => { if (state.isAnimating) return state; const currentFace = state.faces[state.currentFaceIndex]; const nextFaceIndex = currentFace.connectedFaces[0]; // Create rotation matrices for the animation const startRotation = createIdentityMatrix(); const endRotation = multiplyMatrices( createRotationX(FLEXAGON_CONFIG.foldAngle), createRotationY(Math.PI / 3) ); // Apply an initial rotation to better show the 3D structure const initialRotation = createRotationX(Math.PI / 6); const combinedRotation = multiplyMatrices(endRotation, initialRotation); return { ...state, currentFaceIndex: nextFaceIndex, isAnimating: true, animationProgress: 0, transform: startRotation }; }; /** * Updates the animation state * @param {FlexagonState} state - Current state * @param {number} deltaTime - Time since last update in ms * @returns {FlexagonState} Updated state */ const updateAnimation = (state, deltaTime) => { if (!state.isAnimating) return state; const newProgress = Math.min(1, state.animationProgress + deltaTime / FLEXAGON_CONFIG.animationDuration); const easedProgress = easeInOutCubic(newProgress); // Interpolate between start and end transformations const startRotation = createIdentityMatrix(); const endRotation = multiplyMatrices( createRotationX(FLEXAGON_CONFIG.foldAngle), createRotationY(Math.PI / 3) ); const interpolatedMatrix = { values: startRotation.values.map((value, i) => value + (endRotation.values[i] - value) * easedProgress ) }; return { ...state, animationProgress: newProgress, transform: interpolatedMatrix, isAnimating: newProgress < 1 }; }; // ===== Rendering System ===== /** * Projects a 3D point onto 2D canvas coordinates with perspective * @param {Point3D} point - 3D point to project * @param {number} canvasWidth - Width of the canvas * @param {number} canvasHeight - Height of the canvas * @returns {Object} 2D coordinates {x, y} */ const projectPoint = (point, canvasWidth, canvasHeight) => { // Perspective projection with a fixed focal length const focalLength = 500; const scale = focalLength / (focalLength + point.z); return { x: point.x * scale + canvasWidth / 2, y: point.y * scale + canvasHeight / 2 }; }; /** * Calculates the normal vector of a face * @param {Face} face - Face to calculate normal for * @returns {Point3D} Normal vector */ const calculateFaceNormal = (face) => { const v1 = face.vertices[1]; const v2 = face.vertices[2]; return { x: (v1.y - v2.y) * (v1.z - v2.z), y: (v1.z - v2.z) * (v1.x - v2.x), z: (v1.x - v2.x) * (v1.y - v2.y) }; }; /** * Renders the flexagon on the canvas * @param {CanvasRenderingContext2D} ctx - Canvas context * @param {FlexagonState} state - Current state */ const renderFlexagon = (ctx, state) => { const canvas = ctx.canvas; // Clear canvas with a light gray background for debugging ctx.fillStyle = '#f0f0f0'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Debug: Draw coordinate axes ctx.beginPath(); ctx.strokeStyle = 'red'; ctx.moveTo(canvas.width/2, canvas.height/2); ctx.lineTo(canvas.width/2 + 50, canvas.height/2); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = 'green'; ctx.moveTo(canvas.width/2, canvas.height/2); ctx.lineTo(canvas.width/2, canvas.height/2 - 50); ctx.stroke(); // Center the flexagon const centerX = canvas.width / 2; const centerY = canvas.height / 2; const scale = 100; // Increase scale to make faces more visible // Draw all faces for debugging state.faces.forEach((face, index) => { // Transform vertices const projectedPoints = face.vertices.map(vertex => { const transformed = transformPoint(vertex, state.transform); return { x: centerX + transformed.x * scale, y: centerY + transformed.y * scale }; }); // Draw face ctx.beginPath(); ctx.moveTo(projectedPoints[0].x, projectedPoints[0].y); projectedPoints.slice(1).forEach(point => { ctx.lineTo(point.x, point.y); }); ctx.closePath(); // Fill with semi-transparent color ctx.fillStyle = face.color; ctx.globalAlpha = 0.7; ctx.fill(); // Draw edges ctx.strokeStyle = '#000'; ctx.globalAlpha = 1.0; ctx.stroke(); // Draw face number ctx.fillStyle = '#000'; ctx.font = '14px Arial'; const centerPoint = { x: projectedPoints.reduce((sum, p) => sum + p.x, 0) / 3, y: projectedPoints.reduce((sum, p) => sum + p.y, 0) / 3 }; ctx.fillText(index.toString(), centerPoint.x, centerPoint.y); }); // Reset alpha ctx.globalAlpha = 1.0; }; // ===== Interaction System ===== /** * Sets up mouse interaction for the flexagon * @param {HTMLCanvasElement} canvas - Canvas element * @param {Function} onFlex - Callback when flexing occurs */ const setupInteraction = (canvas, onFlex) => { let isDragging = false; let startX = 0; canvas.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; }); canvas.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; if (Math.abs(deltaX) > 50) { // Threshold for flexing onFlex(); isDragging = false; } }); canvas.addEventListener('mouseup', () => { isDragging = false; }); canvas.addEventListener('mouseleave', () => { isDragging = false; }); }; // ===== Main Application ===== const main = () => { const canvas = document.getElementById('flexagonCanvas'); const ctx = canvas.getContext('2d'); // Set canvas size canvas.width = 600; canvas.height = 600; console.log('Canvas initialized:', canvas.width, 'x', canvas.height); // Initialize state let state = createInitialState(); console.log('Initial state created:', { faceCount: state.faces.length, vertices: state.faces[0]?.vertices }); let lastTime = performance.now(); // Animation loop const animate = (currentTime) => { const deltaTime = currentTime - lastTime; lastTime = currentTime; state = updateAnimation(state, deltaTime); renderFlexagon(ctx, state); requestAnimationFrame(animate); }; // Setup interaction setupInteraction(canvas, () => { state = flex(state); }); // Start animation loop requestAnimationFrame(animate); }; // Start the application when the DOM is loaded document.addEventListener('DOMContentLoaded', main);