@anglefeint/astro-theme 0.1.0-alpha.0

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.
Files changed (33) hide show
  1. package/README.md +50 -0
  2. package/package.json +38 -0
  3. package/public/scripts/about-effects.js +535 -0
  4. package/public/scripts/blogpost-effects.js +956 -0
  5. package/public/scripts/home-matrix.js +117 -0
  6. package/public/styles/about-page.css +756 -0
  7. package/public/styles/blog-post.css +390 -0
  8. package/public/styles/home-page.css +186 -0
  9. package/src/assets/theme/placeholders/theme-placeholder-1.jpg +0 -0
  10. package/src/assets/theme/placeholders/theme-placeholder-2.jpg +0 -0
  11. package/src/assets/theme/placeholders/theme-placeholder-3.jpg +0 -0
  12. package/src/assets/theme/placeholders/theme-placeholder-4.jpg +0 -0
  13. package/src/assets/theme/placeholders/theme-placeholder-5.jpg +0 -0
  14. package/src/assets/theme/placeholders/theme-placeholder-about.jpg +0 -0
  15. package/src/assets/theme/red-queen/theme-redqueen1.webp +0 -0
  16. package/src/assets/theme/red-queen/theme-redqueen2.gif +0 -0
  17. package/src/cli-new-page.mjs +118 -0
  18. package/src/cli-new-post.mjs +139 -0
  19. package/src/components/BaseHead.astro +177 -0
  20. package/src/components/Footer.astro +74 -0
  21. package/src/components/FormattedDate.astro +17 -0
  22. package/src/components/Header.astro +328 -0
  23. package/src/components/HeaderLink.astro +35 -0
  24. package/src/consts.ts +12 -0
  25. package/src/content-schema.ts +33 -0
  26. package/src/i18n/config.ts +46 -0
  27. package/src/i18n/messages.ts +220 -0
  28. package/src/i18n/posts.ts +11 -0
  29. package/src/index.ts +5 -0
  30. package/src/layouts/BasePageLayout.astro +67 -0
  31. package/src/layouts/BlogPost.astro +279 -0
  32. package/src/layouts/HomePage.astro +73 -0
  33. package/src/styles/global.css +1867 -0
@@ -0,0 +1,956 @@
1
+ (function() {
2
+ function init() {
3
+ // 阅读进度条
4
+ var progress = document.querySelector('.mesh-read-progress');
5
+ var article = document.querySelector('.mesh-article');
6
+ var toast = document.querySelector('.mesh-stage-toast');
7
+ var stageSeen = { p30: false, p60: false, p90: false };
8
+ var toastTimer = 0;
9
+ var hasScrolled = false;
10
+ function showStageToast(msg) {
11
+ if (!toast) return;
12
+ toast.textContent = msg;
13
+ toast.classList.add('visible');
14
+ clearTimeout(toastTimer);
15
+ toastTimer = setTimeout(function() {
16
+ toast.classList.remove('visible');
17
+ }, 900);
18
+ }
19
+ if (progress) {
20
+ function onScroll() {
21
+ var scrollTop = window.scrollY || document.documentElement.scrollTop;
22
+ var scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
23
+ var p = scrollHeight > 0 ? Math.min(1, scrollTop / scrollHeight) : 1;
24
+ progress.style.setProperty('--read-progress', String(p));
25
+ var btn = document.querySelector('.mesh-back-to-top');
26
+ if (btn) btn.classList.toggle('visible', scrollTop > 400);
27
+ if (!hasScrolled && scrollTop > 6) hasScrolled = true;
28
+ if (!hasScrolled) return;
29
+ if (!stageSeen.p30 && p >= 0.3) {
30
+ stageSeen.p30 = true;
31
+ showStageToast('context parsed');
32
+ }
33
+ if (!stageSeen.p60 && p >= 0.6) {
34
+ stageSeen.p60 = true;
35
+ showStageToast('inference stable');
36
+ }
37
+ if (!stageSeen.p90 && p >= 0.9) {
38
+ stageSeen.p90 = true;
39
+ showStageToast('output finalized');
40
+ }
41
+ }
42
+ onScroll();
43
+ window.addEventListener('scroll', onScroll, { passive: true });
44
+ }
45
+ var backTop = document.querySelector('.mesh-back-to-top');
46
+ if (backTop) {
47
+ backTop.addEventListener('click', function() {
48
+ window.scrollTo({ top: 0, behavior: 'smooth' });
49
+ });
50
+ }
51
+ function initHeroCanvas() {
52
+ var shell = document.querySelector('.hero-shell');
53
+ if (!shell) return;
54
+ var canvas = shell.querySelector('.hero-canvas');
55
+ var wrap = shell.querySelector('.hero-canvas-wrap');
56
+ if (!canvas || !wrap) return;
57
+ var src = canvas.getAttribute('data-hero-src');
58
+ if (!src) return;
59
+
60
+ var heroStart = 0;
61
+ var heroRaf = 0;
62
+ // offscreen canvas for base image
63
+ var baseCanvas = document.createElement('canvas');
64
+ var baseCtx = baseCanvas.getContext('2d');
65
+ // offscreen canvas reused for pixelation
66
+ var pixelCanvas = document.createElement('canvas');
67
+ var pixelCtx = pixelCanvas.getContext('2d');
68
+ // offscreen canvas reused for static bursts
69
+ var noiseCanvas = document.createElement('canvas');
70
+ var noiseCtx = noiseCanvas.getContext('2d');
71
+ // offscreen canvas for edge detection
72
+ var edgeCanvas = document.createElement('canvas');
73
+ var edgeCtx = edgeCanvas.getContext('2d');
74
+ var edgeReady = false;
75
+
76
+ // Phase timing (seconds)
77
+ var EDGE_PHASE = 1.8; // show edge detection
78
+ var REVEAL_PHASE = 2.5; // progressive reveal (pixelated -> sharp)
79
+ var INTRO_END = EDGE_PHASE + REVEAL_PHASE; // after this, ongoing glitch
80
+
81
+ function sizeCanvas() {
82
+ var rect = shell.querySelector('.hero-stack').getBoundingClientRect();
83
+ var dpr = Math.min(window.devicePixelRatio || 1, 2);
84
+ canvas.width = Math.max(2, Math.round(rect.width * dpr));
85
+ canvas.height = Math.max(2, Math.round(rect.height * dpr));
86
+ canvas.style.width = rect.width + 'px';
87
+ canvas.style.height = rect.height + 'px';
88
+ noiseCanvas.width = canvas.width;
89
+ noiseCanvas.height = 64;
90
+ }
91
+
92
+ function drawBase(ctx, img, w, h) {
93
+ var iw = img.width, ih = img.height;
94
+ var scale = Math.max(w / iw, h / ih);
95
+ var sw = w / scale, sh = h / scale;
96
+ var sx = (iw - sw) / 2, sy = (ih - sh) / 2;
97
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, w, h);
98
+ }
99
+
100
+ function buildEdge(img) {
101
+ var w = canvas.width, h = canvas.height;
102
+ edgeCanvas.width = w;
103
+ edgeCanvas.height = h;
104
+ baseCanvas.width = w;
105
+ baseCanvas.height = h;
106
+ drawBase(baseCtx, img, w, h);
107
+ drawBase(edgeCtx, img, w, h);
108
+ // Sobel edge detection
109
+ var src = edgeCtx.getImageData(0, 0, w, h);
110
+ var d = src.data;
111
+ var out = edgeCtx.createImageData(w, h);
112
+ var od = out.data;
113
+ for (var y = 1; y < h - 1; y++) {
114
+ for (var x = 1; x < w - 1; x++) {
115
+ var idx = function(px, py) { return ((py * w) + px) * 4; };
116
+ var i = idx(x, y);
117
+ // grayscale neighbors
118
+ function luma(px, py) {
119
+ var j = idx(px, py);
120
+ return d[j] * 0.299 + d[j+1] * 0.587 + d[j+2] * 0.114;
121
+ }
122
+ var gx = -luma(x-1,y-1) - 2*luma(x-1,y) - luma(x-1,y+1)
123
+ + luma(x+1,y-1) + 2*luma(x+1,y) + luma(x+1,y+1);
124
+ var gy = -luma(x-1,y-1) - 2*luma(x,y-1) - luma(x+1,y-1)
125
+ + luma(x-1,y+1) + 2*luma(x,y+1) + luma(x+1,y+1);
126
+ var mag = Math.min(255, Math.sqrt(gx * gx + gy * gy));
127
+ // cyan-tinted edges
128
+ od[i] = Math.min(255, mag * 0.4);
129
+ od[i+1] = Math.min(255, mag * 0.85);
130
+ od[i+2] = Math.min(255, mag * 1.0);
131
+ od[i+3] = mag > 20 ? Math.min(255, mag * 1.5) : 0;
132
+ }
133
+ }
134
+ edgeCtx.putImageData(out, 0, 0);
135
+ edgeReady = true;
136
+ }
137
+
138
+ function heroRender(t) {
139
+ if (!heroStart) heroStart = t;
140
+ var elapsed = (t - heroStart) * 0.001;
141
+ var ctx = canvas.getContext('2d');
142
+ if (!ctx || !canvas.img) { heroRaf = requestAnimationFrame(heroRender); return; }
143
+ var w = canvas.width, h = canvas.height;
144
+
145
+ ctx.clearRect(0, 0, w, h);
146
+
147
+ if (elapsed < EDGE_PHASE && edgeReady) {
148
+ // Phase 1: Edge detection wireframe
149
+ var edgeFade = Math.min(1, elapsed / 0.5);
150
+ // dark background
151
+ ctx.fillStyle = 'rgba(8, 16, 28, 1)';
152
+ ctx.fillRect(0, 0, w, h);
153
+ // draw edges with fade-in
154
+ ctx.globalAlpha = edgeFade;
155
+ ctx.drawImage(edgeCanvas, 0, 0);
156
+ ctx.globalAlpha = 1;
157
+ // scanning line
158
+ var scanY = (elapsed / EDGE_PHASE) * h;
159
+ ctx.fillStyle = 'rgba(120, 220, 255, 0.3)';
160
+ ctx.fillRect(0, scanY - 1, w, 2);
161
+ var scanGlow = ctx.createLinearGradient(0, scanY - 30, 0, scanY + 30);
162
+ scanGlow.addColorStop(0, 'rgba(120, 220, 255, 0)');
163
+ scanGlow.addColorStop(0.5, 'rgba(120, 220, 255, 0.15)');
164
+ scanGlow.addColorStop(1, 'rgba(120, 220, 255, 0)');
165
+ ctx.fillStyle = scanGlow;
166
+ ctx.fillRect(0, scanY - 30, w, 60);
167
+
168
+ } else if (elapsed < INTRO_END) {
169
+ // Phase 2: Progressive reveal — pixelated to sharp
170
+ var revealT = (elapsed - EDGE_PHASE) / REVEAL_PHASE;
171
+ // pixel size: starts large, shrinks to 1
172
+ var maxBlock = 32;
173
+ var blockSize = Math.max(1, Math.round(maxBlock * (1 - revealT * revealT)));
174
+ // draw pixelated
175
+ if (blockSize > 1) {
176
+ var smallW = Math.max(1, Math.ceil(w / blockSize));
177
+ var smallH = Math.max(1, Math.ceil(h / blockSize));
178
+ if (pixelCanvas.width !== smallW || pixelCanvas.height !== smallH) {
179
+ pixelCanvas.width = smallW;
180
+ pixelCanvas.height = smallH;
181
+ }
182
+ pixelCtx.clearRect(0, 0, smallW, smallH);
183
+ pixelCtx.drawImage(baseCanvas, 0, 0, smallW, smallH);
184
+ ctx.imageSmoothingEnabled = false;
185
+ ctx.drawImage(pixelCanvas, 0, 0, smallW, smallH, 0, 0, w, h);
186
+ ctx.imageSmoothingEnabled = true;
187
+ } else {
188
+ ctx.drawImage(baseCanvas, 0, 0);
189
+ }
190
+ // fade out cyan tint
191
+ var tintAlpha = 0.18 * (1 - revealT);
192
+ ctx.globalCompositeOperation = 'screen';
193
+ ctx.fillStyle = 'rgba(100, 200, 255, ' + tintAlpha + ')';
194
+ ctx.fillRect(0, 0, w, h);
195
+ ctx.globalCompositeOperation = 'source-over';
196
+
197
+ } else {
198
+ // Phase 3: Full image with ongoing scan + glitch
199
+ ctx.drawImage(baseCanvas, 0, 0);
200
+
201
+ // Scanlines
202
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.03)';
203
+ for (var i = 0; i < h; i += 3) {
204
+ ctx.fillRect(0, i, w, 1);
205
+ }
206
+
207
+ // Moving scan bar
208
+ var scanPos = ((elapsed * 40) % (h + 60)) - 30;
209
+ var barGrad = ctx.createLinearGradient(0, scanPos - 30, 0, scanPos + 30);
210
+ barGrad.addColorStop(0, 'rgba(120, 220, 255, 0)');
211
+ barGrad.addColorStop(0.5, 'rgba(120, 220, 255, 0.06)');
212
+ barGrad.addColorStop(1, 'rgba(120, 220, 255, 0)');
213
+ ctx.fillStyle = barGrad;
214
+ ctx.fillRect(0, scanPos - 30, w, 60);
215
+
216
+ // RGB channel glitch (random, ~8% of frames)
217
+ if (Math.random() < 0.08) {
218
+ var glitchY = Math.random() * h;
219
+ var glitchH = 2 + Math.random() * 12;
220
+ var shiftX = (Math.random() - 0.5) * 12;
221
+ ctx.save();
222
+ ctx.globalAlpha = 0.22;
223
+ ctx.drawImage(
224
+ baseCanvas,
225
+ 0,
226
+ Math.floor(glitchY),
227
+ w,
228
+ Math.ceil(glitchH),
229
+ Math.round(shiftX),
230
+ Math.floor(glitchY),
231
+ w,
232
+ Math.ceil(glitchH)
233
+ );
234
+ ctx.restore();
235
+ }
236
+
237
+ // CRT horizontal retrace/dropout: occasional black line, starts 6s after load
238
+ if (elapsed >= 6 && Math.random() < 0.025) {
239
+ var dropoutY = Math.floor(Math.random() * h);
240
+ var dropoutH = 2 + Math.floor(Math.random() * 2);
241
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
242
+ ctx.fillRect(0, dropoutY, w, dropoutH);
243
+ }
244
+
245
+ // Brief static burst (~3% of frames)
246
+ if (Math.random() < 0.03) {
247
+ var burstY = Math.random() * h * 0.8;
248
+ var burstH = 4 + Math.random() * 20;
249
+ if (noiseCanvas.width !== w) {
250
+ noiseCanvas.width = w;
251
+ noiseCanvas.height = 64;
252
+ }
253
+ noiseCtx.clearRect(0, 0, noiseCanvas.width, noiseCanvas.height);
254
+ for (var n = 0; n < 180; n++) {
255
+ var nx = Math.random() * noiseCanvas.width;
256
+ var ny = Math.random() * noiseCanvas.height;
257
+ var nw = 1 + Math.random() * 3;
258
+ var nh = 1 + Math.random() * 2;
259
+ var alpha = 0.08 + Math.random() * 0.18;
260
+ noiseCtx.fillStyle = 'rgba(160,220,255,' + alpha.toFixed(3) + ')';
261
+ noiseCtx.fillRect(nx, ny, nw, nh);
262
+ }
263
+ ctx.drawImage(
264
+ noiseCanvas,
265
+ 0,
266
+ 0,
267
+ w,
268
+ Math.ceil(Math.min(burstH, noiseCanvas.height)),
269
+ 0,
270
+ Math.floor(burstY),
271
+ w,
272
+ Math.ceil(burstH)
273
+ );
274
+ }
275
+ }
276
+
277
+ heroRaf = requestAnimationFrame(heroRender);
278
+ }
279
+
280
+ var img = new Image();
281
+ img.onload = function() {
282
+ canvas.img = img;
283
+ sizeCanvas();
284
+ buildEdge(img);
285
+ wrap.classList.add('ready');
286
+ heroRaf = requestAnimationFrame(heroRender);
287
+ };
288
+ img.src = new URL(src, window.location.href).href;
289
+
290
+ window.addEventListener('resize', function() {
291
+ if (canvas.img) {
292
+ sizeCanvas();
293
+ buildEdge(canvas.img);
294
+ }
295
+ }, { passive: true });
296
+ function onHeroVisibilityChange() {
297
+ if (document.hidden) {
298
+ if (heroRaf) cancelAnimationFrame(heroRaf);
299
+ heroRaf = 0;
300
+ } else if (canvas.img && !heroRaf) {
301
+ heroRaf = requestAnimationFrame(heroRender);
302
+ }
303
+ }
304
+ document.addEventListener('visibilitychange', onHeroVisibilityChange);
305
+ window.addEventListener('beforeunload', function() { cancelAnimationFrame(heroRaf); }, { once: true });
306
+ }
307
+ initHeroCanvas();
308
+ function initRedQueenTv() {
309
+ var shell = document.querySelector('.rq-tv');
310
+ var stage = document.querySelector('.rq-tv-stage');
311
+ var toggle = document.querySelector('.rq-tv-toggle');
312
+ if (!shell || !stage || !toggle) return;
313
+
314
+ var source = stage.getAttribute('data-rq-src') || '';
315
+ var source2 = stage.getAttribute('data-rq-src2') || '';
316
+ if (!source) return;
317
+
318
+ var OPEN_DELAY_MS = 500;
319
+ var COLLAPSE_DELAY_MS = 260;
320
+ var FRAME_MS = 33;
321
+ var FALLBACK_STEP_MS = 650;
322
+ var FALLBACK_GIF_SHOW_MS = 1400;
323
+ var WARMUP_STATIC_MS = 1000;
324
+ var PRELOAD_RETRY_MAX = 6;
325
+ var PRELOAD_TIMEOUT_MS = 12000;
326
+ var RETRY_BASE_MS = 220;
327
+ var HIDDEN_POLL_MS = 140;
328
+
329
+ var sequenceTimer = 0;
330
+ var renderTimer = 0;
331
+ var revealTimer = 0;
332
+ var preloadTimeoutTimer = 0;
333
+ var preloadRetryTimers = new Set();
334
+
335
+ var start = 0;
336
+ var width = 320;
337
+ var height = 240;
338
+ var canvas = null;
339
+ var ctx = null;
340
+ var currentFrame = null;
341
+ var playToken = 0;
342
+ var isPlaying = false;
343
+ var isLoading = false;
344
+ var pendingAutoPlay = false;
345
+ var playlistIndex = 0;
346
+ var imageCache = Object.create(null);
347
+ var mediaDataCache = Object.create(null);
348
+ var hasRevealed = false;
349
+ var forceStaticUntil = 0;
350
+
351
+ function guessImageType(url) {
352
+ var clean = (url || '').split('?')[0].toLowerCase();
353
+ if (clean.endsWith('.gif')) return 'image/gif';
354
+ if (clean.endsWith('.webp')) return 'image/webp';
355
+ if (clean.endsWith('.png')) return 'image/png';
356
+ if (clean.endsWith('.jpg') || clean.endsWith('.jpeg')) return 'image/jpeg';
357
+ return 'image/webp';
358
+ }
359
+
360
+ function resolveItemUrl(item) {
361
+ return new URL(item.url, window.location.href).href;
362
+ }
363
+
364
+ var playlist = [{ url: source, type: guessImageType(source), holdLast: 360 }];
365
+ if (source2) playlist.push({ url: source2, type: guessImageType(source2), holdLast: 500 });
366
+
367
+ function setCollapsed(collapsed) {
368
+ shell.classList.toggle('rq-tv-collapsed', collapsed);
369
+ toggle.setAttribute('aria-expanded', String(!collapsed));
370
+ }
371
+
372
+ function setLoading(loading) {
373
+ isLoading = loading;
374
+ shell.classList.toggle('rq-tv-loading', loading);
375
+ toggle.disabled = loading;
376
+ toggle.setAttribute('aria-busy', String(loading));
377
+ }
378
+
379
+ function clearSequenceTimer() {
380
+ if (!sequenceTimer) return;
381
+ clearTimeout(sequenceTimer);
382
+ sequenceTimer = 0;
383
+ }
384
+
385
+ function clearRenderTimer() {
386
+ if (!renderTimer) return;
387
+ clearTimeout(renderTimer);
388
+ renderTimer = 0;
389
+ }
390
+
391
+ function clearPreloadTimers() {
392
+ if (preloadTimeoutTimer) {
393
+ clearTimeout(preloadTimeoutTimer);
394
+ preloadTimeoutTimer = 0;
395
+ }
396
+ preloadRetryTimers.forEach(function(id) { clearTimeout(id); });
397
+ preloadRetryTimers.clear();
398
+ }
399
+
400
+ function scheduleSequence(fn, delay) {
401
+ clearSequenceTimer();
402
+ sequenceTimer = setTimeout(function runTask() {
403
+ if (document.hidden) {
404
+ sequenceTimer = setTimeout(runTask, HIDDEN_POLL_MS);
405
+ return;
406
+ }
407
+ fn();
408
+ }, delay);
409
+ }
410
+
411
+ function schedulePreloadRetry(fn, delay) {
412
+ var id = setTimeout(function() {
413
+ preloadRetryTimers.delete(id);
414
+ fn();
415
+ }, delay);
416
+ preloadRetryTimers.add(id);
417
+ }
418
+
419
+ function preloadGif(item, token, attempt, done) {
420
+ if (token !== playToken || !isPlaying) return;
421
+ var tryCount = typeof attempt === 'number' ? attempt : 0;
422
+ var cachedData = mediaDataCache[item.url];
423
+ if (cachedData instanceof ArrayBuffer) {
424
+ if (typeof ImageDecoder === 'undefined') {
425
+ done(true);
426
+ return;
427
+ }
428
+ var cachedDecoder = new ImageDecoder({ data: cachedData, type: item.type });
429
+ cachedDecoder.tracks.ready.then(async function() {
430
+ var result = await cachedDecoder.decode({ frameIndex: 0 });
431
+ if (result && result.image && result.image.close) result.image.close();
432
+ done(true);
433
+ }).catch(function() {
434
+ if (tryCount >= PRELOAD_RETRY_MAX) {
435
+ done(false);
436
+ return;
437
+ }
438
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
439
+ schedulePreloadRetry(function() {
440
+ preloadGif(item, token, tryCount + 1, done);
441
+ }, retryDelay);
442
+ });
443
+ return;
444
+ }
445
+ fetch(resolveItemUrl(item))
446
+ .then(function(response) { return response.arrayBuffer(); })
447
+ .then(function(buffer) {
448
+ mediaDataCache[item.url] = buffer;
449
+ if (typeof ImageDecoder === 'undefined') {
450
+ done(true);
451
+ return;
452
+ }
453
+ var decoder = new ImageDecoder({ data: buffer, type: item.type });
454
+ decoder.tracks.ready.then(async function() {
455
+ var result = await decoder.decode({ frameIndex: 0 });
456
+ if (result && result.image && result.image.close) result.image.close();
457
+ done(true);
458
+ }).catch(function() {
459
+ if (tryCount >= PRELOAD_RETRY_MAX) {
460
+ done(false);
461
+ return;
462
+ }
463
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
464
+ schedulePreloadRetry(function() {
465
+ preloadGif(item, token, tryCount + 1, done);
466
+ }, retryDelay);
467
+ });
468
+ }).catch(function() {
469
+ if (tryCount >= PRELOAD_RETRY_MAX) {
470
+ done(false);
471
+ return;
472
+ }
473
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
474
+ schedulePreloadRetry(function() {
475
+ preloadGif(item, token, tryCount + 1, done);
476
+ }, retryDelay);
477
+ });
478
+ }
479
+
480
+ function preloadImage(item, token, attempt, done) {
481
+ if (token !== playToken || !isPlaying) return;
482
+ var tryCount = typeof attempt === 'number' ? attempt : 0;
483
+ var cached = imageCache[item.url];
484
+ if (cached && cached.complete) {
485
+ done(true);
486
+ return;
487
+ }
488
+ var img = new Image();
489
+ img.onload = function() {
490
+ imageCache[item.url] = img;
491
+ done(true);
492
+ };
493
+ img.onerror = function() {
494
+ if (tryCount >= PRELOAD_RETRY_MAX) {
495
+ done(false);
496
+ return;
497
+ }
498
+ var retryDelay = Math.min(1800, RETRY_BASE_MS * (tryCount + 1));
499
+ schedulePreloadRetry(function() {
500
+ preloadImage(item, token, tryCount + 1, done);
501
+ }, retryDelay);
502
+ };
503
+ img.src = resolveItemUrl(item);
504
+ }
505
+
506
+ function preloadAssets(token, done) {
507
+ clearPreloadTimers();
508
+ var left = playlist.length;
509
+ var failed = false;
510
+ function finish(ok) {
511
+ if (left <= -999) return;
512
+ left = -1000;
513
+ clearPreloadTimers();
514
+ done(ok);
515
+ }
516
+ preloadTimeoutTimer = setTimeout(function() {
517
+ finish(false);
518
+ }, PRELOAD_TIMEOUT_MS);
519
+ function markDone(ok) {
520
+ if (!ok) failed = true;
521
+ left -= 1;
522
+ if (left <= 0) finish(!failed);
523
+ }
524
+ playlist.forEach(function(item) {
525
+ if (item.type === 'image/gif') preloadGif(item, token, 0, markDone);
526
+ else preloadImage(item, token, 0, markDone);
527
+ });
528
+ }
529
+
530
+ function revealMonitor(token) {
531
+ if (hasRevealed) return;
532
+ hasRevealed = true;
533
+ if (revealTimer) clearTimeout(revealTimer);
534
+ revealTimer = setTimeout(function runReveal() {
535
+ if (token !== playToken || !isPlaying) return;
536
+ if (document.hidden) {
537
+ revealTimer = setTimeout(runReveal, HIDDEN_POLL_MS);
538
+ return;
539
+ }
540
+ forceStaticUntil = performance.now() + WARMUP_STATIC_MS;
541
+ setCollapsed(false);
542
+ startRenderLoop();
543
+ revealTimer = 0;
544
+ }, 0);
545
+ }
546
+
547
+ function createCanvas() {
548
+ if (canvas) return;
549
+ canvas = document.createElement('canvas');
550
+ canvas.className = 'rq-tv-screen';
551
+ stage.appendChild(canvas);
552
+ ctx = canvas.getContext('2d');
553
+ resizeCanvas();
554
+ }
555
+
556
+ function destroyCanvas() {
557
+ if (!canvas) return;
558
+ canvas.remove();
559
+ canvas = null;
560
+ ctx = null;
561
+ }
562
+
563
+ function resizeCanvas() {
564
+ if (!canvas) return;
565
+ var rect = stage.getBoundingClientRect();
566
+ var dpr = Math.min(window.devicePixelRatio || 1, 2);
567
+ var w = Math.max(2, Math.round(rect.width * dpr));
568
+ var h = Math.max(2, Math.round(rect.height * dpr));
569
+ canvas.width = w;
570
+ canvas.height = h;
571
+ canvas.style.width = '100%';
572
+ canvas.style.height = '100%';
573
+ width = w;
574
+ height = h;
575
+ }
576
+
577
+ function releaseCurrentFrame() {
578
+ if (currentFrame && currentFrame.close) currentFrame.close();
579
+ currentFrame = null;
580
+ }
581
+
582
+ function clearCanvas() {
583
+ if (!ctx) return;
584
+ ctx.clearRect(0, 0, width, height);
585
+ }
586
+
587
+ function stopRenderLoop() {
588
+ clearRenderTimer();
589
+ }
590
+
591
+ function teardownPlayback() {
592
+ isPlaying = false;
593
+ playToken++;
594
+ setLoading(false);
595
+ forceStaticUntil = 0;
596
+ clearSequenceTimer();
597
+ clearPreloadTimers();
598
+ if (revealTimer) {
599
+ clearTimeout(revealTimer);
600
+ revealTimer = 0;
601
+ }
602
+ stopRenderLoop();
603
+ releaseCurrentFrame();
604
+ clearCanvas();
605
+ destroyCanvas();
606
+ }
607
+
608
+ function drawFallback(elapsed) {
609
+ if (!ctx) return;
610
+ ctx.fillStyle = 'rgba(124, 186, 224, 0.15)';
611
+ ctx.beginPath();
612
+ ctx.ellipse(width * 0.5, height * 0.52, 36, 44, 0, 0, Math.PI * 2);
613
+ ctx.fill();
614
+ ctx.strokeStyle = 'rgba(176, 226, 250, 0.66)';
615
+ ctx.stroke();
616
+ ctx.fillStyle = 'rgba(182, 232, 255, 0.08)';
617
+ for (var i = 0; i < 6; i++) {
618
+ var y = (elapsed * 34 + i * 42) % height;
619
+ ctx.fillRect(0, y, width, 1);
620
+ }
621
+ }
622
+
623
+ function drawOverlay(elapsed) {
624
+ if (!ctx) return;
625
+ ctx.globalCompositeOperation = 'screen';
626
+ for (var i = 0; i < 7; i++) {
627
+ var y = (elapsed * 28 + i * 36) % height;
628
+ ctx.fillStyle = i % 2 ? 'rgba(188,238,255,0.05)' : 'rgba(188,238,255,0.08)';
629
+ ctx.fillRect(0, y, width, 1);
630
+ }
631
+ ctx.globalCompositeOperation = 'source-over';
632
+ ctx.globalCompositeOperation = 'screen';
633
+ ctx.fillStyle = 'rgba(154, 230, 255, 0.10)';
634
+ ctx.fillRect(0, 0, width, height);
635
+ ctx.globalCompositeOperation = 'source-over';
636
+ var sweepX = (Math.sin(elapsed * 0.9) * 0.5 + 0.5) * width;
637
+ var sweep = ctx.createLinearGradient(sweepX - 80, 0, sweepX + 80, 0);
638
+ sweep.addColorStop(0, 'rgba(176, 230, 255, 0)');
639
+ sweep.addColorStop(0.5, 'rgba(176, 230, 255, 0.12)');
640
+ sweep.addColorStop(1, 'rgba(176, 230, 255, 0)');
641
+ ctx.fillStyle = sweep;
642
+ ctx.fillRect(0, 0, width, height);
643
+ }
644
+
645
+ function drawFrame(frame) {
646
+ if (!ctx) return;
647
+ var item = playlist[playlistIndex] || null;
648
+ var isGif = item && item.type === 'image/gif';
649
+ var fw = frame.displayWidth || frame.width;
650
+ var fh = frame.displayHeight || frame.height;
651
+ var zoom = 1.32;
652
+ var scale = Math.max(width / fw, height / fh) * zoom;
653
+ var dw = fw * scale;
654
+ var dh = fh * scale;
655
+ var shiftX = -Math.min(74, width * 0.18);
656
+ var shiftY = isGif ? 5 : 0;
657
+ var dx = (width - dw) / 2 + shiftX;
658
+ var dy = (height - dh) / 2 + shiftY;
659
+ ctx.save();
660
+ ctx.translate(width, 0);
661
+ ctx.scale(-1, 1);
662
+ ctx.drawImage(frame, dx, dy, dw, dh);
663
+ ctx.restore();
664
+ }
665
+
666
+ function renderTick() {
667
+ if (!isPlaying || !ctx) {
668
+ renderTimer = 0;
669
+ return;
670
+ }
671
+ var now = performance.now();
672
+ var elapsed = (now - start) * 0.001;
673
+ var inStaticPhase = now < forceStaticUntil;
674
+ ctx.clearRect(0, 0, width, height);
675
+ if (!inStaticPhase && currentFrame) drawFrame(currentFrame);
676
+ else drawFallback(elapsed);
677
+ drawOverlay(elapsed);
678
+ renderTimer = setTimeout(renderTick, FRAME_MS);
679
+ }
680
+
681
+ function startRenderLoop() {
682
+ if (renderTimer) return;
683
+ start = performance.now();
684
+ renderTick();
685
+ }
686
+
687
+ function collapseAfterPlayback(token) {
688
+ if (token !== playToken) return;
689
+ scheduleSequence(function() {
690
+ if (token !== playToken) return;
691
+ isPlaying = false;
692
+ stopRenderLoop();
693
+ releaseCurrentFrame();
694
+ clearCanvas();
695
+ destroyCanvas();
696
+ setCollapsed(true);
697
+ }, COLLAPSE_DELAY_MS);
698
+ }
699
+
700
+ function fallbackPlay(index, token, attempt) {
701
+ if (token !== playToken || !isPlaying) return;
702
+ playlistIndex = index;
703
+ if (index >= playlist.length) {
704
+ collapseAfterPlayback(token);
705
+ return;
706
+ }
707
+ var item = playlist[index];
708
+ var tryCount = typeof attempt === 'number' ? attempt : 0;
709
+ var cached = item.type === 'image/gif' ? null : imageCache[item.url];
710
+ if (cached && cached.complete) {
711
+ releaseCurrentFrame();
712
+ currentFrame = cached;
713
+ if (index === 0) revealMonitor(token);
714
+ var cachedHold = item.holdLast || FALLBACK_STEP_MS;
715
+ if (index === 0 && forceStaticUntil > performance.now()) {
716
+ cachedHold += Math.ceil(forceStaticUntil - performance.now());
717
+ }
718
+ scheduleSequence(function() {
719
+ fallbackPlay(index + 1, token, 0);
720
+ }, cachedHold);
721
+ return;
722
+ }
723
+ var img = new Image();
724
+ img.onload = function() {
725
+ if (token !== playToken || !isPlaying) return;
726
+ if (item.type !== 'image/gif') imageCache[item.url] = img;
727
+ releaseCurrentFrame();
728
+ currentFrame = img;
729
+ if (index === 0) revealMonitor(token);
730
+ var holdMs = item.holdLast || FALLBACK_STEP_MS;
731
+ if (item.type === 'image/gif') holdMs = Math.max(holdMs, FALLBACK_GIF_SHOW_MS);
732
+ if (index === 0 && forceStaticUntil > performance.now()) {
733
+ holdMs += Math.ceil(forceStaticUntil - performance.now());
734
+ }
735
+ scheduleSequence(function() {
736
+ fallbackPlay(index + 1, token, 0);
737
+ }, holdMs);
738
+ };
739
+ img.onerror = function() {
740
+ var retryDelay = Math.min(1200, 180 * (tryCount + 1));
741
+ scheduleSequence(function() {
742
+ fallbackPlay(index, token, tryCount + 1);
743
+ }, retryDelay);
744
+ };
745
+ img.src = resolveItemUrl(item);
746
+ }
747
+
748
+ function decodeWithImageDecoder(index, token) {
749
+ if (token !== playToken || !isPlaying) return;
750
+ playlistIndex = index;
751
+ if (index >= playlist.length) {
752
+ collapseAfterPlayback(token);
753
+ return;
754
+ }
755
+ var item = playlist[index];
756
+ function decodeFromData(data) {
757
+ if (token !== playToken || !isPlaying) return;
758
+ var decoder = new ImageDecoder({ data: data, type: item.type });
759
+ var frameIndex = 0;
760
+ function decodeNext() {
761
+ if (token !== playToken || !isPlaying) return;
762
+ decoder.decode({ frameIndex: frameIndex }).then(function(result) {
763
+ if (token !== playToken || !isPlaying) return;
764
+ releaseCurrentFrame();
765
+ currentFrame = result.image;
766
+ if (index === 0) revealMonitor(token);
767
+ var baseDelay = result.image.duration ? result.image.duration / 1000 : 33;
768
+ var delay = Math.max(16, baseDelay);
769
+ if (index === 0 && forceStaticUntil > performance.now()) {
770
+ delay += Math.ceil(forceStaticUntil - performance.now());
771
+ }
772
+ frameIndex++;
773
+ if (frameIndex >= decoder.tracks.selectedTrack.frameCount) {
774
+ var holdDelay = item.holdLast || 0;
775
+ scheduleSequence(function() {
776
+ decodeWithImageDecoder(index + 1, token);
777
+ }, holdDelay);
778
+ return;
779
+ }
780
+ scheduleSequence(decodeNext, delay);
781
+ }).catch(function() {
782
+ frameIndex = 0;
783
+ scheduleSequence(decodeNext, 100);
784
+ });
785
+ }
786
+ decoder.tracks.ready.then(function() {
787
+ if (token !== playToken || !isPlaying) return;
788
+ decodeNext();
789
+ }).catch(function() {
790
+ if (token !== playToken || !isPlaying) return;
791
+ fallbackPlay(index, token, 0);
792
+ });
793
+ }
794
+
795
+ var cachedData = mediaDataCache[item.url];
796
+ if (cachedData instanceof ArrayBuffer) {
797
+ decodeFromData(cachedData);
798
+ return;
799
+ }
800
+ fetch(resolveItemUrl(item))
801
+ .then(function(response) { return response.arrayBuffer(); })
802
+ .then(function(buffer) {
803
+ mediaDataCache[item.url] = buffer;
804
+ decodeFromData(buffer);
805
+ }).catch(function() {
806
+ if (token !== playToken || !isPlaying) return;
807
+ fallbackPlay(index, token, 0);
808
+ });
809
+ }
810
+
811
+ function startPlayback() {
812
+ if (isLoading) return;
813
+ teardownPlayback();
814
+ setLoading(true);
815
+ createCanvas();
816
+ if (!ctx) {
817
+ setLoading(false);
818
+ setCollapsed(true);
819
+ return;
820
+ }
821
+ isPlaying = true;
822
+ hasRevealed = false;
823
+ setCollapsed(true);
824
+ var token = playToken;
825
+ preloadAssets(token, function(ok) {
826
+ if (token !== playToken || !isPlaying) return;
827
+ setLoading(false);
828
+ if (!ok) {
829
+ isPlaying = false;
830
+ destroyCanvas();
831
+ setCollapsed(true);
832
+ return;
833
+ }
834
+ if (typeof ImageDecoder !== 'undefined') decodeWithImageDecoder(0, token);
835
+ else fallbackPlay(0, token, 0);
836
+ });
837
+ }
838
+
839
+ function queueAutoPlay() {
840
+ function run() {
841
+ clearSequenceTimer();
842
+ sequenceTimer = setTimeout(function() {
843
+ if (document.hidden) {
844
+ pendingAutoPlay = true;
845
+ return;
846
+ }
847
+ startPlayback();
848
+ }, OPEN_DELAY_MS);
849
+ }
850
+ if (document.readyState === 'complete') run();
851
+ else window.addEventListener('load', run, { once: true });
852
+ }
853
+
854
+ toggle.addEventListener('click', function() {
855
+ startPlayback();
856
+ });
857
+
858
+ function onTvVisibilityChange() {
859
+ if (document.hidden) {
860
+ stopRenderLoop();
861
+ } else {
862
+ if (pendingAutoPlay && !isPlaying) {
863
+ pendingAutoPlay = false;
864
+ startPlayback();
865
+ return;
866
+ }
867
+ if (isPlaying && !renderTimer) startRenderLoop();
868
+ }
869
+ }
870
+
871
+ window.addEventListener('resize', function() {
872
+ if (canvas) resizeCanvas();
873
+ }, { passive: true });
874
+ document.addEventListener('visibilitychange', onTvVisibilityChange);
875
+ window.addEventListener('beforeunload', function() { teardownPlayback(); }, { once: true });
876
+
877
+ setCollapsed(true);
878
+ queueAutoPlay();
879
+ }
880
+ initRedQueenTv();
881
+
882
+ // 鼠标跟随光斑
883
+ var glow = document.querySelector('.mesh-mouse-glow');
884
+ if (glow) {
885
+ var raf;
886
+ var x = 0, y = 0;
887
+ document.addEventListener('mousemove', function(e) {
888
+ x = e.clientX;
889
+ y = e.clientY;
890
+ if (!raf) raf = requestAnimationFrame(function() {
891
+ glow.style.setProperty('--mouse-x', x + 'px');
892
+ glow.style.setProperty('--mouse-y', y + 'px');
893
+ raf = 0;
894
+ });
895
+ });
896
+ }
897
+
898
+ // 悬浮预览卡:为链接添加 data-preview
899
+ document.querySelectorAll('.mesh-prose-body a[href]').forEach(function(a) {
900
+ var href = a.getAttribute('href') || '';
901
+ if (!href || href.startsWith('#')) return;
902
+ a.classList.add('mesh-link-preview');
903
+ try {
904
+ a.setAttribute('data-preview', href.startsWith('http') ? new URL(href, location.origin).hostname : href);
905
+ } catch (_) {
906
+ a.setAttribute('data-preview', href);
907
+ }
908
+ });
909
+
910
+ // 段落滚动浮现
911
+ var paras = document.querySelectorAll('.mesh-prose-body p, .mesh-prose-body h2, .mesh-prose-body h3, .mesh-prose-body pre, .mesh-prose-body blockquote, .mesh-prose-body ul, .mesh-prose-body ol');
912
+ if (window.IntersectionObserver) {
913
+ var io = new IntersectionObserver(function(entries) {
914
+ entries.forEach(function(e) {
915
+ if (e.isIntersecting) {
916
+ e.target.classList.add('mesh-para-visible');
917
+ io.unobserve(e.target);
918
+ }
919
+ });
920
+ }, { rootMargin: '0px 0px -60px 0px', threshold: 0.1 });
921
+ paras.forEach(function(p) { io.observe(p); });
922
+ } else {
923
+ paras.forEach(function(p) { p.classList.add('mesh-para-visible'); });
924
+ }
925
+
926
+ // Regenerate 按钮
927
+ var regen = document.querySelector('.mesh-regenerate');
928
+ var article = document.querySelector('.mesh-article');
929
+ var scan = document.querySelector('.mesh-load-scan');
930
+ if (regen && article) {
931
+ regen.addEventListener('click', function() {
932
+ regen.disabled = true;
933
+ regen.classList.add('mesh-regenerating');
934
+ article.classList.add('mesh-regenerate-flash');
935
+ if (scan) {
936
+ scan.style.animation = 'none';
937
+ scan.offsetHeight;
938
+ scan.style.animation = 'mesh-scan 0.8s ease-out forwards';
939
+ scan.style.top = '0';
940
+ scan.style.opacity = '1';
941
+ }
942
+ setTimeout(function() {
943
+ article.classList.remove('mesh-regenerate-flash');
944
+ regen.classList.remove('mesh-regenerating');
945
+ regen.disabled = false;
946
+ }, 1200);
947
+ });
948
+ }
949
+ }
950
+
951
+ if (document.readyState === 'loading') {
952
+ document.addEventListener('DOMContentLoaded', init);
953
+ } else {
954
+ init();
955
+ }
956
+ })();