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