svg-stroke-reveal
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSkill: svg-stroke-reveal
Skill: svg-stroke-reveal
Component vanilla HTML/CSS/JS — nét trong SVG "vẽ" dần ra như có tay đang kẻ, theo % cuộn qua viewport hoặc một lần khi vào khung nhìn. Không dùng thư viện ngoài (không Anime.js, không GSAP).
<path>基于原生 HTML/CSS/JS 的组件——让 SVG 中的 描边像被手绘一样,根据滚动穿过 viewport 的百分比,或在进入视口时一次性绘制出来。不使用外部库(不使用 Anime.js,也不使用 GSAP)。
<path>WHAT
是什么
Purpose và context
目的与上下文
- Purpose: nét trong SVG "vẽ" dần ra theo % cuộn hoặc một lần khi vào viewport (stroke-dasharray/dashoffset), vanilla không lib ngoài; mặc định dùng bản gốc nguyên văn ở
<path>.Rheinmir/uiux-asset - Trigger (when to use):
- Logo/chữ ký/icon cần hiệu ứng "vẽ dần" khi vào viewport (chạy 1 lần).
- Biểu đồ đường, sơ đồ hành trình, đường route cần "vẽ ra" liên tục theo tiến trình cuộn (gắn với vị trí cuộn thật, không phải chạy 1 lần).
- User nói "vẽ svg khi cuộn", "svg stroke draw", "logo vẽ dần", "path animation reveal", "stroke-dashoffset", hoặc /svg-stroke-reveal.
- Non-goals: KHÔNG dùng cho 7 hiệu ứng cuộn chung (opacity/translate reveal, stagger text, highlight, nav co, scroll-to-top, scrollspy, parallax) — đó là . Không dùng cho
skills/scroll-effectschưa convert outline (xem Rules).<text>
- 目的: SVG 中的 描边根据滚动百分比,或在进入 viewport 时一次性“绘制”出来;使用原生实现,不依赖外部库;默认原封不动地使用
<path>中的源文件。Rheinmir/uiux-asset - 触发条件(何时使用):
- Logo、签名或图标需要在进入 viewport 时呈现“逐步绘制”效果(仅运行一次)。
- 折线图、路线图或 route 需要根据实际滚动进度连续“绘制”出来(与真实滚动位置绑定,而不是只运行一次)。
- 用户说“滚动时绘制 SVG”“svg stroke draw”“logo 逐步绘制”“path animation reveal”“stroke-dashoffset”或 。
/svg-stroke-reveal
- 非目标: 不用于 7 种通用滚动效果(opacity/translate reveal、文本 stagger、highlight、导航收缩、scroll-to-top、scrollspy、parallax)——这些属于 。不用于尚未转换为 outline 的
skills/scroll-effects(见规则)。<text>
Mental model
思维模型
[data-svgr] > <path>×N → getTotalLength() → dasharray = dashoffset = length (ẩn) → mode "once" (IO một lần + CSS transition) hoặc mode "scroll" (IO bật cờ + 1 listener rAF ghi dashoffset theo progress) → reduce-motion: dashoffset = 0 ngay[data-svgr] > <path>×N → getTotalLength() → dasharray = dashoffset = length(隐藏)→ mode "once"(IO 一次 + CSS transition)或 mode "scroll"(IO 开启标志 + 1 个 rAF listener 记录基于进度的 dashoffset)→ reduce-motion:立即将 dashoffset = 0Input và output contract
输入输出约定
| Field | Required? | Ý nghĩa | |
|---|---|---|---|
| In | SVG thật của dự án (các | có | |
| In | mode | có | logo/chữ ký → once; biểu đồ/hành trình → scroll |
| In | | không | có thì đổi token màu theo nó |
| Out | SVG gắn | có | copy nguyên file gốc hoặc adapt |
| Out | bằng chứng verify | có | ảnh trước/giữa/sau (scroll) + once chỉ trigger 1 lần |
| 字段 | 必填? | 含义 | |
|---|---|---|---|
| 输入 | 项目中真实的 SVG(带描边的 | 是 | |
| 输入 | mode | 是 | logo/签名 → once;图表/路线 → scroll |
| 输入 | 项目的 | 否 | 如有,则按其调整颜色 token |
| 输出 | 带有 | 是 | 原样复制源文件,或改编 |
| 输出 | | 是 | 滚动前/中/后截图 + 确认 once 仅触发一次 |
Rules và capabilities
规则与能力
- RULE-01 (MUST): đo trên toạ độ nội tại của path, KHÔNG bị ảnh hưởng bởi CSS transform — nhưng nếu bạn scale phần tử chứa path bằng
getTotalLength()vớitransform: scale(sx, sy)(scale KHÔNG ĐỀU hai trục) SAU khi đã set dasharray, tốc độ vẽ theo px sẽ méo lệch giữa hai trục vì animation vẫn chạy theo length gốc trong khi hiển thị đã bị bóp méo tỷ lệ. Luôn set kích thước quasx != sy+viewBox/widthđồng nhất tỉ lệ, tránh CSS scale lệch trục trên chính phần tử mang path.height - RULE-02 (MUST): không stroke-draw được — phải convert chữ/font sang outline
<text>thật trước (Illustrator "Create Outlines", Figma "Flatten", hoặc tool convert font-to-path) rồi mới áp<path>. Áp thẳng lên[data-svgr]sẽ không có gì xảy ra vì<text>không có<text>.getTotalLength() - RULE-03 (MUST): Mode "once" dùng CSS transition (mượt, rẻ CPU); mode "scroll" set dashoffset trực tiếp mỗi frame qua JS (bắt buộc để đồng bộ khít với vị trí cuộn) — đừng đổi ngược hai cách này.
- RULE-04 (MUST): Một riêng cho mỗi mode (không gộp chung logic once/scroll vào 1 observer vì hành vi khác nhau: unobserve-1-lần vs toggle cờ liên tục) và một scroll listener rAF-throttle DUY NHẤT cho tất cả SVG mode "scroll" trên trang — không gắn nhiều listener
IntersectionObserverrời cho mỗi SVG.scroll - RULE-05 (MUST): Verify bằng : chụp trước/giữa/sau khi cuộn qua SVG mode "scroll" để thấy dashoffset giảm dần, và assert SVG mode "once" chỉ trigger 1 lần dù cuộn ra vào nhiều lần.
playwright-verify - Capabilities: đọc bản gốc (web/git) + asset skill + ; ghi HTML/CSS/JS vào dự án đích; chạy trình duyệt headless để verify.
design.md
- RULE-01(必须): 测量的是 path 的固有坐标系,不受 CSS transform 影响——但是,如果在设置 dasharray 之后,再通过
getTotalLength()对承载 path 的容器进行缩放,且transform: scale(sx, sy)(两轴缩放比例不一致),那么按 px 计算的绘制速度会在两个轴之间产生偏差,因为动画仍按原始 length 运行,而显示结果已被非均匀缩放。始终通过sx != sy+viewBox/width设置尺寸并保持比例一致,避免在直接承载 path 的元素上使用非等比 CSS scale。height - RULE-02(必须): 无法进行 stroke-draw——必须先将文字/字体转换为真实的 outline
<text>(Illustrator "Create Outlines"、Figma "Flatten",或使用 font-to-path 转换工具),然后才能应用<path>。直接应用到[data-svgr]上不会产生任何效果,因为<text>没有<text>。getTotalLength() - RULE-03(必须): mode "once" 使用 CSS transition(流畅、CPU 开销低);mode "scroll" 必须通过 JS 在每一帧直接设置 dashoffset(这是与实际滚动位置紧密同步的必要条件)——不要互换这两种实现方式。
- RULE-04(必须): 每种 mode 使用独立的 (不要把 once/scroll 逻辑合并到同一个 observer 中,因为两者行为不同:一次性 unobserve 与持续切换标志),并且页面上所有 mode 为 "scroll" 的 SVG 只能共用一个经过 rAF 节流的 scroll listener——不要为每个 SVG 分别绑定独立的
IntersectionObserverlistener。scroll - RULE-05(必须): 使用 进行验证:滚动经过 mode "scroll" 的 SVG 时拍摄前/中/后截图,以显示 dashoffset 逐渐减小,并断言 mode "once" 即使多次滚出再滚入也只触发一次。
playwright-verify - 能力:读取源文件(web/git)+ asset skill + ;将 HTML/CSS/JS 写入目标项目;运行 headless 浏览器进行验证。
design.md
Failure boundaries
失败边界
- Áp lên chưa convert outline → không có gì xảy ra: blocked, yêu cầu convert sang
<text>trước (RULE-02).<path> - Phần tử mang path bị CSS scale lệch trục → tốc độ vẽ méo: failed, chuyển kích thước qua +
viewBox/width(RULE-01).height - Assert verify đỏ (dashoffset không giảm dần, once trigger lại) → failed, sửa rồi verify lại.
- 应用到尚未转换为 outline 的 → 不会产生任何效果:blocked,要求先转换为
<text>(RULE-02)。<path> - 承载 path 的元素使用非等比 CSS scale → 绘制速度发生偏差:failed,改用 +
viewBox/width设置尺寸(RULE-01)。height - 验证断言失败(dashoffset 没有逐渐减小,once 再次触发)→ failed,修复后重新验证。
HOW
如何执行
Main workflow
主流程
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | SVG + yêu cầu | Chọn mode | mode | |
| W02 | effect | bảng Pen gốc | Mở link chạy, copy nguyên file | file gốc trong dự án | không được kéo lib ngoài → B01 |
| W03 | effect | file + SVG thật + | Chỉ sửa nội dung/ảnh/màu (thay path | SVG đã adapt | — |
| W04 | deterministic | trang chạy qua HTTP | Verify bằng | ảnh + assert | đỏ → sửa, lặp W04 |
| 步骤 | 类型 | 输入 | 操作 | 输出/退出条件 | 失败/下一步 |
|---|---|---|---|---|---|
| W01 | 判断 | SVG + 需求 | 选择 mode | mode | |
| W02 | 效果 | 源 Pen | 打开可运行的链接,将原始 | 项目中的源文件 | 不允许引入外部库 → B01 |
| W03 | 效果 | 文件 + 真实 SVG + | 只修改内容/图片/颜色(替换 path 的 | 已适配的 SVG | — |
| W04 | 确定性 | 通过 HTTP 运行的页面 | 使用 | 截图 + 断言 | 失败 → 修复并重复 W04 |
Branches
分支
| ID | Kind | Guard | Hành vi | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | conditional_required | dự án KHÔNG được kéo lib ngoài (bản gốc) | dùng bản rút gọn vanilla | — | W04 |
| B02 | conditional_required | | | — | W04 |
| ID | 类型 | 守卫条件 | 行为 | 跳过/失败 | 汇合点 |
|---|---|---|---|---|---|
| B01 | 条件必需 | 项目不允许引入外部库(源文件) | 使用精简的原生 | — | W04 |
| B02 | 条件必需 | | 初始化时立即设置 | — | W04 |
Validation và stopping
验证与停止条件
Assert tất định ở W04 (mode scroll: dashoffset giảm dần qua ảnh trước/giữa/sau; mode once: chỉ trigger 1 lần dù cuộn ra vào). Cần mắt: độ mượt, thứ tự nét. Dừng khi assert xanh.
在 W04 执行确定性断言(mode scroll:通过前/中/后截图确认 dashoffset 逐渐减小;mode once:即使多次滚出再滚入,也只触发一次)。还需人工检查:流畅度和描边顺序。断言通过后停止。
Examples
示例
- Positive: "logo vẽ dần khi vào màn hình" → trên logo 4 path → vào viewport 30% thì vẽ lần lượt (stagger 160ms theo thứ tự DOM), cuộn ra vào lại không vẽ lại.
data-svgr="once" - Boundary/failure: chữ ký là font script → không có
<text>, không vẽ gì → dừng, yêu cầu convert outline (Figma "Flatten") thànhgetTotalLength()rồi mới áp<path>.[data-svgr]
- 正向: “logo 进入屏幕时逐步绘制” → 在包含 4 个 path 的 logo 上设置 → 进入 viewport 达 30% 时依次绘制(按 DOM 顺序以 160ms stagger)→ 滚出后再滚入不会重新绘制。
data-svgr="once" - 边界/失败: 签名采用 script 字体的 → 没有
<text>,不会绘制 → 停止,要求先通过 Figma "Flatten" 转换为getTotalLength(),然后再应用<path>。[data-svgr]
Reference — Bản gốc — dùng TRƯỚC (copy nguyên file, đừng viết lại)
参考 — 源文件 — 优先使用(完整复制,不要重写)
Code gốc NGUYÊN VĂN của tác giả (CodePen public = MIT) nằm ở repo ngoài — gallery chạy thật: https://rheinmir.github.io/uiux-asset/scroll-effects/ .
Quy trình: mở link chạy để xem đúng hiệu ứng → copy nguyên file (+ thư mục nếu file trỏ tới) vào dự án → giữ comment ghi công dòng đầu → chỉ sửa nội dung/ảnh/màu. Chạy local qua HTTP, không .
nếu cần cả bộ offline.
Rheinmir/uiux-asset.html_assets/file://git clone --depth 1 https://github.com/Rheinmir/uiux-asset| Pen gốc — tác giả | Link |
|---|---|
| Interactive Cicada Genomics Landing Page — alyona-mysiura | chạy · code |
| Scroll-Driven SVG Map Editorial Gallery — knyttneve | chạy · code |
| Scroll-Driven Godzilla Walk-and-Destroy Animation — creativeocean | chạy · code |
assets/作者的原始代码(CodePen public = MIT)位于外部仓库 ——实际运行 gallery:https://rheinmir.github.io/uiux-asset/scroll-effects/ 。
流程:打开可运行链接以查看正确效果 → 将原始 文件(如果文件有引用,还包括 目录)完整复制到项目 → 保留首行署名注释 → 只修改内容/图片/颜色。通过 HTTP 在本地运行,不要使用 。
如需离线获取整套资源,可执行 。
Rheinmir/uiux-asset.html_assets/file://git clone --depth 1 https://github.com/Rheinmir/uiux-asset| 源 Pen — 作者 | 链接 |
|---|---|
| Interactive Cicada Genomics Landing Page — alyona-mysiura | 运行 · 代码 |
| Scroll-Driven SVG Map Editorial Gallery — knyttneve | 运行 · 代码 |
| Scroll-Driven Godzilla Walk-and-Destroy Animation — creativeocean | 运行 · 代码 |
本 skill 中的 只是手写的精简 vanilla 版本——仅在项目不允许引入外部库时使用;默认使用上述源文件。
assets/Reference — Assets
参考 — Assets
assets/demo.htmlassets/svg-stroke-reveal.cssassets/svg-stroke-reveal.jsd=design.mdassets/demo.htmlassets/svg-stroke-reveal.cssassets/svg-stroke-reveal.jsd=design.mdReference — Cơ chế
参考 — 实现机制
- Với MỖI trong
<path>: JS gọi[data-svgr], set inlinepath.getTotalLength()vàstyle.strokeDasharray = lengthlúc init → nét ẩn hoàn toàn (dash đúng bằng chiều dài path, offset đẩy hết dash ra ngoài).style.strokeDashoffset = length - Mode (threshold 1 lần — logo/chữ ký):
data-svgr="once"threshold 0.3, khi vào viewport → setIntersectionObservercho tất cả path con → CSSstrokeDashoffset = 0chạy 1 lần →transition: stroke-dashoffsetngay sau khi trigger, không lặp lại kể cả cuộn ra rồi cuộn lại.unobserve - Mode (progress liên tục — biểu đồ/hành trình):
data-svgr="scroll"chỉ bật/tắt cờ "đang trong viewport"; khi đang bật, một scroll listener rAF-throttle (dùng chung 1 listener, không tạo nhiều) tínhIntersectionObserverkẹp 0..1, rồi ghi trực tiếpprogress = (innerHeight - rect.top) / (innerHeight + rect.height)mỗi frame — KHÔNG dùng CSS transition ở mode này (transition sẽ làm nét trễ nhịp so với vị trí cuộn thật).strokeDashoffset = length * (1 - progress) - Path phức tạp nhiều con (logo nhiều nét, chữ ký) — mode "once" stagger bằng
<path>, JS gántransition-delay: calc(var(--svgr-i) * 160ms)theo THỨ TỰ path trong DOM (path đầu vẽ trước).--svgr-i - → JS set
prefers-reduced-motion: reducengay cho toàn bộ path lúc init, bỏ qua cả hai observer/listener — hiện full nét tức thì, không animate.strokeDashoffset = 0
- 对于 中的每个
[data-svgr]:JS 在初始化时调用<path>,设置内联path.getTotalLength()和style.strokeDasharray = length→ 描边完全隐藏(dash 与 path 长度相等,offset 将整个 dash 推出范围之外)。style.strokeDashoffset = length - Mode (单次 threshold——logo/签名):
data-svgr="once"threshold 为 0.3,进入 viewport 后 → 将所有子 path 的IntersectionObserver→ CSSstrokeDashoffset = 0运行一次 → 触发后立即transition: stroke-dashoffset,即使滚出后再滚入也不会重复。unobserve - Mode (连续进度——图表/路线):
data-svgr="scroll"仅用于开启/关闭“当前位于 viewport 内”的标志;标志开启时,由一个经过 rAF 节流的 scroll listener(全局共用一个,不创建多个)计算IntersectionObserver,并限制在 0..1 范围内,然后每帧直接写入progress = (innerHeight - rect.top) / (innerHeight + rect.height)——此 mode 不使用 CSS transition(transition 会使描边进度落后于真实滚动位置)。strokeDashoffset = length * (1 - progress) - 对包含多个复杂子 的元素(多笔画 logo、签名)——mode "once" 使用
<path>实现 stagger,JS 按 path 在 DOM 中的顺序分配transition-delay: calc(var(--svgr-i) * 160ms)(第一个 path 先绘制)。--svgr-i - → JS 在初始化时立即为所有 path 设置
prefers-reduced-motion: reduce,跳过两个 observer/listener——立即显示完整描边,不执行动画。strokeDashoffset = 0