-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
493 lines (457 loc) · 23.1 KB
/
script.js
File metadata and controls
493 lines (457 loc) · 23.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
const copy = {
zh: {
headerBrand: "第三边界",
navProducts: "产品",
navResearch: "研究",
navContact: "联系",
heroEyebrow: "Third Boundary Labs",
heroBrand: "第三边界",
heroLine: "构建 AI 时代的新可信边界。",
heroText: "从全仓库代码审计到自适应攻防评估,我们验证模型、工具、身份与行动链之间的真实边界。",
heroPrimary: "查看产品",
heroSecondary: "阅读研究",
indexOneLabel: "第一边界",
indexOne: "网络",
indexTwoLabel: "第二边界",
indexTwo: "应用",
indexThreeLabel: "第三边界",
indexThree: "AI",
thesisTitle: "新的边界不是一堵墙,而是一张可验证的行动图。",
thesisBody: "当 AI 系统可以阅读仓库、调用工具、写入代码、携带记忆并代表用户行动,可信边界必须从静态访问控制升级为持续的攻击图、身份图和行动证明。",
thesisLink: "查看边界模型",
modelTitle: "从连接,到代码,再到行动。",
modelBody: "第三边界关注的不是一个入口,而是 AI 系统在模型、工具、仓库、身份、策略和外部环境之间形成的完整行动面。",
layerOneTitle: "网络边界",
layerOneBody: "谁可以连接、扫描、进入和横向移动。",
layerTwoTitle: "应用边界",
layerTwoBody: "代码、依赖、API、数据流和业务逻辑是否可信。",
layerThreeTitle: "AI 行动边界",
layerThreeBody: "模型如何被诱导,工具如何被调用,身份如何被证明,行动如何被审计。",
workTitle: "研究与基础设施方向",
workOneTitle: "全仓库代码审计",
workOneBody: "将架构、入口点、权限路径和敏感 sink 组织成可验证的证据图。",
workTwoTitle: "Agent 攻击框架",
workTwoBody: "系统评估 jailbreak、工具滥用、上下文投毒和身份混淆。",
workThreeTitle: "自适应攻防评估",
workThreeBody: "让攻击随防御变化而演化,让防御基于真实失败案例迭代。",
workFourTitle: "Agent 身份与防御基础设施",
workFourBody: "为 Agent 行动建立身份、授权、策略执行、审计和证明层。",
journalEyebrow: "Research notes",
journalTitle: "越狱只是表面,真实问题是行动链路失控。",
journalBody: "研究页将记录 jailbreak、adaptive attack、Agent 身份验证和防御绕过案例,把研究转化为可复现实验与工程接口。",
productsEyebrow: "Products",
productsTitle: "把 AI 边界变成可验证的基础设施。",
productsLede: "第三边界提供从仓库审计、攻防评估到身份与运行时防御的完整产品线,帮助团队知道 AI 系统能做什么、做过什么、应该被允许做什么。",
productLinesTitle: "产品线",
productLinesLede: "每个模块都可以独立使用,也可以组合成面向 Agent 应用和代码仓库的持续安全评估平台。",
productOneTitle: "全仓库代码审计",
productOneBody: "把大型仓库转成证据图,定位入口点、权限路径、敏感 sink 和可复现攻击链。",
productOneA: "架构理解、威胁建模和热点排序",
productOneB: "候选漏洞证据包与对抗验证",
productOneC: "面向前端和报告系统的结构化产物",
productTwoTitle: "Agent 攻击评估",
productTwoBody: "针对现有防御运行系统化攻击,覆盖 jailbreak、工具滥用、上下文投毒和身份混淆。",
productTwoA: "可重复的攻击场景和评分基准",
productTwoB: "跨模型、跨工具链的防御对比",
productTwoC: "适配企业内部 Agent 和第三方 Agent 平台",
productThreeTitle: "自适应防御评估",
productThreeBody: "让攻击随防御变化而演化,持续发现策略、过滤器、上下文隔离和工具权限里的薄弱点。",
productThreeA: "Adaptive attack 与 adaptive defense 回归测试",
productThreeB: "防御绕过样本库和失败模式归因",
productThreeC: "上线前评估和上线后漂移监测",
productFourTitle: "AI 身份与行动证明",
productFourBody: "为 Agent 行动建立身份、授权、策略执行、审计和证明层,让每次关键操作都能被追踪和验证。",
productFourA: "Agent 身份、会话边界和授权声明",
productFourB: "工具调用策略与高风险动作拦截",
productFourC: "面向审计、合规和事故复盘的行动日志",
methodTitle: "交付方式",
methodLede: "从一次性评估到持续基础设施,产品可以按风险和成熟度逐步接入。",
methodOneTitle: "评估试点",
methodOneBody: "选择一个真实仓库或 Agent 应用,输出风险地图、攻击路径和可复现报告。",
methodTwoTitle: "持续平台",
methodTwoBody: "接入 CI、评估集、策略更新和运行时日志,让边界评估成为发布流程的一部分。",
methodThreeTitle: "身份基础设施",
methodThreeBody: "为内部 Agent、自动化流程和高权限工具调用建立可验证身份与行动证明。",
productContactTitle: "从一次边界评估开始。",
researchEyebrow: "Research",
researchTitle: "研究 AI 边界如何移动。",
researchLede: "我们把 jailbreak、Agent 攻击、自适应防御、身份验证和仓库审计研究写成可复现实验,而不是泛泛而谈的趋势判断。",
latestTitle: "最新研究",
latestLede: "研究页的目标是让攻击、失败模式和防御接口都能被复现、比较和工程化。",
postOneMeta: "Jailbreak / 评估",
postOneTitle: "越狱不是提示词问题,是行动边界问题",
postOneBody: "分析为什么只靠 prompt filter 无法覆盖工具调用、记忆污染和身份混淆带来的组合风险。",
postOneA: "失败案例分类",
postOneB: "可复现实验结构",
postTwoMeta: "Adaptive Attack",
postTwoTitle: "当防御改变,攻击应该如何重新搜索",
postTwoBody: "介绍 adaptive attack 的评估框架:攻击如何根据拒答、审计、策略拦截和工具失败信号继续演化。",
postTwoA: "攻击反馈循环",
postTwoB: "防御回归测试",
postThreeMeta: "Repository Audit",
postThreeTitle: "大仓库审计为什么需要证据图",
postThreeBody: "从入口点、权限路径和敏感 sink 出发,把模型推理转化为可审计、可复核、可复现的结构化证据。",
postThreeA: "证据等级",
postThreeB: "对抗验证",
postFourMeta: "Identity",
postFourTitle: "Agent 身份验证应该证明什么",
postFourBody: "讨论 Agent 身份、授权声明、工具调用上下文和行动日志之间的边界,以及为什么传统 API key 不够。",
postFourA: "身份声明",
postFourB: "行动证明",
briefTitle: "研究输出会直接回流到产品。",
briefBody: "每篇文章都应该沉淀为攻击样本、评估基准、防御接口或身份策略,而不是只停留在观点。",
briefAction: "订阅或合作",
contactEyebrow: "Third Boundary Labs",
contactTitle: "在边界移动之前,把它画出来。",
contactAction: "hello@thirdboundary.ai"
},
en: {
headerBrand: "Third Boundary",
navProducts: "Products",
navResearch: "Research",
navContact: "Contact",
heroEyebrow: "Third Boundary Labs",
heroBrand: "Third Boundary",
heroLine: "Defining the trust boundary for AI.",
heroText: "From full-repository audit to adaptive attack evaluation, we verify the real boundary between models, tools, identity, code, and action chains.",
heroPrimary: "View products",
heroSecondary: "Read research",
indexOneLabel: "First boundary",
indexOne: "Network",
indexTwoLabel: "Second boundary",
indexTwo: "Application",
indexThreeLabel: "Third boundary",
indexThree: "AI",
thesisTitle: "The new boundary is not a wall. It is a verifiable graph of action.",
thesisBody: "When AI systems can read repositories, call tools, write code, carry memory, and act for users, trust must move from static access control to live attack graphs, identity graphs, and action proof.",
thesisLink: "See the boundary model",
modelTitle: "From connection, to code, to action.",
modelBody: "The third boundary is not a single entry point. It is the complete action surface formed by AI systems, tools, repositories, identity, policy, and external environments.",
layerOneTitle: "Network boundary",
layerOneBody: "Who can connect, scan, enter, and move laterally.",
layerTwoTitle: "Application boundary",
layerTwoBody: "Whether code, dependencies, APIs, data flows, and business logic can be trusted.",
layerThreeTitle: "AI action boundary",
layerThreeBody: "How models are induced, tools are invoked, identity is proven, and actions are audited.",
workTitle: "Research and infrastructure tracks",
workOneTitle: "Full-repository audit",
workOneBody: "Turn architecture, entry points, permission paths, and sensitive sinks into a verifiable evidence graph.",
workTwoTitle: "Agent attack framework",
workTwoBody: "Evaluate jailbreaks, tool abuse, context poisoning, and identity confusion as repeatable systems.",
workThreeTitle: "Adaptive attack and defense",
workThreeBody: "Let attacks evolve with defenses, and let defenses improve from real failure cases.",
workFourTitle: "Agent identity and defense infrastructure",
workFourBody: "Build identity, authorization, policy enforcement, audit, and proof layers for agent action.",
journalEyebrow: "Research notes",
journalTitle: "Jailbreaks are the symptom. Uncontrolled action chains are the problem.",
journalBody: "Research notes will document jailbreak research, adaptive attacks, agent authentication, and defense bypasses as reproducible experiments and engineering interfaces.",
productsEyebrow: "Products",
productsTitle: "Turn AI boundaries into verifiable infrastructure.",
productsLede: "Third Boundary provides a product line for repository audit, adversarial evaluation, identity, and runtime defense so teams can know what AI systems can do, what they did, and what they should be allowed to do.",
productLinesTitle: "Product lines",
productLinesLede: "Each module can run on its own, or combine into a continuous evaluation platform for agent applications and code repositories.",
productOneTitle: "Full-repository audit",
productOneBody: "Turn large repositories into evidence graphs that expose entry points, permission paths, sensitive sinks, and reproducible attack chains.",
productOneA: "Architecture understanding, threat modeling, and hotspot ranking",
productOneB: "Candidate finding evidence packs and adversarial verification",
productOneC: "Structured artifacts for frontend views and reports",
productTwoTitle: "Agent attack evaluation",
productTwoBody: "Run systematic attacks against existing defenses, covering jailbreaks, tool abuse, context poisoning, and identity confusion.",
productTwoA: "Repeatable attack scenarios and scoring baselines",
productTwoB: "Defense comparison across models and toolchains",
productTwoC: "Support for internal agents and third-party agent platforms",
productThreeTitle: "Adaptive defense evaluation",
productThreeBody: "Let attacks evolve as defenses change, continuously finding weak points in policy, filters, context isolation, and tool permissions.",
productThreeA: "Adaptive attack and adaptive defense regression tests",
productThreeB: "Defense-bypass sample library and failure-mode attribution",
productThreeC: "Pre-launch evaluation and post-launch drift monitoring",
productFourTitle: "AI identity and action proof",
productFourBody: "Establish identity, authorization, policy enforcement, audit, and proof layers for agent action so every critical operation can be traced and verified.",
productFourA: "Agent identity, session boundaries, and authorization claims",
productFourB: "Tool-call policy and high-risk action interception",
productFourC: "Action logs for audit, compliance, and incident review",
methodTitle: "Delivery model",
methodLede: "From one-off evaluation to continuous infrastructure, the product can be adopted according to risk and maturity.",
methodOneTitle: "Evaluation pilot",
methodOneBody: "Select one real repository or agent application and receive a risk map, attack paths, and a reproducible report.",
methodTwoTitle: "Continuous platform",
methodTwoBody: "Integrate CI, eval sets, policy updates, and runtime logs so boundary evaluation becomes part of release.",
methodThreeTitle: "Identity infrastructure",
methodThreeBody: "Build verifiable identity and action proof for internal agents, automation flows, and high-privilege tool calls.",
productContactTitle: "Start with one boundary evaluation.",
researchEyebrow: "Research",
researchTitle: "Studying how AI boundaries move.",
researchLede: "We write jailbreaks, agent attacks, adaptive defenses, authentication, and repository audit research as reproducible experiments, not trend commentary.",
latestTitle: "Latest research",
latestLede: "The goal is to make attacks, failure modes, and defense interfaces reproducible, comparable, and engineering-ready.",
postOneMeta: "Jailbreak / Evaluation",
postOneTitle: "Jailbreaks are not a prompt problem. They are an action-boundary problem.",
postOneBody: "Why prompt filters alone cannot cover the compound risk from tool calls, memory poisoning, and identity confusion.",
postOneA: "Failure case taxonomy",
postOneB: "Reproducible experiment structure",
postTwoMeta: "Adaptive Attack",
postTwoTitle: "When defenses change, how should attacks search again?",
postTwoBody: "An evaluation framework for adaptive attacks that evolve from refusal, audit, policy blocking, and tool-failure signals.",
postTwoA: "Attack feedback loops",
postTwoB: "Defense regression tests",
postThreeMeta: "Repository Audit",
postThreeTitle: "Why large-repository audit needs evidence graphs",
postThreeBody: "Starting from entry points, permission paths, and sensitive sinks, model reasoning becomes structured evidence that can be audited and reproduced.",
postThreeA: "Evidence levels",
postThreeB: "Adversarial verification",
postFourMeta: "Identity",
postFourTitle: "What should agent authentication prove?",
postFourBody: "The boundary between agent identity, authorization claims, tool-call context, and action logs, and why ordinary API keys are not enough.",
postFourA: "Identity claims",
postFourB: "Action proof",
briefTitle: "Research output should feed directly into product.",
briefBody: "Every article should become attack samples, evaluation baselines, defense interfaces, or identity policy rather than stopping at opinion.",
briefAction: "Subscribe or collaborate",
contactEyebrow: "Third Boundary Labs",
contactTitle: "Map the boundary before it moves.",
contactAction: "hello@thirdboundary.ai"
}
};
const languageButtons = document.querySelectorAll("[data-lang]");
const translatedNodes = document.querySelectorAll("[data-i18n]");
const languageStorageKey = "third-boundary-language-v2";
const languageSwitch = document.querySelector(".language-switch");
const languageTrigger = document.querySelector(".language-trigger");
const currentLanguageLabel = document.querySelector("[data-language-current]");
const languageLabels = {
en: "EN",
zh: "中"
};
function closeLanguageMenu() {
languageSwitch?.classList.remove("is-open");
languageTrigger?.setAttribute("aria-expanded", "false");
}
function setLanguage(language, shouldPersist = true) {
const activeLanguage = copy[language] ? language : "en";
const nextCopy = copy[activeLanguage];
document.documentElement.lang = activeLanguage === "zh" ? "zh-CN" : "en";
translatedNodes.forEach((node) => {
const key = node.dataset.i18n;
if (nextCopy[key]) {
node.textContent = nextCopy[key];
}
});
languageButtons.forEach((button) => {
const isActive = button.dataset.lang === activeLanguage;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
if (currentLanguageLabel) {
currentLanguageLabel.textContent = languageLabels[activeLanguage];
}
if (shouldPersist) {
localStorage.setItem(languageStorageKey, activeLanguage);
}
}
languageButtons.forEach((button) => {
button.addEventListener("click", () => {
setLanguage(button.dataset.lang);
closeLanguageMenu();
});
});
languageTrigger?.addEventListener("click", (event) => {
event.stopPropagation();
const isOpen = languageSwitch?.classList.toggle("is-open");
languageTrigger.setAttribute("aria-expanded", String(Boolean(isOpen)));
});
document.addEventListener("click", (event) => {
if (!languageSwitch?.contains(event.target)) {
closeLanguageMenu();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeLanguageMenu();
}
});
const urlLanguage = new URLSearchParams(window.location.search).get("lang");
const storedLanguage = localStorage.getItem(languageStorageKey);
setLanguage(urlLanguage || storedLanguage || "en", Boolean(urlLanguage || storedLanguage));
const header = document.querySelector(".site-header");
const themedSections = Array.from(document.querySelectorAll("[data-header-theme]"));
function updateHeaderTheme() {
if (!header) {
return;
}
const headerHeight = header.getBoundingClientRect().height;
const probeY = Math.min(window.innerHeight - 1, headerHeight + 8);
const themedSection = themedSections.find((section) => {
const rect = section.getBoundingClientRect();
return rect.top <= probeY && rect.bottom > probeY;
});
header.classList.toggle("is-light", themedSection?.dataset.headerTheme === "light");
}
window.addEventListener("scroll", updateHeaderTheme, { passive: true });
window.addEventListener("resize", updateHeaderTheme);
updateHeaderTheme();
const revealObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
revealObserver.unobserve(entry.target);
}
});
},
{ threshold: 0.16 }
);
document.querySelectorAll(".reveal").forEach((node) => revealObserver.observe(node));
const canvas = document.getElementById("boundary-canvas");
if (canvas) {
const context = canvas.getContext("2d");
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let width = 0;
let height = 0;
let dpr = 1;
let nodes = [];
let animationFrame = 0;
function resizeCanvas() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
width = canvas.clientWidth;
height = canvas.clientHeight;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
context.setTransform(dpr, 0, 0, dpr, 0, 0);
createNodes();
}
function createNodes() {
const count = Math.max(58, Math.floor((width * height) / 18000));
const seam = width * 0.66;
nodes = Array.from({ length: count }, (_, index) => {
const side = index % 5 === 0 ? 1 : Math.random();
const x = side > 0.63
? seam + Math.random() * width * 0.27
: width * (0.1 + Math.random() * 0.66);
const y = height * (0.12 + Math.random() * 0.74);
return {
x,
y,
baseX: x,
baseY: y,
radius: 0.7 + Math.random() * 1.8,
phase: Math.random() * Math.PI * 2,
speed: 0.0004 + Math.random() * 0.001,
side: x > seam ? 1 : 0
};
});
}
function drawNoise() {
context.save();
context.globalAlpha = 0.08;
for (let i = 0; i < 160; i += 1) {
const x = Math.random() * width;
const y = Math.random() * height;
const shade = 130 + Math.random() * 80;
context.fillStyle = `rgb(${shade}, ${shade - 14}, ${shade - 35})`;
context.fillRect(x, y, 1, 1);
}
context.restore();
}
function drawGrid(time) {
const seam = width * 0.66;
context.save();
context.lineWidth = 1;
context.strokeStyle = "rgba(242, 234, 220, 0.055)";
for (let x = -80; x < width + 120; x += 72) {
context.beginPath();
context.moveTo(x + Math.sin(time * 0.00025 + x) * 10, 0);
context.lineTo(x - 140, height);
context.stroke();
}
context.strokeStyle = "rgba(215, 255, 100, 0.5)";
context.lineWidth = 1.3;
context.beginPath();
for (let y = -24; y <= height + 24; y += 18) {
const drift = Math.sin(y * 0.015 + time * 0.0011) * 10;
const x = seam + drift;
if (y === -24) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.stroke();
context.strokeStyle = "rgba(181, 99, 66, 0.32)";
context.lineWidth = 1;
context.beginPath();
context.moveTo(seam + 34, 0);
context.lineTo(seam - 112, height);
context.stroke();
context.restore();
}
function drawConnections(time) {
context.save();
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
node.x = node.baseX + Math.cos(time * node.speed + node.phase) * 10;
node.y = node.baseY + Math.sin(time * node.speed * 1.3 + node.phase) * 8;
for (let j = i + 1; j < nodes.length; j += 1) {
const other = nodes[j];
const distance = Math.hypot(node.x - other.x, node.y - other.y);
if (distance < 142) {
const alpha = (1 - distance / 142) * (node.side || other.side ? 0.24 : 0.12);
context.strokeStyle = `rgba(242, 234, 220, ${alpha})`;
context.lineWidth = 1;
context.beginPath();
context.moveTo(node.x, node.y);
context.lineTo(other.x, other.y);
context.stroke();
}
}
}
nodes.forEach((node, index) => {
const pulse = 0.45 + Math.sin(time * 0.002 + node.phase) * 0.35;
context.fillStyle = node.side
? `rgba(215, 255, 100, ${0.36 + pulse * 0.28})`
: `rgba(242, 234, 220, ${0.22 + pulse * 0.14})`;
context.beginPath();
context.arc(node.x, node.y, node.radius + (index % 9 === 0 ? pulse * 1.5 : 0), 0, Math.PI * 2);
context.fill();
});
context.restore();
}
function drawFieldMarks(time) {
context.save();
context.lineWidth = 1;
for (let index = 0; index < 8; index += 1) {
const x = width * (0.46 + (index % 4) * 0.12) + Math.sin(time * 0.0007 + index) * 8;
const y = height * (0.22 + Math.floor(index / 4) * 0.46) + Math.cos(time * 0.0006 + index) * 7;
const length = 10 + (index % 3) * 7;
context.strokeStyle = index % 2 === 0 ? "rgba(215, 255, 100, 0.22)" : "rgba(242, 234, 220, 0.12)";
context.beginPath();
context.moveTo(x - length, y);
context.lineTo(x + length * 0.25, y);
context.stroke();
if (index % 3 === 0) {
context.strokeStyle = "rgba(215, 255, 100, 0.1)";
context.beginPath();
context.arc(x, y, 12 + index * 2, 0.18 * Math.PI, 0.76 * Math.PI);
context.stroke();
}
}
context.restore();
}
function draw(time = 0) {
context.clearRect(0, 0, width, height);
context.fillStyle = "#080907";
context.fillRect(0, 0, width, height);
drawGrid(time);
drawConnections(time);
drawFieldMarks(time);
drawNoise();
if (!prefersReducedMotion) {
animationFrame = requestAnimationFrame(draw);
}
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
draw();
if (prefersReducedMotion && animationFrame) {
cancelAnimationFrame(animationFrame);
}
}