@anglefeint/astro-theme 0.1.13 → 0.1.15

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.
@@ -0,0 +1,604 @@
1
+ export function initRedQueenTv(prefersReducedMotion) {
2
+ var shell = document.querySelector('.rq-tv');
3
+ var stage = document.querySelector('.rq-tv-stage');
4
+ var toggle = document.querySelector('.rq-tv-toggle');
5
+ if (!shell || !stage || !toggle) return;
6
+
7
+ var source = stage.getAttribute('data-rq-src') || '';
8
+ var source2 = stage.getAttribute('data-rq-src2') || '';
9
+ if (!source) return;
10
+
11
+ var OPEN_DELAY_MS = 500;
12
+ var COLLAPSE_DELAY_MS = 260;
13
+ var FRAME_MS = 33;
14
+ var FALLBACK_STEP_MS = 650;
15
+ var FALLBACK_GIF_SHOW_MS = 1400;
16
+ var WARMUP_STATIC_MS = 1000;
17
+ var PRELOAD_RETRY_MAX = 6;
18
+ var PRELOAD_TIMEOUT_MS = 12000;
19
+ var RETRY_BASE_MS = 220;
20
+ var HIDDEN_POLL_MS = 140;
21
+
22
+ var sequenceTimer = 0;
23
+ var renderTimer = 0;
24
+ var revealTimer = 0;
25
+ var preloadTimeoutTimer = 0;
26
+ var preloadRetryTimers = new Set();
27
+
28
+ var start = 0;
29
+ var width = 320;
30
+ var height = 240;
31
+ var canvas = null;
32
+ var ctx = null;
33
+ var currentFrame = null;
34
+ var playToken = 0;
35
+ var isPlaying = false;
36
+ var isLoading = false;
37
+ var pendingAutoPlay = false;
38
+ var playlistIndex = 0;
39
+ var imageCache = Object.create(null);
40
+ var mediaDataCache = Object.create(null);
41
+ var hasRevealed = false;
42
+ var forceStaticUntil = 0;
43
+
44
+ function guessImageType(url) {
45
+ var clean = (url || '').split('?')[0].toLowerCase();
46
+ if (clean.endsWith('.gif')) return 'image/gif';
47
+ if (clean.endsWith('.webp')) return 'image/webp';
48
+ if (clean.endsWith('.png')) return 'image/png';
49
+ if (clean.endsWith('.jpg') || clean.endsWith('.jpeg')) return 'image/jpeg';
50
+ return 'image/webp';
51
+ }
52
+
53
+ function resolveItemUrl(item) {
54
+ return new URL(item.url, window.location.href).href;
55
+ }
56
+
57
+ var playlist = [{ url: source, type: guessImageType(source), holdLast: 360 }];
58
+ if (source2) playlist.push({ url: source2, type: guessImageType(source2), holdLast: 500 });
59
+
60
+ if (prefersReducedMotion) {
61
+ setCollapsed(false);
62
+ var staticImg = new Image();
63
+ staticImg.className = 'rq-tv-screen';
64
+ staticImg.alt = '';
65
+ staticImg.decoding = 'async';
66
+ staticImg.loading = 'lazy';
67
+ staticImg.src = resolveItemUrl(playlist[0]);
68
+ stage.innerHTML = '';
69
+ stage.appendChild(staticImg);
70
+ toggle.hidden = true;
71
+ toggle.setAttribute('aria-hidden', 'true');
72
+ return;
73
+ }
74
+
75
+ function setCollapsed(collapsed) {
76
+ shell.classList.toggle('rq-tv-collapsed', collapsed);
77
+ toggle.setAttribute('aria-expanded', String(!collapsed));
78
+ }
79
+
80
+ function setLoading(loading) {
81
+ isLoading = loading;
82
+ shell.classList.toggle('rq-tv-loading', loading);
83
+ toggle.disabled = loading;
84
+ toggle.setAttribute('aria-busy', String(loading));
85
+ }
86
+
87
+ function clearSequenceTimer() {
88
+ if (!sequenceTimer) return;
89
+ clearTimeout(sequenceTimer);
90
+ sequenceTimer = 0;
91
+ }
92
+
93
+ function clearRenderTimer() {
94
+ if (!renderTimer) return;
95
+ clearTimeout(renderTimer);
96
+ renderTimer = 0;
97
+ }
98
+
99
+ function clearPreloadTimers() {
100
+ if (preloadTimeoutTimer) {
101
+ clearTimeout(preloadTimeoutTimer);
102
+ preloadTimeoutTimer = 0;
103
+ }
104
+ preloadRetryTimers.forEach(function(id) { clearTimeout(id); });
105
+ preloadRetryTimers.clear();
106
+ }
107
+
108
+ function scheduleSequence(fn, delay) {
109
+ clearSequenceTimer();
110
+ sequenceTimer = setTimeout(function runTask() {
111
+ if (document.hidden) {
112
+ sequenceTimer = setTimeout(runTask, HIDDEN_POLL_MS);
113
+ return;
114
+ }
115
+ fn();
116
+ }, delay);
117
+ }
118
+
119
+ function schedulePreloadRetry(fn, delay) {
120
+ var id = setTimeout(function() {
121
+ preloadRetryTimers.delete(id);
122
+ fn();
123
+ }, delay);
124
+ preloadRetryTimers.add(id);
125
+ }
126
+
127
+ function preloadGif(item, token, attempt, done) {
128
+ if (token !== playToken || !isPlaying) return;
129
+ var tryCount = typeof attempt === 'number' ? attempt : 0;
130
+ var cachedData = mediaDataCache[item.url];
131
+
132
+ if (cachedData instanceof ArrayBuffer) {
133
+ if (typeof ImageDecoder === 'undefined') {
134
+ done(true);
135
+ return;
136
+ }
137
+ var cachedDecoder = new ImageDecoder({ data: cachedData, type: item.type });
138
+ cachedDecoder.tracks.ready.then(async function() {
139
+ var result = await cachedDecoder.decode({ frameIndex: 0 });
140
+ if (result && result.image && result.image.close) result.image.close();
141
+ done(true);
142
+ }).catch(function() {
143
+ if (tryCount >= PRELOAD_RETRY_MAX) {
144
+ done(false);
145
+ return;
146
+ }
147
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
148
+ schedulePreloadRetry(function() {
149
+ preloadGif(item, token, tryCount + 1, done);
150
+ }, retryDelay);
151
+ });
152
+ return;
153
+ }
154
+
155
+ fetch(resolveItemUrl(item))
156
+ .then(function(response) { return response.arrayBuffer(); })
157
+ .then(function(buffer) {
158
+ mediaDataCache[item.url] = buffer;
159
+ if (typeof ImageDecoder === 'undefined') {
160
+ done(true);
161
+ return;
162
+ }
163
+ var decoder = new ImageDecoder({ data: buffer, type: item.type });
164
+ decoder.tracks.ready.then(async function() {
165
+ var result = await decoder.decode({ frameIndex: 0 });
166
+ if (result && result.image && result.image.close) result.image.close();
167
+ done(true);
168
+ }).catch(function() {
169
+ if (tryCount >= PRELOAD_RETRY_MAX) {
170
+ done(false);
171
+ return;
172
+ }
173
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
174
+ schedulePreloadRetry(function() {
175
+ preloadGif(item, token, tryCount + 1, done);
176
+ }, retryDelay);
177
+ });
178
+ }).catch(function() {
179
+ if (tryCount >= PRELOAD_RETRY_MAX) {
180
+ done(false);
181
+ return;
182
+ }
183
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
184
+ schedulePreloadRetry(function() {
185
+ preloadGif(item, token, tryCount + 1, done);
186
+ }, retryDelay);
187
+ });
188
+ }
189
+
190
+ function preloadImage(item, token, attempt, done) {
191
+ if (token !== playToken || !isPlaying) return;
192
+ var tryCount = typeof attempt === 'number' ? attempt : 0;
193
+ var cached = imageCache[item.url];
194
+ if (cached && cached.complete) {
195
+ done(true);
196
+ return;
197
+ }
198
+
199
+ var img = new Image();
200
+ img.onload = function() {
201
+ imageCache[item.url] = img;
202
+ done(true);
203
+ };
204
+ img.onerror = function() {
205
+ if (tryCount >= PRELOAD_RETRY_MAX) {
206
+ done(false);
207
+ return;
208
+ }
209
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
210
+ schedulePreloadRetry(function() {
211
+ preloadImage(item, token, tryCount + 1, done);
212
+ }, retryDelay);
213
+ };
214
+ img.src = resolveItemUrl(item);
215
+ }
216
+
217
+ function preloadAssets(token, done) {
218
+ clearPreloadTimers();
219
+ var left = playlist.length;
220
+ var failed = false;
221
+ function finish(ok) {
222
+ if (left <= -999) return;
223
+ left = -1000;
224
+ clearPreloadTimers();
225
+ done(ok);
226
+ }
227
+
228
+ preloadTimeoutTimer = setTimeout(function() {
229
+ finish(false);
230
+ }, PRELOAD_TIMEOUT_MS);
231
+
232
+ function markDone(ok) {
233
+ if (!ok) failed = true;
234
+ left -= 1;
235
+ if (left <= 0) finish(!failed);
236
+ }
237
+
238
+ playlist.forEach(function(item) {
239
+ if (item.type === 'image/gif') preloadGif(item, token, 0, markDone);
240
+ else preloadImage(item, token, 0, markDone);
241
+ });
242
+ }
243
+
244
+ function revealMonitor(token) {
245
+ if (hasRevealed) return;
246
+ hasRevealed = true;
247
+ if (revealTimer) clearTimeout(revealTimer);
248
+ revealTimer = setTimeout(function runReveal() {
249
+ if (token !== playToken || !isPlaying) return;
250
+ if (document.hidden) {
251
+ revealTimer = setTimeout(runReveal, HIDDEN_POLL_MS);
252
+ return;
253
+ }
254
+ forceStaticUntil = performance.now() + WARMUP_STATIC_MS;
255
+ setCollapsed(false);
256
+ startRenderLoop();
257
+ revealTimer = 0;
258
+ }, 0);
259
+ }
260
+
261
+ function createCanvas() {
262
+ if (canvas) return;
263
+ canvas = document.createElement('canvas');
264
+ canvas.className = 'rq-tv-screen';
265
+ stage.appendChild(canvas);
266
+ ctx = canvas.getContext('2d');
267
+ resizeCanvas();
268
+ }
269
+
270
+ function destroyCanvas() {
271
+ if (!canvas) return;
272
+ canvas.remove();
273
+ canvas = null;
274
+ ctx = null;
275
+ }
276
+
277
+ function resizeCanvas() {
278
+ if (!canvas) return;
279
+ var rect = stage.getBoundingClientRect();
280
+ var dpr = Math.min(window.devicePixelRatio || 1, 2);
281
+ var w = Math.max(2, Math.round(rect.width * dpr));
282
+ var h = Math.max(2, Math.round(rect.height * dpr));
283
+ canvas.width = w;
284
+ canvas.height = h;
285
+ canvas.style.width = '100%';
286
+ canvas.style.height = '100%';
287
+ width = w;
288
+ height = h;
289
+ }
290
+
291
+ function releaseCurrentFrame() {
292
+ if (currentFrame && currentFrame.close) currentFrame.close();
293
+ currentFrame = null;
294
+ }
295
+
296
+ function clearCanvas() {
297
+ if (!ctx) return;
298
+ ctx.clearRect(0, 0, width, height);
299
+ }
300
+
301
+ function stopRenderLoop() {
302
+ clearRenderTimer();
303
+ }
304
+
305
+ function teardownPlayback() {
306
+ isPlaying = false;
307
+ playToken++;
308
+ setLoading(false);
309
+ forceStaticUntil = 0;
310
+ clearSequenceTimer();
311
+ clearPreloadTimers();
312
+ if (revealTimer) {
313
+ clearTimeout(revealTimer);
314
+ revealTimer = 0;
315
+ }
316
+ stopRenderLoop();
317
+ releaseCurrentFrame();
318
+ clearCanvas();
319
+ destroyCanvas();
320
+ }
321
+
322
+ function drawFallback(elapsed) {
323
+ if (!ctx) return;
324
+ ctx.fillStyle = 'rgba(124, 186, 224, 0.15)';
325
+ ctx.beginPath();
326
+ ctx.ellipse(width * 0.5, height * 0.52, 36, 44, 0, 0, Math.PI * 2);
327
+ ctx.fill();
328
+ ctx.strokeStyle = 'rgba(176, 226, 250, 0.66)';
329
+ ctx.stroke();
330
+ ctx.fillStyle = 'rgba(182, 232, 255, 0.08)';
331
+ for (var i = 0; i < 6; i++) {
332
+ var y = (elapsed * 34 + i * 42) % height;
333
+ ctx.fillRect(0, y, width, 1);
334
+ }
335
+ }
336
+
337
+ function drawOverlay(elapsed) {
338
+ if (!ctx) return;
339
+ ctx.globalCompositeOperation = 'screen';
340
+ for (var i = 0; i < 7; i++) {
341
+ var y = (elapsed * 28 + i * 36) % height;
342
+ ctx.fillStyle = i % 2 ? 'rgba(188,238,255,0.05)' : 'rgba(188,238,255,0.08)';
343
+ ctx.fillRect(0, y, width, 1);
344
+ }
345
+ ctx.globalCompositeOperation = 'source-over';
346
+ ctx.globalCompositeOperation = 'screen';
347
+ ctx.fillStyle = 'rgba(154, 230, 255, 0.10)';
348
+ ctx.fillRect(0, 0, width, height);
349
+ ctx.globalCompositeOperation = 'source-over';
350
+ var sweepX = (Math.sin(elapsed * 0.9) * 0.5 + 0.5) * width;
351
+ var sweep = ctx.createLinearGradient(sweepX - 80, 0, sweepX + 80, 0);
352
+ sweep.addColorStop(0, 'rgba(176, 230, 255, 0)');
353
+ sweep.addColorStop(0.5, 'rgba(176, 230, 255, 0.12)');
354
+ sweep.addColorStop(1, 'rgba(176, 230, 255, 0)');
355
+ ctx.fillStyle = sweep;
356
+ ctx.fillRect(0, 0, width, height);
357
+ }
358
+
359
+ function drawFrame(frame) {
360
+ if (!ctx) return;
361
+ var item = playlist[playlistIndex] || null;
362
+ var isGif = item && item.type === 'image/gif';
363
+ var fw = frame.displayWidth || frame.width;
364
+ var fh = frame.displayHeight || frame.height;
365
+ var zoom = 1.32;
366
+ var scale = Math.max(width / fw, height / fh) * zoom;
367
+ var dw = fw * scale;
368
+ var dh = fh * scale;
369
+ var shiftX = -Math.min(74, width * 0.18);
370
+ var shiftY = isGif ? 5 : 0;
371
+ var dx = (width - dw) / 2 + shiftX;
372
+ var dy = (height - dh) / 2 + shiftY;
373
+ ctx.save();
374
+ ctx.translate(width, 0);
375
+ ctx.scale(-1, 1);
376
+ ctx.drawImage(frame, dx, dy, dw, dh);
377
+ ctx.restore();
378
+ }
379
+
380
+ function renderTick() {
381
+ if (!isPlaying || !ctx) {
382
+ renderTimer = 0;
383
+ return;
384
+ }
385
+ var now = performance.now();
386
+ var elapsed = (now - start) * 0.001;
387
+ var inStaticPhase = now < forceStaticUntil;
388
+ ctx.clearRect(0, 0, width, height);
389
+ if (!inStaticPhase && currentFrame) drawFrame(currentFrame);
390
+ else drawFallback(elapsed);
391
+ drawOverlay(elapsed);
392
+ renderTimer = setTimeout(renderTick, FRAME_MS);
393
+ }
394
+
395
+ function startRenderLoop() {
396
+ if (renderTimer) return;
397
+ start = performance.now();
398
+ renderTick();
399
+ }
400
+
401
+ function collapseAfterPlayback(token) {
402
+ if (token !== playToken) return;
403
+ scheduleSequence(function() {
404
+ if (token !== playToken) return;
405
+ isPlaying = false;
406
+ stopRenderLoop();
407
+ releaseCurrentFrame();
408
+ clearCanvas();
409
+ destroyCanvas();
410
+ setCollapsed(true);
411
+ }, COLLAPSE_DELAY_MS);
412
+ }
413
+
414
+ function fallbackPlay(index, token, attempt) {
415
+ if (token !== playToken || !isPlaying) return;
416
+ playlistIndex = index;
417
+ if (index >= playlist.length) {
418
+ collapseAfterPlayback(token);
419
+ return;
420
+ }
421
+
422
+ var item = playlist[index];
423
+ var tryCount = typeof attempt === 'number' ? attempt : 0;
424
+ var cached = item.type === 'image/gif' ? null : imageCache[item.url];
425
+
426
+ if (cached && cached.complete) {
427
+ releaseCurrentFrame();
428
+ currentFrame = cached;
429
+ if (index === 0) revealMonitor(token);
430
+ var cachedHold = item.holdLast || FALLBACK_STEP_MS;
431
+ if (index === 0 && forceStaticUntil > performance.now()) {
432
+ cachedHold += Math.ceil(forceStaticUntil - performance.now());
433
+ }
434
+ scheduleSequence(function() {
435
+ fallbackPlay(index + 1, token, 0);
436
+ }, cachedHold);
437
+ return;
438
+ }
439
+
440
+ var img = new Image();
441
+ img.onload = function() {
442
+ if (token !== playToken || !isPlaying) return;
443
+ if (item.type !== 'image/gif') imageCache[item.url] = img;
444
+ releaseCurrentFrame();
445
+ currentFrame = img;
446
+ if (index === 0) revealMonitor(token);
447
+ var holdMs = item.holdLast || FALLBACK_STEP_MS;
448
+ if (item.type === 'image/gif') holdMs = Math.max(holdMs, FALLBACK_GIF_SHOW_MS);
449
+ if (index === 0 && forceStaticUntil > performance.now()) {
450
+ holdMs += Math.ceil(forceStaticUntil - performance.now());
451
+ }
452
+ scheduleSequence(function() {
453
+ fallbackPlay(index + 1, token, 0);
454
+ }, holdMs);
455
+ };
456
+ img.onerror = function() {
457
+ var retryDelay = Math.min(1200, 180 * (tryCount + 1));
458
+ scheduleSequence(function() {
459
+ fallbackPlay(index, token, tryCount + 1);
460
+ }, retryDelay);
461
+ };
462
+ img.src = resolveItemUrl(item);
463
+ }
464
+
465
+ function decodeWithImageDecoder(index, token) {
466
+ if (token !== playToken || !isPlaying) return;
467
+ playlistIndex = index;
468
+ if (index >= playlist.length) {
469
+ collapseAfterPlayback(token);
470
+ return;
471
+ }
472
+
473
+ var item = playlist[index];
474
+
475
+ function decodeFromData(data) {
476
+ if (token !== playToken || !isPlaying) return;
477
+ var decoder = new ImageDecoder({ data: data, type: item.type });
478
+ var frameIndex = 0;
479
+
480
+ function decodeNext() {
481
+ if (token !== playToken || !isPlaying) return;
482
+ decoder.decode({ frameIndex: frameIndex }).then(function(result) {
483
+ if (token !== playToken || !isPlaying) return;
484
+ releaseCurrentFrame();
485
+ currentFrame = result.image;
486
+ if (index === 0) revealMonitor(token);
487
+ var baseDelay = result.image.duration ? result.image.duration / 1000 : 33;
488
+ var delay = Math.max(16, baseDelay);
489
+ if (index === 0 && forceStaticUntil > performance.now()) {
490
+ delay += Math.ceil(forceStaticUntil - performance.now());
491
+ }
492
+ frameIndex++;
493
+ if (frameIndex >= decoder.tracks.selectedTrack.frameCount) {
494
+ var holdDelay = item.holdLast || 0;
495
+ scheduleSequence(function() {
496
+ decodeWithImageDecoder(index + 1, token);
497
+ }, holdDelay);
498
+ return;
499
+ }
500
+ scheduleSequence(decodeNext, delay);
501
+ }).catch(function() {
502
+ frameIndex = 0;
503
+ scheduleSequence(decodeNext, 100);
504
+ });
505
+ }
506
+
507
+ decoder.tracks.ready.then(function() {
508
+ if (token !== playToken || !isPlaying) return;
509
+ decodeNext();
510
+ }).catch(function() {
511
+ if (token !== playToken || !isPlaying) return;
512
+ fallbackPlay(index, token, 0);
513
+ });
514
+ }
515
+
516
+ var cachedData = mediaDataCache[item.url];
517
+ if (cachedData instanceof ArrayBuffer) {
518
+ decodeFromData(cachedData);
519
+ return;
520
+ }
521
+
522
+ fetch(resolveItemUrl(item))
523
+ .then(function(response) { return response.arrayBuffer(); })
524
+ .then(function(buffer) {
525
+ mediaDataCache[item.url] = buffer;
526
+ decodeFromData(buffer);
527
+ }).catch(function() {
528
+ if (token !== playToken || !isPlaying) return;
529
+ fallbackPlay(index, token, 0);
530
+ });
531
+ }
532
+
533
+ function startPlayback() {
534
+ if (isLoading) return;
535
+ teardownPlayback();
536
+ setLoading(true);
537
+ createCanvas();
538
+ if (!ctx) {
539
+ setLoading(false);
540
+ setCollapsed(true);
541
+ return;
542
+ }
543
+
544
+ isPlaying = true;
545
+ hasRevealed = false;
546
+ setCollapsed(true);
547
+ var token = playToken;
548
+
549
+ preloadAssets(token, function(ok) {
550
+ if (token !== playToken || !isPlaying) return;
551
+ setLoading(false);
552
+ if (!ok) {
553
+ isPlaying = false;
554
+ destroyCanvas();
555
+ setCollapsed(true);
556
+ return;
557
+ }
558
+ if (typeof ImageDecoder !== 'undefined') decodeWithImageDecoder(0, token);
559
+ else fallbackPlay(0, token, 0);
560
+ });
561
+ }
562
+
563
+ function queueAutoPlay() {
564
+ function run() {
565
+ clearSequenceTimer();
566
+ sequenceTimer = setTimeout(function() {
567
+ if (document.hidden) {
568
+ pendingAutoPlay = true;
569
+ return;
570
+ }
571
+ startPlayback();
572
+ }, OPEN_DELAY_MS);
573
+ }
574
+
575
+ if (document.readyState === 'complete') run();
576
+ else window.addEventListener('load', run, { once: true });
577
+ }
578
+
579
+ toggle.addEventListener('click', function() {
580
+ startPlayback();
581
+ });
582
+
583
+ function onTvVisibilityChange() {
584
+ if (document.hidden) {
585
+ stopRenderLoop();
586
+ } else {
587
+ if (pendingAutoPlay && !isPlaying) {
588
+ pendingAutoPlay = false;
589
+ startPlayback();
590
+ return;
591
+ }
592
+ if (isPlaying && !renderTimer) startRenderLoop();
593
+ }
594
+ }
595
+
596
+ window.addEventListener('resize', function() {
597
+ if (canvas) resizeCanvas();
598
+ }, { passive: true });
599
+ document.addEventListener('visibilitychange', onTvVisibilityChange);
600
+ window.addEventListener('beforeunload', function() { teardownPlayback(); }, { once: true });
601
+
602
+ setCollapsed(true);
603
+ queueAutoPlay();
604
+ }