// Firefly Jar: catch the fireflies while they glow. // A Twiddle Play tutorial game. Plain JavaScript, no libraries. // ---------- The canvas ---------- const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); let W = 0; // canvas width in CSS pixels let H = 0; // canvas height in CSS pixels function resize() { // Draw at the screen's real pixel density (capped at 2 for speed), so nothing looks blurry. const dpr = Math.min(window.devicePixelRatio || 1, 2); W = canvas.clientWidth; H = canvas.clientHeight; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // from now on, 1 unit = 1 CSS pixel makeStars(); } // ---------- The night sky ---------- let stars = []; function makeStars() { stars = []; for (let i = 0; i < 60; i++) { stars.push({ x: Math.random() * W, y: Math.random() * H * 0.7, r: 0.4 + Math.random() * 1.2 }); } } function drawBackground() { const sky = ctx.createLinearGradient(0, 0, 0, H); sky.addColorStop(0, '#0b1030'); sky.addColorStop(1, '#23306a'); ctx.fillStyle = sky; ctx.fillRect(0, 0, W, H); ctx.fillStyle = 'rgba(255, 255, 255, 0.75)'; for (const s of stars) { ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill(); } // dark hills along the bottom ctx.fillStyle = '#081420'; ctx.beginPath(); ctx.moveTo(0, H); ctx.lineTo(0, H - 80); ctx.quadraticCurveTo(W * 0.3, H - 130, W * 0.55, H - 85); ctx.quadraticCurveTo(W * 0.8, H - 45, W, H - 100); ctx.lineTo(W, H); ctx.closePath(); ctx.fill(); } // ---------- Fireflies ---------- const TOP = 90; // fireflies stay below the score line... const BOTTOM = 150; // ...and above the jar at the bottom let fireflies = []; let speed = 50; // pixels per second let blinkSpeed = 1.7; // how fast the glow cycles function makeFirefly() { return { x: 30 + Math.random() * (W - 60), y: TOP + Math.random() * Math.max(1, H - TOP - BOTTOM), angle: Math.random() * Math.PI * 2, // the direction it flies blink: Math.random() * Math.PI * 2, // where it is in its glow cycle }; } function isLit(f) { return Math.sin(f.blink) > 0.3; } function moveFirefly(f, dt) { f.angle += (Math.random() - 0.5) * 4 * dt; // wander a little f.x += Math.cos(f.angle) * speed * dt; f.y += Math.sin(f.angle) * speed * dt; f.blink += blinkSpeed * dt; // turn back at the edges of the play area if (f.x < 20 || f.x > W - 20) f.angle = Math.PI - f.angle; if (f.y < TOP || f.y > H - BOTTOM) f.angle = -f.angle; f.x = Math.max(20, Math.min(W - 20, f.x)); f.y = Math.max(TOP, Math.min(H - BOTTOM, f.y)); } function drawFirefly(f) { const glow = Math.max(0, Math.sin(f.blink)); // 0 = dark, 1 = brightest ctx.globalCompositeOperation = 'lighter'; // add light instead of paint ctx.globalAlpha = glow * 0.35; ctx.fillStyle = '#ffdc6e'; ctx.beginPath(); ctx.arc(f.x, f.y, 10 + glow * 14, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = isLit(f) ? '#fff3b0' : '#56607e'; ctx.beginPath(); ctx.arc(f.x, f.y, 5, 0, Math.PI * 2); ctx.fill(); } // ---------- The game loop ---------- let lastTime = 0; function update(dt) { for (const f of fireflies) moveFirefly(f, dt); } function draw() { drawBackground(); for (const f of fireflies) drawFirefly(f); } function frame(time) { // dt = seconds since the last frame, capped so one slow frame can't make things jump const dt = lastTime ? Math.min((time - lastTime) / 1000, 0.05) : 0; lastTime = time; update(dt); draw(); requestAnimationFrame(frame); } // ---------- Start ---------- resize(); window.addEventListener('resize', resize); fireflies.push(makeFirefly()); requestAnimationFrame(frame);