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
| Standard | Documentation |
|---|---|
| W3C DTCG | designtokens.org |
| SVG Filters | developer.mozilla.org/SVG/Element/filter |
| CSS Filters | developer.mozilla.org/CSS/filter |
Quick Reference
Load from: tokens/distortion.tokens.json
Intensity Levels
| Token | Value | Use |
|---|---|---|
subtle | 5% | Barely perceptible |
medium | 25% | Clear effect |
strong | 50% | Pronounced warp |
extreme | 80% | Heavy deformation |
Glitch Parameters
| Token | Value | Use |
|---|---|---|
rgb-offset-subtle | 2px | Slight color fringing |
rgb-offset-strong | 10px | Obvious separation |
slice-count-few | 3 | Minimal disruption |
slice-count-many | 15 | Heavy 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 Motion | Reduced Alternative |
|---|---|
| Animated glitch | Static or disabled |
| Wave/ripple | Static or very subtle |
| VHS effects | Static scanlines only |
| Chromatic aberration | Reduced 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