Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <meta name="theme-color" content="#000000" /> | |
| <meta name="description" content="Web site created using create-react-app" /> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" | |
| rel="stylesheet" /> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block" | |
| rel="stylesheet" /> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/addons/p5.sound.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/gh/molleindustria/p5.play/lib/p5.play.js"></script> | |
| <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | |
| <!-- | |
| Notice the use of %PUBLIC_URL% in the tags above. | |
| It will be replaced with the URL of the `public` folder during the build. | |
| Only files inside the `public` folder can be referenced from the HTML. | |
| Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | |
| work correctly both with client-side routing and a non-root public URL. | |
| Learn how to configure a non-root public URL by running `npm run build`. | |
| --> | |
| <title>Multimodal Live - Console</title> | |
| </head> | |
| <body> | |
| <noscript>You need to enable JavaScript to run this app.</noscript> | |
| <div id="root"></div> | |
| <script> | |
| let mySketch; | |
| let osc; // oscillator for the tone | |
| let freq = 261; // base frequency of the tone | |
| let selectedCircleIndex = -1; | |
| let currentLevel = 1; | |
| let targetCircles = []; | |
| let matchThreshold = 20; // threshold for position and size matching | |
| let levelComplete = false; | |
| let score = 0; | |
| let moveCount = 0; // track number of moves in current level | |
| let totalMoves = 0; // track total moves across all levels | |
| let matchedCircles = []; // track which circles are matched | |
| let matchAnimationTime = []; // track animation time for each circle | |
| let gameComplete = false; // track if the entire game is complete | |
| let gameOver = false; | |
| let timeLeft = 30; | |
| let timerStarted = false; | |
| let timerInterval = null; | |
| const FINAL_LEVEL = 5; // number of levels in the game | |
| const VIBRANT_COLORS = [ | |
| '#FF0000', // Red | |
| '#00FF00', // Lime | |
| '#0000FF', // Blue | |
| '#FF00FF', // Magenta | |
| '#00FFFF', // Cyan | |
| '#FFD700', // Gold | |
| '#FF4500', // OrangeRed | |
| '#32CD32', // LimeGreen | |
| '#8A2BE2', // BlueViolet | |
| '#FF1493' // DeepPink | |
| ]; | |
| // Function to get n unique random colors from the VIBRANT_COLORS array | |
| function getRandomColors(n) { | |
| const shuffled = [...VIBRANT_COLORS]; | |
| for (let i = shuffled.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; | |
| } | |
| return shuffled.slice(0, n); | |
| } | |
| function calculateLevelScore() { | |
| // Base score for completing level | |
| const baseScore = 1000; | |
| // Penalty for number of moves (encourage efficiency) | |
| const movePenalty = Math.min(moveCount * 10, 500); // cap penalty at 500 | |
| return Math.max(baseScore - movePenalty, 100); // minimum 100 points | |
| } | |
| function isCircleMatched(circle, target) { | |
| const maxDistance = target.radius * 0.20; // 5% of target radius for position tolerance | |
| const maxSizeDiff = target.radius * 0.20; // 5% of target radius for size tolerance | |
| const distX = Math.abs(circle.x - target.x); | |
| const distY = Math.abs(circle.y - target.y); | |
| const distRadius = Math.abs(circle.radius - target.radius); | |
| console.log(`Checking circle ${circle.color}:`); | |
| console.log(` Position: (${circle.x.toFixed(2)}, ${circle.y.toFixed(2)}) vs target (${target.x.toFixed(2)}, ${target.y.toFixed(2)})`); | |
| console.log(` Distance: X=${distX.toFixed(2)}, Y=${distY.toFixed(2)} (max allowed: ${maxDistance.toFixed(2)})`); | |
| console.log(` Radius: ${circle.radius.toFixed(2)} vs ${target.radius.toFixed(2)} (diff: ${distRadius.toFixed(2)}, max allowed: ${maxSizeDiff.toFixed(2)})`); | |
| const isMatched = distX <= maxDistance && | |
| distY <= maxDistance && | |
| distRadius <= maxSizeDiff; | |
| console.log(` MATCH: ${isMatched}`); | |
| return isMatched; | |
| } | |
| function startTimer() { | |
| if (!timerStarted) { | |
| timerStarted = true; | |
| timeLeft = 30; | |
| timerInterval = setInterval(() => { | |
| timeLeft--; | |
| if (timeLeft <= 0) { | |
| clearInterval(timerInterval); | |
| gameOver = true; | |
| } | |
| }, 1000); | |
| } | |
| } | |
| function resetGame() { | |
| currentLevel = 1; | |
| score = 0; | |
| moveCount = 0; | |
| totalMoves = 0; | |
| gameComplete = false; | |
| gameOver = false; | |
| timerStarted = false; | |
| timeLeft = 30; | |
| if (timerInterval) { | |
| clearInterval(timerInterval); | |
| } | |
| matchedCircles = new Array(currentLevel).fill(false); | |
| matchAnimationTime = new Array(currentLevel).fill(0); | |
| generateTargets(); | |
| } | |
| function checkMatch() { | |
| console.log("\n=== Checking Matches ==="); | |
| // Check each circle individually | |
| let allMatched = true; | |
| let anyNewMatch = false; | |
| for(let i = 0; i < window.circles.length; i++) { | |
| const wasMatched = matchedCircles[i]; | |
| const newMatch = isCircleMatched(window.circles[i], targetCircles[i]); | |
| if (newMatch !== matchedCircles[i]) { | |
| console.log(`Circle ${window.circles[i].color} match status changed: ${wasMatched} -> ${newMatch}`); | |
| } | |
| matchedCircles[i] = newMatch; | |
| if (matchedCircles[i] && !wasMatched) { | |
| // New match - play a tone and start animation | |
| anyNewMatch = true; | |
| matchAnimationTime[i] = 0; | |
| if (osc) { | |
| osc.freq(440 + i * 100); // Different tone for each circle | |
| osc.start(); | |
| osc.amp(0.2); | |
| osc.fade(0, 0.3); | |
| } | |
| } | |
| if (!matchedCircles[i]) { | |
| allMatched = false; | |
| } | |
| } | |
| console.log("Current match status:", matchedCircles); | |
| console.log("All matched:", allMatched); | |
| if(allMatched && !levelComplete) { | |
| console.log("*** LEVEL COMPLETE! ***"); | |
| levelComplete = true; | |
| currentLevel++; | |
| score += calculateLevelScore(); | |
| totalMoves += moveCount; | |
| // Reset timer for next level | |
| timerStarted = false; | |
| if (timerInterval) { | |
| clearInterval(timerInterval); | |
| } | |
| timeLeft = 30; | |
| // Play success sound | |
| if (osc) { | |
| osc.freq(880); // Higher note for level complete | |
| osc.start(); | |
| osc.amp(0.3); | |
| osc.fade(0, 0.5); | |
| } | |
| if (currentLevel > FINAL_LEVEL) { | |
| gameComplete = true; | |
| // Play victory sound | |
| if (osc) { | |
| osc.freq(1320); // Even higher note for game complete | |
| osc.start(); | |
| osc.amp(0.4); | |
| osc.fade(0, 1.0); | |
| } | |
| } else { | |
| setTimeout(() => { | |
| levelComplete = false; | |
| generateTargets(); | |
| matchedCircles = new Array(currentLevel).fill(false); | |
| matchAnimationTime = new Array(currentLevel).fill(0); | |
| }, 2000); // Changed to 2 seconds | |
| } | |
| } | |
| } | |
| function generateTargets() { | |
| // Reset move counter for new level | |
| moveCount = 0; | |
| matchedCircles = new Array(currentLevel).fill(false); | |
| matchAnimationTime = new Array(currentLevel).fill(0); | |
| // Calculate the middle 75% of the screen | |
| const margin = { | |
| x: window.innerWidth * 0.125, // 12.5% margin on each side | |
| y: window.innerHeight * 0.125 // 12.5% margin on each side | |
| }; | |
| const playArea = { | |
| width: window.innerWidth * 0.75, // 75% of screen width | |
| height: window.innerHeight * 0.75 // 75% of screen height | |
| }; | |
| // Get random colors for this level | |
| const levelColors = getRandomColors(currentLevel); | |
| // Generate new random positions for target circles | |
| targetCircles = []; | |
| window.circles = []; | |
| // Add circles based on current level (one more circle per level) | |
| for (let i = 0; i < currentLevel; i++) { | |
| const color = levelColors[i]; | |
| const target = { | |
| color: color, | |
| x: margin.x + Math.random() * playArea.width, | |
| y: margin.y + Math.random() * playArea.height, | |
| radius: Math.random() * 50 + 75 | |
| }; | |
| targetCircles.push(target); | |
| // Create corresponding movable circle, positioned in a line in the middle 75% of screen | |
| const startX = margin.x + (playArea.width * (i + 1) / (currentLevel + 1)); | |
| window.circles.push({ | |
| color: color, | |
| x: startX, | |
| y: window.innerHeight/2, | |
| radius: 100 | |
| }); | |
| } | |
| // Update current circles copy | |
| window.circlesCurrent = window.circles.map((c) => ({ ...c })); | |
| } | |
| window.get_circles = function () { | |
| return { | |
| circles: window.circles, | |
| targets: targetCircles, | |
| level: currentLevel, | |
| isComplete: levelComplete | |
| }; | |
| }; | |
| window.change_circle = function (args) { | |
| if (!timerStarted) { | |
| startTimer(); | |
| } | |
| moveCount++; // increment move counter | |
| // Play the tone here | |
| if (osc) { | |
| osc.start(); | |
| osc.freq(freq); | |
| osc.amp(0.3); | |
| osc.fade(0, 0.2); | |
| } | |
| window.circlesCurrent = window.circles.map((c) => ({ ...c })) | |
| const color = args.color; | |
| const findIndex = window.circles.findIndex( | |
| (c) => c.color.toLowerCase() === color.toLowerCase(), | |
| ); | |
| window.circles.splice(findIndex, 1, args); | |
| checkMatch(); | |
| }; | |
| window.circles = [ | |
| {color: "#00FF00", x: window.innerWidth/2, y: window.innerHeight/2, radius: 100}, | |
| ]; | |
| // Generate initial target positions | |
| generateTargets(); | |
| // make a copy of it | |
| window.circlesCurrent = window.circles.map((c) => ({ ...c })) | |
| window.initSketch = function (container) { | |
| console.log("initialize sketch in public/index.html"); | |
| console.log(container); | |
| if (mySketch) { | |
| return; | |
| } | |
| mySketch = new p5((p) => { | |
| p.setup = function () { | |
| console.log(p); | |
| p.createCanvas(window.innerWidth, window.innerHeight); | |
| container.innerHTMl = ""; | |
| container.appendChild(p._renderer.canvas); | |
| // Create the oscillator here, after p5.sound has been initialized | |
| osc = new p5.Oscillator('sine'); | |
| osc.amp(0); // Start with zero amplitude | |
| }; | |
| function getSelectedCircleIndex() { | |
| for (let i = 0; i < circles.length; i++) { | |
| const circle = circles[i]; | |
| if ( | |
| p.mouseX > circle.x - circle.radius && | |
| p.mouseX < circle.x + circle.radius && | |
| p.mouseY > circle.y - circle.radius && | |
| p.mouseY < circle.y + circle.radius | |
| ) { | |
| console.log("clicked : " + i); | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| p.mousePressed = function () { | |
| selectedCircleIndex = getSelectedCircleIndex(); | |
| }; | |
| p.mouseDragged = function () { | |
| if (selectedCircleIndex > -1) { | |
| moveCount++; | |
| const circle = circles[selectedCircleIndex]; | |
| circle.x = p.mouseX; | |
| circle.y = p.mouseY; | |
| checkMatch(); | |
| } | |
| }; | |
| p.mouseReleased = function () { | |
| selectedCircleIndex = -1; | |
| }; | |
| p.draw = function draw() { | |
| p.background(0); | |
| p.blendMode(p.BLEND); | |
| if (gameComplete || gameOver) { | |
| // Draw modal overlay | |
| p.background(0, 200); | |
| // Create centered content box | |
| const boxWidth = 500; | |
| const boxHeight = 400; | |
| const boxX = p.width/2 - boxWidth/2; | |
| const boxY = p.height/2 - boxHeight/2; | |
| // Draw box background | |
| p.fill(20); | |
| p.stroke(255); | |
| p.strokeWeight(2); | |
| p.rect(boxX, boxY, boxWidth, boxHeight, 20); | |
| // Draw content | |
| p.fill(255); | |
| p.noStroke(); | |
| p.textFont('Space Mono'); | |
| p.textAlign(p.CENTER, p.CENTER); | |
| // Title | |
| p.textSize(48); | |
| p.text(gameOver ? 'Time\'s Up!' : 'Game Complete!', p.width/2, boxY + 80); | |
| if (!gameOver) { | |
| // Game complete stats | |
| p.textSize(24); | |
| p.fill(200); | |
| p.text(`Final Score: ${score}`, p.width/2, boxY + 160); | |
| p.text(`Total Moves: ${totalMoves}`, p.width/2, boxY + 200); | |
| p.text(`Average Moves Per Level: ${(totalMoves/FINAL_LEVEL).toFixed(1)}`, p.width/2, boxY + 240); | |
| // Message | |
| p.textSize(18); | |
| p.fill(150); | |
| const efficiency = totalMoves < FINAL_LEVEL * 10 ? "Amazing efficiency!" : | |
| totalMoves < FINAL_LEVEL * 20 ? "Great job!" : | |
| "Well done!"; | |
| p.text(efficiency, p.width/2, boxY + 280); | |
| } else { | |
| // Game over message | |
| p.textSize(24); | |
| p.fill(200); | |
| p.text(`Total Levels Completed: ${currentLevel - 1}`, p.width/2, boxY + 160); | |
| p.text(`Total Moves Made: ${totalMoves + moveCount}`, p.width/2, boxY + 200); | |
| p.text(`Circles Matched: ${matchedCircles.filter(m => m).length}/${currentLevel}`, p.width/2, boxY + 240); | |
| p.textSize(18); | |
| p.fill(150); | |
| p.text('Keep trying! You can do it!', p.width/2, boxY + 280); | |
| } | |
| // Draw try again/play again button | |
| const buttonWidth = 200; | |
| const buttonHeight = 50; | |
| const buttonX = p.width/2 - buttonWidth/2; | |
| const buttonY = boxY + boxHeight - 80; | |
| // Check if mouse is over button | |
| const mouseOverButton = p.mouseX > buttonX && p.mouseX < buttonX + buttonWidth && | |
| p.mouseY > buttonY && p.mouseY < buttonY + buttonHeight; | |
| // Draw button | |
| p.fill(mouseOverButton ? 40 : 30); | |
| p.stroke(255); | |
| p.strokeWeight(2); | |
| p.rect(buttonX, buttonY, buttonWidth, buttonHeight, 10); | |
| // Button text | |
| p.fill(255); | |
| p.noStroke(); | |
| p.textSize(24); | |
| p.text(gameOver ? 'Try Again' : 'Play Again', p.width/2, buttonY + buttonHeight/2); | |
| // Add click handler for button | |
| if (mouseOverButton && p.mouseIsPressed) { | |
| resetGame(); | |
| } | |
| return; // Don't draw the rest of the game | |
| } | |
| // Draw target circles first (semi-transparent) | |
| for(let i = 0; i < targetCircles.length; i++) { | |
| const target = targetCircles[i]; | |
| p.noStroke(); | |
| p.blendMode(p.SCREEN); | |
| const targetColor = p.color(target.color); | |
| targetColor.setAlpha(50); | |
| p.fill(targetColor); | |
| p.circle(target.x, target.y, target.radius * 2); | |
| } | |
| // Update animation times | |
| for(let i = 0; i < matchAnimationTime.length; i++) { | |
| if (matchedCircles[i]) { | |
| matchAnimationTime[i] = (matchAnimationTime[i] + 0.1) % (Math.PI * 2); | |
| } | |
| } | |
| // Draw the movable circles | |
| for(let i = 0; i < window.circles.length; i++) { | |
| const circle = window.circles[i]; | |
| p.noStroke(); | |
| p.blendMode(p.SCREEN); | |
| p.fill(circle.color); | |
| // ease towards the final position | |
| const easing = 0.15; | |
| circlesCurrent[i].x = circlesCurrent[i].x + (circle.x - circlesCurrent[i].x) * easing; | |
| circlesCurrent[i].y = circlesCurrent[i].y + (circle.y - circlesCurrent[i].y) * easing; | |
| circlesCurrent[i].radius = circlesCurrent[i].radius + (circle.radius - circlesCurrent[i].radius) * easing; | |
| // If circle is matched, add white border and pulse animation | |
| if (matchedCircles[i]) { | |
| const pulseAmount = Math.sin(matchAnimationTime[i]) * 5; | |
| p.stroke(255); | |
| p.strokeWeight(2); | |
| p.circle(circlesCurrent[i].x, circlesCurrent[i].y, (circlesCurrent[i].radius * 2) + pulseAmount); | |
| p.noStroke(); | |
| p.circle(circlesCurrent[i].x, circlesCurrent[i].y, circlesCurrent[i].radius * 2); | |
| } else { | |
| p.circle(circlesCurrent[i].x, circlesCurrent[i].y, circlesCurrent[i].radius * 2); | |
| } | |
| } | |
| // Draw UI elements with BLEND mode | |
| p.blendMode(p.BLEND); | |
| // Draw dark semi-transparent background for UI | |
| p.noStroke(); | |
| p.fill(0, 180); | |
| p.rect(0, 20, p.width, 100); | |
| // Draw level and timer | |
| p.fill(255); | |
| p.textFont('Space Mono'); | |
| p.textSize(24); | |
| p.textAlign(p.CENTER, p.TOP); | |
| p.text(`Level: ${window.circles.length} | Time Left: ${timeLeft}s`, p.width/2, 40); | |
| // Draw instructions | |
| p.textSize(14); | |
| p.fill(180); | |
| p.text('Speak to Gemini to move the circle to their place', p.width/2, 75); | |
| if(levelComplete) { | |
| p.textSize(32); | |
| p.textAlign(p.CENTER, p.CENTER); | |
| p.fill(255); | |
| p.text('Level Complete!', p.width/2, 170); | |
| } | |
| }; | |
| }); | |
| }; | |
| </script> | |
| </body> | |
| </html> | |