Spaces:
Running
Running
| import gradio as gr | |
| import html | |
| from game.engine import get_myco_narrative | |
| def cute_mushroom_garden_html() -> str: | |
| """ | |
| Pure iframe srcdoc implementation of a cute mushroom garden. | |
| Generates happy, blinking mushrooms that grow, sway, and shrink. | |
| """ | |
| iframe_content = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| /* Soft magical forest background */ | |
| background: linear-gradient(180deg, #1a2b4c 0%, #2a4c5e 50%, #4a7c59 100%); | |
| } | |
| canvas { display: block; } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="cute-canvas"></canvas> | |
| <script> | |
| const canvas = document.getElementById('cute-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| function resize() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| const mushrooms = []; | |
| const fireflies = []; | |
| // Helper to draw a single cute mushroom | |
| function drawMushroom(m, time) { | |
| ctx.save(); | |
| ctx.translate(m.x, m.y); | |
| // Bouncing and swaying math | |
| const sway = Math.sin(time * 0.002 + m.phase) * 0.1; | |
| const bounce = Math.sin(time * 0.005 + m.phase) * 0.05 + 1; | |
| ctx.rotate(sway); | |
| ctx.scale(m.currentSize, m.currentSize * bounce); | |
| // 1. Draw Stem | |
| ctx.fillStyle = '#fff4e6'; | |
| ctx.beginPath(); | |
| ctx.roundRect(-12, 0, 24, 35, 10); | |
| ctx.fill(); | |
| // 2. Draw Cute Face | |
| ctx.fillStyle = '#4a3b32'; | |
| const isBlinking = (time % m.blinkInterval) < 150; | |
| if (isBlinking) { | |
| // Closed eyes (wincing/blinking) | |
| ctx.fillRect(-7, 12, 4, 2); | |
| ctx.fillRect(3, 12, 4, 2); | |
| } else { | |
| // Open eyes | |
| ctx.beginPath(); ctx.arc(-5, 12, 2.5, 0, Math.PI*2); ctx.fill(); | |
| ctx.beginPath(); ctx.arc(5, 12, 2.5, 0, Math.PI*2); ctx.fill(); | |
| } | |
| // Tiny smile | |
| ctx.strokeStyle = '#4a3b32'; | |
| ctx.lineWidth = 1.5; | |
| ctx.beginPath(); | |
| ctx.arc(0, 15, 3, 0, Math.PI, false); | |
| ctx.stroke(); | |
| // Blush | |
| ctx.fillStyle = 'rgba(255, 150, 150, 0.5)'; | |
| ctx.beginPath(); ctx.arc(-9, 16, 3, 0, Math.PI*2); ctx.fill(); | |
| ctx.beginPath(); ctx.arc(9, 16, 3, 0, Math.PI*2); ctx.fill(); | |
| // 3. Draw Cap | |
| ctx.fillStyle = `hsl(${m.hue}, 80%, 75%)`; // Soft pastel colors | |
| ctx.beginPath(); | |
| ctx.moveTo(-35, 5); | |
| // Bezier curves make it look like a puffy umbrella | |
| ctx.bezierCurveTo(-35, -30, 35, -30, 35, 5); | |
| ctx.bezierCurveTo(35, 15, -35, 15, -35, 5); | |
| ctx.fill(); | |
| // Cap Shadow/Detail | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.beginPath(); ctx.arc(-15, -12, 6, 0, Math.PI*2); ctx.fill(); | |
| ctx.beginPath(); ctx.arc(12, -8, 4, 0, Math.PI*2); ctx.fill(); | |
| ctx.beginPath(); ctx.arc(0, -18, 5, 0, Math.PI*2); ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function animate(time) { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // --- Fireflies Logic --- | |
| if (Math.random() < 0.1 && fireflies.length < 50) { | |
| fireflies.push({ | |
| x: Math.random() * canvas.width, | |
| y: canvas.height + 10, | |
| size: Math.random() * 2 + 1, | |
| speed: Math.random() * 1 + 0.5, | |
| wobble: Math.random() * Math.PI * 2 | |
| }); | |
| } | |
| ctx.fillStyle = '#fffae6'; | |
| for (let i = fireflies.length - 1; i >= 0; i--) { | |
| let f = fireflies[i]; | |
| f.y -= f.speed; | |
| f.x += Math.sin(time * 0.002 + f.wobble) * 0.5; | |
| ctx.globalAlpha = Math.sin(time * 0.005 + f.wobble) * 0.5 + 0.5; | |
| ctx.beginPath(); | |
| ctx.arc(f.x, f.y, f.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| if (f.y < -10) fireflies.splice(i, 1); | |
| } | |
| ctx.globalAlpha = 1.0; | |
| // --- Mushroom Logic --- | |
| // Randomly spawn new mushrooms | |
| if (Math.random() < 0.02 && mushrooms.length < 35) { | |
| mushrooms.push({ | |
| x: Math.random() * canvas.width, | |
| y: (canvas.height * 0.4) + (Math.random() * canvas.height * 0.6), // Spawn in bottom 60% | |
| targetSize: Math.random() * 1.5 + 0.8, | |
| currentSize: 0, | |
| hue: Math.random() * 360, | |
| phase: Math.random() * Math.PI * 2, | |
| blinkInterval: Math.floor(Math.random() * 3000) + 2000, | |
| life: Math.random() * 500 + 300 // How long they stay before shrinking | |
| }); | |
| } | |
| // Sort mushrooms by Y coordinate so closer ones draw on top (Depth sorting) | |
| mushrooms.sort((a, b) => a.y - b.y); | |
| for (let i = mushrooms.length - 1; i >= 0; i--) { | |
| let m = mushrooms[i]; | |
| m.life--; | |
| if (m.life < 0) { | |
| m.targetSize = 0; // Shrink away | |
| } | |
| // Smoothly grow or shrink | |
| m.currentSize += (m.targetSize - m.currentSize) * 0.05; | |
| drawMushroom(m, time); | |
| // Remove if totally shrunk | |
| if (m.life < 0 && m.currentSize < 0.05) { | |
| mushrooms.splice(i, 1); | |
| } | |
| } | |
| requestAnimationFrame(animate); | |
| } | |
| requestAnimationFrame(animate); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Safely escape the HTML | |
| escaped_srcdoc = html.escape(iframe_content) | |
| # Return a clean, borderless container | |
| return f''' | |
| <div style="width: 100%; height: 500px; border-radius: 16px; overflow: hidden; border: 4px solid #f8c8dc; box-shadow: 0 8px 32px rgba(0,0,0,0.15);"> | |
| <iframe | |
| srcdoc="{escaped_srcdoc}" | |
| style="width: 100%; height: 100%; border: none;" | |
| scrolling="no"> | |
| </iframe> | |
| </div> | |
| ''' | |
| def render_shroom_tab(): | |
| # 1. Assign the block to 'demo' so it can be returned | |
| with gr.Blocks() as demo: | |
| with gr.Tabs(): | |
| with gr.Tab("✨ Magic Mushroom Garden"): | |
| # The pure animation | |
| gr.HTML(value=cute_mushroom_garden_html()) | |
| # Subtle attribution | |
| gr.Markdown( | |
| "<sub>*Story generated by Myco AI*</sub>", | |
| elem_id="ai-attribution" | |
| ) | |
| # 2. Define the narrative component ONCE | |
| narrative = gr.Markdown( | |
| value="Myco is searching for a story hidden in the moss...", | |
| elem_id="narrative-card" | |
| ) | |
| # 3. Timer updates the ONE narrative component | |
| gr.Timer(20).tick(fn=get_myco_narrative, outputs=narrative) | |
| return demo | |