Lashtw commited on
Commit
1a5c262
·
verified ·
1 Parent(s): aee1339

Upload 8 files

Browse files
Files changed (1) hide show
  1. src/views/InstructorView.js +133 -7
src/views/InstructorView.js CHANGED
@@ -1,6 +1,12 @@
1
  import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
 
2
  import { generateMonsterSVG, getNextMonster } from "../utils/monsterUtils.js";
3
 
 
 
 
 
 
4
  let cachedChallenges = [];
5
  let currentStudents = [];
6
 
@@ -78,9 +84,21 @@ export async function renderInstructorView() {
78
  <p class="text-gray-400 mt-2 font-mono" id="photo-date">2026.01.27</p>
79
  </div>
80
 
81
- <div id="group-photo-container" class="w-full max-w-7xl flex flex-col items-center overflow-y-auto max-h-[80vh] custom-scrollbar">
 
 
 
 
 
 
 
82
  <!-- Dynamic Content -->
83
  </div>
 
 
 
 
 
84
  </div>
85
 
86
  <div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
@@ -180,6 +198,102 @@ export function setupInstructorEvents() {
180
  const displayRoomCode = document.getElementById('display-room-code');
181
  const navAdminBtn = document.getElementById('nav-admin-btn');
182
  const groupPhotoBtn = document.getElementById('group-photo-btn');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  // Group Photo Logic
185
  groupPhotoBtn.addEventListener('click', () => {
@@ -257,17 +371,29 @@ export function setupInstructorEvents() {
257
  let finalAngle = baseAngle + angleJitter;
258
 
259
  // Collision Avoidance for Bottom Label (approx 90 deg / PI/2)
260
- // If angle is roughly downwards (e.g., between 80deg and 100deg), push it aside
261
  const deg = finalAngle * (180 / Math.PI) % 360;
262
  const normalizedDeg = deg < 0 ? deg + 360 : deg;
263
 
264
- // Avoid "South" (approx 75 to 105 degrees) where the label sticks out
265
- if (normalizedDeg > 75 && normalizedDeg < 105) {
266
- finalAngle += (Math.random() > 0.5 ? 0.5 : -0.5); // Push left or right by ~30 deg
 
 
 
 
 
 
 
 
 
267
  }
268
 
269
- // Radius: Random within range
270
- const radius = minR + Math.random() * (maxR - minR);
 
 
 
271
 
272
  const xOff = Math.cos(finalAngle) * radius;
273
  const yOff = Math.sin(finalAngle) * radius * 0.8;
 
1
  import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
2
+ import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
3
  import { generateMonsterSVG, getNextMonster } from "../utils/monsterUtils.js";
4
 
5
+ // Load html2canvas dynamically
6
+ const script = document.createElement('script');
7
+ script.src = "https://html2canvas.hertzen.com/dist/html2canvas.min.js";
8
+ document.head.appendChild(script);
9
+
10
  let cachedChallenges = [];
11
  let currentStudents = [];
12
 
 
84
  <p class="text-gray-400 mt-2 font-mono" id="photo-date">2026.01.27</p>
85
  </div>
86
 
87
+ <div class="absolute top-6 left-6 z-50 flex space-x-4">
88
+ <button id="snapshot-btn" class="bg-white/10 hover:bg-white/20 text-white border border-white/30 font-bold py-2 px-6 rounded-full backdrop-blur-md transition-all flex items-center space-x-2 shadow-lg group">
89
+ <span class="text-2xl group-hover:scale-110 transition-transform">📸</span>
90
+ <span>拍照 (Snapshot)</span>
91
+ </button>
92
+ </div>
93
+
94
+ <div id="group-photo-container" class="w-full max-w-7xl flex flex-col items-center overflow-y-auto max-h-[80vh] custom-scrollbar relative">
95
  <!-- Dynamic Content -->
96
  </div>
97
+
98
+ <!-- Countdown Overlay -->
99
+ <div id="snapshot-overlay" class="absolute inset-0 z-[60] hidden flex-col items-center justify-center pointer-events-none">
100
+ <div id="countdown-number" class="text-[150px] font-black text-white drop-shadow-[0_0_50px_rgba(0,0,0,0.8)] animate-pulse">3</div>
101
+ </div>
102
  </div>
103
 
104
  <div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
 
198
  const displayRoomCode = document.getElementById('display-room-code');
199
  const navAdminBtn = document.getElementById('nav-admin-btn');
200
  const groupPhotoBtn = document.getElementById('group-photo-btn');
201
+ const snapshotBtn = document.getElementById('snapshot-btn');
202
+ let isSnapshotting = false;
203
+
204
+ // Snapshot Logic
205
+ snapshotBtn.addEventListener('click', async () => {
206
+ if (isSnapshotting || typeof html2canvas === 'undefined') return;
207
+ isSnapshotting = true;
208
+
209
+ const overlay = document.getElementById('snapshot-overlay');
210
+ const countEl = document.getElementById('countdown-number');
211
+ const container = document.getElementById('group-photo-container');
212
+ const modal = document.getElementById('group-photo-modal');
213
+
214
+ // Close button hide
215
+ const closeBtn = modal.querySelector('button');
216
+ if (closeBtn) closeBtn.style.opacity = '0';
217
+ snapshotBtn.style.opacity = '0';
218
+
219
+ overlay.classList.remove('hidden');
220
+ overlay.classList.add('flex');
221
+
222
+ // Countdown Sequence
223
+ const runCountdown = (num) => new Promise(resolve => {
224
+ countEl.textContent = num;
225
+ countEl.style.transform = 'scale(1.5)';
226
+ countEl.style.opacity = '1';
227
+
228
+ // Animation reset
229
+ requestAnimationFrame(() => {
230
+ countEl.style.transition = 'all 0.5s ease-out';
231
+ countEl.style.transform = 'scale(1)';
232
+ countEl.style.opacity = '0.5';
233
+ setTimeout(resolve, 1000);
234
+ });
235
+ });
236
+
237
+ await runCountdown(3);
238
+ await runCountdown(2);
239
+ await runCountdown(1);
240
+
241
+ // Action!
242
+ countEl.textContent = '';
243
+ overlay.classList.add('hidden');
244
+
245
+ // 1. Emojis Explosion
246
+ const emojis = ['🤘', '✌️', '👍', '🫶', '😎', '🔥'];
247
+ const cards = container.querySelectorAll('.group\\/card');
248
+
249
+ cards.forEach(card => {
250
+ // Random Emoji
251
+ const emoji = emojis[Math.floor(Math.random() * emojis.length)];
252
+ const emojiEl = document.createElement('div');
253
+ emojiEl.textContent = emoji;
254
+ emojiEl.className = 'absolute -top-12 left-1/2 transform -translate-x-1/2 text-4xl animate-bounce z-50 drop-shadow-lg';
255
+ emojiEl.style.animationDuration = '0.5s';
256
+ card.appendChild(emojiEl);
257
+
258
+ // Remove after 2s
259
+ setTimeout(() => emojiEl.remove(), 3000);
260
+ });
261
+
262
+ // 2. Capture
263
+ setTimeout(async () => {
264
+ try {
265
+ // Flash Effect
266
+ const flash = document.createElement('div');
267
+ flash.className = 'fixed inset-0 bg-white z-[100] transition-opacity duration-300 pointer-events-none';
268
+ document.body.appendChild(flash);
269
+ setTimeout(() => flash.style.opacity = '0', 50);
270
+ setTimeout(() => flash.remove(), 300);
271
+
272
+ const canvas = await html2canvas(container, {
273
+ backgroundColor: '#111827', // Match bg-gray-900
274
+ scale: 2, // High res
275
+ useCORS: true,
276
+ logging: false
277
+ });
278
+
279
+ // Download
280
+ const link = document.createElement('a');
281
+ const dateStr = new Date().toISOString().slice(0, 10);
282
+ link.download = `VIBE_Class_Photo_${dateStr}.png`;
283
+ link.href = canvas.toDataURL();
284
+ link.click();
285
+
286
+ } catch (e) {
287
+ console.error("Snapshot failed:", e);
288
+ alert("截圖失敗,請手動截圖 (Press PrtSc)");
289
+ } finally {
290
+ // Restore UI
291
+ if (closeBtn) closeBtn.style.opacity = '1';
292
+ snapshotBtn.style.opacity = '1';
293
+ isSnapshotting = false;
294
+ }
295
+ }, 600); // Slight delay for emojis to appear
296
+ });
297
 
298
  // Group Photo Logic
299
  groupPhotoBtn.addEventListener('click', () => {
 
371
  let finalAngle = baseAngle + angleJitter;
372
 
373
  // Collision Avoidance for Bottom Label (approx 90 deg / PI/2)
374
+ // Expanded exclusion zone for better label clearance (60 to 120 degrees)
375
  const deg = finalAngle * (180 / Math.PI) % 360;
376
  const normalizedDeg = deg < 0 ? deg + 360 : deg;
377
 
378
+ // Avoid "South" (approx 60 to 120 degrees) where the label sticks out
379
+ // Also push further away if near the bottom
380
+ if (normalizedDeg > 60 && normalizedDeg < 120) {
381
+ // Push angle strictly out of the zone
382
+ const distTo60 = Math.abs(normalizedDeg - 60);
383
+ const distTo120 = Math.abs(normalizedDeg - 120);
384
+
385
+ if (distTo60 < distTo120) {
386
+ finalAngle = (60 - 10) * (Math.PI / 180); // Move to 50 deg
387
+ } else {
388
+ finalAngle = (120 + 10) * (Math.PI / 180); // Move to 130 deg
389
+ }
390
  }
391
 
392
+ // Radius: Random within range (Push out further if still somewhat south)
393
+ let radius = minR + Math.random() * (maxR - minR);
394
+ if (normalizedDeg > 45 && normalizedDeg < 135) {
395
+ radius += 60; // Extra buffer for bottom elements
396
+ }
397
 
398
  const xOff = Math.cos(finalAngle) * radius;
399
  const yOff = Math.sin(finalAngle) * radius * 0.8;