opensource / cursor-veterinaria.js
🥕🐇
Cursor Zanahoria y Conejo
Un conejo persigue tu cursor, se come la zanahoria a mordiscos y se duerme si te quedas quieto. Cada clic le da más zanahorias.
🧩 Truco universal · JS JavaScript CSS Cursor Animación Un solo archivo MIT License
1 archivo 191 líneas MIT
Prueba el Juego Volver
cursor-veterinaria.js 191 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">zanahoria: class="tok-val">";🥕",
5 class="tok-prop">conejo: class="tok-val">";🐇",
6 class="tok-prop">tamanoZanahoria: class="tok-val">18, // la mitad de antes
7 class="tok-prop">tamanoConejo: class="tok-val">35, // un poquito más grande
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, // false si tu emoji mira a la derecha
11 class="tok-prop">segundosParaDormir: class="tok-val">5,
12 class="tok-prop">huellas: class="tok-val">";🐾",
13 class="tok-prop">corazones: class="tok-val">";🥕",
14 class="tok-prop">distanciaBoca: class="tok-val">24, // a cuántos px de la zanahoria se detiene
15 // contadores
16 class="tok-prop">inicial: class="tok-val">3, // zanahorias con las que empieza
17 class="tok-prop">maximo: class="tok-val">10, // tope para no acumular infinitas
18 class="tok-prop">porClic: class="tok-val">5,
19 class="tok-prop">maximo: class="tok-val">20,
20 };
21
22 // ♿ No ejecutar en celulares ni si el usuario pidió menos movimiento
23 class="tok-kw">if (matchMedia("(class="tok-prop">hover: class="tok-val">none)";).matches) class="tok-kw">return;
24 class="tok-kw">if (matchMedia("(class="tok-prop">prefers-reduced-motion: class="tok-val">reduce)";).matches) class="tok-kw">return;
25
26 // 🎨 Estilos
27 class="tok-kw">const style = document.createElement("style");
28 style.textContent = `
29 *, *::before, *::after { class="tok-prop">cursor: class="tok-val">none !important; }
30 .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; }
31 #cc-carrot { class="tok-prop">font-size: class="tok-val">${CONFIG.tamanoZanahoria}px; class="tok-prop">z-index: class="tok-val">100000; }
32 #cc-bunny { class="tok-prop">font-size: class="tok-val">${CONFIG.tamanoConejo}px; }
33 .cc-item span { class="tok-prop">display: class="tok-val">inline-block; }
34 .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; }
35 #cc-bunny.moving .hop { class="tok-prop">animation: class="tok-val">cc-hop .4s ease-in-out infinite; }
36 #cc-bunny.eating .hop { class="tok-prop">animation: class="tok-val">cc-munch .2s ease-in-out infinite; }
37 #cc-bunny.sleeping .hop { class="tok-prop">transform: class="tok-val">scale(1, .8) translateY(4px); class="tok-prop">filter: class="tok-val">grayscale(.5); }
38 #cc-bunny.jump .hop { class="tok-prop">animation: class="tok-val">cc-big .5s ease-out; }
39
40 #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;
41 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; }
42 #cc-carrot.empty { class="tok-prop">opacity: class="tok-val">0; }
43 /* más oscuro y apagadito */
44 #cc-bunny.hungry .hop { class="tok-prop">filter: class="tok-val">grayscale(1) brightness(.65); }
45 /* o más transparente
46 #cc-bunny.hungry .hop { class="tok-prop">opacity: class="tok-val">.55; } */
47
48
49 @keyframes cc-hop { 50% { class="tok-prop">transform: class="tok-val">translateY(-16px) rotate(-12deg); } }
50 @keyframes cc-munch { 50% { class="tok-prop">transform: class="tok-val">scale(1.15, .9); } }
51 @keyframes cc-big { 40% { class="tok-prop">transform: class="tok-val">translateY(-50px) rotate(-20deg); } }
52 @keyframes cc-fade { to { class="tok-prop">opacity: class="tok-val">0; } }
53 @keyframes cc-zzz { to { class="tok-prop">opacity: class="tok-val">0; class="tok-prop">transform: class="tok-val">translate(14px, -40px); } }
54 @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)); } }
55
56 `;
57 document.head.appendChild(style);
58
59 // 🧱 Elementos
60 class="tok-kw">const carrot = document.createElement("div");
61 class="tok-kw">const bunny = document.createElement("div");
62 carrot.className = bunny.className = "cc-item";
63 carrot.id = "cc-carrot";
64 bunny.id = "cc-bunny";
65
66 carrot.innerHTML = `class="tok-tag"><spanclass="tok-attr">class="tok-tag">>${CONFIG.zanahoria}class="tok-tag"></spanclass="tok-attr">class="tok-tag">>`;
67 bunny.innerHTML = `class="tok-tag"><spanclass="tok-attr"> class="hop"class="tok-tag">>${CONFIG.conejo}class="tok-tag"></spanclass="tok-attr">class="tok-tag">>`;
68 document.body.append(carrot, bunny);
69 class="tok-kw">const piece = carrot.firstChild;
70
71 class="tok-kw">const counter = document.createElement("div");
72 counter.id = "cc-counter";
73 document.body.appendChild(counter);
74
75 class="tok-kw">let stock = CONFIG.inicial;
76 class="tok-kw">const updateCounter = () => {
77 counter.textContent = `${CONFIG.zanahoria} x${stock}`;
78 carrot.classList.toggle("empty", stock === 0);
79 bunny.classList.toggle("hungry", stock === 0);
80 };
81 updateCounter();
82
83
84 // ✨ Efectos sueltos (huellas, zzz, corazones)
85 class="tok-kw">function fx(text, x, y, anim, extra = "") {
86 class="tok-kw">const el = document.createElement("div");
87 el.className = "cc-fx";
88 el.textContent = text;
89 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}`;
90 el.addEventListener("animationend", () => el.remove());
91 document.body.appendChild(el);
92 }
93
94 class="tok-kw">let mx = innerWidth / 2, my = innerHeight / 2;
95 class="tok-kw">let bx = mx - 200, by = my;
96 class="tok-kw">let bites = 0, lastBite = 0, lastZ = 0, walked = 0;
97 class="tok-kw">let lastMove = performance.now();
98
99 class="tok-kw">const placeCarrot = () =>
100 (carrot.style.transform = `translate(${mx}px, ${my}px) translate(-50%, -50%)`);
101 placeCarrot();
102
103 class="tok-kw">function jump() {
104 bunny.classList.remove("jump");
105 void bunny.offsetWidth; // reinicia la animación
106 bunny.classList.add("jump");
107 setTimeout(() => bunny.classList.remove("jump"), 500);
108 }
109
110 addEventListener("mousemove", (e) => {
111 mx = e.clientX; my = e.clientY;
112 lastMove = performance.now();
113 placeCarrot();
114 class="tok-kw">if (bunny.classList.contains("sleeping")) jump(); // ¡se despierta de golpe!
115 });
116
117 addEventListener("click", (e) => {
118 jump();
119 class="tok-kw">if (stock === 0) { bites = 0; piece.style.clipPath = "none"; }
120 stock = Math.min(stock + CONFIG.porClic, CONFIG.maximo);
121 updateCounter();
122 class="tok-kw">for (class="tok-kw">let i = 0; i < CONFIG.porClic; i++) {
123 class="tok-kw">const a = (Math.PI * 2 * i) / CONFIG.porClic;
124 fx(CONFIG.corazones, e.clientX, e.clientY, "cc-pop .8s ease-out forwards",
125 `class="tok-prop">--dx:class="tok-val">${Math.cos(a) * 50}px; class="tok-prop">--dy:class="tok-val">${Math.sin(a) * 50}px;`);
126 }
127 });
128
129 class="tok-kw">function loop(t) {
130 class="tok-kw">const dx = mx - bx, dy = my - by;
131 class="tok-kw">const dist = Math.hypot(dx, dy);
132 class="tok-kw">const near = dist < CONFIG.distanciaBoca + 15;
133
134 // el destino es un punto cerca de la zanahoria, no la zanahoria misma
135 class="tok-kw">const d = dist || 1;
136 class="tok-kw">const tx = mx - (dx / d) * CONFIG.distanciaBoca;
137 class="tok-kw">const ty = my - (dy / d) * CONFIG.distanciaBoca;
138
139 class="tok-kw">const idle = t - lastMove > CONFIG.segundosParaDormir * 1000;
140 class="tok-kw">const sleeping = idle && near;
141 class="tok-kw">const eating = near && !sleeping && stock > 0 && bites < CONFIG.bocados;
142
143 bx += dx * CONFIG.velocidad;
144 by += dy * CONFIG.velocidad;
145
146 bunny.classList.toggle("sleeping", sleeping);
147 bunny.classList.toggle("eating", eating);
148 bunny.classList.toggle("moving", !near);
149
150 // 🐾 huellitas al caminar
151 class="tok-kw">if (!near) {
152 walked += Math.hypot(dx, dy) * CONFIG.velocidad;
153 class="tok-kw">if (walked > 45) {
154 walked = 0;
155 fx(CONFIG.huellas, bx, by + 14, "cc-fade 1.5s forwards",
156 `class="tok-prop">transform: class="tok-val">translate(-50%, -50%) rotate(${Math.random() * 40 - 20}deg); class="tok-prop">filter: class="tok-val">grayscale(1);`);
157 }
158 }
159
160 // 😴 zzz mientras duerme
161 class="tok-kw">if (sleeping && t - lastZ > 900) {
162 lastZ = t;
163 fx("z", bx + 10, by - 20, "cc-zzz 1.5s forwards");
164 }
165
166 // 🥕 mordiscos
167 class="tok-kw">if (eating && t - lastBite > 450) {
168 bites++;
169 lastBite = t;
170 piece.style.clipPath = `inset(0 0 0 ${(bites / CONFIG.bocados) * 100}%)`;
171 class="tok-kw">if (bites >= CONFIG.bocados) {
172 stock--;
173 updateCounter();
174 class="tok-kw">if (stock > 0) {
175 setTimeout(() => { bites = 0; piece.style.clipPath = "none"; }, 400);
176 }
177 }
178 }
179
180 class="tok-kw">if (stock === 0 && t - lastZ > 1200) {
181 lastZ = t;
182 fx("🥕?", bx + 10, by - 24, "cc-zzz 1.5s forwards");
183 }
184
185 class="tok-kw">const flip = (mx < bx) === CONFIG.miraALaIzquierda ? 1 : -1;
186 bunny.style.transform =
187 `translate(${bx}px, ${by}px) translate(-50%, -50%) scaleX(${flip})`;
188 requestAnimationFrame(loop);
189 }
190 requestAnimationFrame(loop);
191 })();