Spaces:
Running
Running
| <html lang="pt-br"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Serene Pebble Pond</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/addons/p5.sound.min.js"></script> | |
| <style> | |
| body, html { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| #defaultCanvas0 { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| z-index: 1; | |
| } | |
| .ui-overlay { | |
| position: absolute; | |
| z-index: 10; | |
| pointer-events: none; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .ui-element { | |
| pointer-events: auto; | |
| } | |
| .ripple { | |
| position: absolute; | |
| border-radius: 50%; | |
| border: 2px solid rgba(255, 255, 255, 0.6); | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.8s ease-out forwards; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-blue-50 to-green-50 flex items-center justify-center min-h-screen"> | |
| <!-- Main Canvas for the Pond --> | |
| <div id="canvas-container" class="w-full h-full absolute"></div> | |
| <!-- UI Overlay --> | |
| <div class="ui-overlay flex flex-col items-center justify-between p-6"> | |
| <!-- Top Bar --> | |
| <div class="w-full flex justify-between items-center fade-in"> | |
| <h1 class="text-3xl font-light text-slate-700 drop-shadow-lg">Serene Pebble Pond</h1> | |
| <div class="flex space-x-4"> | |
| <button id="zenToggle" class="ui-element bg-white/30 backdrop-blur-sm text-slate-700 px-4 py-2 rounded-full shadow-lg hover:bg-white/50 transition-all duration-300 flex items-center space-x-2"> | |
| <i data-feather="moon"></i> | |
| <span id="zenText">Modo Zen: OFF</span> | |
| </button> | |
| <button id="infoBtn" class="ui-element bg-white/30 backdrop-blur-sm text-slate-700 w-10 h-10 rounded-full shadow-lg hover:bg-white/50 transition-all duration-300 flex items-center justify-center"> | |
| <i data-feather="info"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Center Instructions --> | |
| <div id="instructions" class="text-center bg-black/20 backdrop-blur-sm text-white p-4 rounded-lg shadow-xl fade-in"> | |
| <p class="text-lg mb-2">Arraste e solte para lançar uma pedrinha</p> | |
| <p class="text-sm opacity-80">Força e direção controlam o arremesso</p> | |
| </div> | |
| <!-- Bottom Stats --> | |
| <div class="w-full flex justify-between items-end fade-in"> | |
| <div class="bg-white/30 backdrop-blur-sm text-slate-700 px-4 py-2 rounded-lg shadow-lg"> | |
| <p class="text-sm">Pedras lançadas: <span id="pebbleCount">0</span></p> | |
| </div> | |
| <div class="text-slate-700 text-sm bg-white/30 backdrop-blur-sm px-4 py-2 rounded-lg shadow-lg"> | |
| <p>Respire. Relaxe. Aproveite o momento.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Info Modal --> | |
| <div id="infoModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-20 hidden"> | |
| <div class="bg-white rounded-xl shadow-2xl p-6 max-w-md mx-4 fade-in"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-2xl font-light text-slate-700">Sobre o Jogo</h2> | |
| <button id="closeModal" class="text-slate-500 hover:text-slate-700"> | |
| <i data-feather="x"></i> | |
| </button> | |
| </div> | |
| <p class="text-slate-600 mb-4"> | |
| Serene Pebble Pond é uma experiência interativa projetada para promover calma e relaxamento. Simule o ato nostálgico de jogar pedrinhas num lago tranquilo. | |
| </p> | |
| <ul class="text-slate-600 space-y-2 mb-4"> | |
| <li class="flex items-start"> | |
| <i data-feather="check-circle" class="text-green-500 mr-2 mt-1 flex-shrink-0"></i> | |
| <span>Arraste e solte para lançar pedras</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i data-feather="check-circle" class="text-green-500 mr-2 mt-1 flex-shrink-0"></i> | |
| <span>Observe as ondas hipnóticas se espalharem</span> | |
| </li> | |
| <li class="flex items-start"> | |
| <i data-feather="check-circle" class="text-green-500 mr-2 mt-1 flex-shrink-0"></i> | |
| <span>Ative o Modo Zen para relaxamento automático</span> | |
| </li> | |
| </ul> | |
| <p class="text-sm text-slate-500">Encontre sua paz. 🌿</p> | |
| </div> | |
| </div> | |
| <!-- Custom Ripple Container --> | |
| <div id="ripple-container"></div> | |
| // p5.js Sketch | |
| let pebbles = []; | |
| let ripples = []; | |
| let shells = []; | |
| let fishes = []; | |
| let zenMode = false; | |
| let pebbleCount = 0; | |
| let dragStart = null; | |
| let dragVector = null; | |
| function setup() { | |
| let canvas = createCanvas(windowWidth, windowHeight); | |
| canvas.parent('canvas-container'); | |
| frameRate(60); | |
| // Add some initial decorative elements | |
| for (let i = 0; i < 5; i++) { | |
| addRandomShell(); | |
| } | |
| // Add some initial fish | |
| for (let i = 0; i < 3; i++) { | |
| addRandomFish(); | |
| } | |
| } | |
| function drawBeachBackground() { | |
| // Sky gradient | |
| for (let y = 0; y < height * 0.6; y++) { | |
| let inter = map(y, 0, height * 0.6, 0, 1); | |
| let c = lerpColor(color('#87CEEB'), color('#E0F7FA'), inter); | |
| stroke(c); | |
| line(0, y, width, y); | |
| } | |
| // Beach sand area | |
| for (let y = height * 0.6; y < height * 0.75; y++) { | |
| let inter = map(y, height * 0.6, height * 0.75, 0, 1); | |
| let c = lerpColor(color('#F4A460'), color('#CD853F'), inter); | |
| stroke(c); | |
| line(0, y, width, y); | |
| } | |
| // Ocean water area | |
| for (let y = height * 0.75; y < height; y++) { | |
| let inter = map(y, height * 0.75, height, 0, 1); | |
| let c = lerpColor(color('#4A90E2'), color('#1E3F66'), inter); | |
| stroke(c); | |
| line(0, y, width, y); | |
| } | |
| // Add some sand texture | |
| stroke(255, 255, 255, 50); | |
| for (let i = 0; i < 200; i++) { | |
| let x = random(width); | |
| let y = random(height * 0.6, height * 0.75); | |
| let size = random(1, 3); | |
| point(x, y); | |
| } | |
| // Add wave foam at water edge | |
| stroke(255, 255, 255, 80); | |
| for (let x = 0; x < width; x += 20) { | |
| let waveHeight = sin(x * 0.01 + frameCount * 0.05) * 5 + 10; | |
| arc(x, height * 0.75, waveHeight, waveHeight, 0, PI); | |
| } | |
| } | |
| function draw() { | |
| // Clear with semi-transparent for trail effect | |
| background(255, 255, 255, 3); | |
| // Redraw beach background | |
| drawBeachBackground(); | |
| // Update and draw pebbles | |
| for (let i = pebbles.length - 1; i >= 0; i--) { | |
| pebbles[i].update(); | |
| pebbles[i].display(); | |
| if (pebbles[i].shouldRemove()) { | |
| pebbles.splice(i, 1); | |
| } | |
| } | |
| // Update and draw ripples | |
| for (let i = ripples.length - 1; i >= 0; i--) { | |
| ripples[i].update(); | |
| ripples[i].display(); | |
| if (ripples[i].shouldRemove()) { | |
| ripples.splice(i, 1); | |
| } | |
| } | |
| // Draw shells on beach | |
| for (let shell of shells) { | |
| shell.display(); | |
| } | |
| // Update and draw fish | |
| for (let i = fishes.length - 1; i >= 0; i--) { | |
| fishes[i].update(); | |
| fishes[i].display(); | |
| } | |
| // Draw drag vector if dragging | |
| if (dragVector) { | |
| drawDragVector(); | |
| } | |
| // Zen mode automatic ripples | |
| if (zenMode && frameCount % 120 === 0) { | |
| createZenRipple(); | |
| } | |
| } | |
| function mousePressed() { | |
| if (mouseY > height * 0.75) { // Only in water area | |
| dragStart = createVector(mouseX, mouseY); | |
| } | |
| return false; | |
| } | |
| function mouseDragged() { | |
| if (dragStart) { | |
| // Reverse the direction - drag towards beach to throw into ocean | |
| dragVector = createVector(dragStart.x - mouseX, dragStart.y - mouseY); | |
| // Limit vector length for better control | |
| dragVector.limit(150); | |
| } | |
| return false; | |
| } | |
| function mouseReleased() { | |
| if (dragStart && dragVector) { | |
| throwPebble(dragStart.x, dragStart.y, dragVector); | |
| dragStart = null; | |
| dragVector = null; | |
| } | |
| return false; | |
| } | |
| function throwPebble(x, y, vector) { | |
| // Create pebble | |
| let pebble = new Pebble(x, y, vector); | |
| pebbles.push(pebble); | |
| pebbleCount++; | |
| document.getElementById('pebbleCount').textContent = pebbleCount; | |
| // Play splash sound in real implementation | |
| // if (splashSound) { | |
| // splashSound.play(); | |
| // splashSound.setVolume(map(vector.mag(), 0, 150, 0.3, 1.0)); | |
| // } | |
| // Chance to spawn surprise shells | |
| if (random() < 0.15) { | |
| addRandomShell(); | |
| } | |
| // Chance to spawn fish | |
| if (random() < 0.1) { | |
| addRandomFish(); | |
| } | |
| } | |
| function drawDragVector() { | |
| stroke(255, 255, 255, 200); | |
| strokeWeight(2); | |
| drawingContext.setLineDash([5, 5]); | |
| line(dragStart.x, dragStart.y, dragStart.x + dragVector.x, dragStart.y + dragVector.y); | |
| // Draw arrow head | |
| push(); | |
| translate(dragStart.x + dragVector.x, dragStart.y + dragVector.y); | |
| rotate(dragVector.heading()); | |
| fill(255, 255, 255, 200); | |
| triangle(0, 0, -10, -5, -10, 5); | |
| pop(); | |
| drawingContext.setLineDash([]); | |
| } | |
| function createZenRipple() { | |
| let x = random(width * 0.2, width * 0.8); | |
| let y = random(height * 0.8, height * 0.95); | |
| let ripple = new Ripple(x, y, random(5, 15)); | |
| ripples.push(ripple); | |
| } | |
| function addRandomShell() { | |
| if (shells.length < 10) { // Limit number of shells | |
| let shell = new Shell(); | |
| shells.push(shell); | |
| } | |
| } | |
| function addRandomFish() { | |
| if (fishes.length < 8) { // Limit number of fish | |
| let fish = new Fish(); | |
| fishes.push(fish); | |
| } | |
| } | |
| function windowResized() { | |
| resizeCanvas(windowWidth, windowHeight); | |
| drawBeachBackground(); | |
| } | |
| // Pebble Class | |
| class Pebble { | |
| constructor(x, y, velocity) { | |
| this.pos = createVector(x, y); | |
| this.vel = velocity.copy().mult(0.2); // Scale down velocity | |
| this.acc = createVector(0, 0.3); // Gravity | |
| this.size = random(8, 15); | |
| this.color = color(random(180, 220), random(180, 220), random(180, 220)); | |
| this.splashCreated = false; | |
| } | |
| update() { | |
| this.vel.add(this.acc); | |
| this.pos.add(this.vel); | |
| // Check if hit water surface (ocean) | |
| if (!this.splashCreated && this.pos.y > height * 0.75) { | |
| this.createSplash(); | |
| this.splashCreated = true; | |
| } | |
| } | |
| display() { | |
| fill(this.color); | |
| noStroke(); | |
| ellipse(this.pos.x, this.pos.y, this.size * 2, this.size * 2); | |
| } | |
| createSplash() { | |
| let ripple = new Ripple(this.pos.x, this.pos.y, this.vel.mag() * 0.5); | |
| ripples.push(ripple); | |
| // Create HTML ripple effect | |
| createHTMLRipple(this.pos.x, this.pos.y); | |
| // Attract nearby fish to the pebble | |
| attractFishToPebble(this.pos.x, this.pos.y); | |
| } | |
| shouldRemove() { | |
| return this.pos.y > height + 50; | |
| } | |
| } | |
| // Ripple Class | |
| class Ripple { | |
| constructor(x, y, strength) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = 5; | |
| this.maxRadius = strength * 15 + 50; | |
| this.speed = map(strength, 0, 75, 1, 3); | |
| this.alpha = 1; | |
| } | |
| update() { | |
| this.radius += this.speed; | |
| this.alpha = 1 - (this.radius / this.maxRadius); | |
| } | |
| display() { | |
| if (this.alpha <= 0) return; | |
| stroke(255, 255, 255, this.alpha * 150); | |
| strokeWeight(2); | |
| noFill(); | |
| ellipse(this.x, this.y, this.radius * 2, this.radius * 2); | |
| } | |
| shouldRemove() { | |
| return this.radius >= this.maxRadius; | |
| } | |
| } | |
| // Fish Class | |
| class Fish { | |
| constructor() { | |
| this.x = random(width); | |
| this.y = random(height * 0.7, height * 0.95); | |
| this.speed = random(0.5, 2); | |
| this.direction = random() < 0.5 ? 1 : -1; | |
| this.size = random(15, 25); | |
| this.wiggle = 0; | |
| this.wiggleSpeed = random(0.1, 0.3); | |
| this.color = [255, random(100, 150), random(50, 100)]; // Orange tones | |
| this.targetX = null; | |
| this.targetY = null; | |
| this.attractionTime = 0; | |
| this.maxAttractionTime = 180; // 3 seconds at 60fps | |
| } | |
| update() { | |
| // If attracted to a pebble | |
| if (this.targetX !== null && this.targetY !== null) { | |
| let dx = this.targetX - this.x; | |
| let dy = this.targetY - this.y; | |
| let distance = sqrt(dx * dx + dy * dy); | |
| if (distance > 10 && this.attractionTime < this.maxAttractionTime) { | |
| // Move towards target | |
| this.x += (dx / distance) * this.speed * 2; | |
| this.y += (dy / distance) * this.speed * 2; | |
| this.direction = dx > 0 ? 1 : -1; | |
| this.attractionTime++; | |
| } else { | |
| // Stop attraction | |
| this.targetX = null; | |
| this.targetY = null; | |
| this.attractionTime = 0; | |
| } | |
| } else { | |
| // Normal swimming behavior | |
| this.x += this.speed * this.direction; | |
| this.wiggle += this.wiggleSpeed; | |
| // Reverse direction at edges | |
| if (this.x < -50 || this.x > width + 50) { | |
| this.direction *= -1; | |
| } | |
| } | |
| } | |
| display() { | |
| drawingContext.save(); | |
| drawingContext.translate(this.x, this.y + sin(this.wiggle) * 3); | |
| drawingContext.scale(this.direction, 1); | |
| // Fish body | |
| drawingContext.fillStyle = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`; | |
| drawingContext.beginPath(); | |
| drawingContext.ellipse(0, 0, this.size, this.size/2, 0, 0, TWO_PI); | |
| drawingContext.fill(); | |
| // Tail | |
| drawingContext.beginPath(); | |
| drawingContext.moveTo(-this.size, 0); | |
| drawingContext.lineTo(-this.size - 10, -this.size/2); | |
| drawingContext.lineTo(-this.size - 10, this.size/2); | |
| drawingContext.closePath(); | |
| drawingContext.fill(); | |
| // Eye | |
| drawingContext.fillStyle = 'white'; | |
| drawingContext.beginPath(); | |
| drawingContext.arc(this.size * 0.6, -this.size/4, this.size/5, 0, TWO_PI); | |
| drawingContext.fill(); | |
| drawingContext.fillStyle = 'black'; | |
| drawingContext.beginPath(); | |
| drawingContext.arc(this.size * 0.6, -this.size/4, this.size/10, 0, TWO_PI); | |
| drawingContext.fill(); | |
| drawingContext.restore(); | |
| } | |
| // Method to attract fish to pebble | |
| attractTo(x, y) { | |
| this.targetX = x; | |
| this.targetY = y; | |
| this.attractionTime = 0; | |
| } | |
| } | |
| // Function to attract fish to pebble | |
| function attractFishToPebble(x, y) { | |
| for (let fish of fishes) { | |
| // Check if fish is within attraction radius (200 pixels) | |
| let dx = fish.x - x; | |
| let dy = fish.y - y; | |
| let distance = sqrt(dx * dx + dy * dy); | |
| if (distance < 200 && random() < 0.7) { // 70% chance to attract | |
| fish.attractTo(x, y); | |
| } | |
| } | |
| } | |
| // HTML Ripple Effect | |
| function createHTMLRipple(x, y) { | |
| const ripple = document.createElement('div'); | |
| ripple.className = 'ripple'; | |
| ripple.style.left = x + 'px'; | |
| ripple.style.top = y + 'px'; | |
| ripple.style.width = '0px'; | |
| ripple.style.height = '0px'; | |
| document.getElementById('ripple-container').appendChild(ripple); | |
| setTimeout(() => { | |
| ripple.style.transition = 'all 1s ease-out'; | |
| ripple.style.width = '100px'; | |
| ripple.style.height = '100px'; | |
| ripple.style.opacity = '0'; | |
| }, 10); | |
| setTimeout(() => { | |
| ripple.remove(); | |
| }, 1100); | |
| } | |
| // UI Event Listeners | |
| document.getElementById('zenToggle').addEventListener('click', function() { | |
| zenMode = !zenMode; | |
| const zenText = document.getElementById('zenText'); | |
| const icon = this.querySelector('i'); | |
| if (zenMode) { | |
| zenText.textContent = 'Modo Zen: ON'; | |
| this.classList.add('bg-green-200/50'); | |
| feather.icons['sun'].replace(icon); | |
| } else { | |
| zenText.textContent = 'Modo Zen: OFF'; | |
| this.classList.remove('bg-green-200/50'); | |
| feather.icons['moon'].replace(icon); | |
| } | |
| }); | |
| document.getElementById('infoBtn').addEventListener('click', function() { | |
| document.getElementById('infoModal').classList.remove('hidden'); | |
| }); | |
| document.getElementById('closeModal').addEventListener('click', function() { | |
| document.getElementById('infoModal').classList.add('hidden'); | |
| }); | |
| // Close modal on background click | |
| document.getElementById('infoModal').addEventListener('click', function(e) { | |
| if (e.target === this) { | |
| this.classList.add('hidden'); | |
| } | |
| }); | |
| // Initialize feather icons | |
| feather.replace(); | |
| </script> | |
| </body> | |
| </html> | |