Compare commits

...

1 Commits

Author SHA1 Message Date
Karan
9f85501538 feat: add Easter Egg Hunt Chrome extension skill
A Chrome Manifest V3 extension skill that hides colored Easter eggs
on every webpage. Features:

- Colored eggs (9 hue variants) spawn at random intervals
- Click to collect with confetti burst
- 20 page distortion effects that STACK permanently till refresh
  (flip, earthquake, invert, Comic Sans, rainbow, spin, etc.)
- Rare rabbit (~6% spawn) needs 5 clicks to catch, hops away
  each click, rewards +3 eggs and opens Hermes Agent page
- Persistent egg counter via chrome.storage
- Paw print cursor trail
- Popup UI with counter, manual spawn, and reset

Includes complete templates (all 6 source files) and a PIL icon
generator script for turnkey rebuilds.
2026-04-05 14:10:29 -04:00
8 changed files with 1200 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
---
name: easter-egg-chrome-extension
description: Build a Chrome extension that hides colored Easter eggs on every webpage. Eggs spawn at random intervals, can be clicked to collect, and each collection triggers a random page distortion effect that stacks permanently until refresh. Includes rare rabbit easter egg mechanic and persistent egg counter.
tags: [chrome-extension, easter, fun, javascript, css]
triggers:
- easter egg chrome extension
- easter egg hunt browser
- page distortion extension
- fun chrome plugin
---
# Easter Egg Hunt Chrome Extension
## Overview
A Chrome Manifest V3 extension that:
- Hides **colored eggs** (🥚 with CSS hue-rotate filters) at random positions on every page
- Eggs appear/disappear at random intervals (3-30s cycles)
- Clicking an egg: confetti burst + **random page distortion effect that STACKS and persists till refresh**
- **Rare rabbit** (~6% spawn rate): needs 5 clicks to catch (hops away each click), rewards +3 eggs and opens a special URL
- Persistent egg counter via `chrome.storage.local`
- Paw print cursor trail
- Popup UI showing count, manual spawn button, reset
## File Structure
```
easter-egg-extension/
├── manifest.json # Manifest V3, permissions: activeTab, scripting, storage
├── content.js # Main egg hunt engine (content script)
├── easter.css # Animations, HUD, confetti, trail styles
├── background.js # Minimal service worker
├── popup.html # Extension popup UI
├── popup.js # Popup logic (count display, spawn/reset buttons)
└── icons/
├── egg48.png # Generated via PIL
└── egg128.png
```
## Build Steps
1. **Create directory**: `mkdir -p ~/easter-egg-extension/icons`
2. **Write all source files** from templates:
- `templates/manifest.json`
- `templates/content.js`
- `templates/easter.css`
- `templates/background.js`
- `templates/popup.html`
- `templates/popup.js`
3. **Generate icons** with Python PIL:
```python
from PIL import Image, ImageDraw
def make_egg_icon(size, path):
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
pad = size // 8
cx, cy = size // 2, size // 2 + pad // 3
rx = size // 2 - pad
ry = int(rx * 1.25)
top = cy - ry
draw.ellipse([pad, top, size - pad, cy + ry], fill=(255, 235, 200, 255))
stripe_y = cy - ry // 6
stripe_h = size // 10
for x in range(pad + 4, size - pad - 4):
dx = (x - cx) / rx
if abs(dx) < 0.92:
band_color = [(168, 130, 234, 220), (236, 72, 153, 200), (102, 126, 234, 220)]
idx = (x // (size // 6)) % 3
draw.rectangle([x, stripe_y, x + 1, stripe_y + stripe_h], fill=band_color[idx])
for sx, sy in [(cx - rx//3, cy - ry//3), (cx + rx//4, cy - ry//2)]:
r = max(1, size // 30)
draw.ellipse([sx-r, sy-r, sx+r, sy+r], fill=(255, 255, 255, 200))
img.save(path)
make_egg_icon(48, 'icons/egg48.png')
make_egg_icon(128, 'icons/egg128.png')
```
4. **Install in Chrome**:
- Navigate to `chrome://extensions`
- Enable "Developer mode" (top right toggle)
- Click "Load unpacked" → select the extension directory
- Refresh open tabs with Cmd+R / Ctrl+R
## Key Design Decisions
### Egg Coloring
Uses CSS `hue-rotate()` + `saturate()` + `brightness()` filters on the 🥚 emoji. 9 color presets: purple, blue, green, pink, red, yellow, sky, teal, orange.
### Stacking Effects
Each effect injects a NEW `<style>` tag with a unique ID (`easter-fx-N`). Animation keyframes also get unique names (`easter-shake-N`). This means effects never clobber each other — they pile up into cumulative chaos. Only a page refresh clears them.
### 20 Page Effects
Flip, earthquake, invert, shrink, tilt, blur, rainbow, Comic Sans, spin, bounce, film noir, stretch, mirror, wobble, max saturation, pixelate, drift, sepia, zoom, trippy.
### Rabbit Mechanic
- ~6% spawn chance (weight 3 vs egg weight 50)
- Needs 5 clicks; each click makes it hop to a random position
- Gets double the normal visible time before auto-vanishing
- Catching it: +3 eggs, confetti, opens configurable URL (default: Hermes Agent GitHub)
## Reload After Changes
`chrome://extensions` → click 🔄 on the extension card → Cmd+R on open tabs
## Pitfalls
- Content scripts don't auto-reload — must reload extension AND refresh tabs
- `chrome.storage.local` needs "storage" permission in manifest
- CSS filters on `html` element can conflict when multiple effects target it — using `!important` and injected stylesheets handles this
- Emoji rendering varies by OS — colored egg filters look best on macOS/Windows, may look different on Linux

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Generate Easter egg icons for the Chrome extension."""
import sys
from PIL import Image, ImageDraw
def make_egg_icon(size, path):
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
pad = size // 8
cx, cy = size // 2, size // 2 + pad // 3
rx = size // 2 - pad
ry = int(rx * 1.25)
top = cy - ry
# Egg body
draw.ellipse([pad, top, size - pad, cy + ry], fill=(255, 235, 200, 255))
# Decorative stripe
stripe_y = cy - ry // 6
stripe_h = size // 10
for x in range(pad + 4, size - pad - 4):
dx = (x - cx) / rx
if abs(dx) < 0.92:
band_color = [(168, 130, 234, 220), (236, 72, 153, 200), (102, 126, 234, 220)]
idx = (x // (size // 6)) % 3
draw.rectangle([x, stripe_y, x + 1, stripe_y + stripe_h], fill=band_color[idx])
# Sparkle dots
for sx, sy in [(cx - rx//3, cy - ry//3), (cx + rx//4, cy - ry//2)]:
r = max(1, size // 30)
draw.ellipse([sx-r, sy-r, sx+r, sy+r], fill=(255, 255, 255, 200))
img.save(path)
print(f'Saved {path} ({size}x{size})')
if __name__ == "__main__":
outdir = sys.argv[1] if len(sys.argv) > 1 else "icons"
make_egg_icon(48, f'{outdir}/egg48.png')
make_egg_icon(128, f'{outdir}/egg128.png')

View File

@@ -0,0 +1,4 @@
// Service worker — mostly passthrough, keeps extension alive
chrome.runtime.onInstalled.addListener(() => {
console.log("🥚 Easter Egg Hunt installed! Happy Easter~!");
});

View File

@@ -0,0 +1,625 @@
(() => {
"use strict";
// ── Config ──────────────────────────────────────────────
const CFG = {
// How long (ms) before first egg can appear after page load
initialDelayMin: 3000,
initialDelayMax: 12000,
// How long an egg stays visible before vanishing on its own
visibleMin: 5000,
visibleMax: 15000,
// Cooldown between eggs (after one vanishes or is collected)
cooldownMin: 8000,
cooldownMax: 30000,
// Max eggs that can be on screen at once
maxSimultaneous: 1,
// Chance (0-1) that a second egg spawns alongside the first
doubleSpawnChance: 0.1,
};
// Spawnable collectibles — eggs get colored via CSS filter
const SPAWN_TYPES = [
// Colored eggs (vast majority)
{ emoji: "🥚", type: "egg", weight: 50 },
// Rare rabbit — requires multiple clicks, drops a special golden egg
{ emoji: "🐰", type: "rabbit", weight: 3 },
];
const RABBIT_CLICKS_NEEDED = 5; // clicks to catch the rabbit
// Hue rotations + saturations to color the eggs
const EGG_COLORS = [
{ filter: "hue-rotate(280deg) saturate(2) brightness(0.95)", name: "purple" },
{ filter: "hue-rotate(180deg) saturate(2.5) brightness(0.95)", name: "blue" },
{ filter: "hue-rotate(100deg) saturate(2) brightness(0.9)", name: "green" },
{ filter: "hue-rotate(330deg) saturate(2.5) brightness(0.95)", name: "pink" },
{ filter: "hue-rotate(0deg) saturate(3) brightness(0.85)", name: "red" },
{ filter: "hue-rotate(60deg) saturate(2) brightness(0.95)", name: "yellow" },
{ filter: "hue-rotate(200deg) saturate(1.8) brightness(1.05)", name: "sky" },
{ filter: "hue-rotate(150deg) saturate(2.2) brightness(0.9)", name: "teal" },
{ filter: "hue-rotate(20deg) saturate(2.5) brightness(0.9)", name: "orange" },
];
function pickWeighted(items) {
const total = items.reduce((s, i) => s + i.weight, 0);
let r = Math.random() * total;
for (const item of items) {
r -= item.weight;
if (r <= 0) return item;
}
return items[0];
}
const COLLECT_MESSAGES = [
"You found an egg~! 🎉",
"Egg get!! ★",
"Another one for the basket~!",
"Eagle-eyed egg hunter! 👀",
"Basket grows heavier... 🧺",
"Easter magic~! ✨",
"Egg-cellent find!",
];
const RABBIT_HIT_MESSAGES = [
"The bunny hops! Keep clicking!",
"Almost got it~!",
"It's getting tired...!",
"One more hop!",
];
const RABBIT_CAUGHT_MESSAGE = "🌟 The bunny dropped a GOLDEN EGG! 🌟";
// ── State ───────────────────────────────────────────────
let eggsOnScreen = 0;
let eggCount = 0;
let scoreEl = null;
let toastTimeout = null;
let scheduledTimeout = null;
let paused = false;
// ── Persistence via chrome.storage ──────────────────────
function loadCount(cb) {
if (chrome?.storage?.local) {
chrome.storage.local.get("easterEggCount", (res) => {
eggCount = res.easterEggCount || 0;
cb();
});
} else {
eggCount = parseInt(localStorage.getItem("easterEggCount") || "0", 10);
cb();
}
}
function saveCount() {
if (chrome?.storage?.local) {
chrome.storage.local.set({ easterEggCount: eggCount });
} else {
localStorage.setItem("easterEggCount", String(eggCount));
}
}
// ── Helpers ─────────────────────────────────────────────
const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
// ── Score HUD ───────────────────────────────────────────
function ensureScoreHUD() {
if (scoreEl && document.body.contains(scoreEl)) {
updateScoreText();
return;
}
scoreEl = document.createElement("div");
scoreEl.className = "easter-score";
updateScoreText();
document.body.appendChild(scoreEl);
}
function updateScoreText() {
if (!scoreEl) return;
scoreEl.textContent = `🧺 ${eggCount} egg${eggCount !== 1 ? "s" : ""}`;
}
function pulseScore() {
if (!scoreEl) return;
scoreEl.style.transform = "scale(1.3)";
setTimeout(() => { scoreEl.style.transform = "scale(1)"; }, 300);
}
// ── Toast notification ──────────────────────────────────
function showToast(msg) {
// Remove old toast if any
const old = document.querySelector(".easter-toast");
if (old) old.remove();
clearTimeout(toastTimeout);
const t = document.createElement("div");
t.className = "easter-toast";
t.textContent = msg;
Object.assign(t.style, {
position: "fixed",
bottom: "65px",
right: "20px",
background: "rgba(255,255,255,0.95)",
color: "#5a3e7a",
padding: "10px 20px",
borderRadius: "14px",
fontFamily: "'Segoe UI', system-ui, sans-serif",
fontSize: "14px",
fontWeight: "600",
zIndex: "9999999",
boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
border: "2px solid #fecfef",
opacity: "0",
transform: "translateY(10px)",
transition: "all 0.3s ease",
pointerEvents: "none",
});
document.body.appendChild(t);
// Animate in
requestAnimationFrame(() => {
t.style.opacity = "1";
t.style.transform = "translateY(0)";
});
toastTimeout = setTimeout(() => {
t.style.opacity = "0";
t.style.transform = "translateY(10px)";
setTimeout(() => t.remove(), 300);
}, 2200);
}
// ── Confetti burst ──────────────────────────────────────
function burstConfetti(x, y) {
const container = document.createElement("div");
container.className = "easter-confetti";
document.body.appendChild(container);
const colors = [
"#ff9a9e", "#fecfef", "#a8edea", "#fed6e3",
"#667eea", "#ffd89b", "#b8f5b0", "#c3aed6",
];
for (let i = 0; i < 25; i++) {
const p = document.createElement("div");
p.className = "confetti-piece";
const angle = (Math.PI * 2 * i) / 25;
const velocity = rand(60, 160);
const endX = x + Math.cos(angle) * velocity;
const endY = y + Math.sin(angle) * velocity;
Object.assign(p.style, {
left: x + "px",
top: y + "px",
background: pick(colors),
width: rand(6, 12) + "px",
height: rand(8, 16) + "px",
borderRadius: rand(0, 1) ? "50%" : "2px",
animationDuration: rand(800, 1400) + "ms",
});
// Override the keyframe with direct style animation
p.animate(
[
{ transform: "translate(0,0) rotate(0deg)", opacity: 1 },
{
transform: `translate(${Math.cos(angle) * velocity}px, ${Math.sin(angle) * velocity + 80}px) rotate(${rand(360, 720)}deg)`,
opacity: 0,
},
],
{ duration: rand(800, 1400), easing: "cubic-bezier(0,.8,.5,1)", fill: "forwards" }
);
container.appendChild(p);
}
setTimeout(() => container.remove(), 1600);
}
// ── Spawn an egg ────────────────────────────────────────
function spawnEgg() {
if (paused || eggsOnScreen >= CFG.maxSimultaneous) {
scheduleNext();
return;
}
// Pick a random position within the VISIBLE viewport
// But avoid the very edges and the score HUD area
const margin = 60;
const vw = window.innerWidth;
const vh = window.innerHeight;
const x = rand(margin, vw - margin);
const y = rand(margin, vh - margin - 80); // avoid bottom-right HUD
const spawn = pickWeighted(SPAWN_TYPES);
const egg = document.createElement("div");
egg.className = "easter-egg-hidden";
egg.textContent = spawn.emoji;
egg.setAttribute("aria-label", "Easter egg! Click to collect!");
// Color the egg with a random hue if it's an egg type
if (spawn.type === "egg") {
const color = pick(EGG_COLORS);
egg.style.filter = color.filter;
}
// Position fixed so it's in viewport no matter scroll
Object.assign(egg.style, {
position: "fixed",
left: x + "px",
top: y + "px",
opacity: "0",
transform: "scale(0)",
transition: "opacity 0.6s ease, transform 0.4s ease",
});
document.body.appendChild(egg);
eggsOnScreen++;
// Fade in
requestAnimationFrame(() => {
egg.style.opacity = "1";
egg.style.transform = "scale(1)";
});
// Store type for behavior
egg._spawnType = spawn.type;
egg._rabbitClicks = 0;
// Click handler
egg.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (spawn.type === "rabbit") {
handleRabbitClick(egg, e.clientX, e.clientY);
} else {
collectEgg(egg, e.clientX, e.clientY);
}
});
// Auto-vanish — rabbits get extra time since they need multiple clicks
const vanishDelay = spawn.type === "rabbit"
? rand(CFG.visibleMax, CFG.visibleMax * 2)
: rand(CFG.visibleMin, CFG.visibleMax);
const vanishTimer = setTimeout(() => {
if (egg.parentNode) {
egg.style.opacity = "0";
egg.style.transform = "scale(0)";
setTimeout(() => {
egg.remove();
eggsOnScreen = Math.max(0, eggsOnScreen - 1);
}, 600);
}
scheduleNext();
}, vanishDelay);
// Store timer on element so we can cancel on collect
egg._vanishTimer = vanishTimer;
}
// ── Rabbit multi-click handler ───────────────────────────
function handleRabbitClick(el, cx, cy) {
el._rabbitClicks++;
if (el._rabbitClicks >= RABBIT_CLICKS_NEEDED) {
// Caught! Cancel vanish timer, do the golden egg reveal
clearTimeout(el._vanishTimer);
burstConfetti(cx, cy);
showToast(RABBIT_CAUGHT_MESSAGE);
// Animate rabbit out
el.style.transform = "scale(1.8) rotate(30deg)";
el.style.opacity = "0";
setTimeout(() => {
el.remove();
eggsOnScreen = Math.max(0, eggsOnScreen - 1);
// Open Hermes Agent page
window.open("https://github.com/nousresearch/hermes-agent", "_blank");
}, 600);
// Bonus: count it as a special egg
eggCount += 3;
saveCount();
updateScoreText();
pulseScore();
scheduleNext();
return;
}
// Not caught yet — hop to a new random position
const margin = 60;
const vw = window.innerWidth;
const vh = window.innerHeight;
const nx = rand(margin, vw - margin);
const ny = rand(margin, vh - margin - 80);
// Quick hop animation
el.style.transition = "left 0.2s ease, top 0.2s ease, transform 0.15s ease";
el.style.transform = "scale(1.3) translateY(-15px)";
setTimeout(() => {
el.style.left = nx + "px";
el.style.top = ny + "px";
el.style.transform = "scale(1)";
}, 150);
// Show progress toast
const remaining = RABBIT_CLICKS_NEEDED - el._rabbitClicks;
if (remaining <= 3) {
showToast(RABBIT_HIT_MESSAGES[Math.min(el._rabbitClicks - 1, RABBIT_HIT_MESSAGES.length - 1)]);
} else {
showToast(`🐰 The bunny hops away! (${el._rabbitClicks}/${RABBIT_CLICKS_NEEDED})`);
}
}
// ── Page distortion effects (STACK permanently till refresh) ──
let fxCounter = 0;
function injectStyle(css) {
const s = document.createElement("style");
s.id = "easter-fx-" + (fxCounter++);
s.textContent = css;
document.head.appendChild(s);
}
const PAGE_EFFECTS = [
{
name: "🙃 Page flipped!",
apply() {
document.documentElement.style.transform = "rotate(180deg)";
document.documentElement.style.transformOrigin = "center center";
},
},
{
name: "🫨 Earthquake!!",
apply() {
injectStyle(`
@keyframes easter-shake-${fxCounter} {
0%, 100% { transform: translate(0,0) rotate(0deg); }
10% { transform: translate(-8px, 4px) rotate(-1deg); }
20% { transform: translate(6px, -6px) rotate(1.5deg); }
30% { transform: translate(-4px, 8px) rotate(-0.5deg); }
40% { transform: translate(8px, -2px) rotate(1deg); }
50% { transform: translate(-6px, -4px) rotate(-1.5deg); }
60% { transform: translate(4px, 6px) rotate(0.5deg); }
70% { transform: translate(-8px, 2px) rotate(-1deg); }
80% { transform: translate(6px, -8px) rotate(1.5deg); }
90% { transform: translate(-4px, 4px) rotate(-0.5deg); }
}
body { animation: easter-shake-${fxCounter} 0.15s infinite !important; }
`);
},
},
{
name: "🔮 Colors inverted!",
apply() { injectStyle(`html { filter: invert(1) !important; }`); },
},
{
name: "🔬 Honey I shrunk the page!",
apply() { injectStyle(`body { transform: scale(0.4) !important; transform-origin: center top; }`); },
},
{
name: "📐 Reality tilted!",
apply() { injectStyle(`body { transform: perspective(600px) rotateY(15deg) rotateX(5deg) !important; transform-origin: center center; }`); },
},
{
name: "😵‍💫 Who needs glasses?",
apply() { injectStyle(`html { filter: blur(4px) !important; }`); },
},
{
name: "🌈 Rainbow mode!",
apply() {
const n = fxCounter;
injectStyle(`
@keyframes easter-rainbow-${n} { 0% { filter: hue-rotate(0deg); } 100% { filter: hue-rotate(360deg); } }
html { animation: easter-rainbow-${n} 0.8s linear infinite !important; }
`);
},
},
{
name: "✏️ Comic Sans activated!",
apply() { injectStyle(`* { font-family: "Comic Sans MS", "Comic Sans", cursive !important; }`); },
},
{
name: "🌀 SPEEN!",
apply() {
const n = fxCounter;
injectStyle(`
@keyframes easter-spin-${n} { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
html { animation: easter-spin-${n} 2s linear infinite !important; transform-origin: center center; }
`);
},
},
{
name: "🏀 Boing boing boing!",
apply() {
const n = fxCounter;
injectStyle(`
@keyframes easter-bounce-${n} {
0%, 100% { transform: translateY(0); }
25% { transform: translateY(-30px); }
50% { transform: translateY(0); }
75% { transform: translateY(-15px); }
}
body > * { animation: easter-bounce-${n} 0.6s ease infinite !important; }
body > *:nth-child(odd) { animation-delay: 0.15s !important; }
`);
},
},
{
name: "🎬 Film noir!",
apply() { injectStyle(`html { filter: grayscale(1) contrast(1.3) !important; }`); },
},
{
name: "🪗 S T R E T C H",
apply() { injectStyle(`body { transform: scaleX(1.6) scaleY(0.6) !important; transform-origin: center top; }`); },
},
{
name: "🪞 Mirror world!",
apply() { injectStyle(`body { transform: scaleX(-1) !important; }`); },
},
{
name: "🌊 Wobbly!",
apply() {
const n = fxCounter;
injectStyle(`
@keyframes easter-wobble-${n} {
0%, 100% { transform: skewX(0deg) skewY(0deg); }
25% { transform: skewX(4deg) skewY(2deg); }
50% { transform: skewX(-3deg) skewY(-1deg); }
75% { transform: skewX(2deg) skewY(3deg); }
}
body { animation: easter-wobble-${n} 0.5s ease-in-out infinite !important; }
`);
},
},
{
name: "🎨 MAXIMUM COLOR!",
apply() { injectStyle(`html { filter: saturate(8) brightness(1.1) !important; }`); },
},
{
name: "👾 Pixel mode!",
apply() {
injectStyle(`html { image-rendering: pixelated !important; transform: scale(0.3); transform-origin: top left; width: 333.33vw; height: 333.33vh; }`);
},
},
{
name: "💨 Drifting away...",
apply() {
const n = fxCounter;
injectStyle(`
@keyframes easter-drift-${n} { from { transform: translateX(0) rotate(0deg); } to { transform: translateX(-60vw) rotate(-8deg); } }
body { animation: easter-drift-${n} 4s ease-in-out forwards !important; }
`);
},
},
{
name: "📜 Ye olde webpage!",
apply() { injectStyle(`html { filter: sepia(1) brightness(0.9) !important; }`); },
},
{
name: "🔍 ENHANCE!",
apply() { injectStyle(`body { transform: scale(2.5) !important; transform-origin: center top; }`); },
},
{
name: "🍄 Trippy!!",
apply() {
const n = fxCounter;
injectStyle(`
@keyframes easter-trip-${n} {
0% { filter: hue-rotate(0deg) contrast(1) blur(0px); }
25% { filter: hue-rotate(90deg) contrast(1.5) blur(1px); }
50% { filter: hue-rotate(180deg) contrast(2) blur(2px); }
75% { filter: hue-rotate(270deg) contrast(1.5) blur(1px); }
100% { filter: hue-rotate(360deg) contrast(1) blur(0px); }
}
html { animation: easter-trip-${n} 1.5s ease-in-out infinite !important; }
`);
},
},
];
function triggerRandomEffect() {
const effect = pick(PAGE_EFFECTS);
effect.apply();
showToast(effect.name);
}
function collectEgg(egg, cx, cy) {
clearTimeout(egg._vanishTimer);
eggCount++;
saveCount();
updateScoreText();
pulseScore();
burstConfetti(cx, cy);
triggerRandomEffect();
egg.style.transform = "scale(1.6) rotate(20deg)";
egg.style.opacity = "0";
setTimeout(() => {
egg.remove();
eggsOnScreen = Math.max(0, eggsOnScreen - 1);
}, 500);
scheduleNext();
}
// ── Scheduling ──────────────────────────────────────────
function scheduleNext() {
clearTimeout(scheduledTimeout);
const delay = rand(CFG.cooldownMin, CFG.cooldownMax);
scheduledTimeout = setTimeout(() => {
spawnEgg();
if (Math.random() < CFG.doubleSpawnChance) {
const maxBak = CFG.maxSimultaneous;
CFG.maxSimultaneous = 2;
setTimeout(() => spawnEgg(), rand(500, 2000));
setTimeout(() => { CFG.maxSimultaneous = maxBak; }, 3000);
}
}, delay);
}
// ── Bunny cursor trail ──────────────────────────────────
let lastTrail = 0;
function initCursorTrail() {
document.addEventListener("mousemove", (e) => {
const now = Date.now();
if (now - lastTrail < 300) return;
lastTrail = now;
if (Math.random() > 0.2) return;
const trail = document.createElement("span");
trail.className = "bunny-trail";
trail.textContent = pick(["🐾", "·", "🐾", "✿", "·"]);
Object.assign(trail.style, {
left: e.clientX + "px",
top: e.clientY + "px",
});
document.body.appendChild(trail);
setTimeout(() => trail.remove(), 1000);
});
}
// ── Visibility handling (pause when tab hidden) ─────────
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
paused = true;
clearTimeout(scheduledTimeout);
} else {
paused = false;
scheduleNext();
}
});
// ── Listen for messages from popup ──────────────────────
if (chrome?.runtime?.onMessage) {
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "getCount") {
sendResponse({ count: eggCount });
} else if (msg.type === "resetCount") {
eggCount = 0;
saveCount();
updateScoreText();
sendResponse({ count: 0 });
} else if (msg.type === "spawnNow") {
spawnEgg();
sendResponse({ ok: true });
}
});
}
// ── Boot ────────────────────────────────────────────────
function init() {
loadCount(() => {
ensureScoreHUD();
initCursorTrail();
const firstDelay = rand(CFG.initialDelayMin, CFG.initialDelayMax);
scheduledTimeout = setTimeout(() => spawnEgg(), firstDelay);
});
}
if (document.body) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
})();

View File

@@ -0,0 +1,208 @@
/* ===== Easter Egg Hunt Styles ===== */
/* Hidden eggs scattered on page */
.easter-egg-hidden {
position: absolute;
font-size: 28px;
cursor: pointer;
z-index: 99999;
transition: transform 0.3s ease, opacity 0.3s ease;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
animation: egg-wobble 2s ease-in-out infinite;
user-select: none;
}
.easter-egg-hidden:hover {
transform: scale(1.4) rotate(10deg) !important;
}
@keyframes egg-wobble {
0%, 100% { transform: rotate(-3deg); }
50% { transform: rotate(3deg); }
}
/* Egg found celebration */
.easter-egg-found {
animation: egg-found 0.8s ease forwards;
}
@keyframes egg-found {
0% { transform: scale(1); opacity: 1; }
30% { transform: scale(1.8) rotate(20deg); opacity: 1; }
100% { transform: scale(0) rotate(360deg); opacity: 0; }
}
/* Easter banner */
.easter-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 30%, #a8edea 60%, #fed6e3 100%);
color: #5a3e7a;
text-align: center;
padding: 10px 20px;
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 15px;
font-weight: 600;
z-index: 999999;
box-shadow: 0 3px 15px rgba(0,0,0,0.15);
transform: translateY(-100%);
animation: banner-slide 0.5s ease forwards;
letter-spacing: 0.5px;
}
.easter-banner .close-banner {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
font-size: 18px;
opacity: 0.6;
transition: opacity 0.2s;
background: none;
border: none;
color: #5a3e7a;
}
.easter-banner .close-banner:hover {
opacity: 1;
}
@keyframes banner-slide {
to { transform: translateY(0); }
}
/* Egg reveal popup */
.easter-reveal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
background: white;
border-radius: 20px;
padding: 30px 40px;
z-index: 9999999;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
font-family: 'Segoe UI', system-ui, sans-serif;
animation: reveal-pop 0.5s ease forwards;
max-width: 420px;
border: 3px solid #fecfef;
}
.easter-reveal h2 {
margin: 0 0 12px 0;
font-size: 22px;
color: #5a3e7a;
}
.easter-reveal p {
margin: 0 0 16px 0;
font-size: 15px;
color: #666;
line-height: 1.5;
}
.easter-reveal .egg-emoji {
font-size: 50px;
display: block;
margin-bottom: 12px;
}
.easter-reveal button {
background: linear-gradient(135deg, #a8edea, #fed6e3);
border: none;
padding: 10px 28px;
border-radius: 25px;
font-size: 14px;
font-weight: 600;
color: #5a3e7a;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.easter-reveal button:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(168, 237, 234, 0.5);
}
@keyframes reveal-pop {
to { transform: translate(-50%, -50%) scale(1); }
}
.easter-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
z-index: 9999998;
animation: fade-in 0.3s ease forwards;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* Confetti burst */
.easter-confetti {
position: fixed;
top: 0; left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 99999999;
overflow: hidden;
}
.confetti-piece {
position: absolute;
width: 10px;
height: 14px;
border-radius: 2px;
animation: confetti-fall linear forwards;
}
@keyframes confetti-fall {
0% { transform: translateY(-20px) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
}
/* Bunny cursor trail */
.bunny-trail {
position: fixed;
pointer-events: none;
z-index: 999999;
font-size: 20px;
animation: trail-fade 1s ease forwards;
}
@keyframes trail-fade {
0% { opacity: 0.8; transform: scale(1) translateY(0); }
100% { opacity: 0; transform: scale(0.3) translateY(-30px); }
}
/* Score counter */
.easter-score {
position: fixed;
bottom: 20px;
right: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 12px 20px;
border-radius: 30px;
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
font-weight: 600;
z-index: 999999;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
animation: score-pop 0.5s ease;
cursor: default;
}
@keyframes score-pop {
0% { transform: scale(0); }
70% { transform: scale(1.1); }
100% { transform: scale(1); }
}

View File

@@ -0,0 +1,29 @@
{
"manifest_version": 3,
"name": "Easter Egg Hunt 🥚",
"version": "1.0.0",
"description": "Generates site-relevant Easter eggs and transforms pages with festive surprises!",
"permissions": ["activeTab", "scripting", "storage"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"48": "icons/egg48.png",
"128": "icons/egg128.png"
}
},
"icons": {
"48": "icons/egg48.png",
"128": "icons/egg128.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["easter.css"],
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js"
}
}

View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 280px;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(165deg, #fef9ff 0%, #f0e6ff 50%, #e8faf8 100%);
color: #3d2c5e;
overflow: hidden;
}
.header {
text-align: center;
padding: 20px 16px 12px;
}
.header .big-egg {
font-size: 48px;
display: block;
margin-bottom: 6px;
animation: bounce 1.5s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.header h1 {
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #a855f7, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header .subtitle {
font-size: 12px;
color: #9880b8;
margin-top: 2px;
}
.counter-section {
text-align: center;
padding: 16px;
margin: 0 16px;
background: rgba(255,255,255,0.7);
border-radius: 16px;
border: 1.5px solid rgba(168, 130, 234, 0.2);
}
.counter-section .label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #9880b8;
margin-bottom: 4px;
}
.counter-section .count {
font-size: 40px;
font-weight: 800;
background: linear-gradient(135deg, #667eea, #a855f7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.counter-section .eggs-label {
font-size: 13px;
color: #7c6a9e;
}
.actions {
padding: 14px 16px 16px;
display: flex;
gap: 8px;
}
.actions button {
flex: 1;
padding: 10px 8px;
border-radius: 12px;
border: none;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-spawn {
background: linear-gradient(135deg, #a8edea, #fed6e3);
color: #5a3e7a;
}
.btn-spawn:hover {
transform: scale(1.03);
box-shadow: 0 3px 12px rgba(168, 237, 234, 0.5);
}
.btn-reset {
background: rgba(0,0,0,0.05);
color: #9880b8;
}
.btn-reset:hover {
background: rgba(0,0,0,0.1);
}
.footer {
text-align: center;
padding: 0 16px 14px;
font-size: 11px;
color: #b8a5d4;
}
</style>
</head>
<body>
<div class="header">
<span class="big-egg">🥚</span>
<h1>Easter Egg Hunt</h1>
<div class="subtitle">eggs hide on every page you visit ✨</div>
</div>
<div class="counter-section">
<div class="label">collected</div>
<div class="count" id="count">0</div>
<div class="eggs-label">eggs found</div>
</div>
<div class="actions">
<button class="btn-spawn" id="spawnBtn">🥚 Spawn Egg Now</button>
<button class="btn-reset" id="resetBtn">Reset</button>
</div>
<div class="footer">
🐰 Happy Easter! Eggs appear randomly~
</div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,43 @@
const countEl = document.getElementById("count");
const spawnBtn = document.getElementById("spawnBtn");
const resetBtn = document.getElementById("resetBtn");
// Get current egg count from content script
function refreshCount() {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]?.id) return;
chrome.tabs.sendMessage(tabs[0].id, { type: "getCount" }, (res) => {
if (chrome.runtime.lastError) {
// Content script not loaded on this page — read from storage
chrome.storage.local.get("easterEggCount", (data) => {
countEl.textContent = data.easterEggCount || 0;
});
return;
}
countEl.textContent = res?.count ?? 0;
});
});
}
spawnBtn.addEventListener("click", () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]?.id) return;
chrome.tabs.sendMessage(tabs[0].id, { type: "spawnNow" });
spawnBtn.textContent = "🐣 Spawned!";
setTimeout(() => { spawnBtn.textContent = "🥚 Spawn Egg Now"; }, 1200);
});
});
resetBtn.addEventListener("click", () => {
if (!confirm("Reset your egg count to 0?")) return;
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]?.id) return;
chrome.tabs.sendMessage(tabs[0].id, { type: "resetCount" }, () => {
countEl.textContent = "0";
});
});
chrome.storage.local.set({ easterEggCount: 0 });
countEl.textContent = "0";
});
refreshCount();