/* fx.jsx — sound engine, confetti, abstract thumbnails. Exported to window. */ /* ============ SOUND ============ */ let _ac = null; const sfx = { muted: false, ctx() { if (!_ac) { try { _ac = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){} } if (_ac && _ac.state === "suspended") _ac.resume(); return _ac; }, tone(freq, t0, dur, type = "sine", gain = 0.18, slide = null) { const ac = this.ctx(); if (!ac) return; const o = ac.createOscillator(), g = ac.createGain(); o.type = type; o.frequency.setValueAtTime(freq, t0); if (slide) o.frequency.exponentialRampToValueAtTime(slide, t0 + dur); g.gain.setValueAtTime(0.0001, t0); g.gain.exponentialRampToValueAtTime(gain, t0 + 0.012); g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); o.connect(g); g.connect(ac.destination); o.start(t0); o.stop(t0 + dur + 0.02); }, play(name) { if (this.muted) return; const ac = this.ctx(); if (!ac) return; const n = ac.currentTime; if (name === "complete") { [523.25, 659.25, 783.99, 1046.5].forEach((f, i) => this.tone(f, n + i*0.075, 0.28, "triangle", 0.16)); this.tone(1318.5, n + 0.30, 0.5, "sine", 0.08); } else if (name === "levelup") { [523.25, 698.46, 880, 1174.7, 1567.98].forEach((f,i)=>this.tone(f, n+i*0.09, 0.4, "sawtooth", 0.10)); this.tone(2093, n+0.5, 0.6, "sine", 0.06); } else if (name === "check") { this.tone(880, n, 0.09, "square", 0.07, 1320); } else if (name === "uncheck") { this.tone(440, n, 0.09, "square", 0.05, 300); } else if (name === "drop") { this.tone(160, n, 0.12, "sine", 0.14, 90); this.tone(320, n, 0.06, "triangle", 0.05); } else if (name === "pick") { this.tone(420, n, 0.05, "sine", 0.05, 560); } else if (name === "click") { this.tone(660, n, 0.04, "sine", 0.04); } else if (name === "move") { this.tone(523, n, 0.07, "triangle", 0.06, 660); } }, }; /* ============ CONFETTI ============ */ let _cv = null, _cx = null, _parts = [], _raf = null; function ensureCanvas() { if (_cv) return; _cv = document.createElement("canvas"); _cv.className = "confetti-canvas"; document.body.appendChild(_cv); _cx = _cv.getContext("2d"); const resize = () => { _cv.width = innerWidth * devicePixelRatio; _cv.height = innerHeight * devicePixelRatio; _cv.style.width = innerWidth+"px"; _cv.style.height = innerHeight+"px"; _cx.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0); }; resize(); addEventListener("resize", resize); } function tick() { _cx.clearRect(0, 0, innerWidth, innerHeight); _parts = _parts.filter(p => p.life > 0); for (const p of _parts) { p.vy += 0.16; p.x += p.vx; p.y += p.vy; p.vx *= 0.99; p.rot += p.vr; p.life -= 1; _cx.save(); _cx.translate(p.x, p.y); _cx.rotate(p.rot); _cx.globalAlpha = Math.min(1, p.life / 22); _cx.fillStyle = p.color; if (p.shape === 0) _cx.fillRect(-p.s/2, -p.s/2, p.s, p.s*0.5); else { _cx.beginPath(); _cx.arc(0,0,p.s/2,0,7); _cx.fill(); } _cx.restore(); } if (_parts.length) _raf = requestAnimationFrame(tick); else { _raf = null; _cx.clearRect(0,0,innerWidth,innerHeight); } } function fireConfetti(x, y, opts = {}) { ensureCanvas(); const accent = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#ff7a00"; const colors = opts.colors || [accent, "#ff9633", "#f5b942", "#ffffff", "#ffd28a"]; const n = opts.count || 90; const spread = opts.spread || 1; for (let i = 0; i < n; i++) { const a = (Math.random() * Math.PI * 2); const sp = (4 + Math.random() * 9) * spread; _parts.push({ x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - (opts.up ? 6 : 3), s: 5 + Math.random() * 7, color: colors[(Math.random()*colors.length)|0], rot: Math.random()*7, vr: (Math.random()-0.5)*0.4, life: 70 + Math.random()*45, shape: Math.random() < 0.6 ? 0 : 1, }); } if (!_raf) _raf = requestAnimationFrame(tick); } /* ============ ABSTRACT THUMBNAIL ============ */ /* themed, generated visuals — no stock photos. */ function Thumb({ kind, className }) { const styleFor = (k) => { const A = "var(--accent)", G = "var(--gold)", P = "var(--c-feature)"; const M = { garagem: `repeating-linear-gradient(115deg, #161616 0 22px, #1c1c1c 22px 44px), radial-gradient(60% 90% at 30% 110%, rgba(var(--accent-rgb),0.4), transparent 60%)`, inventario: `linear-gradient(0deg, rgba(0,0,0,0.4), transparent), repeating-conic-gradient(from 0deg at 50% 50%, #1a1a1a 0 25%, #161616 0 50%)`, veiculo: `radial-gradient(80% 120% at 70% 20%, rgba(var(--accent-rgb),0.45), transparent 55%), linear-gradient(160deg, #0e0e14, #1a1320)`, mdt: `linear-gradient(180deg, #10131c, #0a0d14), radial-gradient(50% 60% at 20% 30%, rgba(91,135,255,0.4), transparent)`, cnh: `linear-gradient(135deg, #1e1a12, #14130d), radial-gradient(60% 80% at 80% 20%, rgba(245,185,66,0.45), transparent)`, multas: `linear-gradient(135deg, #1a1212, #120d0d), radial-gradient(60% 80% at 20% 80%, rgba(240,69,58,0.4), transparent)`, }; return M[k] || `radial-gradient(70% 100% at 50% 0%, rgba(var(--accent-rgb),0.3), transparent), linear-gradient(160deg,#161616,#101010)`; }; return (
{kind === "inventario" && [...Array(12)].map((_,i)=>( ))} {kind === "mdt" && <> } {kind === "cnh" && <> } {(kind==="garagem"||kind==="veiculo") && <> } {kind === "multas" && <> }
); } /* themed self-contained SVG data URL — used as the card cover fallback so the slot looks good when empty and the user can drop a real image to override. */ function thumbDataUrl(kind) { const tint = { garagem: "#ff7a00", inventario: "#ff9633", veiculo: "#a855f7", mdt: "#5b9bff", cnh: "#f5b942", multas: "#f0453a" }[kind] || "#ff7a00"; const motif = { garagem: ``, inventario: `${[0,1,2,3,4].map(i=>``).join("")}`, veiculo: ``, mdt: ``, cnh: ``, multas: ``, }[kind] || ""; const svg = `` + `` + `` + `` + `` + `` + `` + `` + [0,1,2,3,4,5,6,7,8,9].map(i=>``).join("") + `${motif}` + ``; return "data:image/svg+xml," + encodeURIComponent(svg); } window.FX = { sfx, fireConfetti, Thumb, thumbDataUrl, readImageFile }; /* ============ IMAGE IMPORT (downscale to keep localStorage sane) ============ */ function readImageFile(file, maxDim, cb) { if (!file || !file.type || !file.type.startsWith("image/")) { cb(null); return; } const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { const cap = maxDim || 1280; const scale = Math.min(1, cap / Math.max(img.width, img.height)); const w = Math.max(1, Math.round(img.width * scale)); const h = Math.max(1, Math.round(img.height * scale)); const cv = document.createElement("canvas"); cv.width = w; cv.height = h; cv.getContext("2d").drawImage(img, 0, 0, w, h); let url; try { url = cv.toDataURL("image/webp", 0.82); if (url.indexOf("data:image/webp") !== 0) throw 0; } catch (e) { url = cv.toDataURL("image/jpeg", 0.85); } cb(url); }; img.onerror = () => cb(null); img.src = reader.result; }; reader.onerror = () => cb(null); reader.readAsDataURL(file); }