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.
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 ở , là bản vanilla nhẹ dự phòng.
- 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à (Three.js/shader). Skill này chỉ CSS , không canvas/WebGL.
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"
. Hai biến thể:
(ghi
mỗi frame, không transition) và
(đổi rời rạc theo panel, có transition qua
).
Input 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 vanilla |
| Out | trang/component HTML có hiệu ứng mask reveal | có | chạy qua HTTP local, có , có nhánh |
| 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) |
Rules và capabilities
- RULE-01 (MUST): Hỗ trợ trình duyệt : Safari (kể cả Safari hiện đại) chỉ nhận / — PHẢI viết cả chuẩn LẪN (đã 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.
- 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 ) có // hoặc — đây là lỗi hay gặp nhất khi nhúng vào layout có sẵn (nhiều trang đặt trê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.
- RULE-03 (MUST): Biến thể 2 dùng để trình duyệt animate được custom property nằm trong /gradient — thiếu khai báo này, đổi bằng JS sẽ nhảy tức thì thay vì mượt qua transition.
- RULE-04 (MUST):
prefers-reduced-motion: reduce
→ cả 2 biến thể set = 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.
- RULE-05 (MUST): Một scroll listener rAF-throttle DUY NHẤT cho mọi trên trang; một DUY NHẤT mỗi cho các panel của nó — không gắn nhiều listener rời hoặc nhiều observer trùng vai trò.
- 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.
- 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.
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.
- Sticky không pin → rà toàn bộ chuỗi cha tìm / trước khi báo bug.
- Mở bằng → không hợp lệ; chạy lại qua HTTP.
- 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 .
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 vanilla | 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 (+ ) → giữ comment ghi công → chỉ sửa nội dung/ảnh/màu | 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):
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
.
git clone --depth 1 https://github.com/Rheinmir/uiux-asset
nếu cần cả bộ offline.
| 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 |
trong skill này chỉ là bản rút gọn vanilla tự viết — dùng khi dự án KHÔNG được kéo lib ngoài; mặc định dùng bản gốc ở trên.
Assets
+
assets/mask-reveal-transition.css
+
assets/mask-reveal-transition.js
— đọc rồi ADAPT (đổi ảnh,
độ mềm viền, nội dung panel).
Branches
| 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 + + , đọc rồi ADAPT (ảnh, , nội dung panel) | — | W03 |
| B02 | user_optional | cần cả bộ offline | git clone --depth 1 https://github.com/Rheinmir/uiux-asset
| — | 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.
Examples
- Positive: portfolio case-study 2 cột "ảnh cố định trái, chữ cuộn phải" → copy nguyên
pinned-split-screen-mask-reveal.html
từ , đổi ảnh panel → playwright assert mỗi vào giữa khung thì ảnh đổi và cột ảnh không cuộn theo trang.
- Boundary/failure: nhúng vào layout có wrapper → ảnh cuộn theo trang, không pin → rà chuỗi cha, bỏ ở wrapper, không báo là bug của component. Minify xoá → Safari không có lỗ hổng mask → trả lại prefix.
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ó
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.
Biến thể 1 — Blob/ink mask reveal ()
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>
ghi
trực tiếp mỗi frame (không CSS transition) theo scroll progress: scroll listener rAF-throttle đo
stage.getBoundingClientRect()
,
progress = (vh - rect.top) / (vh + rect.height)
kẹp 0..1, nhân với bán kính tối đa (đường chéo khung / 2 + margin) →
over.style.setProperty('--mrt-r', ...)
. KHÔNG dùng transition ở biến thể này — transition sẽ làm vòng loang trễ nhịp so với vị trí cuộn thật (cùng lý do
mode "scroll" không dùng transition).
Biến thể 2 — Pinned split-screen mask reveal ()
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
@property --mrt-r { syntax: "<length>"; ... }
) 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ỗ.
Origin
Batch 180926 — bản gốc NGUYÊN VĂN nằm ở
Rheinmir/uiux-asset/scroll-effects/
(xem mục "Bản gốc" ở trên);
là bản rút gọn vanilla tự viết.