@anglefeint/astro-theme 0.1.21 → 0.1.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anglefeint/astro-theme",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "description": "Anglefeint core theme package for Astro",
6
6
  "keywords": [
@@ -36,6 +36,10 @@ export type Messages = {
36
36
  backToBlog: string;
37
37
  related: string;
38
38
  regenerate: string;
39
+ toastP10: string;
40
+ toastP30: string;
41
+ toastP60: string;
42
+ toastDone: string;
39
43
  };
40
44
  };
41
45
 
@@ -71,6 +75,10 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
71
75
  backToBlog: 'Back to blog',
72
76
  related: 'Related',
73
77
  regenerate: 'Regenerate',
78
+ toastP10: 'context parsed 10%',
79
+ toastP30: 'context parsed 30%',
80
+ toastP60: 'inference stable 60%',
81
+ toastDone: 'output finalized',
74
82
  },
75
83
  },
76
84
  ja: {
@@ -104,6 +112,10 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
104
112
  backToBlog: 'ブログへ戻る',
105
113
  related: '関連記事',
106
114
  regenerate: '再生成',
115
+ toastP10: '文脈解析 10%',
116
+ toastP30: '文脈解析 30%',
117
+ toastP60: '推論安定 60%',
118
+ toastDone: '出力確定',
107
119
  },
108
120
  },
109
121
  ko: {
@@ -137,6 +149,10 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
137
149
  backToBlog: '블로그로 돌아가기',
138
150
  related: '관련 글',
139
151
  regenerate: '재생성',
152
+ toastP10: '컨텍스트 파싱 10%',
153
+ toastP30: '컨텍스트 파싱 30%',
154
+ toastP60: '추론 안정화 60%',
155
+ toastDone: '출력 완료',
140
156
  },
141
157
  },
142
158
  es: {
@@ -170,6 +186,10 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
170
186
  backToBlog: 'Volver al blog',
171
187
  related: 'Relacionados',
172
188
  regenerate: 'Regenerar',
189
+ toastP10: 'contexto analizado 10%',
190
+ toastP30: 'contexto analizado 30%',
191
+ toastP60: 'inferencia estable 60%',
192
+ toastDone: 'salida finalizada',
173
193
  },
174
194
  },
175
195
  zh: {
@@ -203,6 +223,10 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
203
223
  backToBlog: '返回博客',
204
224
  related: '相关文章',
205
225
  regenerate: '重新生成',
226
+ toastP10: '语境解析 10%',
227
+ toastP30: '语境解析 30%',
228
+ toastP60: '推理稳定 60%',
229
+ toastDone: '输出完成',
206
230
  },
207
231
  },
208
232
  };
@@ -90,7 +90,15 @@ const enableRedQueen = THEME.EFFECTS.ENABLE_RED_QUEEN;
90
90
  )}
91
91
  <div class="ai-read-progress" aria-hidden="true"></div>
92
92
  <button type="button" class="ai-back-to-top" aria-label="Back to top" title="Back to top">↑</button>
93
- <div class="ai-stage-toast" aria-live="polite" aria-atomic="true"></div>
93
+ <div
94
+ class="ai-stage-toast"
95
+ aria-live="polite"
96
+ aria-atomic="true"
97
+ data-toast-p10={messages.blog.toastP10}
98
+ data-toast-p30={messages.blog.toastP30}
99
+ data-toast-p60={messages.blog.toastP60}
100
+ data-toast-done={messages.blog.toastDone}
101
+ ></div>
94
102
  <div class="ai-mouse-glow" aria-hidden="true"></div>
95
103
  <div class="ai-depth-blur" aria-hidden="true"></div>
96
104
  <div class="ai-thinking-dots" aria-hidden="true"><span></span><span></span><span></span></div>
@@ -100,12 +108,22 @@ const enableRedQueen = THEME.EFFECTS.ENABLE_RED_QUEEN;
100
108
  {heroImage && (
101
109
  <div class="hero-shell">
102
110
  <div class="hero-pane">
103
- <div class="hero-image">
104
- <div class="hero-stack">
105
- <div class="hero-canvas-wrap" aria-hidden="true">
106
- <canvas class="hero-canvas" data-hero-src={heroImage.src}></canvas>
111
+ <div class="hero-image">
112
+ <div class="hero-stack">
113
+ <div class="hero-base-wrap">
114
+ <Image
115
+ class="hero-base-image"
116
+ src={heroImage}
117
+ alt={title}
118
+ loading="eager"
119
+ decoding="async"
120
+ fetchpriority="high"
121
+ />
122
+ </div>
123
+ <div class="hero-canvas-wrap" aria-hidden="true">
124
+ <canvas class="hero-canvas" data-hero-src={heroImage.src}></canvas>
125
+ </div>
107
126
  </div>
108
- </div>
109
127
  <div class="hero-frame" aria-hidden="true">
110
128
  <span>neural monitor</span>
111
129
  <span class="hero-frame-dot"></span>
@@ -15,6 +15,12 @@ export function initHeroCanvas(prefersReducedMotion) {
15
15
  var heroStart = 0;
16
16
  var heroRaf = 0;
17
17
  var resizeTimer = 0;
18
+ var prepareTimer = 0;
19
+ var prepareIdleHandle = 0;
20
+ var isInViewport = true;
21
+ var heroObserver = null;
22
+ var heroEffectsPrepared = false;
23
+ var heroEffectsPreparing = false;
18
24
  var baseCanvas = document.createElement('canvas');
19
25
  var baseCtx = baseCanvas.getContext('2d');
20
26
  var pixelCanvas = document.createElement('canvas');
@@ -117,6 +123,10 @@ export function initHeroCanvas(prefersReducedMotion) {
117
123
  }
118
124
 
119
125
  function heroRender(t) {
126
+ if (prefersReducedMotion || document.hidden || !isInViewport) {
127
+ heroRaf = 0;
128
+ return;
129
+ }
120
130
  if (!heroStart) heroStart = t;
121
131
  var elapsed = (t - heroStart) * 0.001;
122
132
  frameCount++;
@@ -237,20 +247,78 @@ export function initHeroCanvas(prefersReducedMotion) {
237
247
  }
238
248
  }
239
249
 
250
+ if (!prefersReducedMotion && !document.hidden && isInViewport) {
251
+ heroRaf = requestAnimationFrame(heroRender);
252
+ } else {
253
+ heroRaf = 0;
254
+ }
255
+ }
256
+
257
+ function startHeroLoop() {
258
+ if (prefersReducedMotion || document.hidden || !isInViewport || !canvas.img || !heroEffectsPrepared || heroRaf) return;
240
259
  heroRaf = requestAnimationFrame(heroRender);
241
260
  }
242
261
 
262
+ function stopHeroLoop() {
263
+ if (!heroRaf) return;
264
+ cancelAnimationFrame(heroRaf);
265
+ heroRaf = 0;
266
+ }
267
+
268
+ function drawStaticFrame(img) {
269
+ var w = canvas.width;
270
+ var h = canvas.height;
271
+ baseCanvas.width = w;
272
+ baseCanvas.height = h;
273
+ drawBase(baseCtx, img, w, h);
274
+ ctx.clearRect(0, 0, w, h);
275
+ ctx.drawImage(baseCanvas, 0, 0);
276
+ }
277
+
278
+ function scheduleHeroEffectsPrepare() {
279
+ if (prefersReducedMotion || !canvas.img || heroEffectsPrepared || heroEffectsPreparing) return;
280
+ heroEffectsPreparing = true;
281
+
282
+ function runPrepare() {
283
+ prepareTimer = 0;
284
+ prepareIdleHandle = 0;
285
+ if (!canvas.img) {
286
+ heroEffectsPreparing = false;
287
+ return;
288
+ }
289
+ buildEdge(canvas.img);
290
+ heroEffectsPrepared = true;
291
+ heroEffectsPreparing = false;
292
+ startHeroLoop();
293
+ }
294
+
295
+ if (typeof window.requestIdleCallback === 'function') {
296
+ prepareIdleHandle = window.requestIdleCallback(runPrepare, { timeout: 1200 });
297
+ return;
298
+ }
299
+
300
+ function afterLoad() {
301
+ prepareTimer = setTimeout(runPrepare, 180);
302
+ }
303
+
304
+ if (document.readyState === 'complete') afterLoad();
305
+ else window.addEventListener('load', afterLoad, { once: true });
306
+ }
307
+
243
308
  var img = new Image();
244
309
  img.onload = function() {
245
310
  canvas.img = img;
246
311
  sizeCanvas();
247
- buildEdge(img);
312
+ drawStaticFrame(img);
248
313
  wrap.classList.add('ready');
249
314
  if (prefersReducedMotion) {
250
- ctx.drawImage(baseCanvas, 0, 0);
251
315
  return;
252
316
  }
253
- heroRaf = requestAnimationFrame(heroRender);
317
+ // Keep first paint static for LCP, then prepare heavy effects when idle.
318
+ scheduleHeroEffectsPrepare();
319
+ };
320
+ img.onerror = function() {
321
+ wrap.classList.add('ready');
254
322
  };
255
323
  img.src = new URL(src, window.location.href).href;
256
324
 
@@ -260,23 +328,39 @@ export function initHeroCanvas(prefersReducedMotion) {
260
328
  resizeTimer = setTimeout(function() {
261
329
  resizeTimer = 0;
262
330
  sizeCanvas();
263
- buildEdge(canvas.img);
331
+ drawStaticFrame(canvas.img);
332
+ if (heroEffectsPrepared) buildEdge(canvas.img);
264
333
  }, 180);
265
334
  }, { passive: true });
266
335
 
267
336
  function onHeroVisibilityChange() {
268
337
  if (prefersReducedMotion) return;
269
338
  if (document.hidden) {
270
- if (heroRaf) cancelAnimationFrame(heroRaf);
271
- heroRaf = 0;
272
- } else if (canvas.img && !heroRaf) {
273
- heroRaf = requestAnimationFrame(heroRender);
339
+ stopHeroLoop();
340
+ } else {
341
+ startHeroLoop();
274
342
  }
275
343
  }
276
344
 
345
+ if (!prefersReducedMotion && typeof IntersectionObserver !== 'undefined') {
346
+ heroObserver = new IntersectionObserver(function(entries) {
347
+ var entry = entries[0];
348
+ if (!entry) return;
349
+ isInViewport = entry.isIntersecting;
350
+ if (isInViewport) startHeroLoop();
351
+ else stopHeroLoop();
352
+ }, { threshold: 0.02 });
353
+ heroObserver.observe(shell);
354
+ }
355
+
277
356
  document.addEventListener('visibilitychange', onHeroVisibilityChange);
278
357
  window.addEventListener('beforeunload', function() {
279
358
  if (resizeTimer) clearTimeout(resizeTimer);
280
- cancelAnimationFrame(heroRaf);
359
+ stopHeroLoop();
360
+ if (prepareTimer) clearTimeout(prepareTimer);
361
+ if (prepareIdleHandle && typeof window.cancelIdleCallback === 'function') {
362
+ window.cancelIdleCallback(prepareIdleHandle);
363
+ }
364
+ if (heroObserver) heroObserver.disconnect();
281
365
  }, { once: true });
282
366
  }
@@ -1,9 +1,22 @@
1
1
  export function initReadProgressAndBackToTop(prefersReducedMotion) {
2
2
  var progress = document.querySelector('.ai-read-progress');
3
3
  var toast = document.querySelector('.ai-stage-toast');
4
- var stageSeen = { p30: false, p60: false, p90: false };
4
+ var stageSeen = { p10: false, p30: false, p60: false, done: false };
5
5
  var toastTimer = 0;
6
6
  var hasScrolled = false;
7
+ var toastText = {
8
+ p10: 'context parsed 10%',
9
+ p30: 'context parsed 30%',
10
+ p60: 'inference stable 60%',
11
+ done: 'output finalized',
12
+ };
13
+
14
+ if (toast && toast.dataset) {
15
+ toastText.p10 = toast.dataset.toastP10 || toastText.p10;
16
+ toastText.p30 = toast.dataset.toastP30 || toastText.p30;
17
+ toastText.p60 = toast.dataset.toastP60 || toastText.p60;
18
+ toastText.done = toast.dataset.toastDone || toastText.done;
19
+ }
7
20
 
8
21
  function showStageToast(msg) {
9
22
  if (!toast) return;
@@ -25,17 +38,21 @@ export function initReadProgressAndBackToTop(prefersReducedMotion) {
25
38
  if (btn) btn.classList.toggle('visible', scrollTop > 400);
26
39
  if (!hasScrolled && scrollTop > 6) hasScrolled = true;
27
40
  if (!hasScrolled) return;
41
+ if (!stageSeen.p10 && p >= 0.1) {
42
+ stageSeen.p10 = true;
43
+ showStageToast(toastText.p10);
44
+ }
28
45
  if (!stageSeen.p30 && p >= 0.3) {
29
46
  stageSeen.p30 = true;
30
- showStageToast('context parsed');
47
+ showStageToast(toastText.p30);
31
48
  }
32
49
  if (!stageSeen.p60 && p >= 0.6) {
33
50
  stageSeen.p60 = true;
34
- showStageToast('inference stable');
51
+ showStageToast(toastText.p60);
35
52
  }
36
- if (!stageSeen.p90 && p >= 0.9) {
37
- stageSeen.p90 = true;
38
- showStageToast('output finalized');
53
+ if (!stageSeen.done && p >= 0.9) {
54
+ stageSeen.done = true;
55
+ showStageToast(toastText.done);
39
56
  }
40
57
  }
41
58
 
@@ -8,7 +8,7 @@ export function initRedQueenTv(prefersReducedMotion) {
8
8
  var source2 = stage.getAttribute('data-rq-src2') || '';
9
9
  if (!source) return;
10
10
 
11
- var OPEN_DELAY_MS = 500;
11
+ var OPEN_DELAY_MS = 2000;
12
12
  var COLLAPSE_DELAY_MS = 260;
13
13
  var FRAME_MS = 33;
14
14
  var FALLBACK_STEP_MS = 650;
@@ -24,6 +24,10 @@ export function initRedQueenTv(prefersReducedMotion) {
24
24
  var revealTimer = 0;
25
25
  var preloadTimeoutTimer = 0;
26
26
  var preloadRetryTimers = new Set();
27
+ var autoDelayTimer = 0;
28
+ var autoIdleHandle = 0;
29
+ var autoLoadHandler = null;
30
+ var autoStarted = false;
27
31
 
28
32
  var start = 0;
29
33
  var width = 320;
@@ -105,6 +109,21 @@ export function initRedQueenTv(prefersReducedMotion) {
105
109
  preloadRetryTimers.clear();
106
110
  }
107
111
 
112
+ function clearAutoStartTimers() {
113
+ if (autoDelayTimer) {
114
+ clearTimeout(autoDelayTimer);
115
+ autoDelayTimer = 0;
116
+ }
117
+ if (autoIdleHandle && typeof window.cancelIdleCallback === 'function') {
118
+ window.cancelIdleCallback(autoIdleHandle);
119
+ autoIdleHandle = 0;
120
+ }
121
+ if (autoLoadHandler) {
122
+ window.removeEventListener('load', autoLoadHandler);
123
+ autoLoadHandler = null;
124
+ }
125
+ }
126
+
108
127
  function scheduleSequence(fn, delay) {
109
128
  clearSequenceTimer();
110
129
  sequenceTimer = setTimeout(function runTask() {
@@ -283,6 +302,7 @@ export function initRedQueenTv(prefersReducedMotion) {
283
302
  forceStaticUntil = 0;
284
303
  clearSequenceTimer();
285
304
  clearPreloadTimers();
305
+ clearAutoStartTimers();
286
306
  if (revealTimer) {
287
307
  clearTimeout(revealTimer);
288
308
  revealTimer = 0;
@@ -535,22 +555,53 @@ export function initRedQueenTv(prefersReducedMotion) {
535
555
  }
536
556
 
537
557
  function queueAutoPlay() {
538
- function run() {
539
- clearSequenceTimer();
540
- sequenceTimer = setTimeout(function() {
541
- if (document.hidden) {
542
- pendingAutoPlay = true;
543
- return;
544
- }
545
- startPlayback();
546
- }, OPEN_DELAY_MS);
558
+ var readyByDelay = false;
559
+ var readyByLoad = document.readyState === 'complete';
560
+ var readyByIdle = false;
561
+
562
+ function tryAutoStart() {
563
+ if (autoStarted || !readyByDelay || !readyByLoad || !readyByIdle) return;
564
+ if (document.hidden) {
565
+ pendingAutoPlay = true;
566
+ return;
567
+ }
568
+ autoStarted = true;
569
+ clearAutoStartTimers();
570
+ startPlayback();
547
571
  }
548
572
 
549
- if (document.readyState === 'complete') run();
550
- else window.addEventListener('load', run, { once: true });
573
+ autoDelayTimer = setTimeout(function() {
574
+ autoDelayTimer = 0;
575
+ readyByDelay = true;
576
+ tryAutoStart();
577
+ }, OPEN_DELAY_MS);
578
+
579
+ if (!readyByLoad) {
580
+ autoLoadHandler = function() {
581
+ readyByLoad = true;
582
+ autoLoadHandler = null;
583
+ tryAutoStart();
584
+ };
585
+ window.addEventListener('load', autoLoadHandler, { once: true });
586
+ }
587
+
588
+ if (typeof window.requestIdleCallback === 'function') {
589
+ autoIdleHandle = window.requestIdleCallback(function() {
590
+ autoIdleHandle = 0;
591
+ readyByIdle = true;
592
+ tryAutoStart();
593
+ }, { timeout: 1500 });
594
+ } else {
595
+ setTimeout(function() {
596
+ readyByIdle = true;
597
+ tryAutoStart();
598
+ }, 600);
599
+ }
551
600
  }
552
601
 
553
602
  toggle.addEventListener('click', function() {
603
+ autoStarted = true;
604
+ clearAutoStartTimers();
554
605
  startPlayback();
555
606
  });
556
607
 
@@ -14,11 +14,48 @@ function prefersReducedMotionEnabled() {
14
14
 
15
15
  export function initBlogpostEffects() {
16
16
  var prefersReducedMotion = prefersReducedMotionEnabled();
17
- initReadProgressAndBackToTop(prefersReducedMotion);
18
- initNetworkCanvas(prefersReducedMotion);
19
- initHeroCanvas(prefersReducedMotion);
20
- if (document.querySelector('.rq-tv-stage')) {
17
+ var networkStarted = false;
18
+ var redQueenStarted = false;
19
+
20
+ function startNetworkCanvas() {
21
+ if (networkStarted) return;
22
+ networkStarted = true;
23
+ initNetworkCanvas(prefersReducedMotion);
24
+ }
25
+
26
+ function startRedQueenTv() {
27
+ if (redQueenStarted) return;
28
+ if (!document.querySelector('.rq-tv-stage')) return;
29
+ redQueenStarted = true;
21
30
  initRedQueenTv(prefersReducedMotion);
22
31
  }
32
+
33
+ function scheduleDeferredStarts() {
34
+ function fallbackSchedule() {
35
+ function runAfterLoad() {
36
+ setTimeout(startNetworkCanvas, 120);
37
+ setTimeout(startRedQueenTv, 460);
38
+ }
39
+
40
+ if (document.readyState === 'complete') runAfterLoad();
41
+ else window.addEventListener('load', runAfterLoad, { once: true });
42
+ }
43
+
44
+ if (typeof window.requestIdleCallback !== 'function') {
45
+ fallbackSchedule();
46
+ return;
47
+ }
48
+
49
+ window.requestIdleCallback(function() {
50
+ startNetworkCanvas();
51
+ window.requestIdleCallback(function() {
52
+ startRedQueenTv();
53
+ }, { timeout: 2200 });
54
+ }, { timeout: 1200 });
55
+ }
56
+
57
+ initReadProgressAndBackToTop(prefersReducedMotion);
58
+ initHeroCanvas(prefersReducedMotion);
23
59
  initPostInteractions(prefersReducedMotion);
60
+ scheduleDeferredStarts();
24
61
  }
@@ -89,19 +89,31 @@ body.ai-page::-webkit-scrollbar-thumb {
89
89
  #13070f 100%
90
90
  );
91
91
  }
92
+ .ai-glow::after {
93
+ content: "";
94
+ position: absolute;
95
+ inset: 0;
96
+ pointer-events: none;
97
+ background:
98
+ radial-gradient(circle at 30% 28%, rgba(164, 220, 255, 0.1), rgba(164, 220, 255, 0) 46%),
99
+ radial-gradient(circle at 72% 64%, rgba(255, 112, 140, 0.08), rgba(255, 112, 140, 0) 52%);
100
+ opacity: 0.38;
101
+ transform: translateZ(0);
102
+ will-change: opacity;
103
+ }
92
104
  .ai-glow-shift {
93
105
  animation: ai-glow-shift 25s ease-in-out infinite;
94
106
  }
95
107
  @keyframes ai-glow-shift {
96
108
  0%,
97
109
  100% {
98
- filter: brightness(1) saturate(1);
110
+ opacity: 0.32;
99
111
  }
100
112
  33% {
101
- filter: brightness(1.02) saturate(1.1) hue-rotate(5deg);
113
+ opacity: 0.52;
102
114
  }
103
115
  66% {
104
- filter: brightness(0.99) saturate(1.05) hue-rotate(-3deg);
116
+ opacity: 0.4;
105
117
  }
106
118
  }
107
119
  /* 柔和光晕:中心径向青蓝光 */
@@ -27,16 +27,27 @@
27
27
  0 24px 48px rgba(8, 18, 30, 0.68),
28
28
  0 0 34px rgba(8, 18, 30, 0.64);
29
29
  }
30
- .hero-stack {
31
- position: relative;
32
- aspect-ratio: 1020 / 510;
33
- background: #0a1218;
34
- }
35
- .hero-canvas-wrap {
36
- position: absolute;
37
- inset: 0;
38
- z-index: 1;
39
- opacity: 0;
30
+ .hero-stack {
31
+ position: relative;
32
+ aspect-ratio: 1020 / 510;
33
+ background: #0a1218;
34
+ }
35
+ .hero-base-wrap {
36
+ position: absolute;
37
+ inset: 0;
38
+ z-index: 0;
39
+ }
40
+ .hero-base-image {
41
+ width: 100%;
42
+ height: 100%;
43
+ object-fit: cover;
44
+ display: block;
45
+ }
46
+ .hero-canvas-wrap {
47
+ position: absolute;
48
+ inset: 0;
49
+ z-index: 1;
50
+ opacity: 0;
40
51
  transition: opacity 260ms ease;
41
52
  }
42
53
  .hero-canvas-wrap.ready { opacity: 1; }
@@ -154,9 +165,7 @@
154
165
  will-change: transform, opacity;
155
166
  transition:
156
167
  transform 320ms ease,
157
- opacity 220ms ease,
158
- box-shadow 320ms ease,
159
- background 320ms ease;
168
+ opacity 220ms ease;
160
169
  }
161
170
  .rq-tv::before {
162
171
  content: "";