@anglefeint/astro-theme 0.1.11 → 0.1.13
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 +10 -1
- package/public/styles/theme-ai.css +2 -52
- package/src/components/shared/CommonFooter.astro +15 -4
- package/src/components/shared/CommonHeader.astro +11 -47
- package/src/components/shared/LangSwitcher.astro +57 -0
- package/src/components/shared/SocialMenu.astro +34 -6
- package/src/layouts/BlogPost.astro +1 -62
- package/src/scripts/blogpost-effects.js +112 -0
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anglefeint/astro-theme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Anglefeint core theme package for Astro",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"astro",
|
|
8
|
+
"astro-theme",
|
|
9
|
+
"blog",
|
|
10
|
+
"portfolio",
|
|
11
|
+
"cyberpunk",
|
|
12
|
+
"hacker",
|
|
13
|
+
"matrix"
|
|
14
|
+
],
|
|
6
15
|
"license": "MIT",
|
|
7
16
|
"files": [
|
|
8
17
|
"src/index.ts",
|
|
@@ -216,12 +216,13 @@ body.ai-page::-webkit-scrollbar-thumb {
|
|
|
216
216
|
opacity: 0.7;
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
-
.ai-network {
|
|
219
|
+
.ai-network-canvas {
|
|
220
220
|
position: absolute;
|
|
221
221
|
inset: -8%;
|
|
222
222
|
width: 116%;
|
|
223
223
|
height: 116%;
|
|
224
224
|
opacity: 1;
|
|
225
|
+
pointer-events: none;
|
|
225
226
|
filter: brightness(1.28) saturate(1.08);
|
|
226
227
|
animation: ai-drift 25s ease-in-out infinite;
|
|
227
228
|
}
|
|
@@ -240,57 +241,6 @@ body.ai-page::-webkit-scrollbar-thumb {
|
|
|
240
241
|
transform: translate(0.5%, -0.8%) scale(1.005);
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
|
-
.ai-network .ai-dots circle {
|
|
244
|
-
transform-origin: center;
|
|
245
|
-
animation:
|
|
246
|
-
ai-dot-float 8s ease-in-out infinite,
|
|
247
|
-
ai-dot-breathe 5s ease-in-out infinite;
|
|
248
|
-
}
|
|
249
|
-
.ai-network .ai-dots circle:nth-child(odd) {
|
|
250
|
-
animation-delay: -3s;
|
|
251
|
-
}
|
|
252
|
-
.ai-network .ai-dots circle:nth-child(3n) {
|
|
253
|
-
animation-delay: -5s;
|
|
254
|
-
}
|
|
255
|
-
.ai-network .ai-dots circle:nth-child(5n) {
|
|
256
|
-
animation-duration: 10s;
|
|
257
|
-
}
|
|
258
|
-
.ai-network .ai-dots circle:nth-child(4n) {
|
|
259
|
-
animation-delay: -1s;
|
|
260
|
-
}
|
|
261
|
-
.ai-network .ai-lines line {
|
|
262
|
-
stroke-dasharray: 6 12;
|
|
263
|
-
animation: ai-line-pulse 4s linear infinite;
|
|
264
|
-
}
|
|
265
|
-
.ai-network .ai-lines line:nth-child(odd) {
|
|
266
|
-
animation-delay: -2s;
|
|
267
|
-
}
|
|
268
|
-
@keyframes ai-line-pulse {
|
|
269
|
-
to {
|
|
270
|
-
stroke-dashoffset: -18;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
@keyframes ai-dot-float {
|
|
274
|
-
0%,
|
|
275
|
-
100% {
|
|
276
|
-
transform: translate(0, 0);
|
|
277
|
-
}
|
|
278
|
-
33% {
|
|
279
|
-
transform: translate(1.5px, -2px);
|
|
280
|
-
}
|
|
281
|
-
66% {
|
|
282
|
-
transform: translate(-1px, 1.5px);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
@keyframes ai-dot-breathe {
|
|
286
|
-
0%,
|
|
287
|
-
100% {
|
|
288
|
-
opacity: 1;
|
|
289
|
-
}
|
|
290
|
-
50% {
|
|
291
|
-
opacity: 0.75;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
244
|
body.ai-page .ai-content,
|
|
295
245
|
body.ai-page footer {
|
|
296
246
|
position: relative;
|
|
@@ -15,10 +15,11 @@ const { tagline = SITE_TAGLINE, scanlines = false } = Astro.props as Props;
|
|
|
15
15
|
|
|
16
16
|
<footer>
|
|
17
17
|
© {today.getFullYear()} {SITE_TITLE}. {tagline}
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
<div class="social-links">
|
|
19
|
+
<SocialMenu links={SOCIAL_LINKS} />
|
|
20
|
+
</div>
|
|
21
|
+
{import.meta.env.DEV && SOCIAL_LINKS.length === 0 && (
|
|
22
|
+
<p class="social-hint">Configure social links in <code>src/site.config.ts</code></p>
|
|
22
23
|
)}
|
|
23
24
|
{scanlines && <div class="ai-scanlines" aria-hidden="true"></div>}
|
|
24
25
|
</footer>
|
|
@@ -36,6 +37,16 @@ const { tagline = SITE_TAGLINE, scanlines = false } = Astro.props as Props;
|
|
|
36
37
|
gap: 1em;
|
|
37
38
|
margin-top: 1em;
|
|
38
39
|
}
|
|
40
|
+
.social-hint {
|
|
41
|
+
margin: 0.6rem 0 0;
|
|
42
|
+
font-size: 0.78rem;
|
|
43
|
+
color: var(--chrome-text-muted, rgb(var(--text-muted)));
|
|
44
|
+
opacity: 0.82;
|
|
45
|
+
}
|
|
46
|
+
.social-hint code {
|
|
47
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
48
|
+
font-size: 0.75rem;
|
|
49
|
+
}
|
|
39
50
|
.social-links a {
|
|
40
51
|
text-decoration: none;
|
|
41
52
|
color: var(--chrome-text-muted, rgb(var(--text-muted)));
|
|
@@ -151,53 +151,17 @@ const localeOptions = SUPPORTED_LOCALES.map((targetLocale) => ({
|
|
|
151
151
|
gap: 0.4rem;
|
|
152
152
|
position: relative;
|
|
153
153
|
flex-wrap: nowrap;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
border:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
.lang-label {
|
|
167
|
-
display: inline-block;
|
|
168
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
169
|
-
font-size: 0.6rem;
|
|
170
|
-
text-transform: uppercase;
|
|
171
|
-
letter-spacing: 0.12em;
|
|
172
|
-
color: rgba(190, 226, 248, 0.78);
|
|
173
|
-
white-space: nowrap;
|
|
174
|
-
}
|
|
175
|
-
.lang-select {
|
|
176
|
-
appearance: none;
|
|
177
|
-
-webkit-appearance: none;
|
|
178
|
-
-moz-appearance: none;
|
|
179
|
-
background:
|
|
180
|
-
linear-gradient(45deg, transparent 50%, rgba(204, 236, 252, 0.82) 50%) calc(100% - 0.9rem) 50% / 0.35rem 0.35rem no-repeat,
|
|
181
|
-
linear-gradient(135deg, rgba(204, 236, 252, 0.82) 50%, transparent 50%) calc(100% - 0.7rem) 50% / 0.35rem 0.35rem no-repeat,
|
|
182
|
-
rgba(9, 22, 40, 0.68);
|
|
183
|
-
border: 1px solid rgba(132, 214, 255, 0.2);
|
|
184
|
-
border-radius: 999px;
|
|
185
|
-
padding: 0.2rem 1.45rem 0.2rem 0.55rem;
|
|
186
|
-
min-width: 6rem;
|
|
187
|
-
font-size: 0.62rem;
|
|
188
|
-
line-height: 1;
|
|
189
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
190
|
-
color: rgba(204, 236, 252, 0.86);
|
|
191
|
-
cursor: pointer;
|
|
192
|
-
}
|
|
193
|
-
.lang-select:focus {
|
|
194
|
-
outline: none;
|
|
195
|
-
border-color: rgba(132, 214, 255, 0.5);
|
|
196
|
-
box-shadow: 0 0 0 2px rgba(98, 180, 228, 0.18);
|
|
197
|
-
}
|
|
198
|
-
.lang-select option {
|
|
199
|
-
color: #ccecfb;
|
|
200
|
-
background: #0b1c32;
|
|
154
|
+
--lang-switcher-border: rgba(132, 214, 255, 0.2);
|
|
155
|
+
--lang-switcher-bg: rgba(6, 16, 30, 0.35);
|
|
156
|
+
--lang-label-color: rgba(190, 226, 248, 0.78);
|
|
157
|
+
--lang-select-arrow: rgba(204, 236, 252, 0.82);
|
|
158
|
+
--lang-select-bg: rgba(9, 22, 40, 0.68);
|
|
159
|
+
--lang-select-border: rgba(132, 214, 255, 0.2);
|
|
160
|
+
--lang-select-text: rgba(204, 236, 252, 0.86);
|
|
161
|
+
--lang-select-focus-border: rgba(132, 214, 255, 0.5);
|
|
162
|
+
--lang-select-focus-ring: rgba(98, 180, 228, 0.18);
|
|
163
|
+
--lang-select-option-text: #ccecfb;
|
|
164
|
+
--lang-select-option-bg: #0b1c32;
|
|
201
165
|
}
|
|
202
166
|
.nav-status {
|
|
203
167
|
display: none;
|
|
@@ -33,3 +33,60 @@ const { label, currentLocale, options } = Astro.props as Props;
|
|
|
33
33
|
}
|
|
34
34
|
</select>
|
|
35
35
|
</div>
|
|
36
|
+
|
|
37
|
+
<style>
|
|
38
|
+
.lang-switcher {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 0.4rem;
|
|
42
|
+
padding: 0.2rem 0.4rem;
|
|
43
|
+
margin-right: 0.1rem;
|
|
44
|
+
border: 1px solid var(--lang-switcher-border, rgba(132, 214, 255, 0.2));
|
|
45
|
+
border-radius: 999px;
|
|
46
|
+
background: var(--lang-switcher-bg, rgba(6, 16, 30, 0.35));
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
}
|
|
49
|
+
.lang-label {
|
|
50
|
+
display: inline-block;
|
|
51
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
52
|
+
font-size: 0.6rem;
|
|
53
|
+
text-transform: uppercase;
|
|
54
|
+
letter-spacing: 0.12em;
|
|
55
|
+
color: var(--lang-label-color, rgba(190, 226, 248, 0.78));
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
}
|
|
58
|
+
.lang-select {
|
|
59
|
+
appearance: none;
|
|
60
|
+
-webkit-appearance: none;
|
|
61
|
+
-moz-appearance: none;
|
|
62
|
+
background:
|
|
63
|
+
linear-gradient(45deg, transparent 50%, var(--lang-select-arrow, rgba(204, 236, 252, 0.82)) 50%) calc(100% - 0.9rem) 50% /
|
|
64
|
+
0.35rem 0.35rem no-repeat,
|
|
65
|
+
linear-gradient(135deg, var(--lang-select-arrow, rgba(204, 236, 252, 0.82)) 50%, transparent 50%) calc(100% - 0.7rem) 50% /
|
|
66
|
+
0.35rem 0.35rem no-repeat,
|
|
67
|
+
var(--lang-select-bg, rgba(9, 22, 40, 0.68));
|
|
68
|
+
border: 1px solid var(--lang-select-border, rgba(132, 214, 255, 0.2));
|
|
69
|
+
border-radius: 999px;
|
|
70
|
+
padding: 0.2rem 1.45rem 0.2rem 0.55rem;
|
|
71
|
+
min-width: 6rem;
|
|
72
|
+
font-size: 0.62rem;
|
|
73
|
+
line-height: 1;
|
|
74
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
75
|
+
color: var(--lang-select-text, rgba(204, 236, 252, 0.86));
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
}
|
|
78
|
+
.lang-select:focus {
|
|
79
|
+
outline: none;
|
|
80
|
+
border-color: var(--lang-select-focus-border, rgba(132, 214, 255, 0.5));
|
|
81
|
+
box-shadow: 0 0 0 2px var(--lang-select-focus-ring, rgba(98, 180, 228, 0.18));
|
|
82
|
+
}
|
|
83
|
+
.lang-select option {
|
|
84
|
+
color: var(--lang-select-option-text, #ccecfb);
|
|
85
|
+
background: var(--lang-select-option-bg, #0b1c32);
|
|
86
|
+
}
|
|
87
|
+
@media (max-width: 720px) {
|
|
88
|
+
.lang-switcher {
|
|
89
|
+
margin-right: 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -5,16 +5,44 @@ import Icon from './Icon.astro';
|
|
|
5
5
|
interface Props {
|
|
6
6
|
links?: SocialLink[];
|
|
7
7
|
iconSize?: number;
|
|
8
|
+
showPlaceholdersWhenEmpty?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
links = SOCIAL_LINKS,
|
|
13
|
+
iconSize = 32,
|
|
14
|
+
showPlaceholdersWhenEmpty = true,
|
|
15
|
+
} = Astro.props as Props;
|
|
16
|
+
const placeholderLinks: SocialLink[] = [
|
|
17
|
+
{ href: '', label: 'Mastodon', icon: 'mastodon' },
|
|
18
|
+
{ href: '', label: 'Twitter', icon: 'twitter' },
|
|
19
|
+
{ href: '', label: 'GitHub', icon: 'github' },
|
|
20
|
+
];
|
|
21
|
+
const resolvedLinks = links.length > 0 || !showPlaceholdersWhenEmpty ? links : placeholderLinks;
|
|
11
22
|
---
|
|
12
23
|
|
|
13
24
|
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
resolvedLinks.map((link) => (
|
|
26
|
+
links.length > 0 ? (
|
|
27
|
+
<a href={link.href} target="_blank" rel="noopener noreferrer">
|
|
28
|
+
<span class="sr-only">{link.label}</span>
|
|
29
|
+
{link.icon ? <Icon name={link.icon} size={iconSize} /> : <span>{link.label}</span>}
|
|
30
|
+
</a>
|
|
31
|
+
) : (
|
|
32
|
+
<span class="social-placeholder" aria-hidden="true" title="Configure social links in src/site.config.ts">
|
|
33
|
+
{link.icon ? <Icon name={link.icon} size={iconSize} /> : <span>{link.label}</span>}
|
|
34
|
+
</span>
|
|
35
|
+
)
|
|
19
36
|
))
|
|
20
37
|
}
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.social-placeholder {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
opacity: 0.45;
|
|
45
|
+
filter: saturate(0.7);
|
|
46
|
+
cursor: default;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
@@ -48,29 +48,6 @@ const hasSystemMeta = Boolean(aiModel || aiMode || aiState);
|
|
|
48
48
|
const hasResponseMeta = aiLatencyMs !== undefined || aiConfidence !== undefined;
|
|
49
49
|
const hasStats = aiModel || wordCount !== undefined || tokenCount !== undefined;
|
|
50
50
|
const confidenceText = aiConfidence !== undefined ? aiConfidence.toFixed(2) : undefined;
|
|
51
|
-
|
|
52
|
-
// 点云:全屏分散但不过于稀疏
|
|
53
|
-
const w = 1200; const h = 800;
|
|
54
|
-
const pts: { x: number; y: number; r: number; depth: number }[] = [];
|
|
55
|
-
const pad = 50;
|
|
56
|
-
for (let i = 0; i < 58; i++) {
|
|
57
|
-
const x = pad + Math.random() * (w - pad * 2);
|
|
58
|
-
const y = pad + Math.random() * (h - pad * 2);
|
|
59
|
-
const depth = Math.random();
|
|
60
|
-
pts.push({
|
|
61
|
-
x, y,
|
|
62
|
-
r: 1.1 + depth * 1.3,
|
|
63
|
-
depth,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
const connectDist = 138;
|
|
67
|
-
const edges: [number, number][] = [];
|
|
68
|
-
for (let i = 0; i < pts.length; i++) {
|
|
69
|
-
for (let j = i + 1; j < pts.length; j++) {
|
|
70
|
-
const d = Math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y);
|
|
71
|
-
if (d < connectDist) edges.push([i, j]);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
51
|
---
|
|
75
52
|
|
|
76
53
|
<AiShell
|
|
@@ -99,45 +76,7 @@ for (let i = 0; i < pts.length; i++) {
|
|
|
99
76
|
<span class="ai-particle" style={`--x: ${20 + (i * 7) % 60}%; --y: ${10 + (i * 11) % 70}%; --d: ${3 + (i % 4)}s`}></span>
|
|
100
77
|
))}
|
|
101
78
|
</div>
|
|
102
|
-
<
|
|
103
|
-
<defs>
|
|
104
|
-
<linearGradient id="ai-line-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
105
|
-
<stop offset="0%" stop-color="rgba(180,235,255,0.26)" />
|
|
106
|
-
<stop offset="50%" stop-color="rgba(236,252,255,0.74)" />
|
|
107
|
-
<stop offset="100%" stop-color="rgba(180,235,255,0.26)" />
|
|
108
|
-
</linearGradient>
|
|
109
|
-
<filter id="ai-dot-glow">
|
|
110
|
-
<feGaussianBlur stdDeviation="2.2" result="blur" />
|
|
111
|
-
<feMerge>
|
|
112
|
-
<feMergeNode in="blur" />
|
|
113
|
-
<feMergeNode in="SourceGraphic" />
|
|
114
|
-
</feMerge>
|
|
115
|
-
</filter>
|
|
116
|
-
</defs>
|
|
117
|
-
<g class="ai-lines">
|
|
118
|
-
{edges.map(([i, j]) => (
|
|
119
|
-
<line
|
|
120
|
-
x1={pts[i].x}
|
|
121
|
-
y1={pts[i].y}
|
|
122
|
-
x2={pts[j].x}
|
|
123
|
-
y2={pts[j].y}
|
|
124
|
-
stroke="url(#ai-line-grad)"
|
|
125
|
-
stroke-width="0.4"
|
|
126
|
-
/>
|
|
127
|
-
))}
|
|
128
|
-
</g>
|
|
129
|
-
<g class="ai-dots">
|
|
130
|
-
{pts.map((p) => (
|
|
131
|
-
<circle
|
|
132
|
-
cx={p.x}
|
|
133
|
-
cy={p.y}
|
|
134
|
-
r={p.r}
|
|
135
|
-
fill={`rgba(232,255,255,${0.72 + p.depth * 0.48})`}
|
|
136
|
-
filter="url(#ai-dot-glow)"
|
|
137
|
-
/>
|
|
138
|
-
))}
|
|
139
|
-
</g>
|
|
140
|
-
</svg>
|
|
79
|
+
<canvas class="ai-network-canvas" aria-hidden="true"></canvas>
|
|
141
80
|
</div>
|
|
142
81
|
<aside class="rq-tv rq-tv-collapsed">
|
|
143
82
|
<div class="rq-tv-stage" data-rq-src={themeRedqueen1.src} data-rq-src2={themeRedqueen2.src}></div>
|
|
@@ -53,6 +53,118 @@ export function initBlogpostEffects() {
|
|
|
53
53
|
window.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' });
|
|
54
54
|
});
|
|
55
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();
|
|
56
168
|
function initHeroCanvas() {
|
|
57
169
|
var shell = document.querySelector('.hero-shell');
|
|
58
170
|
if (!shell) return;
|