Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Cosmic Canvas | Interactive Particle Art</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #00f3ff; | |
| --secondary: #ff00aa; | |
| --dark: #0a0a12; | |
| --glass: rgba(255, 255, 255, 0.05); | |
| } | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: var(--dark); | |
| font-family: 'Space Grotesk', sans-serif; | |
| color: white; | |
| } | |
| canvas { | |
| display: block; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| z-index: 0; | |
| } | |
| /* Custom Scrollbar for panels */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(0,0,0,0.3); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--primary); | |
| border-radius: 3px; | |
| } | |
| /* Glassmorphism Utilities */ | |
| .glass-panel { | |
| background: var(--glass); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); | |
| } | |
| .neon-text { | |
| text-shadow: 0 0 5px var(--primary), 0 0 10px var(--primary); | |
| } | |
| .neon-text-pink { | |
| text-shadow: 0 0 5px var(--secondary), 0 0 10px var(--secondary); | |
| } | |
| /* Range Slider Styling */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 16px; | |
| width: 16px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| cursor: pointer; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px var(--primary); | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: rgba(255,255,255,0.2); | |
| border-radius: 2px; | |
| } | |
| /* Animations */ | |
| @keyframes float { | |
| 0% { transform: translateY(0px); } | |
| 50% { transform: translateY(-10px); } | |
| 100% { transform: translateY(0px); } | |
| } | |
| .animate-float { | |
| animation: float 6s ease-in-out infinite; | |
| } | |
| @keyframes pulse-glow { | |
| 0%, 100% { box-shadow: 0 0 10px var(--primary); } | |
| 50% { box-shadow: 0 0 25px var(--primary), 0 0 10px var(--secondary); } | |
| } | |
| .btn-glow:hover { | |
| animation: pulse-glow 1.5s infinite; | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s ease-in forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| </head> | |
| <body class="antialiased selection:bg-cyan-500 selection:text-black"> | |
| <!-- Canvas Layer --> | |
| <canvas id="canvas1"></canvas> | |
| <!-- UI Overlay --> | |
| <div class="relative z-10 w-full h-screen pointer-events-none flex flex-col justify-between p-4 md:p-8"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-start pointer-events-auto"> | |
| <div class="glass-panel p-4 rounded-xl border-l-4 border-cyan-400 animate-float"> | |
| <h1 class="text-3xl md:text-5xl font-bold tracking-tighter neon-text">COSMIC<span class="text-pink-500 neon-text-pink">CANVAS</span></h1> | |
| <p class="text-xs md:text-sm text-gray-300 mt-1 tracking-widest uppercase">Interactive Particle Simulation</p> | |
| <div class="mt-2 text-[10px] text-gray-400"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="underline hover:text-cyan-400 transition-colors">anycoder</a> | |
| </div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button id="audioBtn" class="glass-panel p-3 rounded-full hover:bg-white/10 transition-all btn-glow group" title="Toggle Ambient Audio"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-cyan-400 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" /> | |
| </svg> | |
| </button> | |
| <button id="infoBtn" class="glass-panel p-3 rounded-full hover:bg-white/10 transition-all btn-glow group"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-pink-500 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Center Interaction Hint --> | |
| <div id="hint" class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none transition-opacity duration-500"> | |
| <p class="text-cyan-200 text-lg md:text-2xl font-light tracking-widest opacity-70">CLICK & DRAG TO CREATE</p> | |
| <p class="text-gray-400 text-sm mt-2">MOVE MOUSE TO INTERACT</p> | |
| </div> | |
| <!-- Controls Footer --> | |
| <footer class="pointer-events-auto w-full max-w-4xl mx-auto"> | |
| <div class="glass-panel rounded-2xl p-4 md:p-6 grid grid-cols-1 md:grid-cols-4 gap-6 items-end"> | |
| <!-- Mode Selector --> | |
| <div class="col-span-1 md:col-span-1"> | |
| <label class="block text-xs font-bold text-cyan-400 uppercase tracking-wider mb-2">Simulation Mode</label> | |
| <div class="flex gap-2"> | |
| <button class="mode-btn flex-1 py-2 text-xs border border-cyan-500/30 rounded bg-cyan-500/20 text-cyan-300 hover:bg-cyan-500 hover:text-black transition-all active" data-mode="particles">Flow</button> | |
| <button class="mode-btn flex-1 py-2 text-xs border border-pink-500/30 rounded bg-pink-500/20 text-pink-300 hover:bg-pink-500 hover:text-black transition-all" data-mode="constellation">Web</button> | |
| </div> | |
| </div> | |
| <!-- Sliders --> | |
| <div class="col-span-1 md:col-span-2 grid grid-cols-2 gap-4"> | |
| <div> | |
| <div class="flex justify-between text-xs text-gray-400 mb-1"> | |
| <span>Particle Count</span> | |
| <span id="countVal">150</span> | |
| </div> | |
| <input type="range" id="particleCount" min="50" max="400" value="150"> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-xs text-gray-400 mb-1"> | |
| <span>Speed</span> | |
| <span id="speedVal">1.0x</span> | |
| </div> | |
| <input type="range" id="speedControl" min="0.1" max="3.0" step="0.1" value="1.0"> | |
| </div> | |
| </div> | |
| <!-- Actions --> | |
| <div class="col-span-1 md:col-span-1 flex gap-2"> | |
| <button id="resetBtn" class="flex-1 bg-gray-700 hover:bg-gray-600 text-white py-2 px-4 rounded text-sm font-medium transition-colors border border-gray-600"> | |
| Reset | |
| </button> | |
| <button id="colorBtn" class="flex-1 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white py-2 px-4 rounded text-sm font-medium transition-all shadow-lg shadow-cyan-500/30"> | |
| Theme | |
| </button> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <!-- Info Modal --> | |
| <div id="infoModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300"> | |
| <div class="glass-panel max-w-md w-full p-8 rounded-2xl transform scale-95 transition-transform duration-300" id="modalContent"> | |
| <h2 class="text-2xl font-bold text-white mb-4">How to use Cosmic Canvas</h2> | |
| <ul class="space-y-3 text-gray-300 text-sm"> | |
| <li class="flex items-start"><span class="text-cyan-400 mr-2">●</span> <strong>Flow Mode:</strong> Particles follow the mouse and leave a cosmic trail.</li> | |
| <li class="flex items-start"><span class="text-pink-400 mr-2">●</span> <strong>Web Mode:</strong> Particles connect to each other forming a neural network.</li> | |
| <li class="flex items-start"><span class="text-yellow-400 mr-2">●</span> <strong>Audio:</strong> Toggle the ambient soundscape for immersion.</li> | |
| <li class="flex items-start"><span class="text-green-400 mr-2">●</span> <strong>Click & Drag:</strong> Creates a burst of energy and sound.</li> | |
| </ul> | |
| <button id="closeModal" class="mt-6 w-full bg-white/10 hover:bg-white/20 text-white py-3 rounded-lg transition-colors border border-white/10"> | |
| Start Creating | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * COSMIC CANVAS ENGINE | |
| * Handles Particle System, Audio Synthesis, and UI Logic | |
| */ | |
| const canvas = document.getElementById('canvas1'); | |
| const ctx = canvas.getContext('2d'); | |
| // State | |
| let particlesArray = []; | |
| let hue = 0; | |
| let isMouseDown = false; | |
| let mode = 'particles'; // 'particles' or 'constellation' | |
| let animationId; | |
| let audioStarted = false; | |
| // Configuration | |
| const config = { | |
| particleCount: 150, | |
| baseSpeed: 1.0, | |
| connectionDistance: 100, | |
| mouseRadius: 150, | |
| themeHue: 0 // 0 = Cyan/Blue, 180 = Purple/Pink, etc. | |
| }; | |
| // Mouse Object | |
| const mouse = { | |
| x: undefined, | |
| y: undefined, | |
| } | |
| // Resize Handling | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| init(); | |
| }); | |
| // Mouse Events | |
| window.addEventListener('mousemove', (e) => { | |
| mouse.x = e.x; | |
| mouse.y = e.y; | |
| // Hide hint on first interaction | |
| const hint = document.getElementById('hint'); | |
| if(hint.style.opacity !== '0') hint.style.opacity = '0'; | |
| }); | |
| window.addEventListener('mousedown', () => { | |
| isMouseDown = true; | |
| if(audioStarted) playBurstSound(); | |
| }); | |
| window.addEventListener('mouseup', () => { | |
| isMouseDown = false; | |
| }); | |
| window.addEventListener('mouseout', () => { | |
| mouse.x = undefined; | |
| mouse.y = undefined; | |
| }); | |
| // --- Audio System (Tone.js) --- | |
| let synth, drone, filter; | |
| async function initAudio() { | |
| await Tone.start(); | |
| // PolySynth for interaction sounds | |
| synth = new Tone.PolySynth(Tone.Synth, { | |
| oscillator: { type: "fatsawtooth" }, | |
| envelope: { attack: 0.01, decay: 0.1, sustain: 0.1, release: 1 } | |
| }).toDestination(); | |
| synth.volume.value = -10; | |
| // Drone for background ambience | |
| filter = new Tone.AutoFilter({ | |
| frequency: 0.1, | |
| baseFrequency: 200, | |
| octaves: 2.6 | |
| }).toDestination().start(); | |
| drone = new Tone.Oscillator(110, "sine").connect(filter).start(); | |
| drone.volume.value = -20; | |
| // Reverb for spacey feel | |
| const reverb = new Tone.Reverb({ decay: 5, wet: 0.5 }).toDestination(); | |
| synth.connect(reverb); | |
| drone.connect(reverb); | |
| audioStarted = true; | |
| } | |
| function playBurstSound() { | |
| if(!synth) return; | |
| const notes = ["C4", "E4", "G4", "A4", "C5"]; | |
| const note = notes[Math.floor(Math.random() * notes.length)]; | |
| synth.triggerAttackRelease(note, "8n"); | |
| } | |
| function toggleAudio() { | |
| if (!audioStarted) { | |
| initAudio(); | |
| document.getElementById('audioBtn').classList.add('bg-cyan-500/40'); | |
| } else { | |
| Tone.Destination.mute = !Tone.Destination.mute; | |
| document.getElementById('audioBtn').classList.toggle('bg-cyan-500/40'); | |
| } | |
| } | |
| // --- Particle Class --- | |
| class Particle { | |
| constructor() { | |
| this.x = Math.random() * canvas.width; | |
| this.y = Math.random() * canvas.height; | |
| this.size = Math.random() * 3 + 1; | |
| // Random velocity vector | |
| this.speedX = (Math.random() * 3 - 1.5) * config.baseSpeed; | |
| this.speedY = (Math.random() * 3 - 1.5) * config.baseSpeed; | |
| this.color = `hsl(${config.themeHue + Math.random() * 60}, 100%, 50%)`; | |
| } | |
| update() { | |
| // Movement | |
| this.x += this.speedX; | |
| this.y += this.speedY; | |
| // Bounce off edges | |
| if (this.x > canvas.width || this.x < 0) this.speedX = -this.speedX; | |
| if (this.y > canvas.height || this.y < 0) this.speedY = -this.speedY; | |
| // Mouse Interaction | |
| if (mouse.x != undefined) { | |
| let dx = mouse.x - this.x; | |
| let dy = mouse.y - this.y; | |
| let distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < config.mouseRadius) { | |
| const forceDirectionX = dx / distance; | |
| const forceDirectionY = dy / distance; | |
| const force = (config.mouseRadius - distance) / config.mouseRadius; | |
| // Push away or attract based on click | |
| const directionMultiplier = isMouseDown ? 1 : -1; | |
| const directionX = forceDirectionX * force * directionMultiplier * 5; | |
| const directionY = forceDirectionY * force * directionMultiplier * 5; | |
| this.x += directionX; | |
| this.y += directionY; | |
| } | |
| } | |
| } | |
| draw() { | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| // --- System Logic --- | |
| function init() { | |
| particlesArray = []; | |
| for (let i = 0; i < config.particleCount; i++) { | |
| particlesArray.push(new Particle()); | |
| } | |
| } | |
| function handleParticles() { | |
| for (let i = 0; i < particlesArray.length; i++) { | |
| particlesArray[i].update(); | |
| particlesArray[i].draw(); | |
| // Constellation Mode Logic | |
| if (mode === 'constellation') { | |
| for (let j = i; j < particlesArray.length; j++) { | |
| let dx = particlesArray[i].x - particlesArray[j].x; | |
| let dy = particlesArray[i].y - particlesArray[j].y; | |
| let distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < config.connectionDistance) { | |
| ctx.beginPath(); | |
| // Dynamic opacity based on distance | |
| let opacity = 1 - (distance / config.connectionDistance); | |
| ctx.strokeStyle = `hsla(${config.themeHue}, 100%, 50%, ${opacity})`; | |
| ctx.lineWidth = 1; | |
| ctx.moveTo(particlesArray[i].x, particlesArray[i].y); | |
| ctx.lineTo(particlesArray[j].x, particlesArray[j].y); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function animate() { | |
| // Clear canvas with trail effect | |
| ctx.fillStyle = 'rgba(10, 10, 18, 0.15)'; // Low opacity for trails | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| handleParticles(); | |
| // Cycle global hue slowly for dynamic color shifting if not using fixed theme | |
| // hue += 0.5; | |
| animationId = requestAnimationFrame(animate); | |
| } | |
| // --- UI Logic --- | |
| // Mode Switching | |
| const modeBtns = document.querySelectorAll('.mode-btn'); | |
| modeBtns.forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| // Update UI | |
| modeBtns.forEach(b => { | |
| b.classList.remove('active', 'bg-cyan-500', 'text-black', 'bg-pink-500'); | |
| // Reset to default styles based on type | |
| if(b.dataset.mode === 'particles') { | |
| b.className = 'mode-btn flex-1 py-2 text-xs border border-cyan-500/30 rounded bg-cyan-500/20 text-cyan-300 hover:bg-cyan-500 hover:text-black transition-all'; | |
| } else { | |
| b.className = 'mode-btn flex-1 py-2 text-xs border border-pink-500/30 rounded bg-pink-500/20 text-pink-300 hover:bg-pink-500 hover:text-black transition-all'; | |
| } | |
| }); | |
| mode = e.target.dataset.mode; | |
| // Set Active Style | |
| if(mode === 'particles') { | |
| e.target.classList.add('bg-cyan-500', 'text-black'); | |
| } else { | |
| e.target.classList.add('bg-pink-500', 'text-black'); | |
| } | |
| }); | |
| }); | |
| // Controls | |
| document.getElementById('particleCount').addEventListener('input', (e) => { | |
| config.particleCount = parseInt(e.target.value); | |
| document.getElementById('countVal').innerText = config.particleCount; | |
| init(); | |
| }); | |
| document.getElementById('speedControl').addEventListener('input', (e) => { | |
| config.baseSpeed = parseFloat(e.target.value); | |
| document.getElementById('speedVal').innerText = config.baseSpeed + 'x'; | |
| // Update existing particles speed | |
| particlesArray.forEach(p => { | |
| p.speedX = (Math.random() * 3 - 1.5) * config.baseSpeed; | |
| p.speedY = (Math.random() * 3 - 1.5) * config.baseSpeed; | |
| }); | |
| }); | |
| document.getElementById('resetBtn').addEventListener('click', () => { | |
| init(); | |
| }); | |
| document.getElementById('colorBtn').addEventListener('click', () => { | |
| // Shift hue theme | |
| config.themeHue = (config.themeHue + 60) % 360; | |
| // Update particle colors | |
| particlesArray.forEach(p => { | |
| p.color = `hsl(${config.themeHue + Math.random() * 60}, 100%, 50%)`; | |
| }); | |
| }); | |
| // Audio Toggle | |
| document.getElementById('audioBtn').addEventListener('click', toggleAudio); | |
| // Modal Logic | |
| const modal = document.getElementById('infoModal'); | |
| const modalContent = document.getElementById('modalContent'); | |
| const infoBtn = document.getElementById('infoBtn'); | |
| const closeModal = document.getElementById('closeModal'); | |
| function openModal() { | |
| modal.classList.remove('pointer-events-none', 'opacity-0'); | |
| modalContent.classList.remove('scale-95'); | |
| modalContent.classList.add('scale-100'); | |
| } | |
| function hideModal() { | |
| modal.classList.add('pointer-events-none', 'opacity-0'); | |
| modalContent.classList.remove('scale-100'); | |
| modalContent.classList.add('scale-95'); | |
| } | |
| infoBtn.addEventListener('click', openModal); | |
| closeModal.addEventListener('click', hideModal); | |
| modal.addEventListener('click', (e) => { | |
| if(e.target === modal) hideModal(); | |
| }); | |
| // Initialization | |
| init(); | |
| animate(); | |
| // Show modal on first load after a short delay | |
| setTimeout(() => { | |
| // Check if user has interacted, if not, maybe show hint | |
| }, 2000); | |
| </script> | |
| </body> | |
| </html> |