Play Breakout on your calendar - events become bricks that temporarily hide for 10 minutes when hit
Size
14.8 KB
Version
1.1.3
Created
Oct 22, 2025
Updated
1 day ago
1// ==UserScript==
2// @name Google Calendar Breakout Game
3// @description Play Breakout on your calendar - events become bricks that temporarily hide for 10 minutes when hit
4// @version 1.1.3
5// @match https://*.calendar.google.com/*
6// ==/UserScript==
7(function() {
8 'use strict';
9
10 console.log('Google Calendar Breakout Game initialized');
11
12 // Game state
13 let gameActive = false;
14 let canvas, ctx;
15 let paddle, ball;
16 let hiddenEvents = new Map(); // Track hidden events with timestamps
17 let animationFrameId;
18
19 // Game constants
20 const PADDLE_WIDTH = 100;
21 const PADDLE_HEIGHT = 15;
22 const BALL_RADIUS = 8;
23 const BALL_SPEED = 6;
24 const HIDE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds
25
26 // Confetti animation
27 class Confetti {
28 constructor(x, y) {
29 this.x = x;
30 this.y = y;
31 this.size = Math.random() * 8 + 4;
32 this.speedX = Math.random() * 6 - 3;
33 this.speedY = Math.random() * -8 - 2;
34 this.gravity = 0.3;
35 this.color = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'][Math.floor(Math.random() * 6)];
36 this.rotation = Math.random() * 360;
37 this.rotationSpeed = Math.random() * 10 - 5;
38 }
39
40 update() {
41 this.speedY += this.gravity;
42 this.x += this.speedX;
43 this.y += this.speedY;
44 this.rotation += this.rotationSpeed;
45 }
46
47 draw(ctx) {
48 ctx.save();
49 ctx.translate(this.x, this.y);
50 ctx.rotate(this.rotation * Math.PI / 180);
51 ctx.fillStyle = this.color;
52 ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size);
53 ctx.restore();
54 }
55
56 isOffScreen(canvasHeight) {
57 return this.y > canvasHeight;
58 }
59 }
60
61 let confettiParticles = [];
62 let gameOverDisplayed = false;
63
64 // Paddle object
65 class Paddle {
66 constructor(canvasWidth, canvasHeight) {
67 this.width = PADDLE_WIDTH;
68 this.height = PADDLE_HEIGHT;
69 this.x = canvasWidth / 2 - this.width / 2;
70 this.y = canvasHeight - 30;
71 this.speed = 8;
72 this.canvasWidth = canvasWidth;
73 }
74
75 draw(ctx) {
76 ctx.fillStyle = '#4285f4';
77 ctx.fillRect(this.x, this.y, this.width, this.height);
78 }
79
80 moveLeft() {
81 this.x = Math.max(0, this.x - this.speed);
82 }
83
84 moveRight() {
85 this.x = Math.min(this.canvasWidth - this.width, this.x + this.speed);
86 }
87 }
88
89 // Ball object
90 class Ball {
91 constructor(canvasWidth, canvasHeight) {
92 this.radius = BALL_RADIUS;
93 this.x = canvasWidth / 2;
94 this.y = canvasHeight - 50;
95 this.dx = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
96 this.dy = -BALL_SPEED;
97 this.canvasWidth = canvasWidth;
98 this.canvasHeight = canvasHeight;
99 }
100
101 draw(ctx) {
102 ctx.beginPath();
103 ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
104 ctx.fillStyle = '#ea4335';
105 ctx.fill();
106 ctx.closePath();
107 }
108
109 update(paddle) {
110 this.x += this.dx;
111 this.y += this.dy;
112
113 // Wall collision
114 if (this.x + this.radius > this.canvasWidth || this.x - this.radius < 0) {
115 this.dx = -this.dx;
116 }
117 if (this.y - this.radius < 0) {
118 this.dy = -this.dy;
119 }
120
121 // Paddle collision
122 if (this.y + this.radius > paddle.y &&
123 this.x > paddle.x &&
124 this.x < paddle.x + paddle.width) {
125 this.dy = -Math.abs(this.dy);
126 // Add some angle variation based on where ball hits paddle
127 const hitPos = (this.x - paddle.x) / paddle.width;
128 this.dx = BALL_SPEED * (hitPos - 0.5) * 2;
129 }
130
131 // Ball falls off bottom - reset
132 if (this.y - this.radius > this.canvasHeight) {
133 this.reset();
134 }
135 }
136
137 reset() {
138 this.x = this.canvasWidth / 2;
139 this.y = this.canvasHeight - 50;
140 this.dx = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
141 this.dy = -BALL_SPEED;
142 }
143 }
144
145 // Get all calendar event elements
146 function getCalendarEvents() {
147 return Array.from(document.querySelectorAll('div[data-eventchip][role="button"]'));
148 }
149
150 // Hide an event temporarily
151 function hideEvent(eventElement) {
152 if (!eventElement || hiddenEvents.has(eventElement)) return;
153
154 console.log('Hiding event temporarily');
155 eventElement.style.opacity = '0';
156 eventElement.style.pointerEvents = 'none';
157
158 const hideTime = Date.now();
159 hiddenEvents.set(eventElement, hideTime);
160
161 // Schedule reappearance after 10 minutes
162 setTimeout(() => {
163 showEvent(eventElement);
164 }, HIDE_DURATION);
165 }
166
167 // Show an event again
168 function showEvent(eventElement) {
169 if (!eventElement) return;
170
171 console.log('Showing event again');
172 eventElement.style.opacity = '';
173 eventElement.style.pointerEvents = '';
174 hiddenEvents.delete(eventElement);
175 }
176
177 // Check collision between ball and events
178 function checkEventCollisions() {
179 const events = getCalendarEvents();
180 const canvasRect = canvas.getBoundingClientRect();
181
182 events.forEach(event => {
183 if (hiddenEvents.has(event)) return; // Skip already hidden events
184
185 const eventRect = event.getBoundingClientRect();
186
187 // Convert event position to canvas coordinates
188 const eventX = eventRect.left - canvasRect.left;
189 const eventY = eventRect.top - canvasRect.top;
190 const eventWidth = eventRect.width;
191 const eventHeight = eventRect.height;
192
193 // Check if ball intersects with event
194 const closestX = Math.max(eventX, Math.min(ball.x, eventX + eventWidth));
195 const closestY = Math.max(eventY, Math.min(ball.y, eventY + eventHeight));
196
197 const distanceX = ball.x - closestX;
198 const distanceY = ball.y - closestY;
199 const distanceSquared = distanceX * distanceX + distanceY * distanceY;
200
201 if (distanceSquared < ball.radius * ball.radius) {
202 // Collision detected!
203 hideEvent(event);
204
205 // Bounce ball
206 if (Math.abs(distanceX) > Math.abs(distanceY)) {
207 ball.dx = -ball.dx;
208 } else {
209 ball.dy = -ball.dy;
210 }
211
212 // Check if all events are now hidden
213 checkGameOver();
214 }
215 });
216 }
217
218 // Check if all events are hidden (game over)
219 function checkGameOver() {
220 const allEvents = getCalendarEvents();
221 const allHidden = allEvents.every(event => hiddenEvents.has(event));
222
223 if (allHidden && allEvents.length > 0 && !gameOverDisplayed) {
224 console.log('Game Over! All events cleared!');
225 gameOverDisplayed = true;
226 triggerConfetti();
227 }
228 }
229
230 // Trigger confetti celebration
231 function triggerConfetti() {
232 // Create confetti particles from multiple positions
233 for (let i = 0; i < 150; i++) {
234 const x = Math.random() * canvas.width;
235 const y = Math.random() * canvas.height * 0.3; // Start from top third of screen
236 confettiParticles.push(new Confetti(x, y));
237 }
238 }
239
240 // Update and draw confetti
241 function updateConfetti() {
242 confettiParticles = confettiParticles.filter(particle => {
243 particle.update();
244 particle.draw(ctx);
245 return !particle.isOffScreen(canvas.height);
246 });
247 }
248
249 // Draw game over message
250 function drawGameOver() {
251 if (!gameOverDisplayed) return;
252
253 // Draw semi-transparent background
254 ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
255 ctx.fillRect(0, 0, canvas.width, canvas.height);
256
257 // Draw "GAME OVER" text
258 ctx.fillStyle = '#FFD700';
259 ctx.font = 'bold 80px Arial';
260 ctx.textAlign = 'center';
261 ctx.textBaseline = 'middle';
262 ctx.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2 - 50);
263
264 // Draw subtitle
265 ctx.fillStyle = '#FFFFFF';
266 ctx.font = 'bold 40px Arial';
267 ctx.fillText('All Events Cleared! 🎉', canvas.width / 2, canvas.height / 2 + 50);
268 }
269
270 // Game loop
271 function gameLoop() {
272 if (!gameActive) return;
273
274 // Clear canvas
275 ctx.clearRect(0, 0, canvas.width, canvas.height);
276
277 // Update and draw
278 ball.update(paddle);
279 checkEventCollisions();
280
281 paddle.draw(ctx);
282 ball.draw(ctx);
283
284 // Update and draw confetti if game over
285 if (gameOverDisplayed) {
286 updateConfetti();
287 drawGameOver();
288 }
289
290 animationFrameId = requestAnimationFrame(gameLoop);
291 }
292
293 // Keyboard controls
294 const keys = {};
295 function handleKeyDown(e) {
296 if (!gameActive) return;
297 keys[e.key] = true;
298
299 if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
300 paddle.moveLeft();
301 }
302 if (keys['ArrowRight'] || keys['d'] || keys['D']) {
303 paddle.moveRight();
304 }
305 }
306
307 function handleKeyUp(e) {
308 keys[e.key] = false;
309 }
310
311 // Mouse controls
312 function handleMouseMove(e) {
313 if (!gameActive) return;
314 const rect = canvas.getBoundingClientRect();
315 const mouseX = e.clientX - rect.left;
316 paddle.x = Math.max(0, Math.min(canvas.width - paddle.width, mouseX - paddle.width / 2));
317 }
318
319 // Create game canvas overlay
320 function createGameCanvas() {
321 canvas = document.createElement('canvas');
322 canvas.id = 'breakout-game-canvas';
323 canvas.style.cssText = `
324 position: fixed;
325 top: 0;
326 left: 0;
327 width: 100%;
328 height: 100%;
329 z-index: 9998;
330 pointer-events: none;
331 `;
332 document.body.appendChild(canvas);
333
334 // Set canvas size
335 canvas.width = window.innerWidth;
336 canvas.height = window.innerHeight;
337 ctx = canvas.getContext('2d');
338
339 // Initialize game objects
340 paddle = new Paddle(canvas.width, canvas.height);
341 ball = new Ball(canvas.width, canvas.height);
342
343 // Handle window resize
344 window.addEventListener('resize', () => {
345 canvas.width = window.innerWidth;
346 canvas.height = window.innerHeight;
347 paddle.canvasWidth = canvas.width;
348 ball.canvasWidth = canvas.width;
349 ball.canvasHeight = canvas.height;
350 });
351
352 console.log('Game canvas created');
353 }
354
355 // Create toggle button
356 function createToggleButton() {
357 const button = document.createElement('button');
358 button.id = 'breakout-toggle-btn';
359 button.textContent = '🎮 Start Breakout';
360 button.style.cssText = `
361 position: fixed;
362 top: 20px;
363 right: 20px;
364 z-index: 9999;
365 padding: 12px 20px;
366 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
367 color: white;
368 border: none;
369 border-radius: 8px;
370 font-size: 16px;
371 font-weight: bold;
372 cursor: pointer;
373 box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
374 transition: all 0.3s ease;
375 font-family: 'Google Sans', Roboto, Arial, sans-serif;
376 `;
377
378 button.addEventListener('mouseenter', () => {
379 button.style.transform = 'translateY(-2px)';
380 button.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
381 });
382
383 button.addEventListener('mouseleave', () => {
384 button.style.transform = 'translateY(0)';
385 button.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
386 });
387
388 button.addEventListener('click', toggleGame);
389 document.body.appendChild(button);
390
391 console.log('Toggle button created');
392 }
393
394 // Toggle game on/off
395 function toggleGame() {
396 gameActive = !gameActive;
397 const button = document.getElementById('breakout-toggle-btn');
398
399 if (gameActive) {
400 button.textContent = '⏸️ Stop Breakout';
401 button.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)';
402 canvas.style.pointerEvents = 'auto';
403
404 // Add event listeners
405 document.addEventListener('keydown', handleKeyDown);
406 document.addEventListener('keyup', handleKeyUp);
407 canvas.addEventListener('mousemove', handleMouseMove);
408
409 // Reset ball position
410 ball.reset();
411
412 // Start game loop
413 gameLoop();
414
415 console.log('Game started');
416 } else {
417 button.textContent = '🎮 Start Breakout';
418 button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
419 canvas.style.pointerEvents = 'none';
420
421 // Remove event listeners
422 document.removeEventListener('keydown', handleKeyDown);
423 document.removeEventListener('keyup', handleKeyUp);
424 canvas.removeEventListener('mousemove', handleMouseMove);
425
426 // Stop game loop
427 if (animationFrameId) {
428 cancelAnimationFrame(animationFrameId);
429 }
430
431 // Clear canvas
432 ctx.clearRect(0, 0, canvas.width, canvas.height);
433
434 console.log('Game stopped');
435 }
436 }
437
438 // Restore any hidden events on page unload
439 window.addEventListener('beforeunload', () => {
440 hiddenEvents.forEach((time, event) => {
441 showEvent(event);
442 });
443 });
444
445 // Initialize game when calendar is ready
446 function init() {
447 // Wait for calendar to load
448 const checkCalendar = setInterval(() => {
449 const events = getCalendarEvents();
450 if (events.length > 0) {
451 clearInterval(checkCalendar);
452 console.log(`Found ${events.length} calendar events`);
453
454 createGameCanvas();
455 createToggleButton();
456
457 console.log('Breakout game ready! Click the button to start playing.');
458 console.log('Controls: Arrow keys or A/D to move paddle, or use your mouse!');
459 }
460 }, 1000);
461 }
462
463 // Start initialization
464 if (document.readyState === 'loading') {
465 document.addEventListener('DOMContentLoaded', init);
466 } else {
467 init();
468 }
469})();