mask-reveal-transition
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSkill: mask-reveal-transition
Skill: mask-reveal-transition
Component vanilla HTML/CSS/JS — ảnh lộ ra qua một mask hình dạng hữu cơ (vòng loang mực, tâm cố định, bán kính tăng dần) thay vì crossfade/opacity thường. Hai biến thể dùng chung một cơ chế nền: đo tiến trình cuộn TRONG section bằng , map về 0..1, rồi ghi bán kính loang lên CSS custom property.
getBoundingClientRect()基于 vanilla HTML/CSS/JS 的组件——图像通过有机形状的 mask 显露(墨迹飞溅斑块,中心固定,半径逐渐增大),而不是常见的 crossfade/opacity。这两个变体共享同一套底层机制:在 section 内使用 测量滚动进度,映射到 0..1,再将斑块半径写入 CSS custom property。
getBoundingClientRect()WHAT
WHAT(是什么)
Purpose và context
目的与上下文
- Purpose: lộ/chuyển ảnh qua mask hình dạng hữu cơ (vòng loang mực) theo tiến trình cuộn, hoặc pin split-screen sticky đổi ảnh qua mask theo panel đang đọc — ưu tiên code gốc nguyên văn ở ,
Rheinmir/uiux-assetlà bản vanilla nhẹ dự phòng.assets/ - Trigger (when to use):
- Chuyển ảnh nghệ thuật/portfolio kiểu "vết mực loang" đồng bộ theo cuộn (không phải crossfade phẳng).
- Layout case-study/portfolio 2 cột: ảnh cố định bên trái, đoạn văn cuộn bên phải, ảnh đổi theo đoạn đang đọc.
- Non-goals: KHÔNG dùng cho reveal đơn giản (mờ + trượt nhẹ khi vào viewport) — đó là pattern #1, không có mask hình dạng. KHÔNG dùng cho chuyển ảnh toàn màn hình kiểu zoom-blur nặng GPU — đó là
skills/scroll-effects(Three.js/shader). Skill này chỉ CSSskills/blur, không canvas/WebGL.mask-image
- 目的: 根据滚动进度,通过有机形状的 mask(墨迹斑块)显露或切换图像,或在阅读中的 active panel 变化时,为 pinned split-screen sticky 布局通过 mask 切换图像——优先使用 中的原版代码,
Rheinmir/uiux-asset是轻量级 vanilla 备用版本。assets/ - 触发条件(何时使用):
- 艺术化或 portfolio 图像切换,呈现与滚动同步的“墨迹飞溅”效果(不是平面 crossfade)。
- 两列 case-study/portfolio 布局:图像固定在左侧,正文在右侧滚动,并根据当前阅读的段落切换图像。
- 非目标: 不用于简单 reveal(进入 viewport 时淡入并轻微滑动)——那是 的 pattern #1,不包含形状 mask。也不用于全屏重度 GPU zoom-blur 图像切换——那是
skills/scroll-effects(Three.js/shader)。本 skill 仅使用 CSSskills/blur,不使用 canvas/WebGL。mask-image
Mental model
心智模型
section → đo progress cuộn (getBoundingClientRect → 0..1) hoặc panel active (IntersectionObserver) → --mrt-r → mask-image radial-gradient trên ảnh "trên" → lỗ trong suốt loang rộng → lộ ảnh "dưới"[data-mrt-blob]--mrt-r[data-mrt-split]@propertysection → 测量滚动进度(getBoundingClientRect → 0..1)或 active panel(IntersectionObserver)→ --mrt-r → 对“上层”图像应用 mask-image radial-gradient → 透明斑块空洞 → 显露“下层”图像--mrt-r@propertyInput và output contract
输入与输出约定
| Field | Required? | Ý nghĩa | |
|---|---|---|---|
| In | ảnh (2 ảnh cho blob; mỗi panel một ảnh cho split) | có | nội dung thay vào bản gốc/asset |
| In | ràng buộc dự án có được kéo lib/file ngoài không | không (mặc định: được) | không được → dùng |
| Out | trang/component HTML có hiệu ứng mask reveal | có | chạy qua HTTP local, có |
| Out | bằng chứng playwright | có | ảnh trước/giữa/sau (biến thể 1), assert đổi ảnh + pin (biến thể 2) |
| 字段 | 必需? | 含义 | |
|---|---|---|---|
| In | 图像(blob 需要 2 张;split 的每个 panel 各 1 张) | 是 | 用于替换原版或 asset 中的内容 |
| In | 项目是否允许引入外部库或文件 | 否(默认:允许) | 不允许 → 使用 |
| Out | 带 mask reveal 效果的页面或 component HTML | 是 | 通过本地 HTTP 运行,包含 |
| Out | playwright 验证证据 | 是 | 前、中、后截图(变体 1),断言图像切换和 pin 效果(变体 2) |
Rules và capabilities
规则与能力
- RULE-01 (MUST): Hỗ trợ trình duyệt : Safari (kể cả Safari hiện đại) chỉ nhận
mask-image/-webkit-mask-image— PHẢI viết cả-webkit-mask-repeatchuẩn LẪNmask-image(đã có sẵn trong CSS asset, đừng xoá prefix khi adapt/minify). Thiếu prefix = ảnh trên không bao giờ có lỗ hổng trên Safari, coi như hiệu ứng không chạy.-webkit-mask-image - RULE-02 (MUST): không hoạt động nếu BẤT KỲ phần tử cha nào (kể cả xa, lên tới
position: sticky) có<body>/overflow: hidden/autohoặcscroll— đây là lỗi hay gặp nhất khi nhúngclipvào layout có sẵn (nhiều trang đặt.mrt-split-stickytrên wrapper để chặn scroll ngang, vô tình giết sticky của mọi phần tử con). Kiểm tra toàn bộ chuỗi cha trước khi báo "sticky không chạy" là bug.overflow: hidden - RULE-03 (MUST): Biến thể 2 dùng để trình duyệt animate được custom property nằm trong
@property --mrt-r/gradient — thiếu khai báo này, đổicalc()bằng JS sẽ nhảy tức thì thay vì mượt qua transition.--mrt-r - RULE-04 (MUST): → cả 2 biến thể set
prefers-reduced-motion: reduce= bán kính tối đa ngay lập tức (lộ ảnh hoàn toàn), tắt animate/transition — đã code sẵn trong JS/CSS, đừng xoá nhánh này khi adapt.--mrt-r - RULE-05 (MUST): Một scroll listener rAF-throttle DUY NHẤT cho mọi trên trang; một
[data-mrt-blob]DUY NHẤT mỗiIntersectionObservercho các panel của nó — không gắn nhiều listener[data-mrt-split]rời hoặc nhiều observer trùng vai trò.scroll - RULE-06 (MUST): Verify bằng : chụp trước/giữa/sau khi cuộn qua biến thể 1 để thấy vòng loang lớn dần, và assert biến thể 2 đổi đúng ảnh khi mỗi panel vào giữa khung nhìn + cột ảnh thực sự pin (không cuộn theo trang) trong lúc đọc.
playwright-verify - Capabilities: đọc/copy file từ repo asset ngoài hoặc của skill; ghi file HTML/CSS/JS dự án; chạy trình duyệt headless để verify.
assets/
- RULE-01 (MUST):支持 的浏览器。 Safari(包括现代 Safari)只识别
mask-image/-webkit-mask-image——必须同时编写标准-webkit-mask-repeat和mask-image(CSS asset 中已经包含,适配或压缩时不要删除 prefix)。缺少 prefix 时,上层图像在 Safari 上永远不会出现空洞,应视为效果未运行。-webkit-mask-image - RULE-02 (MUST):无法正常工作的情况: 任何父级元素(无论距离多远,直至
position: sticky)设置了<body>/overflow: hidden/auto或scroll,clip都会失效——这是将position: sticky嵌入现有布局时最常见的错误(许多页面会在 wrapper 上设置.mrt-split-sticky来阻止横向滚动,却无意中让所有子元素的 sticky 失效)。在将问题判定为 bug 之前,必须检查整条父级链。overflow: hidden - RULE-03 (MUST):变体 2 使用 ,使浏览器能够对位于
@property --mrt-r或 gradient 内的 custom property 进行动画;缺少此声明时,通过 JavaScript 修改calc()会立即跳变,而不是平滑 transition。--mrt-r - RULE-04 (MUST):当 时,两个变体都应立即将
prefers-reduced-motion: reduce设置为最大半径(完整显露图像),并关闭 animate/transition——JavaScript/CSS 中已经实现此分支,适配时不要删除。--mrt-r - RULE-05 (MUST):页面上所有 只能使用一个经过 rAF 节流的 scroll listener;每个
[data-mrt-blob]的 panels 只能使用一个[data-mrt-split]——不要添加多个独立 scroll listener 或多个职责重复的 observer。IntersectionObserver - RULE-06 (MUST):使用 进行验证:滚动经过变体 1 时截取前、中、后画面,确认斑块逐渐增大;断言变体 2 在每个 panel 进入视口中央时切换到正确图像,并确认阅读期间图像列真正固定在原位、不随页面滚动。
playwright-verify - 能力:从外部 repo asset 或本 skill 的 中读取或复制文件;写入项目的 HTML/CSS/JS 文件;运行 headless 浏览器进行验证。
assets/
Failure boundaries
失败边界
- Hiệu ứng không chạy trên Safari → thiếu → sửa prefix, không kết luận là bug khác.
-webkit-mask-image - Sticky không pin → rà toàn bộ chuỗi cha tìm /
overflowtrước khi báo bug.clip - Mở bằng → không hợp lệ; chạy lại qua HTTP.
file:// - Yêu cầu thực ra là reveal opacity/translate hoặc chuyển ảnh shader toàn màn hình → không thuộc skill, chuyển hoặc
skills/scroll-effects.skills/blur
- 效果在 Safari 上未运行 → 缺少 → 补上 prefix,不要归结为其他 bug。
-webkit-mask-image - Sticky 未固定 → 检查整条父级链中的 /
overflow,然后再报告 bug。clip - 通过 打开 → 无效;改用 HTTP 重新运行。
file:// - 实际需求是 opacity/translate reveal 或全屏 shader 图像切换 → 不属于本 skill;改用 或
skills/scroll-effects。skills/blur
HOW
HOW(如何实现)
Main workflow
主工作流
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | yêu cầu | Chọn biến thể (blob hay split) và nguồn: bản gốc (mặc định) hay | lựa chọn | không được kéo lib ngoài → B01 |
| W02 | effect | pen gốc | Mở link chạy xem đúng hiệu ứng → copy nguyên file | file trong dự án | — |
| W03 | deterministic | trang | Chạy local qua HTTP, verify bằng | ảnh + assert | lỗi Safari/sticky → xem Failure boundaries, sửa, lặp |
Chi tiết từng bước (nguồn chân lý cho W01–W03):
| 步骤 | 类型 | 输入 | 操作 | 输出或退出条件 | 失败或下一步 |
|---|---|---|---|---|---|
| W01 | judgment | 需求 | 选择变体(blob 或 split)以及来源:原版(默认)或 | 选择结果 | 不能引入外部库 → B01 |
| W02 | effect | 原版 Pen | 打开链接并运行以查看正确效果 → 复制完整 | 项目中的文件 | — |
| W03 | deterministic | 页面 | 通过本地 HTTP 运行,使用 | 截图和断言 | Safari/sticky 错误 → 查看失败边界,修复后重试 |
每个步骤的详细信息(W01–W03 的事实来源):
Bản gốc — dùng TRƯỚC (copy nguyên file, đừng viết lại)
原版 Pen——优先使用(复制完整文件,不要重写)
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 |
|---|---|
| Ink Transition Scroll Effect — iamryanyu | chạy · code |
| Pinned Split-Screen Mask Reveal — gridmorphic | chạy · code |
| Parallax Jungle Leaves Reveal — Mamboleoo | chạy · code |
assets/作者原封不动的原始代码(CodePen public = MIT)位于外部仓库 ——实际运行 gallery:https://rheinmir.github.io/uiux-asset/scroll-effects/ 。
Rheinmir/uiux-asset流程:打开链接并运行以查看正确效果 → 将完整 文件(以及文件引用的 文件夹)复制到项目中 → 保留首行署名注释 → 仅修改内容、图像和颜色。通过本地 HTTP 运行,不要使用 。
.html_assets/file://如果需要完整的离线版本,可执行:
git clone --depth 1 https://github.com/Rheinmir/uiux-asset| 原版 Pen——作者 | 链接 |
|---|---|
| Ink Transition Scroll Effect — iamryanyu | 运行 · 代码 |
| Pinned Split-Screen Mask Reveal — gridmorphic | 运行 · 代码 |
| Parallax Jungle Leaves Reveal — Mamboleoo | 运行 · 代码 |
本 skill 中的 只是自行编写的精简 vanilla 版本——仅在项目不允许引入外部库时使用;默认使用上述原版。
assets/Assets
Assets
assets/demo.htmlassets/mask-reveal-transition.cssassets/mask-reveal-transition.js--mrt-edgeassets/demo.htmlassets/mask-reveal-transition.cssassets/mask-reveal-transition.js--mrt-edgeBranches
分支
| ID | Kind | Guard | Hành vi | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | conditional_required | dự án KHÔNG được kéo lib/file ngoài | dùng | — | W03 |
| B02 | user_optional | cần cả bộ offline | | — | W02 |
| ID | 类型 | 条件 | 行为 | 跳过或失败 | 汇合点 |
|---|---|---|---|---|---|
| B01 | conditional_required | 项目不允许引入外部库或文件 | 使用 | — | W03 |
| B02 | user_optional | 需要完整的离线版本 | | — | W02 |
Validation và stopping
验证与停止条件
Tất định (playwright): biến thể 1 chụp trước/giữa/sau khi cuộn thấy vòng loang lớn dần; biến thể 2 assert đổi đúng ảnh khi mỗi panel vào giữa khung nhìn + cột ảnh thực sự pin. Cần review: cảm giác "mực loang" đúng chiều (lộ dần, không che dần). Dừng khi các assert trên xanh.
确定性验证(playwright):变体 1 截取滚动前、中、后的画面,确认斑块逐渐增大;变体 2 断言每个 panel 进入视口中央时切换到正确图像,并确认图像列真正固定。人工检查:确认“墨迹飞溅”方向正确(逐步显露,而不是逐步遮盖)。上述断言全部通过后停止。
Examples
示例
- Positive: portfolio case-study 2 cột "ảnh cố định trái, chữ cuộn phải" → copy nguyên từ
pinned-split-screen-mask-reveal.html, đổi ảnh panel → playwright assert mỗiRheinmir/uiux-assetvào giữa khung thì ảnh đổi và cột ảnh không cuộn theo trang.[data-mrt-panel] - Boundary/failure: nhúng vào layout có wrapper
.mrt-split-sticky→ ảnh cuộn theo trang, không pin → rà chuỗi cha, bỏoverflow: hiddenở wrapper, không báo là bug của component. Minify xoáoverflow→ Safari không có lỗ hổng mask → trả lại prefix.-webkit-mask-image
- 正向示例: portfolio case-study 两列布局“左侧固定图像,右侧滚动文字” → 从 复制完整的
Rheinmir/uiux-asset→ 更换 panel 图像 → 使用 playwright 断言每个pinned-split-screen-mask-reveal.html进入视口中央时图像发生切换,并且图像列不会随页面滚动。[data-mrt-panel] - 边界或失败案例: 将 嵌入带
.mrt-split-sticky的 wrapper → 图像随页面滚动,无法固定 → 检查父级链,移除 wrapper 上的overflow: hidden,不要将其报告为 component 的 bug。压缩代码时删除overflow→ Safari 上不会出现 mask 空洞 → 恢复 prefix。-webkit-mask-image
Reference — Cơ chế nền chung
参考——通用底层机制
Với mỗi section ( hoặc ): 2 ảnh xếp lớp tuyệt đối trong cùng khung — ảnh dưới (z-index thấp, luôn hiện sẵn) và ảnh trên (z-index cao, mang mask). Ảnh trên có
[data-mrt-blob][data-mrt-split]css
mask-image: radial-gradient(circle at 50% 50%, transparent 0, transparent var(--mrt-r), black calc(var(--mrt-r) + var(--mrt-edge)), black 100%);— vùng trong bán kính trong suốt (ảnh trên biến mất tại đó → lộ ảnh dưới), vùng ngoài vẫn đặc (ảnh trên che kín). tăng dần = vòng trong suốt loang rộng ra = ảnh dưới "loang" ra từ tâm, đúng cảm giác vết mực — không phải chiều ngược lại (chấm đặc lớn dần), vì như vậy sẽ che dần thay vì lộ dần.
--mrt-r--mrt-r + --mrt-edge--mrt-r对于每个 section( 或 ):两张图像在同一个 frame 中绝对定位叠放——下层图像(z-index 较低,始终可见)和上层图像(z-index 较高,带 mask)。上层图像使用:
[data-mrt-blob][data-mrt-split]css
mask-image: radial-gradient(circle at 50% 50%, transparent 0, transparent var(--mrt-r), black calc(var(--mrt-r) + var(--mrt-edge)), black 100%);——半径 内的区域是透明的(上层图像在此消失 → 显露下层图像),半径 之外的区域仍然不透明(上层图像继续遮挡)。随着 增大,透明斑块从中心向外扩散,下层图像像墨迹一样从中心晕开——而不是相反方向(逐渐扩大的不透明圆点),因为后者会遮盖图像,而不是显露图像。
--mrt-r--mrt-r + --mrt-edge--mrt-rBiến thể 1 — Blob/ink mask reveal ([data-mrt-blob]
)
[data-mrt-blob]变体 1——Blob/ink mask reveal([data-mrt-blob]
)
[data-mrt-blob]html
<section data-mrt-blob>
<div class="mrt-blob-frame">
<img class="mrt-blob-under" src="B.jpg" alt="…">
<img class="mrt-blob-over" src="A.jpg" alt="…">
</div>
</section>--mrt-rstage.getBoundingClientRect()progress = (vh - rect.top) / (vh + rect.height)over.style.setProperty('--mrt-r', ...)svg-stroke-revealhtml
<section data-mrt-blob>
<div class="mrt-blob-frame">
<img class="mrt-blob-under" src="B.jpg" alt="…">
<img class="mrt-blob-over" src="A.jpg" alt="…">
</div>
</section>--mrt-rstage.getBoundingClientRect()progress = (vh - rect.top) / (vh + rect.height)over.style.setProperty('--mrt-r', ...)svg-stroke-revealBiến thể 2 — Pinned split-screen mask reveal ([data-mrt-split]
)
[data-mrt-split]变体 2——Pinned split-screen mask reveal([data-mrt-split]
)
[data-mrt-split]html
<div class="mrt-split-section" data-mrt-split>
<div class="mrt-split-sticky">
<img class="mrt-split-under" alt="…">
<img class="mrt-split-over" alt="">
</div>
<div class="mrt-split-scroll">
<article data-mrt-panel data-mrt-src="1.jpg">…</article>
<article data-mrt-panel data-mrt-src="2.jpg">…</article>
</div>
</div>Cột ảnh dùng thuần CSS (KHÔNG GSAP/ScrollTrigger — pin theo cách rẻ nhất trình duyệt hỗ trợ sẵn). JS chỉ có nhiệm vụ đổi ảnh/mask theo panel đang active: một (threshold 0.5) gắn trên từng , khi panel vào tâm khung nhìn → gọi : đẩy ảnh hiện tại lên lớp "trên" (che kín, tức thì, ép reflow), đặt ảnh mới xuống lớp "dưới", rồi set về bán kính tối đa để CSS transition (đăng ký qua ) tự chạy mượt. Ở đây đổi RỜI RẠC theo sự kiện panel, không theo từng px cuộn — nên (khác biến thể 1) CSS transition trên là đúng chỗ.
position: stickyIntersectionObserver[data-mrt-panel]revealTo(src)--mrt-r = 0requestAnimationFrame--mrt-r@property --mrt-r { syntax: "<length>"; ... }--mrt-r--mrt-rhtml
<div class="mrt-split-section" data-mrt-split>
<div class="mrt-split-sticky">
<img class="mrt-split-under" alt="…">
<img class="mrt-split-over" alt="">
</div>
<div class="mrt-split-scroll">
<article data-mrt-panel data-mrt-src="1.jpg">…</article>
<article data-mrt-panel data-mrt-src="2.jpg">…</article>
</div>
</div>图像列使用纯 CSS (不使用 GSAP/ScrollTrigger——以浏览器原生支持的最廉价方式实现 pin)。JavaScript 只负责根据 active panel 切换图像和 mask:在每个 上使用一个 (threshold 为 0.5),当 panel 进入视口中央时调用 :将当前图像推入“上层”(不透明、,立即触发 reflow),把新图像放到“下层”,然后通过 将 设置为最大半径,使 CSS transition(通过 注册)平滑运行。此处 根据 panel 事件离散变化,而不是跟随每一个滚动像素,因此变体 2 适合在 上使用 CSS transition。
position: sticky[data-mrt-panel]IntersectionObserverrevealTo(src)--mrt-r = 0requestAnimationFrame--mrt-r@property --mrt-r { syntax: "<length>"; ... }--mrt-r--mrt-rOrigin
来源
Batch 180926 — bản gốc NGUYÊN VĂN nằm ở (xem mục "Bản gốc" ở trên); là bản rút gọn vanilla tự viết.
Rheinmir/uiux-asset/scroll-effects/assets/Batch 180926——原封不动的原版位于 (见上文“原版”); 是自行编写的精简 vanilla 版本。
Rheinmir/uiux-asset/scroll-effects/assets/