@anglefeint/astro-theme 0.1.14 → 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.
- package/package.json +1 -1
- package/src/scripts/about/background.js +150 -0
- package/src/scripts/about/interactions.js +101 -0
- package/src/scripts/about/modals.js +347 -0
- package/src/scripts/about/reading-ui.js +60 -0
- package/src/scripts/about/runtime.js +20 -0
- package/src/scripts/about-effects.js +12 -551
- package/src/scripts/blogpost/hero-canvas.js +253 -0
- package/src/scripts/blogpost/interactions.js +73 -0
- package/src/scripts/blogpost/network-canvas.js +117 -0
- package/src/scripts/blogpost/read-progress.js +52 -0
- package/src/scripts/blogpost/red-queen-tv.js +604 -0
- package/src/scripts/blogpost-effects.js +20 -1084
|
@@ -1,1086 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
}
|