SuperAPI AI Directory

mask-reveal-transition

Design & creation

View repository

Updated 10-16 01:13

About this skill

Skill: mask-reveal-transition

Vanilla HTML/CSS/JS component — the image is revealed through a mask with an organic shape (ink-splatter ring, fixed center, increasing radius) instead of a normal crossfade/opacity. Two variants share one underlying mechanism: measure scroll progress WITHIN the section using getBoundingClientRect(), map to 0..1, then write the splatter radius to a CSS custom property.

WHAT

Purpose and context

  • Purpose: reveal/switch image through an organic-shaped mask (ink-splatter ring) according to scroll progress, or a sticky split-screen pin that changes image through a mask according to the panel being read — prioritize the original verbatim code at Rheinmir/uiux-asset, assets/ is a lightweight vanilla fallback.
  • Trigger (when to use):
    • Artistic image transition/portfolio in the style of an "ink splatter" synchronized with scroll (not a flat crossfade).
    • Case-study/portfolio 2-column layout: image fixed on the left, paragraph scrolling on the right, image changes according to the paragraph being read.
  • Non-goals: NOT used for simple reveals (fade + slight slide on entering viewport) — that is skills/scroll-effects pattern #1, without a shape mask. NOT used for full-screen image transitions of the GPU-heavy zoom-blur type — that is skills/blur (Three.js/shader). This skill is CSS only mask-image, no 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". Two variants: [data-mrt-blob] (write --mrt-r each frame, no transition) and [data-mrt-split] (discrete change per panel, with transition via @property).

Input and output contract

Field Required? Meaning
In image (2 images for blob; one image per panel for split) yes content substituted into the original/asset
In project constraint on whether external libs/files may be pulled in no (default: allowed) not allowed → use assets/ vanilla
Out HTML page/component with mask reveal effect yes run over local HTTP, with -webkit-mask-image, with branch prefers-reduced-motion
Out playwright evidence yes before/during/after images (variant 1), assert image change + pin (variant 2)

Rules and capabilities

  • RULE-01 (MUST): Browser support mask-image: Safari (including modern Safari) only accepts -webkit-mask-image/-webkit-mask-repeat — MUST write both the standard mask-image AND -webkit-mask-image (already present in the CSS asset, do not remove the prefix when adapting/minifying). Missing prefix = the top image will never have a hole on Safari, meaning the effect does not run.
  • RULE-02 (MUST): position: sticky does not work if ANY parent element (even far up, up to <body>) has overflow: hidden/auto/scroll or clip — this is the most common error when embedding .mrt-split-sticky into an existing layout (many pages place overflow: hidden on a wrapper to block horizontal scroll, accidentally killing sticky for all child elements). Check the entire parent chain before reporting "sticky not working" as a bug.
  • RULE-03 (MUST): Variant 2 uses @property --mrt-r so the browser can animate a custom property contained in calc()/gradient — without this declaration, changing --mrt-r via JS will jump instantly instead of smoothly through a transition.
  • RULE-04 (MUST): prefers-reduced-motion: reduce → both variants set --mrt-r = maximum radius immediately (fully revealing the image), disable animate/transition — already coded in JS/CSS, do not remove this branch when adapting.
  • RULE-05 (MUST): A SINGLE rAF-throttled scroll listener for all [data-mrt-blob] on the page; a SINGLE IntersectionObserver per [data-mrt-split] for its panels — do not attach multiple separate scroll listeners or multiple observers with duplicate roles.
  • RULE-06 (MUST): Verify with playwright-verify: capture before/during/after scrolling through variant 1 to see the spreading ring grow, and assert variant 2 changes the correct image as each panel enters the middle of the viewport + the image column actually pins (does not scroll with the page) while reading.
  • Capabilities: read/copy files from an external asset repo or the skill's assets/ ; write project HTML/CSS/JS files; run a headless browser to verify.

Failure boundaries

  • Effect does not run on Safari → missing -webkit-mask-image → fix the prefix, do not conclude it is a different bug.
  • Sticky does not pin → scan the entire parent chain for overflow/clip before reporting a bug.
  • Opened via file:// → invalid; rerun over HTTP.
  • The request is actually for an opacity/translate reveal or a full-screen shader image transition → not part of this skill, hand off to skills/scroll-effects or skills/blur.

HOW

Main workflow

Step Type Inputs Action Outputs/exit Failure/next
W01 judgment request Choose the variant (blob or split) and source: original (default) or assets/ vanilla choice external lib must not be pulled in → B01
W02 effect original pen Open the link to check the effect is correct → copy the entire file .html (+ _assets/) → keep the attribution comment → only edit content/images/colors file in the project —
W03 deterministic page Run locally over HTTP, verify with playwright-verify image + assert Safari/sticky error → see Failure boundaries, fix, repeat

Step-by-step details (source of truth for W01–W03):

Original version — use BEFORE (copy the file as-is, do not rewrite)

The author's EXACT original code (public CodePen = MIT) is in the external repo Rheinmir/uiux-asset — live gallery: https://rheinmir.github.io/uiux-asset/scroll-effects/ . Process: open link run to see the exact effect → copy the file as-is .html (+ folder _assets/ if the file references it) into the project → keep the attribution comment on the first line → only edit content/images/colors. Run locally over HTTP, not file://. git clone --depth 1 https://github.com/Rheinmir/uiux-asset if you need the whole set offline.

Original Pen — author Link
Ink Transition Scroll Effect — iamryanyu run · code
Pinned Split-Screen Mask Reveal — gridmorphic run · code
Parallax Jungle Leaves Reveal — Mamboleoo run · code

assets/ in this skill is just a self-written vanilla trimmed version — use when the project is NOT allowed to pull external libs; by default use the original version above.

Assets

assets/demo.html + assets/mask-reveal-transition.css + assets/mask-reveal-transition.js — read then ADAPT (change images, --mrt-edge edge softness, panel content).

Branches

ID Kind Guard Behavior Skip / failure Rejoin
B01 conditional_required project is NOT allowed to pull external libs/files use assets/demo.html + .css + .js, read then ADAPT (images, --mrt-edge, panel content) — W03
B02 user_optional need the whole set offline git clone --depth 1 https://github.com/Rheinmir/uiux-asset — W02

Validation and stopping

Deterministic (playwright): variant 1 captures before/during/after scrolling and sees the ink blot grow; variant 2 asserts the image changes exactly when each panel enters the middle of the viewport + the image column truly pins. Needs review: the "ink spreading" feel in the right direction (revealing gradually, not covering gradually). Stop when the above asserts are green.

Examples

  • Positive: portfolio case-study 2 columns "image fixed on left, text scrolls on right" → copy as-is pinned-split-screen-mask-reveal.html from Rheinmir/uiux-asset, change panel images → playwright assert each [data-mrt-panel] entering the middle of the viewport changes the image and the image column does not scroll with the page.
  • Boundary/failure: embed .mrt-split-sticky into a layout with a wrapper overflow: hidden → image scrolls with the page, does not pin → inspect the parent chain, remove overflow on the wrapper, do not report it as a bug of the component. Minify removes -webkit-mask-image → Safari has no mask bug → restore the prefix.

Reference — Common underlying mechanism

For each section ([data-mrt-blob] or [data-mrt-split]): 2 images stacked absolutely within the same frame — the bottom image (low z-index, always visible) and the top image (high z-index, carrying the mask). The top image has

mask-image: radial-gradient(circle at 50% 50%, transparent 0, transparent var(--mrt-r), black calc(var(--mrt-r) + var(--mrt-edge)), black 100%);

— the region within radius --mrt-r transparent (the top image disappears there → revealing the bottom image), the region outside --mrt-r + --mrt-edge still solid (the top image fully covers). --mrt-r increasing = the transparent ring spreads outward = the bottom image "bleeds" out from the center, exactly like an ink stain — not the reverse direction (a solid dot growing larger), because that would cover gradually instead of revealing gradually.

Variant 1 — Blob/ink mask reveal ([data-mrt-blob])

<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-r writes directly each frame (no CSS transition) according to scroll progress: an rAF-throttled scroll listener measures stage.getBoundingClientRect(), progress = (vh - rect.top) / (vh + rect.height) clamped to 0..1, multiplied by the maximum radius (frame diagonal / 2 + margin) → over.style.setProperty('--mrt-r', ...). Do NOT use transition in this variant — transition would make the spreading ring lag behind the actual scroll position (same reason svg-stroke-reveal mode "scroll" doesn't use transition).

Variant 2 — Pinned split-screen mask reveal ([data-mrt-split])

<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>

The image column uses pure CSS position: sticky (NO GSAP/ScrollTrigger — pinning the cheapest way the browser natively supports). JS only has the task of switching image/mask according to the active panel: an IntersectionObserver (threshold 0.5) attached to each [data-mrt-panel], when a panel enters the center of the viewport → call revealTo(src): push the current image to the "top" layer (fully covering, --mrt-r = 0 instantly, force reflow), place the new image on the "bottom" layer, then requestAnimationFrame set --mrt-r to the maximum radius so the CSS transition (registered via @property --mrt-r { syntax: "<length>"; ... }) runs smoothly on its own. Here --mrt-r changes DISCRETELY according to panel events, not per scroll pixel — so (unlike variant 1) a CSS transition on --mrt-r is the right fit.

Origin

Batch 180926 — the original VERBATIM version is located at Rheinmir/uiux-asset/scroll-effects/ (see the "Original" section above); assets/ is a self-written simplified vanilla version.