compositing

ساخت کامپوزیشن‌های چندلایه با blend mode، افکت شیشه‌ای، split screen و PiP

این Skill برای طراحی و پیاده‌سازی انیمیشن‌های چندلایه و افکت‌های compositing در وب است؛ از blend mode و backdrop blur تا crossfade، split screen و picture-in-picture.

با الگوهای آماده، زمان‌بندی استاندارد و پشتیبانی از reduced motion، می‌توانید تجربه‌های بصری پیچیده را منظم، قابل‌استفاده و سازگار با نیازهای دسترس‌پذیری بسازید.

چه چیزی می‌گیرید

  • الگوهای glass morphism، scrim و backdrop blur متحرک
  • transition برای blend mode، crossfade و افکت‌های رنگی لایه‌ها
  • انیمیشن stagger برای لایه‌های روی‌هم‌چیده و split screen افقی یا عمودی
  • ساخت PiP قابل‌جابجایی با قابلیت minimize، expand و snap به گوشه‌ها
  • راهکارهای reduced motion برای غیرفعال‌کردن حرکت‌های غیرضروری

برای چه کاری خوب است

  • طراحان و توسعه‌دهندگان UI با رابط‌های چندلایه و تعاملی
  • ساخت داشبورد، مدیاپلیر، تجربه‌های storytelling و صفحات معرفی محصول
  • پروژه‌هایی که به glass effect، overlay، split view یا PiP نیاز دارند

برای چه کاری نیست

  • انیمیشن‌های ساده تک‌عنصری که به compositing چندلایه نیاز ندارند
  • تدوین و رندر ویدیو در سطح نرم‌افزارهای تخصصی مانند After Effects
  • پروژه‌هایی که مرورگر هدف از backdrop-filter یا blend mode پشتیبانی نمی‌کند

دربارهٔ این اسکیل

Compositing

Layer-based animation and visual effects compositing.

Standards Reference

StandardDocumentation
W3C DTCGdesigntokens.org
CSS Blend Modesdeveloper.mozilla.org/CSS/mix-blend-mode
CSS Backdrop Filterdeveloper.mozilla.org/CSS/backdrop-filter

Quick Reference

Load from: tokens/compositing.tokens.json

Layer Timing

TokenValueUse
stagger.tight50msRapid layer sequence
stagger.normal100msStandard stagger
crossfade.normal400msLayer crossfade

Backdrop Blur

TokenValueUse
subtle4pxLight glass effect
medium12pxStandard glass
heavy24pxFrosted glass

Selection Guide

What compositing effect?
│
├── BLEND MODES → Color mixing
│   ├── Multiply → darken
│   ├── Screen → lighten
│   ├── Overlay → contrast
│   └── Transition between modes
│
├── GLASS/BLUR → Frosted effects
│   ├── Backdrop blur → glass morphism
│   ├── Scrim → darkened overlay
│   └── Animated blur changes
│
├── LAYERS → Stacked elements
│   ├── Z-index animation
│   ├── Staggered reveals
│   └── Crossfade transitions
│
├── SPLIT SCREEN → Multiple views
│   ├── Horizontal split
│   ├── Vertical split
│   └── Animated splits
│
└── PICTURE-IN-PICTURE → Inset content
    ├── Corner PiP
    ├── Draggable PiP
    └── Expand/collapse

Patterns

Glass Morphism

Frosted glass effect with animation.

CSS:

.glass {
  background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(12px); /* {motion.compositing.backdrop-blur.medium} */
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  transition: backdrop-filter 300ms ease-out;
}

.glass.focused {
  backdrop-filter: blur(24px); /* {motion.compositing.backdrop-blur.heavy} */
}

Animated blur:

function animateBlur(element, fromBlur, toBlur, duration = 300) {
  const startTime = performance.now();

  function update(currentTime) {
    const progress = Math.min((currentTime - startTime) / duration, 1);
    const eased = 1 - Math.pow(1 - progress, 3);

    const currentBlur = fromBlur + (toBlur - fromBlur) * eased;
    element.style.backdropFilter = `blur(${currentBlur}px)`;

    if (progress < 1) {
      requestAnimationFrame(update);
    }
  }

  requestAnimationFrame(update);
}

Scrim Overlay

Animated darkening overlay.

CSS:

.scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0);
  pointer-events: none;
  transition: background 300ms ease-out;
}

.scrim.active {
  background: rgba(0, 0, 0, 0.5);
  /* {motion.compositing.opacity-layers.scrim-medium} */
  pointer-events: auto;
}

/* With blur */
.scrim.blur.active {
  backdrop-filter: blur(4px);
}

Blend Mode Transition

Animated blend mode changes.

JavaScript:

class BlendTransition {
  constructor(element) {
    this.element = element;
    this.overlay = this.createOverlay();
  }

  createOverlay() {
    const overlay = document.createElement('div');
    overlay.style.cssText = `
      position: absolute;
      inset: 0;
      opacity: 0;
      transition: opacity 300ms;
    `;
    this.element.style.position = 'relative';
    this.element.appendChild(overlay);
    return overlay;
  }

  transitionTo(blendMode, color, duration = 300) {
    // Fade in new blend
    this.overlay.style.background = color;
    this.overlay.style.mixBlendMode = blendMode;

    this.overlay.animate([
      { opacity: 0 },
      { opacity: 1 }
    ], {
      duration,
      fill: 'forwards',
      easing: 'ease-out'
    });
  }

  remove(duration = 300) {
    this.overlay.animate([
      { opacity: 1 },
      { opacity: 0 }
    ], {
      duration,
      fill: 'forwards',
      easing: 'ease-in'
    }).onfinish = () => {
      this.overlay.style.opacity = 0;
    };
  }
}

Layer Stagger

Sequential layer animations.

CSS:

.layer-stack .layer {
  opacity: 0;
  transform: translateY(20px);
}

.layer-stack.animate .layer {
  animation: layer-in 400ms ease-out forwards;
}

.layer-stack.animate .layer:nth-child(1) { animation-delay: 0ms; }
.layer-stack.animate .layer:nth-child(2) { animation-delay: 100ms; }
/* {motion.compositing.layer-timing.stagger.normal} */
.layer-stack.animate .layer:nth-child(3) { animation-delay: 200ms; }
.layer-stack.animate .layer:nth-child(4) { animation-delay: 300ms; }

@keyframes layer-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Framer Motion:

const containerVariants = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.1 // {motion.compositing.layer-timing.stagger.normal}
    }
  }
};

const layerVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 }
};

<motion.div variants={containerVariants} initial="hidden" animate="visible">
  {layers.map((layer, i) => (
    <motion.div key={i} variants={layerVariants}>
      {layer}
    </motion.div>
  ))}
</motion.div>

Crossfade Layers

Smooth layer crossfade transition.

CSS:

.crossfade-container {
  position: relative;
}

.crossfade-layer {
  position: absolute;
  inset: 0;
  transition: opacity 400ms ease-out;
  /* {motion.compositing.layer-timing.crossfade.normal} */
}

.crossfade-layer.active {
  opacity: 1;
  z-index: 1;
}

.crossfade-layer:not(.active) {
  opacity: 0;
  z-index: 0;
}

JavaScript:

function crossfade(fromLayer, toLayer, duration = 400) {
  // Prepare toLayer
  toLayer.style.opacity = '0';
  toLayer.style.zIndex = '1';

  // Animate both simultaneously
  fromLayer.animate([
    { opacity: 1 },
    { opacity: 0 }
  ], { duration, fill: 'forwards' });

  toLayer.animate([
    { opacity: 0 },
    { opacity: 1 }
  ], { duration, fill: 'forwards' });
}

Split Screen

Animated split screen effect.

CSS:

.split-container {
  display: flex;
  height: 100vh;
  overflow: hidden;
}

.split-left,
.split-right {
  flex: 1;
  transition: flex 800ms cubic-bezier(0.4, 0, 0.2, 1);
  /* {motion.compositing.split-screen.duration.normal} */
}

.split-container.focus-left .split-left {
  flex: 2;
}

.split-container.focus-left .split-right {
  flex: 1;
}

/* With gap */
.split-container.with-gap {
  gap: 8px; /* {motion.compositing.split-screen.gap.normal} */
}

Vertical split:

.split-vertical {
  flex-direction: column;
}

.split-top,
.split-bottom {
  flex: 1;
  transition: flex 800ms cubic-bezier(0.4, 0, 0.2, 1);
}

Picture-in-Picture

Floating PiP element.

CSS:

.pip-container {
  position: fixed;
  bottom: 24px;
  right: 24px;
  width: 320px;
  aspect-ratio: 16/9;
  border-radius: 12px; /* {motion.compositing.picture-in-picture.corner-radius} */
  overflow: hidden;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
  /* {motion.compositing.picture-in-picture.transition} */
  z-index: 1000;
}

.pip-container.minimized {
  width: 160px;
  opacity: 0.8;
}

.pip-container.expanded {
  width: 100%;
  height: 100%;
  bottom: 0;
  right: 0;
  border-radius: 0;
}

Draggable PiP:

class DraggablePiP {
  constructor(element) {
    this.element = element;
    this.isDragging = false;
    this.currentPos = { x: 0, y: 0 };
    this.bind();
  }

  bind() {
    this.element.addEventListener('mousedown', (e) => this.startDrag(e));
    document.addEventListener('mousemove', (e) => this.drag(e));
    document.addEventListener('mouseup', () => this.endDrag());
  }

  startDrag(e) {
    this.isDragging = true;
    this.element.style.transition = 'none';
    this.startPos = {
      x: e.clientX - this.currentPos.x,
      y: e.clientY - this.currentPos.y
    };
  }

  drag(e) {
    if (!this.isDragging) return;

    this.currentPos = {
      x: e.clientX - this.startPos.x,
      y: e.clientY - this.startPos.y
    };

    this.element.style.transform = `translate(${this.currentPos.x}px, ${this.currentPos.y}px)`;
  }

  endDrag() {
    this.isDragging = false;
    this.element.style.transition = 'transform 300ms ease-out';
    this.snapToCorner();
  }

  snapToCorner() {
    const rect = this.element.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;

    // Determine closest corner
    const snapX = centerX < window.innerWidth / 2 ? 24 : window.innerWidth - rect.width - 24;
    const snapY = centerY < window.innerHeight / 2 ? 24 : window.innerHeight - rect.height - 24;

    this.element.style.left = `${snapX}px`;
    this.element.style.top = `${snapY}px`;
    this.element.style.right = 'auto';
    this.element.style.bottom = 'auto';
    this.element.style.transform = 'none';
    this.currentPos = { x: 0, y: 0 };
  }
}

Color Overlay Animation

Animated color grading overlay.

CSS:

.color-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: overlay;
  opacity: 0;
  transition: opacity 500ms ease-out, background 500ms ease-out;
  /* {motion.compositing.color-overlay.fade-speed.normal} */
}

.color-overlay.warm {
  background: rgba(255, 200, 150, 0.3);
  opacity: 1;
}

.color-overlay.cool {
  background: rgba(150, 200, 255, 0.3);
  opacity: 1;
}

.color-overlay.dramatic {
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 50, 0.3),
    rgba(50, 0, 50, 0.3)
  );
  opacity: 1;
}

Layer Effects Animation

Filter transitions.

CSS:

.layer {
  transition: filter 500ms ease-out;
}

.layer.desaturated {
  filter: saturate(0); /* {motion.compositing.layer-effects.saturation.desaturate} */
}

.layer.muted {
  filter: saturate(0.5); /* {motion.compositing.layer-effects.saturation.muted} */
}

.layer.vibrant {
  filter: saturate(1.3) contrast(1.1);
  /* {motion.compositing.layer-effects.saturation.vibrant} */
}

.layer.darkened {
  filter: brightness(0.7);
  /* {motion.compositing.layer-effects.brightness.dark} */
}

Reduced Motion

For prefers-reduced-motion: reduce:

Full MotionReduced Alternative
Layer staggerInstant reveal
CrossfadeInstant swap
Split animationInstant split
PiP transitionInstant position
Blur animationStatic blur

Implementation:

@media (prefers-reduced-motion: reduce) {
  .layer-stack .layer,
  .crossfade-layer,
  .split-left,
  .split-right,
  .pip-container {
    transition: none;
  }

  .layer-stack.animate .layer {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

Cross-References

  • See opacity/ for fade patterns
  • See blur/ for blur effects
  • See masks/ for layer masking
  • See transitions/ for page transitions

References

  • references/blend-modes.md — Blend mode reference guide

دانلود ZIP