Last active
February 1, 2026 20:35
-
-
Save jaymcgavren/b781737cebb4a59bfc7c8d65eb1aaa12 to your computer and use it in GitHub Desktop.
Bouncing images and sounds .html file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- | |
| ChatGPT prompt: | |
| Create a simple app that resides entirely in a single .html file. It should | |
| take three images from browser storage and animate them bouncing around the | |
| screen. Each of the three images should have a distinct sound, also loaded | |
| from browser storage, that plays when it bounces. | |
| Refinement (because ChatGPT didn't use localStorage like it should have): | |
| When the user selects an image or sound file, store it in browser storage | |
| using "window.localStorage.setItem". When the page loads, load images using | |
| "window.localStorage.getItem", if present. Present the menu to load | |
| images/sounds only when images/sounds are not present in | |
| "window.localStorage". | |
| --> | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Bouncing Images (Persistent)</title> | |
| <style> | |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| background: #111; | |
| height: 100%; | |
| font-family: system-ui, sans-serif; | |
| color: #eee; | |
| } | |
| #controls { | |
| position: fixed; | |
| top: 12px; | |
| left: 12px; | |
| background: rgba(0,0,0,0.75); | |
| padding: 12px; | |
| border-radius: 8px; | |
| z-index: 10; | |
| } | |
| #controls label { | |
| display: block; | |
| margin-bottom: 6px; | |
| font-size: 14px; | |
| } | |
| .sprite { | |
| position: absolute; | |
| width: 120px; | |
| pointer-events: none; | |
| user-select: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="controls"></div> | |
| <script> | |
| const SPRITE_COUNT = 3; | |
| const sprites = []; | |
| const controls = document.getElementById("controls"); | |
| /* ---------------- Utilities ---------------- */ | |
| function readFileAsDataURL(file) { | |
| return new Promise(resolve => { | |
| const reader = new FileReader(); | |
| reader.onload = () => resolve(reader.result); | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| function lsKey(type, index) { | |
| return `bounce_${type}_${index}`; | |
| } | |
| /* ---------------- UI Setup ---------------- */ | |
| function createFileInput(labelText, accept, onLoad) { | |
| const label = document.createElement("label"); | |
| label.textContent = labelText; | |
| const input = document.createElement("input"); | |
| input.type = "file"; | |
| input.accept = accept; | |
| input.addEventListener("change", async () => { | |
| const file = input.files[0]; | |
| if (!file) return; | |
| const dataUrl = await readFileAsDataURL(file); | |
| onLoad(dataUrl); | |
| label.remove(); | |
| checkReady(); | |
| }); | |
| label.appendChild(document.createElement("br")); | |
| label.appendChild(input); | |
| controls.appendChild(label); | |
| } | |
| /* ---------------- Sprite Logic ---------------- */ | |
| function createSprite(imgData, sndData) { | |
| const img = document.createElement("img"); | |
| img.src = imgData; | |
| img.className = "sprite"; | |
| document.body.appendChild(img); | |
| const audio = new Audio(sndData); | |
| audio.preload = "auto"; | |
| sprites.push({ | |
| el: img, | |
| audio, | |
| x: Math.random() * (innerWidth - 120), | |
| y: Math.random() * (innerHeight - 120), | |
| vx: (Math.random() * 4 + 2) * (Math.random() < 0.5 ? -1 : 1), | |
| vy: (Math.random() * 4 + 2) * (Math.random() < 0.5 ? -1 : 1) | |
| }); | |
| } | |
| function startAnimation() { | |
| controls.style.display = "none"; | |
| requestAnimationFrame(update); | |
| } | |
| function update() { | |
| const w = innerWidth; | |
| const h = innerHeight; | |
| for (const s of sprites) { | |
| s.x += s.vx; | |
| s.y += s.vy; | |
| let bounced = false; | |
| if (s.x <= 0 || s.x + s.el.width >= w) { | |
| s.vx *= -1; | |
| bounced = true; | |
| } | |
| if (s.y <= 0 || s.y + s.el.height >= h) { | |
| s.vy *= -1; | |
| bounced = true; | |
| } | |
| if (bounced) { | |
| s.audio.currentTime = 0; | |
| s.audio.play().catch(() => {}); | |
| } | |
| s.el.style.transform = `translate(${s.x}px, ${s.y}px)`; | |
| } | |
| requestAnimationFrame(update); | |
| } | |
| /* ---------------- Load / Restore ---------------- */ | |
| function checkReady() { | |
| if (sprites.length === SPRITE_COUNT) { | |
| startAnimation(); | |
| } | |
| } | |
| for (let i = 0; i < SPRITE_COUNT; i++) { | |
| const imgKey = lsKey("img", i); | |
| const sndKey = lsKey("snd", i); | |
| const imgData = localStorage.getItem(imgKey); | |
| const sndData = localStorage.getItem(sndKey); | |
| if (imgData && sndData) { | |
| createSprite(imgData, sndData); | |
| checkReady(); | |
| } else { | |
| if (!imgData) { | |
| createFileInput(`Image ${i + 1}`, "image/*", data => { | |
| localStorage.setItem(imgKey, data); | |
| }); | |
| } | |
| if (!sndData) { | |
| createFileInput(`Sound ${i + 1}`, "audio/*", data => { | |
| localStorage.setItem(sndKey, data); | |
| }); | |
| } | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment