From 47973aeba60a2a7b27fe393d1d501f4a618fc376 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:24:47 -0800 Subject: [PATCH] =?UTF-8?q?feat(avocet):=20easter=20eggs=20=E2=80=94=20hir?= =?UTF-8?q?ed=20confetti,=20century=20mark,=20clean=20sweep,=20midnight=20?= =?UTF-8?q?labeler,=20cursor=20trail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/composables/useEasterEgg.ts | 111 ++++++++++++++++++++++++++++ web/src/views/LabelView.vue | 80 ++++++++++++++++++-- 2 files changed, 184 insertions(+), 7 deletions(-) diff --git a/web/src/composables/useEasterEgg.ts b/web/src/composables/useEasterEgg.ts index 62c48e7..3e3e0ad 100644 --- a/web/src/composables/useEasterEgg.ts +++ b/web/src/composables/useEasterEgg.ts @@ -41,3 +41,114 @@ export function useHackerMode() { return { toggle, restore } } + +/** Fire a confetti burst from a given x,y position. Pure canvas, no dependencies. */ +export function fireConfetti(originX = window.innerWidth / 2, originY = window.innerHeight / 2) { + if (typeof requestAnimationFrame === 'undefined') return + + const canvas = document.createElement('canvas') + canvas.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999;' + canvas.width = window.innerWidth + canvas.height = window.innerHeight + document.body.appendChild(canvas) + const ctx = canvas.getContext('2d')! + + const COLORS = ['#2A6080','#B8622A','#5A9DBF','#D4854A','#FFC107','#4CAF50'] + const particles = Array.from({ length: 80 }, () => ({ + x: originX, + y: originY, + vx: (Math.random() - 0.5) * 14, + vy: (Math.random() - 0.6) * 12, + color: COLORS[Math.floor(Math.random() * COLORS.length)], + size: 5 + Math.random() * 6, + angle: Math.random() * Math.PI * 2, + spin: (Math.random() - 0.5) * 0.3, + life: 1.0, + })) + + let raf = 0 + function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height) + let alive = false + for (const p of particles) { + p.x += p.vx + p.y += p.vy + p.vy += 0.35 // gravity + p.vx *= 0.98 // air friction + p.angle += p.spin + p.life -= 0.016 + if (p.life <= 0) continue + alive = true + ctx.save() + ctx.globalAlpha = p.life + ctx.fillStyle = p.color + ctx.translate(p.x, p.y) + ctx.rotate(p.angle) + ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6) + ctx.restore() + } + if (alive) { + raf = requestAnimationFrame(draw) + } else { + cancelAnimationFrame(raf) + canvas.remove() + } + } + raf = requestAnimationFrame(draw) +} + +/** Enable cursor trail in hacker mode โ€” returns a cleanup function. */ +export function useCursorTrail() { + const DOT_COUNT = 10 + const dots: HTMLElement[] = [] + let positions: { x: number; y: number }[] = [] + let mouseX = 0 + let mouseY = 0 + let raf = 0 + + for (let i = 0; i < DOT_COUNT; i++) { + const dot = document.createElement('div') + dot.style.cssText = [ + 'position:fixed', + 'pointer-events:none', + 'z-index:9998', + 'border-radius:50%', + 'background:#5A9DBF', + 'transition:opacity 0.1s', + ].join(';') + document.body.appendChild(dot) + dots.push(dot) + } + + function onMouseMove(e: MouseEvent) { + mouseX = e.clientX + mouseY = e.clientY + } + + function animate() { + positions.unshift({ x: mouseX, y: mouseY }) + if (positions.length > DOT_COUNT) positions = positions.slice(0, DOT_COUNT) + + dots.forEach((dot, i) => { + const pos = positions[i] + if (!pos) { dot.style.opacity = '0'; return } + const scale = 1 - i / DOT_COUNT + const size = Math.round(8 * scale) + dot.style.left = `${pos.x - size / 2}px` + dot.style.top = `${pos.y - size / 2}px` + dot.style.width = `${size}px` + dot.style.height = `${size}px` + dot.style.opacity = `${(1 - i / DOT_COUNT) * 0.7}` + }) + raf = requestAnimationFrame(animate) + } + + window.addEventListener('mousemove', onMouseMove) + raf = requestAnimationFrame(animate) + + return function cleanup() { + window.removeEventListener('mousemove', onMouseMove) + cancelAnimationFrame(raf) + dots.forEach(d => d.remove()) + } +} diff --git a/web/src/views/LabelView.vue b/web/src/views/LabelView.vue index de15f30..246bcf7 100644 --- a/web/src/views/LabelView.vue +++ b/web/src/views/LabelView.vue @@ -6,9 +6,12 @@ - ๐Ÿ”ฅ On a roll! - โšก Speed round! - ๐ŸŽฏ Fifty deep! + ๐Ÿ”ฅ On a roll! + โšก Speed round! + ๐ŸŽฏ Fifty deep! + ๐Ÿ’ฏ Century! + ๐Ÿงน Clean sweep! + ๐Ÿฆ‰ Midnight labeler!
@@ -61,12 +64,13 @@ @@ -253,9 +316,12 @@ onMounted(async () => { to { transform: scale(1); opacity: 1; } } -.badge-roll { background: #ff6b35; color: #fff; } -.badge-speed { background: #7c3aed; color: #fff; } -.badge-fifty { background: var(--app-accent, #B8622A); color: var(--app-accent-text, #1a2338); } +.badge-roll { background: #ff6b35; color: #fff; } +.badge-speed { background: #7c3aed; color: #fff; } +.badge-fifty { background: var(--app-accent, #B8622A); color: var(--app-accent-text, #1a2338); } +.badge-century { background: #ffd700; color: #1a2338; } +.badge-sweep { background: var(--app-primary, #2A6080); color: #fff; } +.badge-midnight { background: #1a1a2e; color: #7c9dcf; border: 1px solid #7c9dcf; } .header-actions { display: flex;