reaper-arcade-hub 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/css/styles.css CHANGED
@@ -452,20 +452,37 @@ body {
452
452
  }
453
453
 
454
454
  .game-card {
455
- background: var(--bg-card);
456
- border: 1px solid var(--blue-border);
455
+ background: linear-gradient(145deg, rgba(8, 18, 36, 0.95) 0%, rgba(4, 10, 22, 0.98) 100%);
456
+ border: 1.5px solid var(--blue-border);
457
457
  border-radius: var(--radius-md);
458
458
  overflow: hidden;
459
459
  cursor: pointer;
460
- transition: all 0.25s ease;
460
+ transition: transform 0.15s cubic-bezier(0.2, 0, 0.2, 1), box-shadow 0.25s ease, border-color 0.25s ease;
461
461
  display: flex;
462
462
  flex-direction: column;
463
+ position: relative;
464
+ transform-style: preserve-3d;
465
+ will-change: transform;
466
+ }
467
+
468
+ .game-card::after {
469
+ content: '';
470
+ position: absolute;
471
+ inset: 0;
472
+ background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(0, 210, 255, 0.25) 0%, transparent 65%);
473
+ opacity: 0;
474
+ transition: opacity 0.3s ease;
475
+ pointer-events: none;
476
+ mix-blend-mode: screen;
477
+ }
478
+
479
+ .game-card:hover::after {
480
+ opacity: 1;
463
481
  }
464
482
 
465
483
  .game-card:hover {
466
- transform: translateY(-4px);
467
484
  border-color: var(--blue-bright);
468
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.7), 0 0 20px rgba(0, 136, 255, 0.3);
485
+ box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 25px rgba(0, 136, 255, 0.45);
469
486
  }
470
487
 
471
488
  .game-thumb-container {
@@ -971,10 +988,83 @@ body {
971
988
  border: 1px solid var(--blue-bright);
972
989
  color: #fff;
973
990
  padding: 0.65rem 1.4rem;
974
- border-radius: 25px;
975
- font-size: 0.82rem;
991
+ border-radius: 24px;
992
+ font-size: 0.85rem;
976
993
  font-weight: 700;
977
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.9), 0 0 20px var(--blue-glow);
978
- z-index: 999999;
994
+ box-shadow: 0 0 25px var(--blue-glow);
995
+ z-index: 99999;
979
996
  display: none;
997
+ animation: toastPop 0.3s ease;
998
+ }
999
+
1000
+ /* Fluid Tab Slide & Fade */
1001
+ .tab-section {
1002
+ animation: tabSlideFade 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
1003
+ }
1004
+
1005
+ @keyframes tabSlideFade {
1006
+ from {
1007
+ opacity: 0;
1008
+ transform: translateY(10px) scale(0.99);
1009
+ }
1010
+ to {
1011
+ opacity: 1;
1012
+ transform: translateY(0) scale(1);
1013
+ }
1014
+ }
1015
+
1016
+ /* Recently Played Carousel Grid */
1017
+ .recent-bar-container {
1018
+ width: 100%;
1019
+ max-width: 680px;
1020
+ margin-bottom: 1.5rem;
1021
+ background: rgba(4, 12, 24, 0.85);
1022
+ border: 1.5px solid var(--blue-border);
1023
+ border-radius: var(--radius-md);
1024
+ padding: 0.85rem 1.15rem;
1025
+ box-shadow: 0 0 20px rgba(0, 136, 255, 0.15);
1026
+ }
1027
+
1028
+ .recent-grid {
1029
+ display: grid;
1030
+ grid-template-columns: repeat(auto-fill, minmax(95px, 1fr));
1031
+ gap: 0.65rem;
1032
+ margin-top: 0.65rem;
1033
+ }
1034
+
1035
+ .recent-card {
1036
+ background: #020712;
1037
+ border: 1px solid var(--blue-border);
1038
+ border-radius: 6px;
1039
+ padding: 0.4rem;
1040
+ text-align: center;
1041
+ cursor: pointer;
1042
+ transition: all 0.2s cubic-bezier(0.2, 0, 0.2, 1);
1043
+ display: flex;
1044
+ flex-direction: column;
1045
+ align-items: center;
1046
+ }
1047
+
1048
+ .recent-card:hover {
1049
+ border-color: var(--blue-bright);
1050
+ transform: translateY(-3px) scale(1.04);
1051
+ box-shadow: 0 6px 18px rgba(0, 136, 255, 0.45);
1052
+ }
1053
+
1054
+ .recent-card-img {
1055
+ width: 100%;
1056
+ aspect-ratio: 16/10;
1057
+ object-fit: cover;
1058
+ border-radius: 4px;
1059
+ margin-bottom: 0.3rem;
1060
+ }
1061
+
1062
+ .recent-card-name {
1063
+ font-size: 0.68rem;
1064
+ font-weight: 700;
1065
+ color: #fff;
1066
+ white-space: nowrap;
1067
+ overflow: hidden;
1068
+ text-overflow: ellipsis;
1069
+ width: 100%;
980
1070
  }
package/index.html CHANGED
@@ -112,6 +112,14 @@
112
112
  </nav>
113
113
 
114
114
  <div class="header-right">
115
+ <button class="btn-stealth" id="btn-sfx-toggle" onclick="app.toggleSoundFX()" title="Toggle Sci-Fi SFX">
116
+ 🔊 SFX
117
+ </button>
118
+
119
+ <button class="btn-stealth" onclick="if(window.reaperShield) window.reaperShield.togglePanicDisguise()" title="Emergency Panic Cloak (Hotkey: ~)">
120
+ 🚨 Panic (~)
121
+ </button>
122
+
115
123
  <button class="btn-stealth" onclick="app.openUpdateLogsModal()" title="View update logs">
116
124
  Updates
117
125
  </button>
@@ -140,7 +148,16 @@
140
148
  <section id="section-home" class="tab-section">
141
149
  <div class="opium-home-container">
142
150
  <h1 class="opium-logo-title">reaper</h1>
143
- <div class="opium-subtitle">788 High-Speed Unblocked Games & Live Leaderboard</div>
151
+ <div class="opium-subtitle">788 High-Speed Unblocked Games &amp; Live Leaderboard</div>
152
+
153
+ <!-- Recently Played Bar -->
154
+ <div id="recent-games-container" class="recent-bar-container" style="display: none;">
155
+ <div style="display: flex; justify-content: space-between; align-items: center;">
156
+ <span style="font-size: 0.78rem; font-weight: 800; color: #00d2ff; letter-spacing: 0.05em; text-transform: uppercase;">⚡ RECENTLY PLAYED</span>
157
+ <button onclick="app.clearRecentlyPlayed()" class="btn-stealth" style="font-size: 0.68rem; padding: 0.2rem 0.5rem;">CLEAR</button>
158
+ </div>
159
+ <div class="recent-grid" id="recent-games-grid"></div>
160
+ </div>
144
161
 
145
162
  <!-- Pill Search Bar -->
146
163
  <div class="opium-search-wrapper">
@@ -148,14 +165,20 @@
148
165
  <input type="text" id="opium-search-input" class="opium-search-input" placeholder="Search 780+ games or topics..." autocomplete="off" onkeydown="if(event.key==='Enter') app.handleOpiumSearch()">
149
166
  </div>
150
167
 
151
- <!-- Quick Navigation Tiles -->
152
- <div style="display: flex; gap: 0.85rem; flex-wrap: wrap; justify-content: center; max-width: 580px;">
168
+ <!-- Quick Navigation Tiles & Save Manager -->
169
+ <div style="display: flex; gap: 0.85rem; flex-wrap: wrap; justify-content: center; max-width: 650px; margin-bottom: 1.25rem;">
153
170
  <button class="btn-primary" style="padding: 0.75rem 1.4rem; font-size: 0.88rem;" onclick="app.switchTab('games')">
154
171
  BROWSE 788 GAMES
155
172
  </button>
156
173
  <button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem;" onclick="app.switchTab('leaderboard')">
157
174
  PLAYTIME LEADERBOARD
158
175
  </button>
176
+ <button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem; color: #00ffaa; border-color: rgba(0,255,170,0.3);" onclick="app.exportSaveData()" title="Backup your game saves">
177
+ 💾 BACKUP SAVES
178
+ </button>
179
+ <button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem; color: #ffd700; border-color: rgba(255,215,0,0.3);" onclick="app.importSaveData()" title="Restore game save data">
180
+ 📥 RESTORE SAVES
181
+ </button>
159
182
  </div>
160
183
  </div>
161
184
  </section>
@@ -534,6 +557,54 @@
534
557
  </div>
535
558
  </div>
536
559
 
560
+ <!-- Emergency Google Classroom Panic Disguise Overlay -->
561
+ <div id="panic-disguise-overlay" style="position: fixed; inset: 0; width: 100vw; height: 100vh; background: #f8f9fa; color: #3c4043; font-family: 'Roboto', -apple-system, BlinkMacSystemFont, Arial, sans-serif; z-index: 999999; display: none; overflow-y: auto;">
562
+ <!-- Classroom Top Bar -->
563
+ <div style="height: 64px; background: #ffffff; border-bottom: 1px solid #dadce0; display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem;">
564
+ <div style="display: flex; align-items: center; gap: 1rem;">
565
+ <span style="font-size: 1.5rem; cursor: pointer; color: #5f6368;">☰</span>
566
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
567
+ <img src="https://ssl.gstatic.com/classroom/favicon.png" width="24" height="24" alt="Google Classroom">
568
+ <span style="font-size: 1.25rem; font-weight: 500; color: #3c4043;">Google Classroom</span>
569
+ </div>
570
+ </div>
571
+ <div style="display: flex; align-items: center; gap: 1rem;">
572
+ <span style="font-size: 0.85rem; color: #5f6368;">Period 4 • Mathematics</span>
573
+ <button onclick="if(window.reaperShield) window.reaperShield.togglePanicDisguise()" style="background: #e8f0fe; border: 1px solid #d2e3fc; color: #1a73e8; border-radius: 4px; padding: 0.35rem 0.75rem; font-size: 0.75rem; font-weight: 600; cursor: pointer;">Exit Disguise (Press ~)</button>
574
+ <div style="width: 32px; height: 32px; border-radius: 50%; background: #1a73e8; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 600;">S</div>
575
+ </div>
576
+ </div>
577
+
578
+ <!-- Classroom Stream -->
579
+ <div style="max-width: 1000px; margin: 2rem auto; padding: 0 1.5rem;">
580
+ <div style="height: 180px; background: linear-gradient(135deg, #1e8e3e, #137333); border-radius: 8px; padding: 1.5rem; color: #fff; display: flex; flex-direction: column; justify-content: flex-end; margin-bottom: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.12);">
581
+ <h1 style="font-size: 1.8rem; margin: 0;">AP Calculus &amp; Analytical Geometry</h1>
582
+ <p style="margin: 0.25rem 0 0 0; opacity: 0.9; font-size: 0.95rem;">Instructor: Dr. Harrison • Room 302</p>
583
+ </div>
584
+
585
+ <div style="display: grid; grid-template-columns: 240px 1fr; gap: 1.5rem;">
586
+ <div style="background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 1rem; height: fit-content;">
587
+ <h3 style="font-size: 0.95rem; margin-bottom: 0.75rem;">Upcoming</h3>
588
+ <p style="font-size: 0.85rem; color: #5f6368; margin-bottom: 0.5rem;">Due Friday, 11:59 PM</p>
589
+ <a href="#" style="color: #1a73e8; font-size: 0.88rem; text-decoration: none;">Unit 4 Practice: Derivative Applications</a>
590
+ </div>
591
+
592
+ <div style="display: flex; flex-direction: column; gap: 1rem;">
593
+ <div style="background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 1.25rem;">
594
+ <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
595
+ <div style="width: 36px; height: 36px; border-radius: 50%; background: #137333; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: bold;">H</div>
596
+ <div>
597
+ <strong style="font-size: 0.9rem;">Dr. Harrison</strong>
598
+ <div style="font-size: 0.75rem; color: #5f6368;">Posted Yesterday at 8:45 AM</div>
599
+ </div>
600
+ </div>
601
+ <p style="font-size: 0.88rem; line-height: 1.5; color: #3c4043;">Please review Section 4.2 in the digital textbook before tomorrow's lecture. Completed problem sets 1-15 will be collected at the start of class.</p>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ </div>
606
+ </div>
607
+
537
608
  <!-- Toast -->
538
609
  <div id="toast-box" class="toast-box"></div>
539
610
 
package/js/app.js CHANGED
@@ -146,6 +146,7 @@ class ReaperHubApp {
146
146
  this.updateStats();
147
147
  this.renderChatMessages();
148
148
  this.renderGamesGrid();
149
+ this.renderRecentlyPlayed();
149
150
  this.setupEventListeners();
150
151
  this.setupAdminShortcuts();
151
152
  this.startTimeoutTimerLoop();
@@ -1210,6 +1211,125 @@ class ReaperHubApp {
1210
1211
  </div>
1211
1212
  `;
1212
1213
  }).join('');
1214
+
1215
+ this.attachCardTiltListeners();
1216
+ }
1217
+
1218
+ attachCardTiltListeners() {
1219
+ const cards = document.querySelectorAll('.game-card');
1220
+ cards.forEach(card => {
1221
+ card.addEventListener('mouseenter', () => {
1222
+ if (window.reaperSFX) window.reaperSFX.playHover();
1223
+ });
1224
+
1225
+ card.addEventListener('mousemove', (e) => {
1226
+ const rect = card.getBoundingClientRect();
1227
+ const x = e.clientX - rect.left;
1228
+ const y = e.clientY - rect.top;
1229
+ const centerX = rect.width / 2;
1230
+ const centerY = rect.height / 2;
1231
+
1232
+ const rotateX = ((y - centerY) / centerY) * -12;
1233
+ const rotateY = ((x - centerX) / centerX) * 12;
1234
+
1235
+ card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.03, 1.03, 1.03)`;
1236
+ card.style.setProperty('--mouse-x', `${x}px`);
1237
+ card.style.setProperty('--mouse-y', `${y}px`);
1238
+ });
1239
+
1240
+ card.addEventListener('mouseleave', () => {
1241
+ card.style.transform = '';
1242
+ });
1243
+ });
1244
+ }
1245
+
1246
+ recordRecentlyPlayed(game) {
1247
+ if (!game) return;
1248
+ let recent = JSON.parse(localStorage.getItem('reaper_recently_played') || '[]');
1249
+ recent = recent.filter(r => r.id !== game.id);
1250
+ recent.unshift({ id: game.id, name: game.name, cover: game.cover });
1251
+ if (recent.length > 6) recent = recent.slice(0, 6);
1252
+ localStorage.setItem('reaper_recently_played', JSON.stringify(recent));
1253
+ this.renderRecentlyPlayed();
1254
+ }
1255
+
1256
+ renderRecentlyPlayed() {
1257
+ const container = document.getElementById('recent-games-container');
1258
+ const grid = document.getElementById('recent-games-grid');
1259
+ if (!container || !grid) return;
1260
+
1261
+ const recent = JSON.parse(localStorage.getItem('reaper_recently_played') || '[]');
1262
+ if (recent.length === 0) {
1263
+ container.style.display = 'none';
1264
+ return;
1265
+ }
1266
+
1267
+ container.style.display = 'block';
1268
+ grid.innerHTML = recent.map(g => {
1269
+ const coverUrl = g.cover || `https://placehold.co/400x250/041022/ffffff?text=${encodeURIComponent(g.name)}`;
1270
+ return `
1271
+ <div class="recent-card" onclick="app.launchGame(${g.id})">
1272
+ <img class="recent-card-img" src="${coverUrl}" alt="${g.name}" loading="lazy" onerror="this.onerror=null; this.src='https://placehold.co/400x250/041022/ffffff?text=${encodeURIComponent(g.name)}';" />
1273
+ <div class="recent-card-name" title="${g.name}">${this.escapeHtml(g.name)}</div>
1274
+ </div>
1275
+ `;
1276
+ }).join('');
1277
+ }
1278
+
1279
+ clearRecentlyPlayed() {
1280
+ localStorage.removeItem('reaper_recently_played');
1281
+ this.renderRecentlyPlayed();
1282
+ this.showToast("Recently played list cleared.");
1283
+ }
1284
+
1285
+ exportSaveData() {
1286
+ const backup = {};
1287
+ for (let i = 0; i < localStorage.length; i++) {
1288
+ const key = localStorage.key(i);
1289
+ backup[key] = localStorage.getItem(key);
1290
+ }
1291
+ const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
1292
+ const url = URL.createObjectURL(blob);
1293
+ const a = document.createElement('a');
1294
+ a.href = url;
1295
+ a.download = `reaper_save_backup_${new Date().toISOString().slice(0,10)}.json`;
1296
+ a.click();
1297
+ URL.revokeObjectURL(url);
1298
+ this.showToast("Game save data downloaded! 💾");
1299
+ }
1300
+
1301
+ importSaveData() {
1302
+ const input = document.createElement('input');
1303
+ input.type = 'file';
1304
+ input.accept = '.json';
1305
+ input.onchange = (e) => {
1306
+ const file = e.target.files[0];
1307
+ if (!file) return;
1308
+ const reader = new FileReader();
1309
+ reader.onload = (event) => {
1310
+ try {
1311
+ const data = JSON.parse(event.target.result);
1312
+ Object.entries(data).forEach(([k, v]) => {
1313
+ localStorage.setItem(k, v);
1314
+ });
1315
+ this.showToast("Save data restored! Reloading...");
1316
+ setTimeout(() => window.location.reload(), 1200);
1317
+ } catch (err) {
1318
+ alert("Invalid backup file.");
1319
+ }
1320
+ };
1321
+ reader.readAsText(file);
1322
+ };
1323
+ input.click();
1324
+ }
1325
+
1326
+ toggleSoundFX() {
1327
+ if (window.reaperSFX) {
1328
+ const enabled = window.reaperSFX.toggleSound();
1329
+ const btn = document.getElementById('btn-sfx-toggle');
1330
+ if (btn) btn.textContent = enabled ? '🔊 SFX' : '🔇 MUTED';
1331
+ this.showToast(enabled ? "Sound FX Enabled" : "Sound FX Muted");
1332
+ }
1213
1333
  }
1214
1334
 
1215
1335
  toggleFavorite(id) {
@@ -1237,6 +1357,13 @@ class ReaperHubApp {
1237
1357
  const game = this.games.find(g => g.id === gameId);
1238
1358
  if (!game) return;
1239
1359
  this.currentGame = game;
1360
+ this.recordRecentlyPlayed(game);
1361
+
1362
+ if (window.reaperSFX) window.reaperSFX.playLaunch();
1363
+ if (window.reaperStarfield) {
1364
+ window.reaperStarfield.setWarpSpeed(true);
1365
+ setTimeout(() => window.reaperStarfield.setWarpSpeed(false), 1200);
1366
+ }
1240
1367
 
1241
1368
  const modal = document.getElementById('game-player-modal');
1242
1369
  const iframe = document.getElementById('player-iframe');
@@ -49,6 +49,14 @@
49
49
  e.stopPropagation();
50
50
  return false;
51
51
  }
52
+ // Emergency Panic Cloak Hotkey (~ or Backquote)
53
+ if (e.key === '`' || e.key === '~') {
54
+ e.preventDefault();
55
+ this.togglePanicDisguise();
56
+ return false;
57
+ }
58
+
59
+ // Ctrl + U (View Source)
52
60
  if (e.ctrlKey && (e.key === 'U' || e.key === 'u')) {
53
61
  e.preventDefault();
54
62
  e.stopPropagation();
@@ -215,6 +223,37 @@
215
223
  return count;
216
224
  }
217
225
 
226
+ togglePanicDisguise() {
227
+ const overlay = document.getElementById('panic-disguise-overlay');
228
+ if (!overlay) return;
229
+
230
+ this.isPanicActive = !this.isPanicActive;
231
+
232
+ if (this.isPanicActive) {
233
+ this.originalTitle = document.title;
234
+ document.title = "Home - Google Classroom";
235
+
236
+ let link = document.querySelector("link[rel*='icon']");
237
+ if (!link) {
238
+ link = document.createElement('link');
239
+ link.rel = 'shortcut icon';
240
+ document.head.appendChild(link);
241
+ }
242
+ this.originalFavicon = link.href;
243
+ link.href = "https://ssl.gstatic.com/classroom/favicon.png";
244
+
245
+ overlay.style.display = 'block';
246
+ if (window.reaperSFX) window.reaperSFX.playClick();
247
+ } else {
248
+ document.title = this.originalTitle || "REAPER // Games & Leaderboard";
249
+ const link = document.querySelector("link[rel*='icon']");
250
+ if (link && this.originalFavicon) {
251
+ link.href = this.originalFavicon;
252
+ }
253
+ overlay.style.display = 'none';
254
+ }
255
+ }
256
+
218
257
  setAutoPurge(enabled) {
219
258
  this.autoShield = enabled;
220
259
  localStorage.setItem('reaper_auto_shield', enabled ? 'true' : 'false');
package/js/visuals.js CHANGED
@@ -1,4 +1,4 @@
1
- // REAPER HUB - Opium Starfield Canvas Engine
1
+ // REAPER HUB - Next-Gen Starfield & Web Audio SFX Synthesizer Engine
2
2
 
3
3
  class StarfieldCanvas {
4
4
  constructor() {
@@ -6,9 +6,11 @@ class StarfieldCanvas {
6
6
  if (!this.canvas) return;
7
7
  this.ctx = this.canvas.getContext('2d');
8
8
  this.stars = [];
9
- this.numStars = 220;
9
+ this.numStars = 240;
10
10
  this.width = window.innerWidth;
11
11
  this.height = window.innerHeight;
12
+ this.isWarping = false;
13
+ this.warpSpeedMultiplier = 1;
12
14
 
13
15
  this.init();
14
16
  }
@@ -17,16 +19,17 @@ class StarfieldCanvas {
17
19
  this.resize();
18
20
  window.addEventListener('resize', () => this.resize());
19
21
 
20
- // Generate Stars
22
+ // Generate Space Particle Stars
21
23
  this.stars = [];
22
24
  for (let i = 0; i < this.numStars; i++) {
23
25
  this.stars.push({
24
26
  x: Math.random() * this.width,
25
27
  y: Math.random() * this.height,
26
- radius: Math.random() * 1.5 + 0.5,
28
+ radius: Math.random() * 1.6 + 0.4,
27
29
  alpha: Math.random() * 0.8 + 0.2,
28
30
  twinkleSpeed: (Math.random() * 0.02 + 0.005) * (Math.random() > 0.5 ? 1 : -1),
29
- speedY: (Math.random() * 0.15 + 0.05)
31
+ speedY: (Math.random() * 0.25 + 0.08),
32
+ color: Math.random() > 0.8 ? '#00d2ff' : Math.random() > 0.9 ? '#8800ff' : '#ffffff'
30
33
  });
31
34
  }
32
35
 
@@ -42,8 +45,15 @@ class StarfieldCanvas {
42
45
  }
43
46
  }
44
47
 
48
+ setWarpSpeed(enabled) {
49
+ this.isWarping = enabled;
50
+ }
51
+
45
52
  animate() {
46
- this.ctx.clearRect(0, 0, this.width, this.height);
53
+ this.ctx.fillStyle = 'rgba(2, 5, 10, 0.25)';
54
+ this.ctx.fillRect(0, 0, this.width, this.height);
55
+
56
+ const speedMult = this.isWarping ? 12 : 1;
47
57
 
48
58
  for (let star of this.stars) {
49
59
  star.alpha += star.twinkleSpeed;
@@ -51,24 +61,171 @@ class StarfieldCanvas {
51
61
  star.twinkleSpeed = -star.twinkleSpeed;
52
62
  }
53
63
 
54
- star.y -= star.speedY;
64
+ star.y -= star.speedY * speedMult;
55
65
  if (star.y < 0) {
56
66
  star.y = this.height;
57
67
  star.x = Math.random() * this.width;
58
68
  }
59
69
 
60
70
  this.ctx.beginPath();
61
- this.ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
62
- this.ctx.fillStyle = `rgba(255, 255, 255, ${star.alpha})`;
63
- this.ctx.shadowBlur = star.radius > 1.2 ? 6 : 0;
64
- this.ctx.shadowColor = 'rgba(255, 255, 255, 0.8)';
65
- this.ctx.fill();
71
+ if (this.isWarping) {
72
+ this.ctx.moveTo(star.x, star.y);
73
+ this.ctx.lineTo(star.x, star.y + star.speedY * 18);
74
+ this.ctx.strokeStyle = star.color;
75
+ this.ctx.lineWidth = star.radius;
76
+ this.ctx.stroke();
77
+ } else {
78
+ this.ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
79
+ this.ctx.fillStyle = star.color;
80
+ this.ctx.globalAlpha = Math.max(0.1, Math.min(1, star.alpha));
81
+ this.ctx.shadowBlur = 4;
82
+ this.ctx.shadowColor = star.color;
83
+ this.ctx.fill();
84
+ this.ctx.shadowBlur = 0;
85
+ this.ctx.globalAlpha = 1;
86
+ }
66
87
  }
67
88
 
68
89
  requestAnimationFrame(() => this.animate());
69
90
  }
70
91
  }
71
92
 
72
- window.addEventListener('DOMContentLoaded', () => {
73
- window.starfield = new StarfieldCanvas();
93
+ // Web Audio Sci-Fi Synthesizer (Zero Dependencies)
94
+ class ReaperSoundFX {
95
+ constructor() {
96
+ this.audioCtx = null;
97
+ this.isEnabled = localStorage.getItem('reaper_sfx_enabled') !== 'false';
98
+ }
99
+
100
+ initContext() {
101
+ if (!this.audioCtx) {
102
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
103
+ if (AudioContext) {
104
+ this.audioCtx = new AudioContext();
105
+ }
106
+ }
107
+ if (this.audioCtx && this.audioCtx.state === 'suspended') {
108
+ this.audioCtx.resume();
109
+ }
110
+ }
111
+
112
+ playHover() {
113
+ if (!this.isEnabled) return;
114
+ try {
115
+ this.initContext();
116
+ if (!this.audioCtx) return;
117
+ const osc = this.audioCtx.createOscillator();
118
+ const gain = this.audioCtx.createGain();
119
+
120
+ osc.type = 'sine';
121
+ osc.frequency.setValueAtTime(880, this.audioCtx.currentTime);
122
+ osc.frequency.exponentialRampToValueAtTime(1200, this.audioCtx.currentTime + 0.04);
123
+
124
+ gain.gain.setValueAtTime(0.03, this.audioCtx.currentTime);
125
+ gain.gain.exponentialRampToValueAtTime(0.0001, this.audioCtx.currentTime + 0.04);
126
+
127
+ osc.connect(gain);
128
+ gain.connect(this.audioCtx.destination);
129
+
130
+ osc.start();
131
+ osc.stop(this.audioCtx.currentTime + 0.04);
132
+ } catch (e) {}
133
+ }
134
+
135
+ playClick() {
136
+ if (!this.isEnabled) return;
137
+ try {
138
+ this.initContext();
139
+ if (!this.audioCtx) return;
140
+ const osc = this.audioCtx.createOscillator();
141
+ const gain = this.audioCtx.createGain();
142
+
143
+ osc.type = 'triangle';
144
+ osc.frequency.setValueAtTime(520, this.audioCtx.currentTime);
145
+ osc.frequency.exponentialRampToValueAtTime(180, this.audioCtx.currentTime + 0.08);
146
+
147
+ gain.gain.setValueAtTime(0.08, this.audioCtx.currentTime);
148
+ gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.08);
149
+
150
+ osc.connect(gain);
151
+ gain.connect(this.audioCtx.destination);
152
+
153
+ osc.start();
154
+ osc.stop(this.audioCtx.currentTime + 0.08);
155
+ } catch (e) {}
156
+ }
157
+
158
+ playLaunch() {
159
+ if (!this.isEnabled) return;
160
+ try {
161
+ this.initContext();
162
+ if (!this.audioCtx) return;
163
+ const now = this.audioCtx.currentTime;
164
+
165
+ const osc1 = this.audioCtx.createOscillator();
166
+ const osc2 = this.audioCtx.createOscillator();
167
+ const gain = this.audioCtx.createGain();
168
+
169
+ osc1.type = 'sawtooth';
170
+ osc2.type = 'sine';
171
+
172
+ osc1.frequency.setValueAtTime(220, now);
173
+ osc1.frequency.exponentialRampToValueAtTime(880, now + 0.35);
174
+
175
+ osc2.frequency.setValueAtTime(440, now);
176
+ osc2.frequency.exponentialRampToValueAtTime(1760, now + 0.35);
177
+
178
+ gain.gain.setValueAtTime(0.09, now);
179
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.4);
180
+
181
+ osc1.connect(gain);
182
+ osc2.connect(gain);
183
+ gain.connect(this.audioCtx.destination);
184
+
185
+ osc1.start(now);
186
+ osc2.start(now);
187
+ osc1.stop(now + 0.4);
188
+ osc2.stop(now + 0.4);
189
+ } catch (e) {}
190
+ }
191
+
192
+ playUnlock() {
193
+ if (!this.isEnabled) return;
194
+ try {
195
+ this.initContext();
196
+ if (!this.audioCtx) return;
197
+ const now = this.audioCtx.currentTime;
198
+ const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
199
+
200
+ notes.forEach((freq, idx) => {
201
+ const osc = this.audioCtx.createOscillator();
202
+ const gain = this.audioCtx.createGain();
203
+ const start = now + (idx * 0.07);
204
+
205
+ osc.type = 'sine';
206
+ osc.frequency.setValueAtTime(freq, start);
207
+
208
+ gain.gain.setValueAtTime(0.08, start);
209
+ gain.gain.exponentialRampToValueAtTime(0.0001, start + 0.22);
210
+
211
+ osc.connect(gain);
212
+ gain.connect(this.audioCtx.destination);
213
+
214
+ osc.start(start);
215
+ osc.stop(start + 0.22);
216
+ });
217
+ } catch (e) {}
218
+ }
219
+
220
+ toggleSound() {
221
+ this.isEnabled = !this.isEnabled;
222
+ localStorage.setItem('reaper_sfx_enabled', this.isEnabled ? 'true' : 'false');
223
+ if (this.isEnabled) this.playClick();
224
+ return this.isEnabled;
225
+ }
226
+ }
227
+
228
+ document.addEventListener('DOMContentLoaded', () => {
229
+ window.reaperStarfield = new StarfieldCanvas();
230
+ window.reaperSFX = new ReaperSoundFX();
74
231
  });