Play Breakout on your calendar - events become bricks that temporarily hide for 10 minutes when hit
Size
11.6 KB
Version
1.0.1
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.0.1
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 = 4;
24 const HIDE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds
25
26 // Paddle object
27 class Paddle {
28 constructor(canvasWidth, canvasHeight) {
29 this.width = PADDLE_WIDTH;
30 this.height = PADDLE_HEIGHT;
31 this.x = canvasWidth / 2 - this.width / 2;
32 this.y = canvasHeight - 30;
33 this.speed = 8;
34 this.canvasWidth = canvasWidth;
35 }
36
37 draw(ctx) {
38 ctx.fillStyle = '#4285f4';
39 ctx.fillRect(this.x, this.y, this.width, this.height);
40 }
41
42 moveLeft() {
43 this.x = Math.max(0, this.x - this.speed);
44 }
45
46 moveRight() {
47 this.x = Math.min(this.canvasWidth - this.width, this.x + this.speed);
48 }
49 }
50
51 // Ball object
52 class Ball {
53 constructor(canvasWidth, canvasHeight) {
54 this.radius = BALL_RADIUS;
55 this.x = canvasWidth / 2;
56 this.y = canvasHeight - 50;
57 this.dx = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
58 this.dy = -BALL_SPEED;
59 this.canvasWidth = canvasWidth;
60 this.canvasHeight = canvasHeight;
61 }
62
63 draw(ctx) {
64 ctx.beginPath();
65 ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
66 ctx.fillStyle = '#ea4335';
67 ctx.fill();
68 ctx.closePath();
69 }
70
71 update(paddle) {
72 this.x += this.dx;
73 this.y += this.dy;
74
75 // Wall collision
76 if (this.x + this.radius > this.canvasWidth || this.x - this.radius < 0) {
77 this.dx = -this.dx;
78 }
79 if (this.y - this.radius < 0) {
80 this.dy = -this.dy;
81 }
82
83 // Paddle collision
84 if (this.y + this.radius > paddle.y &&
85 this.x > paddle.x &&
86 this.x < paddle.x + paddle.width) {
87 this.dy = -Math.abs(this.dy);
88 // Add some angle variation based on where ball hits paddle
89 const hitPos = (this.x - paddle.x) / paddle.width;
90 this.dx = BALL_SPEED * (hitPos - 0.5) * 2;
91 }
92
93 // Ball falls off bottom - reset
94 if (this.y - this.radius > this.canvasHeight) {
95 this.reset();
96 }
97 }
98
99 reset() {
100 this.x = this.canvasWidth / 2;
101 this.y = this.canvasHeight - 50;
102 this.dx = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
103 this.dy = -BALL_SPEED;
104 }
105 }
106
107 // Get all calendar event elements
108 function getCalendarEvents() {
109 return Array.from(document.querySelectorAll('div[data-eventchip][role="button"]'));
110 }
111
112 // Hide an event temporarily
113 function hideEvent(eventElement) {
114 if (!eventElement || hiddenEvents.has(eventElement)) return;
115
116 console.log('Hiding event temporarily');
117 eventElement.style.opacity = '0';
118 eventElement.style.pointerEvents = 'none';
119
120 const hideTime = Date.now();
121 hiddenEvents.set(eventElement, hideTime);
122
123 // Schedule reappearance after 10 minutes
124 setTimeout(() => {
125 showEvent(eventElement);
126 }, HIDE_DURATION);
127 }
128
129 // Show an event again
130 function showEvent(eventElement) {
131 if (!eventElement) return;
132
133 console.log('Showing event again');
134 eventElement.style.opacity = '';
135 eventElement.style.pointerEvents = '';
136 hiddenEvents.delete(eventElement);
137 }
138
139 // Check collision between ball and events
140 function checkEventCollisions() {
141 const events = getCalendarEvents();
142 const canvasRect = canvas.getBoundingClientRect();
143
144 events.forEach(event => {
145 if (hiddenEvents.has(event)) return; // Skip already hidden events
146
147 const eventRect = event.getBoundingClientRect();
148
149 // Convert event position to canvas coordinates
150 const eventX = eventRect.left - canvasRect.left;
151 const eventY = eventRect.top - canvasRect.top;
152 const eventWidth = eventRect.width;
153 const eventHeight = eventRect.height;
154
155 // Check if ball intersects with event
156 const closestX = Math.max(eventX, Math.min(ball.x, eventX + eventWidth));
157 const closestY = Math.max(eventY, Math.min(ball.y, eventY + eventHeight));
158
159 const distanceX = ball.x - closestX;
160 const distanceY = ball.y - closestY;
161 const distanceSquared = distanceX * distanceX + distanceY * distanceY;
162
163 if (distanceSquared < ball.radius * ball.radius) {
164 // Collision detected!
165 hideEvent(event);
166
167 // Bounce ball
168 if (Math.abs(distanceX) > Math.abs(distanceY)) {
169 ball.dx = -ball.dx;
170 } else {
171 ball.dy = -ball.dy;
172 }
173 }
174 });
175 }
176
177 // Game loop
178 function gameLoop() {
179 if (!gameActive) return;
180
181 // Clear canvas
182 ctx.clearRect(0, 0, canvas.width, canvas.height);
183
184 // Update and draw
185 ball.update(paddle);
186 checkEventCollisions();
187
188 paddle.draw(ctx);
189 ball.draw(ctx);
190
191 animationFrameId = requestAnimationFrame(gameLoop);
192 }
193
194 // Keyboard controls
195 const keys = {};
196 function handleKeyDown(e) {
197 if (!gameActive) return;
198 keys[e.key] = true;
199
200 if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
201 paddle.moveLeft();
202 }
203 if (keys['ArrowRight'] || keys['d'] || keys['D']) {
204 paddle.moveRight();
205 }
206 }
207
208 function handleKeyUp(e) {
209 keys[e.key] = false;
210 }
211
212 // Mouse controls
213 function handleMouseMove(e) {
214 if (!gameActive) return;
215 const rect = canvas.getBoundingClientRect();
216 const mouseX = e.clientX - rect.left;
217 paddle.x = Math.max(0, Math.min(canvas.width - paddle.width, mouseX - paddle.width / 2));
218 }
219
220 // Create game canvas overlay
221 function createGameCanvas() {
222 canvas = document.createElement('canvas');
223 canvas.id = 'breakout-game-canvas';
224 canvas.style.cssText = `
225 position: fixed;
226 top: 0;
227 left: 0;
228 width: 100%;
229 height: 100%;
230 z-index: 9998;
231 pointer-events: none;
232 `;
233 document.body.appendChild(canvas);
234
235 // Set canvas size
236 canvas.width = window.innerWidth;
237 canvas.height = window.innerHeight;
238 ctx = canvas.getContext('2d');
239
240 // Initialize game objects
241 paddle = new Paddle(canvas.width, canvas.height);
242 ball = new Ball(canvas.width, canvas.height);
243
244 // Handle window resize
245 window.addEventListener('resize', () => {
246 canvas.width = window.innerWidth;
247 canvas.height = window.innerHeight;
248 paddle.canvasWidth = canvas.width;
249 ball.canvasWidth = canvas.width;
250 ball.canvasHeight = canvas.height;
251 });
252
253 console.log('Game canvas created');
254 }
255
256 // Create toggle button
257 function createToggleButton() {
258 const button = document.createElement('button');
259 button.id = 'breakout-toggle-btn';
260 button.textContent = '🎮 Start Breakout';
261 button.style.cssText = `
262 position: fixed;
263 top: 20px;
264 right: 20px;
265 z-index: 9999;
266 padding: 12px 20px;
267 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
268 color: white;
269 border: none;
270 border-radius: 8px;
271 font-size: 16px;
272 font-weight: bold;
273 cursor: pointer;
274 box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
275 transition: all 0.3s ease;
276 font-family: 'Google Sans', Roboto, Arial, sans-serif;
277 `;
278
279 button.addEventListener('mouseenter', () => {
280 button.style.transform = 'translateY(-2px)';
281 button.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
282 });
283
284 button.addEventListener('mouseleave', () => {
285 button.style.transform = 'translateY(0)';
286 button.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
287 });
288
289 button.addEventListener('click', toggleGame);
290 document.body.appendChild(button);
291
292 console.log('Toggle button created');
293 }
294
295 // Toggle game on/off
296 function toggleGame() {
297 gameActive = !gameActive;
298 const button = document.getElementById('breakout-toggle-btn');
299
300 if (gameActive) {
301 button.textContent = '⏸️ Stop Breakout';
302 button.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)';
303 canvas.style.pointerEvents = 'auto';
304
305 // Add event listeners
306 document.addEventListener('keydown', handleKeyDown);
307 document.addEventListener('keyup', handleKeyUp);
308 canvas.addEventListener('mousemove', handleMouseMove);
309
310 // Reset ball position
311 ball.reset();
312
313 // Start game loop
314 gameLoop();
315
316 console.log('Game started');
317 } else {
318 button.textContent = '🎮 Start Breakout';
319 button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
320 canvas.style.pointerEvents = 'none';
321
322 // Remove event listeners
323 document.removeEventListener('keydown', handleKeyDown);
324 document.removeEventListener('keyup', handleKeyUp);
325 canvas.removeEventListener('mousemove', handleMouseMove);
326
327 // Stop game loop
328 if (animationFrameId) {
329 cancelAnimationFrame(animationFrameId);
330 }
331
332 // Clear canvas
333 ctx.clearRect(0, 0, canvas.width, canvas.height);
334
335 console.log('Game stopped');
336 }
337 }
338
339 // Restore any hidden events on page unload
340 window.addEventListener('beforeunload', () => {
341 hiddenEvents.forEach((time, event) => {
342 showEvent(event);
343 });
344 });
345
346 // Initialize game when calendar is ready
347 function init() {
348 // Wait for calendar to load
349 const checkCalendar = setInterval(() => {
350 const events = getCalendarEvents();
351 if (events.length > 0) {
352 clearInterval(checkCalendar);
353 console.log(`Found ${events.length} calendar events`);
354
355 createGameCanvas();
356 createToggleButton();
357
358 console.log('Breakout game ready! Click the button to start playing.');
359 console.log('Controls: Arrow keys or A/D to move paddle, or use your mouse!');
360 }
361 }, 1000);
362 }
363
364 // Start initialization
365 if (document.readyState === 'loading') {
366 document.addEventListener('DOMContentLoaded', init);
367 } else {
368 init();
369 }
370})();