distortion

ساخت افکت‌های glitch، موج، VHS و اعوجاج پویا برای رابط کاربری و ویدیو

این skill برای طراحی و پیاده‌سازی افکت‌های distortion مانند glitch، wave، ripple، warp، VHS و chromatic aberration استفاده می‌شود. الگوهای آماده برای CSS، SVG و Canvas دارد و شدت، جابه‌جایی RGB، تعداد slice و پارامترهای موج را مشخص می‌کند.

با آن می‌توانید جلوه‌های دیجیتال و رترو را به‌صورت کنترل‌شده و قابل تنظیم به UI، موشن‌گرافیک یا ویدیو اضافه کنید. راهنمای prefers-reduced-motion نیز کمک می‌کند این افکت‌ها برای دسترس‌پذیری، غیرفعال یا ساده‌سازی شوند.

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

  • توکن‌های شدت از subtle تا extreme برای کنترل میزان اعوجاج
  • الگوهای CSS برای glitch، VHS، scanline و chromatic aberration
  • پیاده‌سازی‌های SVG و Canvas برای wave، ripple، warp و bulge
  • تنظیمات glitch شامل RGB split، جابه‌جایی slice و noise
  • راهکارهای سازگار با `prefers-reduced-motion`

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

  • ساخت landing page یا کمپین دیجیتال با هویت تکنولوژیک، سایبرپانک یا رترو
  • افزودن glitch و distortion به تعاملات UI، نمایشگرهای خلاقانه یا ویدیو
  • طراحی افکت‌های موج و ripple برای واکنش بصری به کلیک، لمس یا حرکت نشانگر

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

  • رابط‌های رسمی و مینیمال که به تصویر کاملاً پایدار و بدون اعوجاج نیاز دارند
  • پروژه‌هایی که باید بدون پردازش Canvas یا SVG filter روی دستگاه‌های ضعیف اجرا شوند
  • زمانی که هدف اصلی، انیمیشن‌های استاندارد ورود، خروج یا جابه‌جایی بدون distortion است

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

Distortion

Warp, glitch, and deformation effects.

Standards Reference

StandardDocumentation
W3C DTCGdesigntokens.org
SVG Filtersdeveloper.mozilla.org/SVG/Element/filter
CSS Filtersdeveloper.mozilla.org/CSS/filter

Quick Reference

Load from: tokens/distortion.tokens.json

Intensity Levels

TokenValueUse
subtle5%Barely perceptible
medium25%Clear effect
strong50%Pronounced warp
extreme80%Heavy deformation

Glitch Parameters

TokenValueUse
rgb-offset-subtle2pxSlight color fringing
rgb-offset-strong10pxObvious separation
slice-count-few3Minimal disruption
slice-count-many15Heavy fragmentation

Selection Guide

What effect?
│
├── WAVE/RIPPLE → Fluid distortion
│   ├── Wave → continuous oscillation
│   ├── Ripple → point-based, decaying
│   └── Use SVG displacement filter
│
├── GLITCH → Digital artifacts
│   ├── RGB split → chromatic aberration
│   ├── Slice → horizontal displacement
│   ├── Noise → static overlay
│   └── Combine for full effect
│
├── WARP → Spatial distortion
│   ├── Bulge → expand from point
│   ├── Pinch → contract to point
│   ├── Twirl → spiral distortion
│   └── Use canvas or SVG filters
│
├── VHS/RETRO → Analog artifacts
│   ├── Scanlines → horizontal lines
│   ├── Color bleed → color smearing
│   ├── Tracking → horizontal bands
│   └── Combine with noise
│
└── CHROMATIC ABERRATION → Lens effect
    └── RGB channel offset

Patterns

Glitch Effect

Digital glitch with RGB split and slices.

CSS (RGB Split):

.glitch {
  position: relative;
}

.glitch::before,
.glitch::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.glitch::before {
  left: 2px; /* {motion.distortion.glitch.rgb-offset-subtle} */
  text-shadow: -2px 0 #ff0000;
  clip-path: inset(0 0 0 0);
  animation: glitch-clip 2s infinite linear alternate-reverse;
}

.glitch::after {
  left: -2px;
  text-shadow: 2px 0 #00ffff;
  animation: glitch-clip 3s infinite linear alternate-reverse;
}

@keyframes glitch-clip {
  0% { clip-path: inset(40% 0 61% 0); }
  20% { clip-path: inset(92% 0 1% 0); }
  40% { clip-path: inset(43% 0 1% 0); }
  60% { clip-path: inset(25% 0 58% 0); }
  80% { clip-path: inset(54% 0 7% 0); }
  100% { clip-path: inset(58% 0 43% 0); }
}

JavaScript (Advanced Glitch):

class GlitchEffect {
  constructor(element) {
    this.element = element;
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.sliceCount = 8; // {motion.distortion.glitch.slice-count-medium}
    this.rgbOffset = 5;  // {motion.distortion.glitch.rgb-offset-medium}
  }

  apply(intensity = 0.5) {
    const { width, height } = this.element.getBoundingClientRect();
    this.canvas.width = width;
    this.canvas.height = height;

    // Draw original
    // ... capture element to canvas

    // Apply glitch slices
    const sliceHeight = height / this.sliceCount;

    for (let i = 0; i < this.sliceCount; i++) {
      if (Math.random() < intensity) {
        const offset = (Math.random() - 0.5) * 30 * intensity;
        const y = i * sliceHeight;

        // Displace slice
        this.ctx.drawImage(
          this.canvas,
          0, y, width, sliceHeight,
          offset, y, width, sliceHeight
        );
      }
    }

    // RGB channel separation
    this.applyRGBSplit(intensity);
  }

  applyRGBSplit(intensity) {
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    const data = imageData.data;
    const offset = Math.floor(this.rgbOffset * intensity);

    // Shift red channel left, blue right
    for (let i = 0; i < data.length; i += 4) {
      const x = (i / 4) % this.canvas.width;

      if (x > offset) {
        data[i] = data[i - offset * 4]; // Red
      }
      if (x < this.canvas.width - offset) {
        data[i + 2] = data[i + offset * 4 + 2]; // Blue
      }
    }

    this.ctx.putImageData(imageData, 0, 0);
  }
}

Wave Distortion

Continuous wave effect using SVG filter.

SVG Filter:

<svg>
  <defs>
    <filter id="wave-filter">
      <feTurbulence
        type="fractalNoise"
        baseFrequency="0.01 0.05"
        numOctaves="2"
        result="noise"
      >
        <animate
          attributeName="baseFrequency"
          dur="10s"
          keyTimes="0;0.5;1"
          values="0.01 0.05;0.02 0.04;0.01 0.05"
          repeatCount="indefinite"
        />
      </feTurbulence>
      <feDisplacementMap
        in="SourceGraphic"
        in2="noise"
        scale="20"
        xChannelSelector="R"
        yChannelSelector="G"
      />
    </filter>
  </defs>
</svg>

<style>
  .wavy {
    filter: url(#wave-filter);
  }
</style>

Ripple Effect

Point-based expanding ripple.

Canvas:

class RippleEffect {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.ripples = [];
  }

  addRipple(x, y) {
    this.ripples.push({
      x, y,
      radius: 0,
      maxRadius: 150,    // {motion.distortion.ripple.radius-medium}
      wavelength: 40,    // {motion.distortion.ripple.wavelength-normal}
      amplitude: 20,
      decay: 0.98        // {motion.distortion.ripple.decay-medium}
    });
  }

  apply(sourceCanvas) {
    const { width, height } = this.canvas;
    const sourceCtx = sourceCanvas.getContext('2d');
    const sourceData = sourceCtx.getImageData(0, 0, width, height);
    const destData = this.ctx.createImageData(width, height);

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        let dx = 0, dy = 0;

        // Calculate displacement from all active ripples
        for (const ripple of this.ripples) {
          const dist = Math.hypot(x - ripple.x, y - ripple.y);

          if (dist < ripple.radius && dist > ripple.radius - ripple.wavelength * 3) {
            const phase = (dist - ripple.radius) / ripple.wavelength;
            const wave = Math.sin(phase * Math.PI * 2) * ripple.amplitude;
            const factor = wave * (1 - dist / ripple.maxRadius);

            dx += (x - ripple.x) / dist * factor;
            dy += (y - ripple.y) / dist * factor;
          }
        }

        // Sample displaced position
        const sx = Math.round(Math.max(0, Math.min(width - 1, x + dx)));
        const sy = Math.round(Math.max(0, Math.min(height - 1, y + dy)));
        const si = (sy * width + sx) * 4;
        const di = (y * width + x) * 4;

        destData.data[di] = sourceData.data[si];
        destData.data[di + 1] = sourceData.data[si + 1];
        destData.data[di + 2] = sourceData.data[si + 2];
        destData.data[di + 3] = sourceData.data[si + 3];
      }
    }

    this.ctx.putImageData(destData, 0, 0);
  }

  update() {
    for (let i = this.ripples.length - 1; i >= 0; i--) {
      const r = this.ripples[i];
      r.radius += 3;
      r.amplitude *= r.decay;

      if (r.radius > r.maxRadius || r.amplitude < 0.5) {
        this.ripples.splice(i, 1);
      }
    }
  }
}

VHS Effect

Retro video distortion.

CSS:

.vhs {
  position: relative;
}

/* Scanlines */
.vhs::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    to bottom,
    transparent 0px,
    transparent 2px,
    rgba(0, 0, 0, 0.3) 2px,
    rgba(0, 0, 0, 0.3) 4px
  );
  pointer-events: none;
}

/* Noise overlay */
.vhs::after {
  content: '';
  position: absolute;
  inset: 0;
  background: url('noise.png');
  opacity: 0.05;
  animation: noise 0.2s steps(4) infinite;
  pointer-events: none;
}

@keyframes noise {
  0% { transform: translate(0, 0); }
  25% { transform: translate(-5%, -5%); }
  50% { transform: translate(5%, 5%); }
  75% { transform: translate(-5%, 5%); }
  100% { transform: translate(5%, -5%); }
}

JavaScript (VHS tracking):

function vhsTracking(ctx, width, height) {
  // Random horizontal bands
  const bandCount = 3 + Math.floor(Math.random() * 3);

  for (let i = 0; i < bandCount; i++) {
    const y = Math.random() * height;
    const bandHeight = 5 + Math.random() * 20;
    const offset = (Math.random() - 0.5) * 30;

    // Get band data
    const bandData = ctx.getImageData(0, y, width, bandHeight);

    // Clear and redraw offset
    ctx.clearRect(0, y, width, bandHeight);
    ctx.putImageData(bandData, offset, y);
  }
}

Chromatic Aberration

Color fringing lens effect.

CSS:

.chromatic-aberration {
  position: relative;
}

.chromatic-aberration::before {
  content: '';
  position: absolute;
  inset: 0;
  background: inherit;
  mix-blend-mode: screen;
  transform: translateX(-3px); /* {motion.distortion.chromatic-aberration.offset-medium} */
  filter: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><filter id='r'><feColorMatrix values='1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0'/></filter></svg>#r");
}

.chromatic-aberration::after {
  content: '';
  position: absolute;
  inset: 0;
  background: inherit;
  mix-blend-mode: screen;
  transform: translateX(3px);
  filter: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><filter id='b'><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0'/></filter></svg>#b");
}

Bulge/Spherize

Expand or contract from point.

Canvas:

function bulge(ctx, cx, cy, radius, strength) {
  const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  const data = imageData.data;
  const destData = ctx.createImageData(imageData);

  for (let y = 0; y < ctx.canvas.height; y++) {
    for (let x = 0; x < ctx.canvas.width; x++) {
      const dx = x - cx;
      const dy = y - cy;
      const dist = Math.sqrt(dx * dx + dy * dy);

      if (dist < radius) {
        const factor = Math.pow(dist / radius, strength);
        const sx = cx + dx * factor;
        const sy = cy + dy * factor;

        const si = (Math.round(sy) * ctx.canvas.width + Math.round(sx)) * 4;
        const di = (y * ctx.canvas.width + x) * 4;

        destData.data[di] = data[si];
        destData.data[di + 1] = data[si + 1];
        destData.data[di + 2] = data[si + 2];
        destData.data[di + 3] = data[si + 3];
      } else {
        const i = (y * ctx.canvas.width + x) * 4;
        destData.data[i] = data[i];
        destData.data[i + 1] = data[i + 1];
        destData.data[i + 2] = data[i + 2];
        destData.data[i + 3] = data[i + 3];
      }
    }
  }

  ctx.putImageData(destData, 0, 0);
}

Reduced Motion

For prefers-reduced-motion: reduce:

Full MotionReduced Alternative
Animated glitchStatic or disabled
Wave/rippleStatic or very subtle
VHS effectsStatic scanlines only
Chromatic aberrationReduced or disabled

Implementation:

@media (prefers-reduced-motion: reduce) {
  .glitch::before,
  .glitch::after {
    animation: none;
  }

  .wavy {
    filter: none;
  }

  .vhs::after {
    animation: none;
  }
}

Cross-References

  • See blur/ for combining blur with distortion
  • See procedural/ for noise-based distortion
  • See glow-shadow/ for combining glow with glitch
  • See opacity/ for distortion fade effects

References

  • references/glitch-effects.md — Advanced glitch implementations

دانلود ZIP