opensource / cursor-cafeteria.js
🐻🍪
Cursor Osito y Galleta
Un osito persigue tu cursor y se come la galleta. Ideal para cafeterías y panaderías.
🧩 Truco universal · JS JavaScript CSS Cursor Animación Un solo archivo MIT License
1 archivo 182 líneas MIT
Prueba el Juego Volver
cursor-cafeteria.js 182 líneas
JavaScript
1 (() => {
2 // ⚙️ CONFIGURACIÓclass="tok-prop">N: class="tok-val">cambia lo que quieras
3 class="tok-kw">const CONFIG = {
4 class="tok-prop">galleta: class="tok-val">";🍪☕", // la comida que sigue al mouse
5 class="tok-prop">osito: class="tok-val">";🐻", // el que se la come
6 class="tok-prop">tamanoGalleta: class="tok-val">22,
7 class="tok-prop">tamanoOsito: class="tok-val">38,
8 class="tok-prop">velocidad: class="tok-val">0.06, // 0.06 lento, 0.2 rápido
9 class="tok-prop">bocados: class="tok-val">5,
10 class="tok-prop">miraALaIzquierda: class="tok-val">true, // el osito mira de frente, da casi igual
11 class="tok-prop">segundosParaDormir: class="tok-val">5,
12 class="tok-prop">huellas: class="tok-val">";🐾",
13 class="tok-prop">filtroHuellas: class="tok-val">";sepia(1)", // huellitas color café
14 class="tok-prop">explosion: class="tok-val">";🍪☕", // lo que salta al hacer clic
15 class="tok-prop">distanciaBoca: class="tok-val">24, // a cuántos px se detiene
16 class="tok-prop">inicial: class="tok-val">3, // galletas con las que empieza
17 class="tok-prop">porClic: class="tok-val">5, // galletas que da cada clic
18 class="tok-prop">maximo: class="tok-val">20, // tope de galletas
19 };
20
21 // ♿ No ejecutar en celulares ni si el usuario pidió menos movimiento
22 class="tok-kw">if (matchMedia("(class="tok-prop">hover: class="tok-val">none)";).matches) class="tok-kw">return;
23 class="tok-kw">if (matchMedia("(class="tok-prop">prefers-reduced-motion: class="tok-val">reduce)";).matches) class="tok-kw">return;
24
25 // 🎨 Estilos
26 class="tok-kw">const style = document.createElement("style");
27 style.textContent = `
28 *, *::before, *::after { class="tok-prop">cursor: class="tok-val">none !important; }
29 .cc-item { class="tok-prop">position: class="tok-val">fixed; class="tok-prop">top: class="tok-val">0; class="tok-prop">left: class="tok-val">0; class="tok-prop">pointer-events: class="tok-val">none; class="tok-prop">z-index: class="tok-val">99999; }
30 #cc-galleta { class="tok-prop">font-size: class="tok-val">${CONFIG.tamanoGalleta}px; class="tok-prop">z-index: class="tok-val">100000; }
31 #cc-osito { class="tok-prop">font-size: class="tok-val">${CONFIG.tamanoOsito}px; }
32 .cc-item span { class="tok-prop">display: class="tok-val">inline-block; }
33 .cc-fx { class="tok-prop">position: class="tok-val">fixed; class="tok-prop">pointer-events: class="tok-val">none; class="tok-prop">z-index: class="tok-val">99998; class="tok-prop">font-size: class="tok-val">18px; }
34 #cc-osito.moving .salto { class="tok-prop">animation: class="tok-val">cc-salto .4s ease-in-out infinite; }
35 #cc-osito.eating .salto { class="tok-prop">animation: class="tok-val">cc-munch .2s ease-in-out infinite; }
36 #cc-osito.sleeping .salto { class="tok-prop">transform: class="tok-val">scale(1, .8) translateY(4px); class="tok-prop">filter: class="tok-val">grayscale(.5); }
37 #cc-osito.jump .salto { class="tok-prop">animation: class="tok-val">cc-big .5s ease-out; }
38 #cc-counter { class="tok-prop">position: class="tok-val">fixed; class="tok-prop">top: class="tok-val">12px; class="tok-prop">right: class="tok-val">16px; class="tok-prop">font: class="tok-val">700 18px system-ui, sans-serif; class="tok-prop">z-index: class="tok-val">100001; class="tok-prop">pointer-events: class="tok-val">none;
39 class="tok-prop">color: class="tok-val">#fff; class="tok-prop">background: class="tok-val">rgba(0,0,0,.6); class="tok-prop">padding: class="tok-val">4px 12px; class="tok-prop">border-radius: class="tok-val">999px; }
40 #cc-galleta.empty { class="tok-prop">opacity: class="tok-val">0; }
41 #cc-osito.hungry .salto { class="tok-prop">filter: class="tok-val">grayscale(1) brightness(.65); }
42 @keyframes cc-salto { 50% { class="tok-prop">transform: class="tok-val">translateY(-16px) rotate(-12deg); } }
43 @keyframes cc-munch { 50% { class="tok-prop">transform: class="tok-val">scale(1.15, .9); } }
44 @keyframes cc-big { 40% { class="tok-prop">transform: class="tok-val">translateY(-50px) rotate(-20deg); } }
45 @keyframes cc-fade { to { class="tok-prop">opacity: class="tok-val">0; } }
46 @keyframes cc-zzz { to { class="tok-prop">opacity: class="tok-val">0; class="tok-prop">transform: class="tok-val">translate(14px, -40px); } }
47 @keyframes cc-pop { to { class="tok-prop">opacity: class="tok-val">0; class="tok-prop">transform: class="tok-val">translate(class="tok-kw">var(--dx), class="tok-kw">var(--dy)); } }
48 `;
49 document.head.appendChild(style);
50
51 // 🧱 Elementos
52 class="tok-kw">const galleta = document.createElement("div");
53 class="tok-kw">const osito = document.createElement("div");
54 galleta.className = osito.className = "cc-item";
55 galleta.id = "cc-galleta";
56 osito.id = "cc-osito";
57 galleta.innerHTML = `class="tok-tag"><spanclass="tok-attr">class="tok-tag">>${CONFIG.galleta}class="tok-tag"></spanclass="tok-attr">class="tok-tag">>`;
58 osito.innerHTML = `class="tok-tag"><spanclass="tok-attr"> class="salto"class="tok-tag">>${CONFIG.osito}class="tok-tag"></spanclass="tok-attr">class="tok-tag">>`;
59 document.body.append(galleta, osito);
60 class="tok-kw">const trozo = galleta.firstChild;
61
62 // 🔢 Contador
63 class="tok-kw">const contador = document.createElement("div");
64 contador.id = "cc-counter";
65 document.body.appendChild(contador);
66
67 class="tok-kw">let galletas = CONFIG.inicial;
68 class="tok-kw">const actualizarContador = () => {
69 contador.textContent = `${CONFIG.galleta} x${galletas}`;
70 galleta.classList.toggle("empty", galletas === 0);
71 osito.classList.toggle("hungry", galletas === 0);
72 };
73 actualizarContador();
74
75 // ✨ Efectos sueltos (huellas, zzz, explosión de galletas)
76 class="tok-kw">function fx(text, x, y, anim, extra = "") {
77 class="tok-kw">const el = document.createElement("div");
78 el.className = "cc-fx";
79 el.textContent = text;
80 el.style.cssText = `class="tok-prop">left:class="tok-val">${x}px; class="tok-prop">top:class="tok-val">${y}px; class="tok-prop">animation:class="tok-val">${anim}; ${extra}`;
81 el.addEventListener("animationend", () => el.remove());
82 document.body.appendChild(el);
83 }
84
85 class="tok-kw">let mx = innerWidth / 2, my = innerHeight / 2;
86 class="tok-kw">let ox = mx - 200, oy = my; // posición del osito
87 class="tok-kw">let bocadosDados = 0, ultimoBocado = 0, ultimaZ = 0, caminado = 0;
88 class="tok-kw">let ultimoMovimiento = performance.now();
89
90 class="tok-kw">const ponerGalleta = () =>
91 (galleta.style.transform = `translate(${mx}px, ${my}px) translate(-50%, -50%)`);
92 ponerGalleta();
93
94 class="tok-kw">function jump() {
95 osito.classList.remove("jump");
96 void osito.offsetWidth; // reinicia la animación
97 osito.classList.add("jump");
98 setTimeout(() => osito.classList.remove("jump"), 500);
99 }
100
101 addEventListener("mousemove", (e) => {
102 mx = e.clientX; my = e.clientY;
103 ultimoMovimiento = performance.now();
104 ponerGalleta();
105 class="tok-kw">if (osito.classList.contains("sleeping")) jump(); // ¡se despierta de golpe!
106 });
107
108 addEventListener("click", (e) => {
109 jump();
110 class="tok-kw">if (galletas === 0) { bocadosDados = 0; trozo.style.clipPath = "none"; }
111 galletas = Math.min(galletas + CONFIG.porClic, CONFIG.maximo);
112 actualizarContador();
113 class="tok-kw">for (class="tok-kw">let i = 0; i < CONFIG.porClic; i++) {
114 class="tok-kw">const a = (Math.PI * 2 * i) / CONFIG.porClic;
115 fx(CONFIG.explosion, e.clientX, e.clientY, "cc-pop .8s ease-out forwards",
116 `class="tok-prop">--dx:class="tok-val">${Math.cos(a) * 50}px; class="tok-prop">--dy:class="tok-val">${Math.sin(a) * 50}px;`);
117 }
118 });
119
120 class="tok-kw">function loop(t) {
121 class="tok-kw">const dx = mx - ox, dy = my - oy;
122 class="tok-kw">const dist = Math.hypot(dx, dy);
123 class="tok-kw">const cerca = dist < CONFIG.distanciaBoca + 15;
124
125 // el destino es un punto cerca de la galleta, no la galleta misma
126 class="tok-kw">const d = dist || 1;
127 class="tok-kw">const tx = mx - (dx / d) * CONFIG.distanciaBoca;
128 class="tok-kw">const ty = my - (dy / d) * CONFIG.distanciaBoca;
129 ox += (tx - ox) * CONFIG.velocidad;
130 oy += (ty - oy) * CONFIG.velocidad;
131
132 class="tok-kw">const quieto = t - ultimoMovimiento > CONFIG.segundosParaDormir * 1000;
133 class="tok-kw">const durmiendo = quieto && cerca;
134 class="tok-kw">const comiendo = cerca && !durmiendo && galletas > 0 && bocadosDados < CONFIG.bocados;
135
136 osito.classList.toggle("sleeping", durmiendo);
137 osito.classList.toggle("eating", comiendo);
138 osito.classList.toggle("moving", !cerca);
139
140 // 🐾 huellitas al caminar
141 class="tok-kw">if (!cerca) {
142 caminado += Math.hypot(tx - ox, ty - oy) * CONFIG.velocidad;
143 class="tok-kw">if (caminado > 45) {
144 caminado = 0;
145 fx(CONFIG.huellas, ox, oy + 14, "cc-fade 1.5s forwards",
146 `class="tok-prop">transform: class="tok-val">translate(-50%, -50%) rotate(${Math.random() * 40 - 20}deg); class="tok-prop">filter: class="tok-val">${CONFIG.filtroHuellas};`);
147 }
148 }
149
150 // 😴 zzz mientras duerme
151 class="tok-kw">if (durmiendo && t - ultimaZ > 900) {
152 ultimaZ = t;
153 fx("z", ox + 10, oy - 20, "cc-zzz 1.5s forwards");
154 }
155
156 // 🍪 mordiscos
157 class="tok-kw">if (comiendo && t - ultimoBocado > 450) {
158 bocadosDados++;
159 ultimoBocado = t;
160 trozo.style.clipPath = `inset(0 0 0 ${(bocadosDados / CONFIG.bocados) * 100}%)`;
161 class="tok-kw">if (bocadosDados >= CONFIG.bocados) {
162 galletas--;
163 actualizarContador();
164 class="tok-kw">if (galletas > 0) {
165 setTimeout(() => { bocadosDados = 0; trozo.style.clipPath = "none"; }, 400);
166 }
167 }
168 }
169
170 // 🥺 pide comida cuando no queda nada
171 class="tok-kw">if (galletas === 0 && t - ultimaZ > 1200) {
172 ultimaZ = t;
173 fx(`${CONFIG.galleta}?`, ox + 10, oy - 24, "cc-zzz 1.5s forwards");
174 }
175
176 class="tok-kw">const flip = (mx < ox) === CONFIG.miraALaIzquierda ? 1 : -1;
177 osito.style.transform =
178 `translate(${ox}px, ${oy}px) translate(-50%, -50%) scaleX(${flip})`;
179 requestAnimationFrame(loop);
180 }
181 requestAnimationFrame(loop);
182 })();