// ================================================================= // --- Configuration --- // Adjust these values to change the animation's layout. // ================================================================= const config = { // Sphere position relative to canvas dimensions sphereCenterX_rel: -0.75, // of canvas width sphereCenterY_rel: -0.75, // of canvas height sphereCenterZ_rel: 1.25, // of canvas width (for depth) // Sphere and tile size relative to canvas height sphereRadius_rel: 1 / 1.6, // of canvas height tileWidth_rel: 1 / 26.6, // of canvas height // Light source position relative to canvas dimensions lightSourceX_rel: 1.0, // of canvas width lightSourceY_rel: 2.0, // of canvas height // Animation settings PER CLICK animationSeconds: 5, rotationDegrees: 30, }; // ================================================================= // This variable manages the p5.js instance. let discoInstance = null; function startDisco() { // If the animation isn't running, create a new one. if (!discoInstance) { discoInstance = new p5((p) => { // These variables will manage the animation's state let targetRotation; let animationStopTime; let startTime; // This function will be called by subsequent button clicks p.extendAnimation = () => { targetRotation += config.rotationDegrees; animationStopTime += config.animationSeconds * 1000; }; p.setup = () => { let discoCanvas = p.createCanvas(window.innerWidth, window.innerHeight); discoCanvas.parent(document.getElementById('disco-wrapper')); p.angleMode(p.DEGREES); // Set the initial animation targets startTime = p.millis(); targetRotation = config.rotationDegrees; animationStopTime = startTime + config.animationSeconds * 1000; // --- Define static scene properties (only calculated once) --- p.sphereRadius = p.height * config.sphereRadius_rel; p.tileWidth = p.height * config.tileWidth_rel; p.sphereCenter = p.createVector( p.width * config.sphereCenterX_rel, p.height * config.sphereCenterY_rel, p.width * config.sphereCenterZ_rel ); p.lightSource = p.createVector( p.width * config.lightSourceX_rel, p.height * config.lightSourceY_rel, 0 ); }; p.windowResized = () => { // This part is more complex with dynamic objects, so it's removed for clarity. // A full implementation would recalculate all scene properties here. p.resizeCanvas(window.innerWidth, window.innerHeight); }; p.draw = () => { const wrapper = document.getElementById('disco-wrapper'); // --- Animation Lifetime Check --- if (p.millis() >= animationStopTime) { wrapper.style.opacity = '0'; setTimeout(() => { p.remove(); discoInstance = null; }, 300); return; // Stop drawing } wrapper.style.opacity = '1'; // Ensure wrapper is visible p.clear(); let allTiles = []; // Map the current time to the current rotation angle based on dynamic targets let rotationY = p.map(p.millis(), startTime, animationStopTime, 0, targetRotation, true); // --- Regenerate Sphere Geometry with Rotation --- const semiCircumference = Math.PI * p.sphereRadius; const numRows = Math.floor(semiCircumference / p.tileWidth); const worldUp = p.createVector(0, 1, 0); for (let i = 1; i < numRows; i++) { const theta = p.map(i + 0.5, 0, numRows, 0, 180); const rowRadius = p.sphereRadius * p.sin(theta); if (rowRadius === 0) continue; const rowCircumference = 2 * Math.PI * rowRadius; const numTilesInRow = Math.floor(rowCircumference / p.tileWidth); const tileStep = 360 / numTilesInRow; for (let j = 0; j < numTilesInRow; j++) { const phi = 180 + j * tileStep + rotationY; const tileCenterX = p.sphereCenter.x + p.sphereRadius * p.sin(theta) * p.cos(phi); const tileCenterY = p.sphereCenter.y + p.sphereRadius * p.cos(theta); const tileCenterZ = p.sphereCenter.z + p.sphereRadius * p.sin(theta) * p.sin(phi); const tileCenter = p.createVector(tileCenterX, tileCenterY, tileCenterZ); const tileNormal = p5.Vector.sub(tileCenter, p.sphereCenter).normalize(); if(tileNormal.z > -0.2) continue; const tileRight = p5.Vector.cross(worldUp, tileNormal).normalize(); const border = -4.5; const visualTileWidth = (p.tileWidth * 2) / 3 - (border * 2); const visualTileHeight = p.tileWidth - (border * 2); const halfVisualWidth = visualTileWidth / 2; const halfVisualHeight = visualTileHeight / 2; const upVec = p.createVector(0, halfVisualHeight, 0); const rightVec = p5.Vector.mult(tileRight, halfVisualWidth); const corners = [ p5.Vector.sub(tileCenter, rightVec).sub(upVec), p5.Vector.add(tileCenter, rightVec).sub(upVec), p5.Vector.add(tileCenter, rightVec).add(upVec), p5.Vector.sub(tileCenter, rightVec).add(upVec), ]; allTiles.push({ corners: corners, normal: tileNormal, center: tileCenter }); } } // --- Reflection and Drawing Logic --- p.fill(255, 230, 200, 150); p.noStroke(); for (const tile of allTiles) { const lightToTileVec = p5.Vector.sub(p.lightSource, tile.center); if (p5.Vector.dot(tile.normal, lightToTileVec) <= 0) { continue; } let projectedCorners = []; for (const corner of tile.corners) { let incidentVec = p5.Vector.sub(corner, p.lightSource); let reflectionVec = p5.Vector.sub( incidentVec, p5.Vector.mult(tile.normal, 2 * p5.Vector.dot(incidentVec, tile.normal)) ); const rayOrigin = corner; const rayDirection = reflectionVec; if (rayDirection.z >= 0) { projectedCorners = []; break; } const t = -rayOrigin.z / rayDirection.z; const pX = rayOrigin.x + t * rayDirection.x; const pY = rayOrigin.y + t * rayDirection.y; projectedCorners.push(p.createVector(pX, pY)); } if(projectedCorners.length === 4) { const proj = projectedCorners; p.quad(proj[0].x, proj[0].y, proj[1].x, proj[1].y, proj[2].x, proj[2].y, proj[3].x, proj[3].y); } } }; }); } else { // If it is running, just call the function to extend its targets. discoInstance.extendAnimation(); } } document.getElementById('party').addEventListener('click', startDisco);