From 360d2f53590ab934deb6601597e5d15aeb965b02 Mon Sep 17 00:00:00 2001 From: 100gle Date: Wed, 12 Aug 2026 20:17:38 +0800 Subject: [PATCH 1/2] perf(website): optimize hook flow animation --- website/src/components/HookIntro.astro | 201 +++++++++++-------------- 1 file changed, 91 insertions(+), 110 deletions(-) diff --git a/website/src/components/HookIntro.astro b/website/src/components/HookIntro.astro index 01d01219..1a4f0525 100644 --- a/website/src/components/HookIntro.astro +++ b/website/src/components/HookIntro.astro @@ -63,10 +63,13 @@ const straightPathD = `M ${CX} ${-44} L ${CX} ${VB_H + 44}`;
- + + + + {hookStages.map((stage, i) => { const yPct = ((FIRST_Y + i * SPACING) / VB_H * 100).toFixed(2); const xPct = (nodeCoords[i].x / VB_W * 100).toFixed(2); return (
-
- {stage.name}() - {stage.note} +
+ {stage.name}() + {stage.note}
); @@ -163,73 +175,15 @@ const straightPathD = `M ${CX} ${-44} L ${CX} ${VB_H + 44}`; ); } - .hook-grid { - position: absolute; - inset: 0; - pointer-events: none; - border-radius: calc(var(--radius) * 1.2); - background-image: radial-gradient(circle, var(--site-grid-dot) 1px, transparent 1px); - background-size: 18px 18px; - opacity: 0.18; - } - - /* ── Trail (hidden until JS activates) ── */ - .hook-trail { - opacity: 0; - stroke-dasharray: 0.14 0.86; - stroke-dashoffset: 0.14; - } - - /* ── Desktop / mobile path toggle (unscoped for SVG) ── */ - :global(.hook-paths-mobile) { display: none; } - @media (max-width: 639px) { - :global(.hook-paths-desktop) { display: none; } - :global(.hook-paths-mobile) { display: block; } - .hook-node { left: 50% !important; } - } - - /* ── Node card — D2 / Mermaid tech-diagram feel ── */ - .hook-card { - display: flex; - flex-direction: column; - gap: 1px; - padding: 7px 14px; - background: var(--card); - white-space: nowrap; - box-shadow: var(--site-shadow-card); - text-align: center; - transition: - border-color 0.4s ease, - box-shadow 0.4s ease; - will-change: box-shadow; - } - - .hook-card-name { - font-family: var(--font-mono); - font-size: 0.7rem; - font-weight: 600; - line-height: 1.3; - color: var(--foreground); - transition: color 0.35s ease; - } - - .hook-card-note { - font-family: var(--font-mono); - font-size: 0.55rem; - line-height: 1.3; - color: var(--muted-foreground); - opacity: 0.6; - transition: opacity 0.35s ease, color 0.35s ease; - } - - /* ── Active node: ping flash ── */ .hook-node.active .hook-card { border-color: var(--primary); animation: ring-ping 2s ease-out forwards; } + .hook-node.active .hook-card-name { color: var(--primary); } + .hook-node.active .hook-card-note { opacity: 1; } @@ -278,12 +232,11 @@ const straightPathD = `M ${CX} ${-44} L ${CX} ${VB_H + 44}`; const CX_MOBILE = 200; const CYCLE_DUR = 7; - const TRAIL_LEAD = 0.018; const FADE_ZONE = 0.06; const NEAR_DIST_SQ = 36 * 36; const PATH_SAMPLE_COUNT = 560; - const HOOK_FRAME_MS = 1000 / 30; const REPEAT_DELAY_MS = 400; + const TRAIL_FRAME_MS = 1000 / 60; type PathSample = { x: number; @@ -313,11 +266,10 @@ const straightPathD = `M ${CX} ${-44} L ${CX} ${VB_H + 44}`; let currentMobile: boolean | null = null; const nodeEls = Array.from(root.querySelectorAll('[data-hook-node]')) as HTMLElement[]; - const dot = root.querySelector('[data-hook-dot]') as SVGCircleElement | null; - const dotTrail = root.querySelector('[data-hook-dot-trail]') as SVGCircleElement | null; + const dot = root.querySelector('[data-hook-dot]') as HTMLElement | null; if (!dot || !nodeEls.length) return; - const mql = window.matchMedia('(max-width: 639px)'); + const mql = window.matchMedia('(width < 40rem)'); const reduceMotionMql = window.matchMedia('(prefers-reduced-motion: reduce)'); function stopAnimation() { @@ -347,6 +299,9 @@ const straightPathD = `M ${CX} ${-44} L ${CX} ${VB_H + 44}`; if (!svgPath || !trail) return; const totalLen = svgPath.getTotalLength(); + const viewBox = svgPath.ownerSVGElement?.viewBox.baseVal; + const viewBoxWidth = viewBox?.width || 400; + const viewBoxHeight = viewBox?.height || 1; const nodeCoords = nodeEls.map((el) => { const idx = Number(el.dataset.hookNode); @@ -386,49 +341,75 @@ const straightPathD = `M ${CX} ${-44} L ${CX} ${VB_H + 44}`; const cycleMs = CYCLE_DUR * 1000; const loopMs = cycleMs + REPEAT_DELAY_MS; const startedAt = performance.now(); + let frameId = 0; + let dotsVisible = false; + let lastTrailUpdate = 0; + + function getSample(v: number) { + const samplePosition = Math.min(PATH_SAMPLE_COUNT, Math.max(0, v * PATH_SAMPLE_COUNT)); + const sampleIndex = Math.floor(samplePosition); + const nextIndex = Math.min(PATH_SAMPLE_COUNT, sampleIndex + 1); + const mix = samplePosition - sampleIndex; + const sample = samples[sampleIndex]; + const next = samples[nextIndex]; + + return { + x: sample.x + (next.x - sample.x) * mix, + y: sample.y + (next.y - sample.y) * mix, + opacity: sample.opacity + (next.opacity - sample.opacity) * mix, + nearest: samples[Math.round(samplePosition)].nearest, + }; + } - function updateFrame() { - const elapsed = (performance.now() - startedAt) % loopMs; + function getTransform(sample: PathSample) { + const x = (sample.x / viewBoxWidth * 100).toFixed(4); + const y = (sample.y / viewBoxHeight * 100).toFixed(4); + return `translate3d(${x}%, ${y}%, 0)`; + } + + function setDotsVisible(visible: boolean) { + if (visible === dotsVisible) return; + dotsVisible = visible; + const visibility = visible ? 'visible' : 'hidden'; + dot!.style.visibility = visibility; + } + + function updateFrame(now: number) { + const elapsed = (now - startedAt) % loopMs; if (elapsed > cycleMs) { - dot!.setAttribute('opacity', '0'); - dotTrail?.setAttribute('opacity', '0'); - trail!.style.opacity = '0'; + if (dotsVisible) { + dot!.style.opacity = '0'; + trail!.style.opacity = '0'; + setDotsVisible(false); + } highlightNode(-1); + frameId = window.requestAnimationFrame(updateFrame); return; } const v = elapsed / cycleMs; - const sampleIndex = Math.min(PATH_SAMPLE_COUNT, Math.max(0, Math.round(v * PATH_SAMPLE_COUNT))); - const sample = samples[sampleIndex]; + const sample = getSample(v); - dot!.setAttribute('cx', String(sample.x)); - dot!.setAttribute('cy', String(sample.y)); - dot!.setAttribute('opacity', String(sample.opacity)); - dot!.setAttribute('visibility', 'visible'); - - if (dotTrail) { - const trailIndex = Math.max(0, sampleIndex - Math.round(TRAIL_LEAD * PATH_SAMPLE_COUNT)); - const trailSample = samples[trailIndex]; - dotTrail.setAttribute('cx', String(trailSample.x)); - dotTrail.setAttribute('cy', String(trailSample.y)); - dotTrail.setAttribute('opacity', String(sample.opacity * 0.35)); - dotTrail.setAttribute('visibility', 'visible'); - } + dot!.style.transform = getTransform(sample); + dot!.style.opacity = String(sample.opacity); + setDotsVisible(true); - trail!.style.strokeDashoffset = String(0.14 - v); - trail!.style.opacity = String(sample.opacity * 0.35); + if (now - lastTrailUpdate >= TRAIL_FRAME_MS) { + trail!.style.strokeDashoffset = String(0.14 - v); + trail!.style.opacity = String(sample.opacity * 0.35); + lastTrailUpdate = now; + } highlightNode(sample.nearest); + frameId = window.requestAnimationFrame(updateFrame); } - updateFrame(); - const intervalId = window.setInterval(updateFrame, HOOK_FRAME_MS); + frameId = window.requestAnimationFrame(updateFrame); stopAnim = () => { - window.clearInterval(intervalId); - dot!.setAttribute('opacity', '0'); - dot!.setAttribute('visibility', 'hidden'); - dotTrail?.setAttribute('opacity', '0'); - dotTrail?.setAttribute('visibility', 'hidden'); + window.cancelAnimationFrame(frameId); + dot!.style.opacity = '0'; + dot!.style.transform = ''; + setDotsVisible(false); trail!.style.opacity = '0'; trail!.style.strokeDashoffset = '0.14'; activeIdx = -1; From cc017bf05f255539e409b7c78c53736f76b910a5 Mon Sep 17 00:00:00 2001 From: 100gle Date: Wed, 12 Aug 2026 20:17:42 +0800 Subject: [PATCH 2/2] fix(website): show contributor avatar placeholders --- website/src/components/Contributors.astro | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/components/Contributors.astro b/website/src/components/Contributors.astro index 17966baf..8f91519a 100644 --- a/website/src/components/Contributors.astro +++ b/website/src/components/Contributors.astro @@ -19,7 +19,7 @@ const hasContributors = contributors.length > 0; {hasContributors && (
- {contributors.map((c, i) => ( + {contributors.map((c) => (
0; width="40" height="40" loading="lazy" - class="avatar-img h-full w-full object-cover" - onerror="this.hidden=true;this.parentElement.querySelector('.avatar-fallback').hidden=false" + class="avatar-img relative z-10 h-full w-full object-cover" + onerror="this.hidden=true" /> -