masks
ساخت reveal، wipe و transitionهای سینمایی با CSS mask و clip-path
این Skill برای طراحی انیمیشنهای مبتنی بر mask و clipping ساخته شده است؛ از circle reveal و wipe تا iris، split، gradient mask و text mask. الگوهای آماده برای CSS، JavaScript، Framer Motion و SVG ارائه میکند.
با استفاده از tokenهای استاندارد سرعت، نرمی لبه و easing میتوانید transitionهای منسجم و قابلکنترل بسازید. پشتیبانی از click-to-reveal، افشای مرحلهای متن و حالت prefers-reduced-motion هم اجرای حرفهای و دسترسپذیر را آسانتر میکند.
چه چیزی میگیرید
- الگوهای Circle Reveal، Wipe، Iris و Split/Barn Door
- انیمیشنهای Linear، Radial و Conic Gradient Mask
- Text Mask و افشای متن بهصورت کاراکتربهکاراکتر یا کلمهبهکلمه
- نمونهکد برای CSS، JavaScript، Framer Motion و SVG
- راهنمای کاهش حرکت و سازگاری با `prefers-reduced-motion`
برای چه کاری خوب است
- طراحی transitionهای سینمایی برای صفحات وب و landing pageها
- ساخت reveal تعاملی بر اساس موقعیت کلیک یا ورودی کاربر
- انیمیشن لوگوتایپ، تیتر، تصویر و محتوای متنی با mask
- توسعه سیستم موشن منسجم با tokenهای سرعت، softness و easing
برای چه کاری نیست
- انیمیشنهای پیچیده سهبعدی، فیزیکی یا مبتنی بر شبیهسازی
- تولید ویدیو یا compositing حرفهای در نرمافزارهای تدوین
- زمانی که فقط به fade یا transitionهای ساده بدون clipping نیاز دارید
دربارهٔ این اسکیل
Masks
Mask and clipping animations for reveals and transitions.
Standards Reference
| Standard | Documentation |
|---|---|
| W3C DTCG | designtokens.org |
| CSS clip-path | developer.mozilla.org/CSS/clip-path |
| CSS mask | developer.mozilla.org/CSS/mask |
| SVG clipPath | developer.mozilla.org/SVG/Element/clipPath |
Quick Reference
Load from: tokens/masks.tokens.json
Reveal Speeds
| Token | Value | Use |
|---|---|---|
fast | 300ms | Quick reveal |
normal | 600ms | Standard reveal |
slow | 1200ms | Dramatic reveal |
dramatic | 2000ms | Very slow reveal |
Feather/Softness
| Token | Value | Use |
|---|---|---|
none | 0px | Hard edge |
subtle | 2px | Slight softness |
soft | 8px | Soft edge |
gradient | 50px | Gradient fade |
Selection Guide
What mask type?
│
├── CIRCLE REVEAL → Radial expansion
│ ├── Center → expand from center
│ ├── Corner → expand from corner
│ └── Cursor → expand from click point
│
├── WIPE → Linear reveal
│ ├── Horizontal → left-to-right
│ ├── Vertical → top-to-bottom
│ ├── Diagonal → angled wipe
│ └── Hard/soft edge options
│
├── IRIS → Aperture style
│ ├── Circular → smooth iris
│ ├── Polygon → blade iris
│ └── With rotation
│
├── SPLIT/BARN DOOR → Two-part reveal
│ ├── Horizontal → side split
│ ├── Vertical → top/bottom split
│ └── Symmetric or offset
│
├── GRADIENT MASK → Soft transitions
│ ├── Linear → directional fade
│ ├── Radial → spotlight effect
│ └── Conic → sweep reveal
│
└── TEXT MASK → Text as clipping
├── All at once
├── Per character
└── Per word
Patterns
Circle Reveal
Expanding circle mask.
CSS (clip-path):
.circle-reveal {
clip-path: circle(0% at 50% 50%);
/* Origin: {motion.masks.circle-reveal.origin.center} */
animation: circle-expand 600ms ease-out forwards;
/* {motion.masks.reveal-speed.normal} */
}
@keyframes circle-expand {
to {
clip-path: circle(150% at 50% 50%);
/* {motion.masks.circle-reveal.end-radius.cover} */
}
}
/* From corner */
.circle-reveal-corner {
clip-path: circle(0% at 0% 0%);
/* {motion.masks.circle-reveal.origin.top-left} */
animation: circle-expand-corner 600ms ease-out forwards;
}
@keyframes circle-expand-corner {
to {
clip-path: circle(200% at 0% 0%);
}
}
JavaScript (Dynamic origin):
function circleReveal(element, originX, originY, duration = 600) {
element.style.clipPath = `circle(0% at ${originX}% ${originY}%)`;
element.animate([
{ clipPath: `circle(0% at ${originX}% ${originY}%)` },
{ clipPath: `circle(150% at ${originX}% ${originY}%)` }
], {
duration,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)', // {motion.masks.easing.standard}
fill: 'forwards'
});
}
// Click-to-reveal
element.addEventListener('click', (e) => {
const rect = element.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
circleReveal(element, x, y);
});
Framer Motion:
<motion.div
initial={{ clipPath: 'circle(0% at 50% 50%)' }}
animate={{ clipPath: 'circle(150% at 50% 50%)' }}
transition={{
duration: 0.6,
ease: [0.4, 0, 0.2, 1]
}}
>
{content}
</motion.div>
Wipe Reveal
Linear wipe transition.
CSS:
/* Left to right wipe */
.wipe-right {
clip-path: inset(0 100% 0 0);
animation: wipe-reveal-right 600ms ease-out forwards;
}
@keyframes wipe-reveal-right {
to {
clip-path: inset(0 0 0 0);
}
}
/* Top to bottom */
.wipe-down {
clip-path: inset(0 0 100% 0);
animation: wipe-reveal-down 600ms ease-out forwards;
}
@keyframes wipe-reveal-down {
to {
clip-path: inset(0 0 0 0);
}
}
/* Diagonal wipe */
.wipe-diagonal {
clip-path: polygon(0 0, 0 0, 0 100%);
animation: wipe-diagonal 600ms ease-out forwards;
}
@keyframes wipe-diagonal {
to {
clip-path: polygon(0 0, 200% 0, 200% 200%, 0 200%);
}
}
With soft edge (gradient mask):
.wipe-soft {
mask-image: linear-gradient(
to right,
black 0%,
black var(--wipe-progress, 0%),
transparent calc(var(--wipe-progress, 0%) + 10%)
/* {motion.masks.wipe.gradient-width.soft} */
);
animation: wipe-soft 600ms ease-out forwards;
}
@keyframes wipe-soft {
0% { --wipe-progress: 0%; }
100% { --wipe-progress: 100%; }
}
Iris Transition
Aperture/iris opening effect.
CSS (Circular iris):
.iris-open {
clip-path: circle(0% at 50% 50%);
animation: iris-expand 800ms ease-out forwards;
}
@keyframes iris-expand {
0% {
clip-path: circle(0% at 50% 50%);
}
100% {
clip-path: circle(75% at 50% 50%);
}
}
/* Hexagonal iris (6 blades) */
.iris-hex {
clip-path: polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%);
animation: iris-hex-open 800ms ease-out forwards;
}
@keyframes iris-hex-open {
to {
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
}
JavaScript (Polygon iris with rotation):
function irisReveal(element, blades = 6, rotation = 15, duration = 800) {
// {motion.masks.iris.blades.hexagonal} = 6
// {motion.masks.iris.rotation.slight} = 15deg
function createPolygon(radius, bladeCount, rotationDeg) {
const points = [];
const angleStep = (Math.PI * 2) / bladeCount;
const rotationRad = rotationDeg * Math.PI / 180;
for (let i = 0; i < bladeCount; i++) {
const angle = i * angleStep + rotationRad;
const x = 50 + Math.cos(angle) * radius;
const y = 50 + Math.sin(angle) * radius;
points.push(`${x}% ${y}%`);
}
return `polygon(${points.join(', ')})`;
}
const startTime = performance.now();
function animate(time) {
const progress = Math.min((time - startTime) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const currentRadius = eased * 80;
const currentRotation = eased * rotation;
element.style.clipPath = createPolygon(currentRadius, blades, currentRotation);
if (progress < 1) {
requestAnimationFrame(animate);
}
}
element.style.clipPath = createPolygon(0, blades, 0);
requestAnimationFrame(animate);
}
Split/Barn Door
Two-part reveal.
CSS (Horizontal split):
.split-horizontal {
position: relative;
overflow: hidden;
}
.split-horizontal::before,
.split-horizontal::after {
content: '';
position: absolute;
top: 0;
width: 50%;
height: 100%;
background: var(--bg-color);
transition: transform 600ms ease-out;
/* {motion.masks.reveal-speed.normal} */
}
.split-horizontal::before {
left: 0;
}
.split-horizontal::after {
right: 0;
}
.split-horizontal.revealed::before {
transform: translateX(-100%);
}
.split-horizontal.revealed::after {
transform: translateX(100%);
}
Vertical split:
.split-vertical::before {
top: 0;
width: 100%;
height: 50%;
}
.split-vertical::after {
bottom: 0;
width: 100%;
height: 50%;
}
.split-vertical.revealed::before {
transform: translateY(-100%);
}
.split-vertical.revealed::after {
transform: translateY(100%);
}
Gradient Mask Animation
Soft gradient-based reveals.
CSS:
.gradient-reveal {
mask-image: linear-gradient(
to right,
black 0%,
transparent 0%
);
animation: gradient-wipe 800ms ease-out forwards;
}
@keyframes gradient-wipe {
0% {
mask-image: linear-gradient(to right, black 0%, transparent 0%);
}
100% {
mask-image: linear-gradient(to right, black 100%, transparent 100%);
}
}
Radial gradient (spotlight):
.spotlight-reveal {
mask-image: radial-gradient(
circle at 50% 50%,
black 0%,
transparent 0%
);
animation: spotlight 800ms ease-out forwards;
}
@keyframes spotlight {
to {
mask-image: radial-gradient(
circle at 50% 50%,
black 100%,
transparent 100%
);
}
}
Conic/sweep gradient:
.sweep-reveal {
mask-image: conic-gradient(
from 0deg at 50% 50%,
black 0deg,
transparent 0deg
);
animation: sweep 1200ms linear forwards;
}
@keyframes sweep {
to {
mask-image: conic-gradient(
from 0deg at 50% 50%,
black 360deg,
transparent 360deg
);
}
}
Text Mask
Using text as a clipping mask.
CSS:
.text-mask {
background: linear-gradient(45deg, #667eea, #764ba2);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
/* Animated background through text */
.text-mask-animated {
background: linear-gradient(
90deg,
#667eea 0%,
#764ba2 50%,
#667eea 100%
);
background-size: 200% 100%;
background-clip: text;
-webkit-background-clip: text;
color: transparent;
animation: shimmer 2s linear infinite;
}
@keyframes shimmer {
to {
background-position: -200% 0;
}
}
SVG Text Mask:
<svg viewBox="0 0 400 100">
<defs>
<clipPath id="text-clip">
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
font-size="60" font-weight="bold">
REVEAL
</text>
</clipPath>
</defs>
<g clip-path="url(#text-clip)">
<image href="image.jpg" width="400" height="100"
style="clip-path: inset(0 100% 0 0); animation: wipe 1s ease-out forwards;">
</image>
</g>
</svg>
Staggered Text Reveal
Per-character mask animation.
JavaScript:
function staggeredTextReveal(container, mode = 'character') {
const text = container.textContent;
container.textContent = '';
const elements = [];
if (mode === 'character') {
// {motion.masks.text-mask.reveal-mode.per-character}
for (const char of text) {
const span = document.createElement('span');
span.textContent = char === ' ' ? '\u00A0' : char;
span.style.clipPath = 'inset(0 100% 0 0)';
span.style.display = 'inline-block';
container.appendChild(span);
elements.push(span);
}
} else if (mode === 'word') {
// {motion.masks.text-mask.reveal-mode.per-word}
const words = text.split(' ');
words.forEach((word, i) => {
const span = document.createElement('span');
span.textContent = word + (i < words.length - 1 ? '\u00A0' : '');
span.style.clipPath = 'inset(0 100% 0 0)';
span.style.display = 'inline-block';
container.appendChild(span);
elements.push(span);
});
}
// Animate with stagger
elements.forEach((el, i) => {
el.animate([
{ clipPath: 'inset(0 100% 0 0)' },
{ clipPath: 'inset(0 0% 0 0)' }
], {
duration: 300,
delay: i * 50, // {motion.masks.text-mask.stagger.normal}
fill: 'forwards',
easing: 'ease-out'
});
});
}
Composite Masks
Combining multiple masks.
CSS:
.composite-mask {
mask-image:
radial-gradient(circle at 30% 30%, black 20%, transparent 20%),
radial-gradient(circle at 70% 70%, black 25%, transparent 25%);
mask-composite: add; /* {motion.masks.composite.operation.add} */
-webkit-mask-composite: source-over;
}
/* Subtract */
.mask-subtract {
mask-image:
linear-gradient(black, black),
radial-gradient(circle at center, transparent 30%, black 30%);
mask-composite: subtract;
-webkit-mask-composite: source-out;
}
Reduced Motion
For prefers-reduced-motion: reduce:
| Full Motion | Reduced Alternative |
|---|---|
| Circle reveal | Instant or fade |
| Wipe transition | Instant or fade |
| Iris opening | Instant reveal |
| Sweep animation | Instant reveal |
Implementation:
@media (prefers-reduced-motion: reduce) {
.circle-reveal,
.wipe-right,
.iris-open,
.sweep-reveal {
animation: none;
clip-path: none;
mask-image: none;
}
/* Optional: use simple fade instead */
.reveal-element {
opacity: 0;
animation: fade-in 200ms ease-out forwards;
}
}
Cross-References
- See
reveals/for reveal animation patterns - See
morphing/for shape transformations - See
blur/for soft mask edges - See
opacity/for fade alternatives
References
references/mask-techniques.md— Advanced masking techniques