Spaces:
Running
Running
Download kenwren/juice.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 4.11 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/kenwren/juice.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/kenwren/juice.js
-
curl -L -o juice.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/kenwren/juice.js
4.11 kB
| // Kenwren juice: synthesised WebAudio sounds (no audio files), confetti particles and screen shake. | |
| // Mute is remembered in localStorage (via the kit's store(), which never throws). Shake respects reduced motion. | |
| // (c) 2026 CyberMax. | |
| import { store } from './cmx.js'; | |
| let ctx = null; | |
| let muted = !!store('kenwren:mute'); | |
| export const isMuted = () => muted; | |
| export function setMuted(m) { muted = !!m; store('kenwren:mute', muted ? 1 : 0); } | |
| const calm = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; } }; | |
| function ac() { | |
| if (muted) return null; | |
| try { | |
| if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)(); | |
| if (ctx.state === 'suspended') ctx.resume(); | |
| return ctx; | |
| } catch { return null; } | |
| } | |
| function tone(freq, at, dur, type = 'sine', vol = 0.18, slide = 0) { | |
| const a = ac(); if (!a) return; | |
| const t = a.currentTime + at; | |
| const o = a.createOscillator(); const g = a.createGain(); | |
| o.type = type; o.frequency.setValueAtTime(freq, t); | |
| if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(40, freq + slide), t + dur); | |
| g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(vol, t + 0.012); g.gain.exponentialRampToValueAtTime(0.0001, t + dur); | |
| o.connect(g); g.connect(a.destination); o.start(t); o.stop(t + dur + 0.02); | |
| } | |
| export const sfx = { | |
| tap() { tone(660, 0, 0.05, 'triangle', 0.08); }, | |
| ok() { tone(784, 0, 0.09, 'triangle'); tone(1175, 0.07, 0.14, 'triangle'); }, | |
| bad() { tone(196, 0, 0.22, 'sawtooth', 0.09, -90); tone(147, 0.05, 0.22, 'square', 0.05, -60); }, | |
| star(k = 0) { tone([523, 659, 784][k] || 784, 0, 0.18, 'triangle', 0.16); tone(([523, 659, 784][k] || 784) * 2, 0.04, 0.16, 'sine', 0.06); }, | |
| win() { [523, 659, 784, 1047].forEach((f, k) => tone(f, k * 0.09, 0.22, 'triangle', 0.15)); }, | |
| lose() { tone(330, 0, 0.18, 'triangle', 0.12, -60); tone(247, 0.16, 0.3, 'triangle', 0.12, -60); }, | |
| }; | |
| export function shake(el) { | |
| if (!el || calm()) return; | |
| el.classList.remove('shake'); void el.offsetWidth; el.classList.add('shake'); | |
| setTimeout(() => el.classList.remove('shake'), 420); | |
| } | |
| let cv = null; let parts = []; let raf = 0; | |
| const COLORS = ['#FFD166', '#FF6A3D', '#2FD3A0', '#B8FFF0', '#FF7AC6', '#ffffff']; | |
| /** Burst of particles from an element (or a point). `n` pieces; bad=true gives a small grey puff instead. */ | |
| export function burst(from, n = 26, bad = false) { | |
| try { | |
| if (!cv) { | |
| cv = document.createElement('canvas'); cv.className = 'fx'; cv.setAttribute('aria-hidden', 'true'); | |
| document.body.appendChild(cv); | |
| } | |
| const dpr = Math.min(2, window.devicePixelRatio || 1); | |
| if (cv.width !== innerWidth * dpr) { cv.width = innerWidth * dpr; cv.height = innerHeight * dpr; } | |
| const r = from && from.getBoundingClientRect ? from.getBoundingClientRect() : { left: innerWidth / 2, top: innerHeight / 3, width: 0, height: 0 }; | |
| const x = r.left + r.width / 2; const y = r.top + r.height / 2; | |
| const count = calm() ? Math.ceil(n / 3) : n; | |
| for (let k = 0; k < count; k++) { | |
| const a = Math.random() * Math.PI * 2; const v = (bad ? 1.5 : 3) + Math.random() * (bad ? 2 : 5); | |
| parts.push({ x, y, vx: Math.cos(a) * v, vy: Math.sin(a) * v - (bad ? 0.5 : 3), life: 1, s: 4 + Math.random() * 5, c: bad ? '#9aa0ad' : COLORS[k % COLORS.length], rot: Math.random() * 6 }); | |
| } | |
| if (!raf) raf = requestAnimationFrame(step); | |
| } catch { /* never break the game */ } | |
| } | |
| function step() { | |
| const g = cv.getContext('2d'); const dpr = cv.width / innerWidth; | |
| g.setTransform(dpr, 0, 0, dpr, 0, 0); g.clearRect(0, 0, innerWidth, innerHeight); | |
| parts = parts.filter((p) => p.life > 0); | |
| for (const p of parts) { | |
| p.x += p.vx; p.y += p.vy; p.vy += 0.22; p.vx *= 0.985; p.life -= 0.022; p.rot += 0.2; | |
| g.globalAlpha = Math.max(0, p.life); g.fillStyle = p.c; | |
| g.save(); g.translate(p.x, p.y); g.rotate(p.rot); g.fillRect(-p.s / 2, -p.s / 4, p.s, p.s / 2); g.restore(); | |
| } | |
| g.globalAlpha = 1; | |
| raf = parts.length ? requestAnimationFrame(step) : 0; | |
| if (!raf) g.clearRect(0, 0, innerWidth, innerHeight); | |
| } | |