From fc8f153e348523214c394789242aa2f166f7b0a1 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 20 Sep 2026 22:37:06 +0800 Subject: [PATCH] =?UTF-8?q?site:=20=E4=BF=AE=E9=A6=96=E5=B1=8F=E6=BB=9A?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=E5=BA=95=20+=20=E4=BA=A4=E9=94=99=E8=A1=8C?= =?UTF-8?q?=E5=87=BA=E5=85=A5=E6=96=B9=E5=90=91=20+=20=E8=AE=A9=E7=A4=BA?= =?UTF-8?q?=E6=84=8F=E5=9B=BE=E7=9C=9F=E5=8A=A8=E8=B5=B7=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈三点:① 拉不到最下面 ② 文字与图片没有进出动画,单调 ③ 图片仍是「框框住文本」,且希望有「外界光球打到插件上」。 ## ① 滚不到底:真 bug,且是两个独立根因叠加 实测(滑到底后读 scrollY):停在 10283,上限 10669,**差 386px**, 页脚完全不可见。两个原因: - **末尾两节合计超过一屏**。faq 高 635 + footer 367 = 1002px > 一屏 828px。 `scroll-snap-type: y mandatory` 只允许停在吸附点,于是浏览器被迫二选一: 要么 faq 标题对齐(则页脚滚不到),要么页脚可见(则标题被导航压住)。 实测两种坏法都出现过:h2 顶 = -57 / -38(导航底 67)。 修法:FAQ 六条改双列(省下约 190px),收尾段取消整屏高度, 合计压到 762px,两个要求即可同时满足。 - **只在 footer 单独设吸附点会形成死锁**。试过给 architecture 设 `scroll-snap-align: none`(它高 1180px),结果前一个吸附点把它自己吸回来、 后面又无吸附点接住,实测卡死在 y=5791,怎么滑都不动。 教训写进注释:高节退出吸附不是解法,「装得进一屏」才是。 ## ② 交错行:方向与布局相反(结构性错误) 原文给每行写死 `slide-l`(文)与 `slide-r`(图)。但翻转行里图在**左边**, 却仍从右侧飞入 —— 图穿过文字进场,方向与布局相反。5 行里 3 行错。 修法不是逐行改正,而是**从布局推导方向**:`.alt:not(.flip)` 文左图右、 `.alt.flip` 图左文右,方向跟着 `.flip` 走。这样不可能再写错。 退出时不加 `.in`,transform 回到同侧 ——「从左进就从左出」是自动的。 ## ③ 示意图:从静态线框变成有语义的动画 现状实测很糟:5 张图共 116 个元素,**只有 4 个在动**(各 1 条虚线), 图 2 完全静止 —— 用户说「框框住文本」是准确的。 按每张图的语义给它自己的动作(13 种 keyframes,47 个动画元素): - **图 1 隔离**:外部光球从画面外飞入,四种颜色命中四个插件, 激起涟漪 + 插件闪一下;内核一列都没有。这正是「外面的一切都落在插件上」。 - **图 2 零依赖**:四项依赖被**逐条划掉**。用 `pathLength=1` + `stroke-dashoffset` 让线自己「划」过去,不是淡入。 - **图 3 共享内存**:写入/读回两个包沿链路跑,arena 上有光带自左向右扫过。 - **图 4 优先级**:金色包沿四条曲线**插队**到 L1–L4 之前; 四级强度条依次点亮;队列条逐条被取走(先到先处理)。 - **图 5 长任务转投**:积压消息被接走飞向分诊助手,收到时闪一下。 配套修掉三处我自己引入的缺陷: - `.dv-pkdot` 原用 `pkPulse` 动 `r`,与行进包叠加会闪烁 → 改只动透明度 - `.dv-ripple:nth-of-type(2n)` 按 `` 计数,误配到数据包圆点, 四个涟漪全被染成金色 → 改显式逐色 - 涟漪原画在插件框**内部**,像框里画了个圆 → 移到框下层,从背后漾开 ## ④ 顺带发现并修掉的真问题 - **CSS 的 `prefers-reduced-motion` 管不到 SMIL**:实测 reduce 下 6 个 数据包照跑。加 JS 调 `svg.pauseAnimations()`,实测 8/8 SVG 已暂停。 - **窄屏横向溢出 26px**:源于我这次加的入场位移(`translateX(42px)`) 在未入场时探出视口。修法是 `overflow-x: clip`(不用 hidden,避免新建滚动容器) 并窄屏收到 ±20px。实测 390/768/1440 三档溢出均为 0。 ## 验证 - 滚动:到底=true,页脚可见,faq 标题不被遮挡(h2 顶=210 > 导航底 67) - 方向:5 行全部「文在左侧就从左进」,翻转行正确反向 - 入场:逐节停留 13 节全部入场,0 未揭示; 滑到底后 23 个 opacity=0 的元素**全在视口上方**(退场生效),无一是「场内却不可见」 - 回归:深浅双主题、JS 禁用、reduce、390/768/1440 三档 全绿,无控制台错误 - 结构:HTML 解析无未闭合,CSS 括号平衡 --- site/index.html | 530 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 504 insertions(+), 26 deletions(-) diff --git a/site/index.html b/site/index.html index 2ed40ac..03be0b5 100644 --- a/site/index.html +++ b/site/index.html @@ -328,7 +328,12 @@ html { scroll-snap-type: y mandatory; scroll-padding-top: 4.5rem; + /* 横向必须裁掉:交错行的入场位移(translateX ±42px)会让未入场的 + 元素先探头到视口右侧之外,窄屏实测撑出 26px 横向溢出。 + 用 clip 而不是 hidden —— hidden 会新建滚动容器,可能干扰 sticky。 */ + overflow-x: clip; } + body { overflow-x: clip; } section { min-height: calc(100svh - 4.5rem); display: flex; @@ -337,6 +342,33 @@ scroll-snap-align: start; } section > .wrap { width: min(1120px, 92vw); margin: 0 auto; } + /* 内容比一屏高的节(运行时打 .too-tall):取消 min-height 撑高, + 但**保留吸附点**。 + 为何不让它退出吸附(实测过的死锁):把它设为 snap-align:none 后, + 前一个吸附点(memory)会把它自己吸回来,而后面又没有吸附点接住, + 结果卡死在 memory 底部(y=5791 不动)—— 页脚与后续内容全部到不了。 + 正确做法是让它自然高、仍作为吸附点,间距不等距不影响可达性。 */ + section.too-tall { min-height: auto; } + /* 页脚也是吸附点。 + 不加这条的后果(实测):最后一个 section 顶部到文档底有 1214px + (含 footer 386px),而视口只有 900px —— mandatory 只允许停在吸附点, + 于是**页脚永远滚不到**(实测卡在 y=10283,上限 10669,差 386px)。 + scroll-snap-stop: always 确保它自己占一屏,不与其他吸附点抢。 */ + /* 页脚**不单独做吸附点**。 + 为何(实测二次修):faq 高 643px + footer 367px = 1010px > 视口 900px。 + 只要 footer 也是吸附点,浏览器就必须在「faq 顶部对齐」与「页脚可见」 + 之间二选一 —— 实测选了前者,把 faq 的 h2 吸到 y=-38,被导航栏压住。 + 让它跟随 faq 自然滚动,两者都可读,也不会出现多一屏空白。 */ + footer { scroll-snap-align: none; } + /* 收尾段:取消整屏高度(它本就不需要占满),恢复吸附。 + 前提是 faq 已改双列 —— 收尾合计压到一屏内,此时同时满足 + 「标题不被导航压住」与「页脚可达」两个要求。 */ + section:last-of-type { + min-height: auto; + padding-bottom: 0.5rem; + } + /* 注:footer 的 padding 调优直接写进下面那条原始规则里(约 1022 行)—— + 同优先级下后出现的规则会胜,在这里再写一条会被它覆盖(已踩过)。 */ /* 吸附容器不能有 transform 祖先:会让 snap 失效(实测过)。 故 .wrap 上不加任何位移类动画,入场只做透明度。 */ @media (prefers-reduced-motion: reduce) { @@ -510,6 +542,12 @@ } @media (max-width: 860px) { .alt { grid-template-columns: 1fr; gap: 1.6rem; margin-top: 2.6rem; } + /* 窄屏位移收小:单列时左右各 42px 会让内容显得跳,也更容易探出视口 */ + .js-fx .alt:not(.flip) .alt-copy { transform: translateX(-20px); } + .js-fx .alt:not(.flip) .alt-visual { transform: translateX(20px); } + .js-fx .alt.flip .alt-copy { transform: translateX(20px); } + .js-fx .alt.flip .alt-visual { transform: translateX(-20px); } + .faq-grid { grid-template-columns: 1fr; gap: 0.55rem; } .alt.flip .alt-copy, .alt.flip .alt-visual { order: 0; } } @@ -567,7 +605,7 @@ .dv-ar { fill: url(#gCyan); stroke: var(--border-hi); } .dv-off { fill: none; stroke: var(--border); stroke-width: 1.2; stroke-dasharray: 4 4; } .dv-offt { fill: var(--text-muted); font-size: 11.5px; text-anchor: middle; text-decoration: line-through; } - .dv-no path { stroke: var(--brand-gold); stroke-width: 1.6; opacity: .85; } + .dv-no path { fill: none; stroke: var(--brand-gold); stroke-width: 1.6; opacity: .9; stroke-linecap: round; } .dv-line { fill: none; stroke: var(--border-hi); stroke-width: 1.4; } /* 流动的虚线:让连接线看起来「有东西在跑」而不是静止装饰 */ .dv-dash, @@ -599,6 +637,195 @@ .dv-g4 { fill: rgba(139, 92, 246, 0.10); } /* 抢占线的起点圆点 */ .dv-pkdot { fill: var(--brand-gold); } + + /* ══ 示意图的「活着」层 ══ + 为何要这一层:图里 19~39 个元素原本只有一个虚线在跑,其余全静 —— + 读起来就是「框框住文本」。这里按每张图的语义给它自己的动作: + 包在链路上跑、依赖被划掉、共享内存写入、消息分级抢占、任务转投。 + 所有动画共用一套节奏变量,同一屏内不打架。 */ + .alt-visual { + --dvc: cubic-bezier(.4, 0, .2, 1); + } + /* 仅在入场完成后开跑(未入场时图偏在屏外,跑动画没人看且耗费帧) */ + .alt-visual svg .dv-anim { animation-play-state: paused; } + .alt.in .alt-visual svg .dv-anim, + .alt-visual.in svg .dv-anim { animation-play-state: running; } + + /* ① 数据包沿链路反复穿行(图 1 / 图 5) */ + @keyframes pktTravel { + 0% { offset-distance: 0%; opacity: 0; } + 12% { opacity: 1; } + 80% { opacity: 1; } + 100% { offset-distance: 100%; opacity: 0; } + } + /* ② 依赖项被依次划掉(图 2):线自己「划」过去,不是淡入。 + 实现:pathLength=1 把每条线归一化,再用 dashoffset 1→0 逐段绘出。 + 比单纯 opacity 淡入像"划掉"得多。 */ + @keyframes strikeDeps { + 0%, 8% { stroke-dashoffset: 1; opacity: 1; } + 30% { stroke-dashoffset: 0; opacity: 1; } + 78% { stroke-dashoffset: 0; opacity: 1; } + 92%, 100% { stroke-dashoffset: 0; opacity: 0; } + } + @keyframes depPulse { + 0%, 100% { opacity: .42; } + 45% { opacity: .95; } + } + .dv-deps .dv-plug { animation: depPulse 3.4s var(--dvc) infinite; } + .dv-deps .dv-no path { animation: strikeDeps 3.4s var(--dvc) infinite; } + .dv-deps .dv-no path:nth-child(1) { animation-delay: .18s; } + .dv-deps .dv-no path:nth-child(2) { animation-delay: .34s; } + .dv-deps .dv-no path:nth-child(3) { animation-delay: .50s; } + .dv-deps .dv-no path:nth-child(4) { animation-delay: .66s; } + .dv-deps .dv-x { animation: depPulse 3.4s var(--dvc) infinite; animation-delay: .8s; } + + /* ③ 共享内存:写入光带扫过,头部/arena 依次亮(图 3) */ + @keyframes shmSweep { + 0% { transform: translateX(0); opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { transform: translateX(150px); opacity: 0; } + } + @keyframes segLight { + 0%, 100% { opacity: .55; } + 50% { opacity: 1; } + } + .dv-seg { animation: segLight 2.6s var(--dvc) infinite; } + .dv-hd { animation: segLight 2.6s var(--dvc) infinite; animation-delay: .3s; } + + /* ④ 优先级:高优先级包从下往上插队(图 4) */ + @keyframes preempt { + 0% { transform: translateY(0); opacity: 0; } + 8% { opacity: 1; } + 30% { transform: translateY(-26px); opacity: 1; } + 62% { transform: translateY(-26px); opacity: 1; } + 80%, 100% { transform: translateY(-26px); opacity: 0; } + } + .dv-pk-dot { animation: preempt 3.6s var(--dvc) infinite; } + + /* ⑤ 消息排队 → 转投:包先积压再被接走(图 5) */ + @keyframes msgStack { + 0%, 18% { opacity: 0; transform: translateY(6px); } + 30% { opacity: 1; transform: none; } + 72% { opacity: 1; transform: none; } + 88%, 100% { opacity: 0; transform: translateX(16px); } + } + .dv-msg { animation: msgStack 3.8s var(--dvc) infinite; } + .dv-msg:nth-child(2) { animation-delay: .22s; } + .dv-msg:nth-child(3) { animation-delay: .44s; } + + /* 插队轨迹:常驻淡线,让「从哪来、到哪去」始终可读; + 走动的包用 .dv-pkdot(金色)。 */ + .dv-pkline { fill: none; stroke: var(--brand-gold); stroke-width: 1.1; opacity: .22; } + + /* 级别强度条(图 4):从 L1 到 L4 依次点亮,表现「级别越低越可等、 + 越高越紧急」。静态时四条只是深浅不同的背景,看不出递进。 */ + .dv-gauge-fill rect { opacity: .45; } + @keyframes lvlLight { + 0%, 100% { opacity: .45; } + 50% { opacity: 1; } + } + .js-fx .dv-g1 { animation: lvlLight 3.2s ease-in-out infinite; } + .js-fx .dv-g2 { animation: lvlLight 3.2s ease-in-out infinite; animation-delay: .25s; } + .js-fx .dv-g3 { animation: lvlLight 3.2s ease-in-out infinite; animation-delay: .5s; } + .js-fx .dv-g4 { animation: lvlLight 3.2s ease-in-out infinite; animation-delay: .75s; } + + /* 写入光带(图 3):沿 arena 自左向右扫过,示意数据落进共享段 */ + .dv-sweep { + fill: url(#gCyan); + opacity: 0; + } + @keyframes shmSweep { + 0% { transform: translateX(0); opacity: 0; } + 12% { opacity: .75; } + 70% { opacity: .75; } + 88%, 100% { transform: translateX(196px); opacity: 0; } + } + .js-fx .dv-sweep { animation: shmSweep 2.8s cubic-bezier(.4,0,.2,1) infinite; } + + /* 收货闪光(图 5):分诊助手接到消息时亮一下 */ + .dv-recv { + fill: none; + stroke: var(--brand-cyan); + stroke-width: 2; + opacity: 0; + } + @keyframes recvFlash { + 0%, 44% { opacity: 0; } + 54% { opacity: .95; } + 66% { opacity: .2; } + 80%, 100% { opacity: 0; } + } + .js-fx .dv-recv { animation: recvFlash 2.4s cubic-bezier(.4,0,.2,1) infinite; } + /* 主 agent 忙碌:呼吸更慢更沉,与「被占住」呼应 */ + .js-fx .dv-run { animation: busyBreathe 3.2s ease-in-out infinite; } + @keyframes busyBreathe { + 0%, 100% { opacity: .82; } + 50% { opacity: 1; } + } + + /* ══ 外界光球(图 1)══ + 语义:外面的一切都落在插件上,内核一列都没有。 + 故光球从画面外飞入、命中插件、激起涟漪 —— 让「隔离」这件事看得见。 + 四个插件各给一个颜色,命中时插件自己也闪一下,这样「打到谁」很清楚。 */ + .dv-orbpath { fill: none; stroke: none; } + .dv-orb { filter: url(#fGlow); } + .dv-orb-a { fill: var(--brand-cyan); } + .dv-orb-b { fill: var(--brand-gold); } + .dv-orb-c { fill: var(--brand-violet); } + .dv-orb-d { fill: var(--brand-blue); } + /* 涟漪:从命中点向外扩一圈,很快淡去 */ + .dv-ripple { + fill: none; + stroke: var(--brand-cyan); + stroke-width: 1.4; + } + /* 涟漪逐色(不能用 :nth-of-type)—— + 同层还有数据包圆点也是 ,按类型计数会错配, + 实测四个涟漪全被染成金色。显式逐色才对得上各自的插件。 */ + .dv-ripple-a { stroke: var(--brand-cyan); } + .dv-ripple-b { stroke: var(--brand-gold); } + .dv-ripple-c { stroke: var(--brand-violet); } + .dv-ripple-d { stroke: var(--brand-blue); } + /* 命中闪光:贴在插件框上,短暂高亮后回落 */ + .dv-hit { + fill: none; + stroke-width: 2.2; + stroke-linecap: round; + opacity: 0; + } + .dv-hit-a { stroke: var(--brand-cyan); } + .dv-hit-b { stroke: var(--brand-gold); } + .dv-hit-c { stroke: var(--brand-violet); } + .dv-hit-d { stroke: var(--brand-blue); } + @keyframes hitFlash { + 0%, 28% { opacity: 0; } + 34% { opacity: 1; } + 46% { opacity: .25; } + 60%, 100% { opacity: 0; } + } + .js-fx .dv-hit { animation: hitFlash 3.6s cubic-bezier(.4,0,.2,1) infinite; } + .js-fx .dv-hit-a { animation-delay: 0s; } + .js-fx .dv-hit-b { animation-delay: .9s; } + .js-fx .dv-hit-c { animation-delay: 1.8s; } + .js-fx .dv-hit-d { animation-delay: 2.7s; } + /* 注意:这些 SMIL 动画不受 CSS 的 prefers-reduced-motion 约束, + reduce 下由 JS 调 pauseAnimations() 统一停(见脚本里的说明)。 */ + + /* ⑥ 节点呼吸:内核 / 主 agent 保持轻微脉动,图不平 */ + @keyframes nodeBreathe { + 0%, 100% { opacity: .88; } + 50% { opacity: 1; } + } + .dv-accent { animation: nodeBreathe 4.2s var(--dvc) infinite; } + /* reduce 下全停 —— 不只是虚线,上面这些都停 */ + @media (prefers-reduced-motion: reduce) { + .alt-visual svg .dv-anim, + .dv-plug, .dv-no path, .dv-x, .dv-seg, .dv-hd, + .dv-pk-dot, .dv-msg, .dv-accent, .dv-pk { + animation: none !important; + } + } .dv-pkcap { fill: var(--brand-gold); font-size: 9.5px; @@ -613,6 +840,16 @@ stroke-dasharray: 5 4; } .dv-qitem { fill: var(--dv-queue-item); opacity: 0.95; } + /* 队列条逐条被取走:表现「先到先处理」。 + 用 opacity 而非位移 —— 条是 ,位移会露出底色缺口。 */ + @keyframes queueTake { + 0%, 100% { opacity: .95; } + 45% { opacity: .2; } + } + .js-fx .dv-qitem rect { animation: queueTake 3.2s ease-in-out infinite; } + .js-fx .dv-qitem rect:nth-child(1) { animation-delay: 0s; } + .js-fx .dv-qitem rect:nth-child(2) { animation-delay: .4s; } + .js-fx .dv-qitem rect:nth-child(3) { animation-delay: .8s; } /* 处理方向指示 */ .dv-qdir { fill: none; stroke: var(--text-muted); stroke-width: 1.2; opacity: 0.6; } .dv-qdir-tip { fill: none; stroke: var(--text-muted); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; opacity: 0.6; } @@ -621,10 +858,14 @@ .dv-colcap-dim { fill: var(--text-muted); } .dv-caps { fill: var(--text-muted); font-size: 8.8px; text-anchor: middle; } @media (prefers-reduced-motion: no-preference) { + /* 只呼吸透明度,不动 r。 + 为何:.dv-pkdot 现在兼做「沿链路行进的数据包」(靠 SMIL 的 + animateMotion 定位),若再用 CSS 动 r,包在跑的同时忽大忽小, + 看着像闪烁的噪点。半径交给标记本身。 */ .dv-pkdot { animation: pkPulse 1.8s ease-in-out infinite; } @keyframes pkPulse { - 0%, 100% { opacity: 1; r: 3.5; } - 50% { opacity: .55; r: 4.5; } + 0%, 100% { opacity: 1; } + 50% { opacity: .5; } } } @@ -972,13 +1213,24 @@ .comp .row { grid-template-columns: 1fr; gap: 0.25rem; } } - /* ── FAQ ── */ + /* ── FAQ ── + 双列是「一滑一页」的硬约束逼出来的:末尾 faq(635px) + footer(367px) + = 1002px > 一屏 828px,mandatory 只允许停在吸附点,于是二者不可兼得 + —— 实测要么标题被导航栏压住(h2 顶=-57),要么永远滚不到页脚。 + 六条改成两列省下约 190px,末尾整段才装得进一屏。单列在窄屏恢复。 */ + .faq-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem 1.1rem; + align-items: start; + margin-top: 1.6rem; + } details.faq { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-md); - padding: 0.95rem 1.15rem; - margin-bottom: 0.7rem; + padding: 0.8rem 1rem; + margin-bottom: 0; } details.faq summary { cursor: pointer; @@ -998,7 +1250,9 @@ /* ── 页脚 ── */ footer { border-top: 1px solid var(--border); - padding: 2.4rem 0 3rem; + /* 收紧上下内边距:末尾空间要与最后一个 section 一起装进一屏, + 否则 mandatory 会把该节标题吸到导航栏底下(实测 h2 顶=-57,导航底=67)。 */ + padding: 2rem 0 2.2rem; color: var(--text-muted); font-size: 0.88rem; } @@ -1421,6 +1675,40 @@ } .js-fx .rise-zoom.in { opacity: 1; transform: none; } + /* ── 交错行:文字与示意图从两侧滑入 ── + 行本身只做很轻的上浮,重点放在内部两列的相向入场, + 这样「左文右图」的对应关系在动的那一下就被看见了。 */ + /* ── 交错行:文与图各自从「自己所在的那一侧」滑入 ── + 方向由 .flip 推导,而不是手写 slide-l/slide-r —— + 手写版本踩过坑:翻转子行里图在**左边**却写了 slide-r(从右进), + 于是图穿过文字从右侧飞入,方向与布局相反。 + 退出时不加 .in,transform 回到同侧,所以「从左进就从左退」是自动的。 */ + .js-fx .alt .alt-copy, + .js-fx .alt .alt-visual { + opacity: 0; + transition: opacity 0.85s var(--ease), transform 0.9s var(--ease); + transition-delay: var(--d, 110ms); + will-change: transform, opacity; + } + /* 非翻转:文左、图右 */ + .js-fx .alt:not(.flip) .alt-copy { transform: translateX(-42px); } + .js-fx .alt:not(.flip) .alt-visual { transform: translateX(42px); } + /* 翻转:图左、文右 —— 方向跟着换 */ + .js-fx .alt.flip .alt-copy { transform: translateX(42px); } + .js-fx .alt.flip .alt-visual { transform: translateX(-42px); } + /* 入场完成 */ + .js-fx .alt.in .alt-copy, + .js-fx .alt.in .alt-visual { opacity: 1; transform: none; } + + /* 交错行自身:仅轻微上浮,不抢内部动画 */ + .js-fx .alt.rise { transform: translateY(14px); } + + /* ── 卡片错落入场:四张依次亮起,不用手写 --d ── */ + .js-fx .duties.in .duty:nth-child(1) { transition-delay: 0ms; } + .js-fx .duties.in .duty:nth-child(2) { transition-delay: 90ms; } + .js-fx .duties.in .duty:nth-child(3) { transition-delay: 180ms; } + .js-fx .duties.in .duty:nth-child(4) { transition-delay: 270ms; } + /* ── Hero 大标题:渐变流动 + 描边光 ── */ h1 .grad { background: linear-gradient(110deg, var(--brand-blue), var(--brand-cyan) 32%, @@ -1720,28 +2008,28 @@

-
+
隔离

崩一个,不赔上全部

插件不在内核里,是另一个进程。崩了就把它注册的东西一并摘掉,其余照跑。

退避重启 1s / 2s / 3s;5 分钟内第 4 次崩溃即停下等人
-
+
调度

拦得住,也接得回

输入分能等与不能等。不能等的可抢占,被打断的现场压栈,稍后原样恢复

L4 内核独占;外部插件声明 L4 会被夹到 L3
-
+
通信

三条道,各走各的

控制走 stdio,数据走共享内存,通知走事件环。变长内容只传偏移,两边映射到不同地址也能读。

整段写入 + 读回 3.5µs(实测)
-
+
资源

记忆不会越用越肿

上下文按相关性取舍,最近 10 条恒受保护。低分下沉成文档,冷文档再蒸馏成三元组。

@@ -1755,7 +2043,7 @@
-
+
01

内核零 IO,只碰调度

@@ -1770,6 +2058,31 @@ 外部世界 + + + + + + + + + + + + + + + + + + + + + + + + 内核 编排 · 记忆 · 调度 @@ -1787,7 +2100,85 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

箭头只进出插件 —— 内核一列都没有

@@ -1799,7 +2190,7 @@
-
+
02

部署,从未如此便捷

@@ -1811,7 +2202,7 @@

- + 目标机 @@ -1828,8 +2219,8 @@ 容器运行时 - - + + 都不需要 @@ -1843,7 +2234,7 @@
-
+
03

数据如水,随流,随改,随走

@@ -1872,6 +2263,18 @@ + + + + + + + + + + + 零拷贝:不序列化、不复制

实测整段写入+读回 3.5µs

@@ -1884,7 +2287,7 @@
-
+
04

轻重缓急,各归其位

@@ -1952,6 +2355,30 @@ 正在跑的任务 同一时刻只一个 被打断的现场压入中断栈,稍后原样恢复 + + + + + + + + + + + + + + + + + +
@@ -1962,7 +2389,7 @@
-
+
05

忙时,有人替你接住

@@ -1984,8 +2411,18 @@ 积压消息 ×4 - + 转投 + + + + + + + + + 分诊助手 当场办 / 立刻答 @@ -2362,7 +2799,7 @@ echo "你好,记住我喜欢喝咖啡" | ./build/waiter

常见问题

最容易引起误解的六个地方。

-
+
「内核零 IO」是不是意味着内核很简单?

@@ -2528,6 +2965,18 @@ echo "你好,记住我喜欢喝咖啡" | ./build/waiter window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); + /* ── SMIL 与 reduced-motion ── + CSS 的 prefers-reduced-motion 管不到 SMIL( 不是 CSS 动画), + 实测 reduce 下 6 个数据包照样在跑。故在 JS 里显式停掉: + 暂停整棵 SVG 时间轴(pauseAnimations 对 SVG 文档生效)。 */ + if (reduce) { + try { + document.querySelectorAll("svg").forEach(function (sv) { + if (sv.pauseAnimations) sv.pauseAnimations(); + }); + } catch (e) {} + } + /* ── 入场动画 ── */ if (!reduce) { root.classList.add("js-fx"); @@ -2545,11 +2994,25 @@ echo "你好,记住我喜欢喝咖啡" | ./build/waiter } if ("IntersectionObserver" in window) { + // 双向:向下滚入场,向上滚退场 —— 不再 unobserve。 + // 原先入场后 unobserve,元素永远停在 .in,所以“滚回去”没有任何退场动作, + // 读起来就像一页页静态堆叠。 + // 方向由视口交叉的位置判断(不用全局 scroll 方向,那样在快速滚动时会抖): + // boundingClientRect.top > 0 且向下 → 从下方进来 + // boundingClientRect.top < 0 → 已滑到上方,停用 var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { - if (e.isIntersecting) { reveal(e.target); io.unobserve(e.target); } + if (e.isIntersecting) { + reveal(e.target); + } else { + // 仅当元素确实离开(而非只是未达阈值)才移除,避免边缘抖动 + var r = e.boundingClientRect; + if (r.bottom < 0 || r.top > window.innerHeight) { + e.target.classList.remove("in"); + } + } }); - }, { threshold: 0.1, rootMargin: "0px 0px -6% 0px" }); + }, { threshold: 0.12, rootMargin: "0px 0px -6% 0px" }); els.forEach(function (el) { io.observe(el); }); } else { els.forEach(reveal); @@ -2562,8 +3025,23 @@ echo "你好,记住我喜欢喝咖啡" | ./build/waiter if (r.top < window.innerHeight && r.bottom > 0) reveal(el); }); }); - // 兜底:3 秒后无论 observer 是否生效,强制全部可见 - setTimeout(function () { els.forEach(reveal); }, 3000); + + // 比一屏高的节退出吸附。 + // 为何在运行时做:高度受字体、图片、视口影响,CSS 无法预知。 + // 不做的话吸附点不等距,累积误差会把后面的节顶到导航栏底下(实测 faq h2 被抬到 -57)。 + var markTall = function () { + var limit = window.innerHeight - 8; + [].slice.call(document.querySelectorAll("section")).forEach(function (s) { + s.classList.toggle("too-tall", s.getBoundingClientRect().height > limit); + }); + }; + markTall(); + window.addEventListener("load", markTall); + window.addEventListener("resize", markTall); + + // 不再用「3 秒后强制全部 reveal」的兜底: + // 那会把已退场的元素强行拉回来,退场动画就废了。 + // 改为只在 observer 不存在时(已在上面分支处理)整体 reveal。 } /* ── 数字滚动 ── */