Skip to content

Commit fd22949

Browse files
committed
feat(docs): refine homepage install experience
1 parent 7fa9a95 commit fd22949

6 files changed

Lines changed: 688 additions & 178 deletions

File tree

website/package-lock.json

Lines changed: 21 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
},
1919
"dependencies": {
2020
"@rspress/core": "^2.0.17",
21-
"codehike": "^1.1.0"
21+
"codehike": "^1.1.0",
22+
"simple-icons": "^16.27.1"
2223
},
2324
"devDependencies": {
2425
"@rslint/core": "^0.5.3",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { useEffect, useRef } from 'react';
2+
3+
type CanvasGridEffectProps = {
4+
cellSize?: number;
5+
className?: string;
6+
intensity?: number;
7+
};
8+
9+
type GridWave = {
10+
bornAt: number;
11+
x: number;
12+
y: number;
13+
};
14+
15+
const GRID_TINT = [125, 182, 255] as const;
16+
const WAVE_LIFETIME = 1_250;
17+
18+
function clamp(value: number, minimum: number, maximum: number) {
19+
return Math.min(Math.max(value, minimum), maximum);
20+
}
21+
22+
/**
23+
* A lightweight interpretation of Canvas UI's Grid interaction.
24+
*
25+
* It keeps the cursor-driven tile wave while avoiding the experimental
26+
* HTML-in-canvas API, so the effect behaves consistently on the Pages site.
27+
* https://canvasui.dev/docs/components/grid
28+
*/
29+
export function CanvasGridEffect({
30+
cellSize = 34,
31+
className,
32+
intensity = 1,
33+
}: CanvasGridEffectProps) {
34+
const canvasRef = useRef<HTMLCanvasElement>(null);
35+
36+
useEffect(() => {
37+
const canvas = canvasRef.current;
38+
const host = canvas?.parentElement;
39+
const context = canvas?.getContext('2d');
40+
if (!canvas || !host || !context) return undefined;
41+
const drawingContext = context;
42+
43+
const motionPreference = window.matchMedia(
44+
'(prefers-reduced-motion: reduce)',
45+
);
46+
let reducedMotion = motionPreference.matches;
47+
let frame = 0;
48+
let isVisible = true;
49+
let isPointerInside = false;
50+
let lastWaveAt = 0;
51+
let previousPointer = { x: -cellSize, y: -cellSize };
52+
let pointer = { x: -cellSize * 4, y: -cellSize * 4 };
53+
let waves: GridWave[] = [];
54+
let width = 1;
55+
let height = 1;
56+
let pixelRatio = 1;
57+
58+
const scheduleDraw = () => {
59+
if (frame || !isVisible) return;
60+
frame = window.requestAnimationFrame(draw);
61+
};
62+
63+
const resize = () => {
64+
const bounds = host.getBoundingClientRect();
65+
width = Math.max(bounds.width, 1);
66+
height = Math.max(bounds.height, 1);
67+
pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
68+
canvas.width = Math.round(width * pixelRatio);
69+
canvas.height = Math.round(height * pixelRatio);
70+
scheduleDraw();
71+
};
72+
73+
function draw(now: number) {
74+
frame = 0;
75+
drawingContext.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
76+
drawingContext.clearRect(0, 0, width, height);
77+
78+
const activeWaves = reducedMotion
79+
? []
80+
: waves.filter((wave) => now - wave.bornAt < WAVE_LIFETIME);
81+
waves = activeWaves;
82+
const columnCount = Math.ceil(width / cellSize) + 1;
83+
const rowCount = Math.ceil(height / cellSize) + 1;
84+
const maxDimension = Math.max(width, height);
85+
86+
for (let row = 0; row < rowCount; row += 1) {
87+
for (let column = 0; column < columnCount; column += 1) {
88+
const centerX = column * cellSize + cellSize / 2;
89+
const centerY = row * cellSize + cellSize / 2;
90+
const pointerDistance = Math.hypot(
91+
centerX - pointer.x,
92+
centerY - pointer.y,
93+
);
94+
const pointerLift = isPointerInside
95+
? Math.exp(-pointerDistance / (cellSize * 2.25))
96+
: 0;
97+
let waveLift = 0;
98+
99+
for (const wave of activeWaves) {
100+
const progress = clamp((now - wave.bornAt) / WAVE_LIFETIME, 0, 1);
101+
const radius = progress * maxDimension * 0.72;
102+
const distance = Math.hypot(centerX - wave.x, centerY - wave.y);
103+
const ring = Math.exp(
104+
-Math.pow((distance - radius) / (cellSize * 1.35), 2),
105+
);
106+
waveLift = Math.max(waveLift, ring * (1 - progress));
107+
}
108+
109+
const lift = clamp(pointerLift * 0.58 + waveLift, 0, 1);
110+
const inset = 4 - lift * 2.2;
111+
const offsetY = -lift * 4;
112+
const alpha = intensity * (0.018 + lift * 0.16);
113+
const fillAlpha = intensity * lift * 0.025;
114+
115+
drawingContext.strokeStyle = `rgba(${GRID_TINT.join(', ')}, ${alpha})`;
116+
drawingContext.lineWidth = 1;
117+
drawingContext.strokeRect(
118+
column * cellSize + inset,
119+
row * cellSize + inset + offsetY,
120+
cellSize - inset * 2,
121+
cellSize - inset * 2,
122+
);
123+
124+
if (fillAlpha > 0.002) {
125+
drawingContext.fillStyle = `rgba(${GRID_TINT.join(', ')}, ${fillAlpha})`;
126+
drawingContext.fillRect(
127+
column * cellSize + inset,
128+
row * cellSize + inset + offsetY,
129+
cellSize - inset * 2,
130+
cellSize - inset * 2,
131+
);
132+
}
133+
}
134+
}
135+
136+
if (activeWaves.length > 0) scheduleDraw();
137+
}
138+
139+
const addWave = (x: number, y: number, now: number) => {
140+
waves = [...waves.slice(-3), { bornAt: now, x, y }];
141+
lastWaveAt = now;
142+
};
143+
144+
const handlePointerMove = (event: PointerEvent) => {
145+
if (reducedMotion) return;
146+
const bounds = host.getBoundingClientRect();
147+
const x = event.clientX - bounds.left;
148+
const y = event.clientY - bounds.top;
149+
const now = performance.now();
150+
const distance = Math.hypot(x - previousPointer.x, y - previousPointer.y);
151+
152+
pointer = { x, y };
153+
isPointerInside = true;
154+
if (distance > cellSize * 1.25 && now - lastWaveAt > 90) {
155+
addWave(x, y, now);
156+
previousPointer = { x, y };
157+
}
158+
scheduleDraw();
159+
};
160+
161+
const handlePointerLeave = () => {
162+
isPointerInside = false;
163+
pointer = { x: -cellSize * 4, y: -cellSize * 4 };
164+
scheduleDraw();
165+
};
166+
167+
const handleMotionChange = () => {
168+
reducedMotion = motionPreference.matches;
169+
if (reducedMotion) waves = [];
170+
scheduleDraw();
171+
};
172+
173+
const resizeObserver = new ResizeObserver(resize);
174+
resizeObserver.observe(host);
175+
const intersectionObserver = new IntersectionObserver(([entry]) => {
176+
isVisible = entry?.isIntersecting ?? true;
177+
if (isVisible) scheduleDraw();
178+
});
179+
intersectionObserver.observe(host);
180+
motionPreference.addEventListener('change', handleMotionChange);
181+
host.addEventListener('pointermove', handlePointerMove);
182+
host.addEventListener('pointerleave', handlePointerLeave);
183+
resize();
184+
185+
return () => {
186+
window.cancelAnimationFrame(frame);
187+
resizeObserver.disconnect();
188+
intersectionObserver.disconnect();
189+
motionPreference.removeEventListener('change', handleMotionChange);
190+
host.removeEventListener('pointermove', handlePointerMove);
191+
host.removeEventListener('pointerleave', handlePointerLeave);
192+
};
193+
}, [cellSize, intensity]);
194+
195+
return <canvas aria-hidden="true" className={className} ref={canvasRef} />;
196+
}

website/theme/components/HomeLayout.tsx

Lines changed: 10 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
SelectionProvider,
1212
useSelectedIndex,
1313
} from 'codehike/utils/selection';
14+
import { CanvasGridEffect } from './CanvasGridEffect';
15+
import { InstallSwitcher } from './InstallSwitcher';
1416
import runtimeTutorialData from '../generated/runtime-tutorial.json';
1517

1618
type Locale = 'zh' | 'en';
@@ -44,41 +46,6 @@ type RuntimeTutorialStep = {
4446
const runtimeTutorialSteps =
4547
runtimeTutorialData as unknown as RuntimeTutorialStep[];
4648

47-
const installCommands = [
48-
{
49-
id: 'unix',
50-
label: 'macOS / Linux',
51-
command:
52-
"curl --proto '=https' --tlsv1.2 -LsSf https://raw.githubusercontent.com/A3S-Lab/a3s/main/install.sh | sh\n\na3s code",
53-
},
54-
{
55-
id: 'windows',
56-
label: 'Windows',
57-
command:
58-
'[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12\nirm https://raw.githubusercontent.com/A3S-Lab/a3s/main/install.ps1 | iex\n\na3s code',
59-
},
60-
{
61-
id: 'rust',
62-
label: 'Rust',
63-
command: 'cargo add a3s-code-core',
64-
},
65-
{
66-
id: 'node',
67-
label: 'Node.js',
68-
command: 'npm install @a3s-lab/code',
69-
},
70-
{
71-
id: 'python',
72-
label: 'Python',
73-
command: 'python -m pip install a3s-code',
74-
},
75-
{
76-
id: 'go',
77-
label: 'Go',
78-
command: 'go get github.com/A3S-Lab/Code/sdk/go/v6',
79-
},
80-
] as const;
81-
8249
const governanceFeatures: Feature[] = [
8350
{
8451
index: '01',
@@ -438,6 +405,7 @@ const copy = {
438405
github: '查看 GitHub',
439406
copy: '复制',
440407
copied: '已复制',
408+
installTabs: '选择安装平台',
441409
turn: '一次 Agent 执行会经过什么',
442410
proposal: '模型请求调用工具',
443411
governed: '执行前检查',
@@ -550,6 +518,7 @@ const copy = {
550518
github: 'View on GitHub',
551519
copy: 'Copy',
552520
copied: 'Copied',
521+
installTabs: 'Choose an install target',
553522
turn: 'What happens during one agent turn',
554523
proposal: 'The model requests a tool',
555524
governed: 'Checks before execution',
@@ -683,64 +652,6 @@ function GitHubIcon() {
683652
);
684653
}
685654

686-
function InstallSwitcher({
687-
locale,
688-
labels,
689-
}: {
690-
locale: Locale;
691-
labels: (typeof copy)[Locale];
692-
}) {
693-
const [activeId, setActiveId] =
694-
useState<(typeof installCommands)[number]['id']>('unix');
695-
const [copied, setCopied] = useState(false);
696-
const active =
697-
installCommands.find((item) => item.id === activeId) ?? installCommands[0];
698-
699-
async function copyActiveCommand() {
700-
await navigator.clipboard.writeText(active.command);
701-
setCopied(true);
702-
window.setTimeout(() => setCopied(false), 1600);
703-
}
704-
705-
return (
706-
<div className="a3s-install">
707-
<div className="a3s-install-tabs" role="tablist" aria-label="Install">
708-
{installCommands.map((item) => (
709-
<button
710-
aria-selected={active.id === item.id}
711-
className={active.id === item.id ? 'is-active' : undefined}
712-
key={item.id}
713-
onClick={() => {
714-
setActiveId(item.id);
715-
setCopied(false);
716-
}}
717-
role="tab"
718-
type="button"
719-
>
720-
{item.label}
721-
</button>
722-
))}
723-
</div>
724-
<div className="a3s-command" role="tabpanel">
725-
<pre>
726-
<code>{active.command}</code>
727-
</pre>
728-
<button
729-
className="a3s-copy-button"
730-
onClick={copyActiveCommand}
731-
type="button"
732-
>
733-
<span aria-hidden="true">{copied ? '✓' : '⧉'}</span>
734-
{copied ? labels.copied : labels.copy}
735-
</button>
736-
</div>
737-
<span className="a3s-install-locale" aria-hidden="true">
738-
{locale === 'zh' ? 'ZH' : 'EN'}
739-
</span>
740-
</div>
741-
);
742-
}
743-
744655
function RuntimeExecutionFlow({ labels }: { labels: (typeof copy)[Locale] }) {
745656
const playerRef = useRef<HTMLDivElement>(null);
746657
const hasStartedRef = useRef(false);
@@ -1512,9 +1423,14 @@ export function HomeLayout() {
15121423
{labels.github}
15131424
</a>
15141425
</div>
1515-
<InstallSwitcher labels={labels} locale={locale} />
1426+
<InstallSwitcher labels={labels} />
15161427
</div>
15171428
<div className="a3s-hero-visual">
1429+
<CanvasGridEffect
1430+
cellSize={42}
1431+
className="a3s-hero-canvas"
1432+
intensity={0.72}
1433+
/>
15181434
<RuntimeExecutionFlow labels={labels} />
15191435
</div>
15201436
</section>

0 commit comments

Comments
 (0)