morphing

تبدیل نرم و طبیعی شکل‌ها، آیکون‌ها، Blobها و متن با SVG و JavaScript

این مهارت برای ساخت انیمیشن‌های morphing و path interpolation بین شکل‌ها، آیکون‌ها، Blobهای ارگانیک، متن و افکت‌های مایع استفاده می‌شود. الگوهای آماده برای CSS، SVG SMIL، Canvas، Flubber، GSAP و Framer Motion ارائه می‌کند.

با انتخاب سرعت، easing، پیچیدگی و روش مناسب، می‌توانید ترنزیشن‌های چشم‌نواز و قابل‌کنترل بسازید؛ همراه با راهکارهای دسترس‌پذیری برای prefers-reduced-motion و مدیریت تفاوت تعداد نقاط مسیرها.

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

  • پیاده‌سازی morph بین مسیرهای SVG و شکل‌های هندسی
  • ترنزیشن آیکون‌ها مانند تبدیل Hamburger به X
  • ساخت Blob و افکت‌های ارگانیک یا liquid با Canvas و JavaScript
  • الگوهای text morph با GSAP و SplitText
  • راهنمای تنظیم سرعت، easing، subdivision و حالت Reduced Motion

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

  • طراحان و توسعه‌دهندگان رابط کاربری که به ترنزیشن‌های خاص برای آیکون‌ها و کامپوننت‌ها نیاز دارند
  • ساخت هویت بصری متحرک، صفحات لندینگ و انیمیشن‌های SVG
  • پروژه‌هایی که تبدیل نرم بین شکل‌ها یا افکت‌های سیال و ارگانیک در آن‌ها اهمیت دارد

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

  • انیمیشن‌های ساده جابه‌جایی، fade یا scale که به تغییر مسیر شکل نیاز ندارند
  • پروژه‌هایی که انیمیشن سنگین باید بدون بهینه‌سازی برای دستگاه‌های ضعیف اجرا شود
  • ساخت مدل‌های سه‌بعدی یا شبیه‌سازی فیزیکی واقع‌گرایانه

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

Morphing

Shape transformation and path interpolation animations.

Standards Reference

StandardDocumentation
W3C DTCGdesigntokens.org
SVG SMILdeveloper.mozilla.org/SVG/SVG_animation_with_SMIL
CSS clip-pathdeveloper.mozilla.org/CSS/clip-path
Flubbergithub.com/veltman/flubber

Quick Reference

Load from: tokens/morphing.tokens.json

Morph Speeds

TokenValueUse
fast300msQuick transition
normal600msStandard morph
slow1200msDramatic transformation
deliberate2000msVery slow, deliberate

Blob Parameters

TokenValueUse
complexity.simple3 pointsSimple blob
complexity.normal6 pointsStandard blob
amplitude.subtle0.1Subtle variation
amplitude.dramatic0.6Dramatic variation

Selection Guide

What morph type?
│
├── ICON-TO-ICON → UI icon transitions
│   ├── Same complexity → direct morph
│   ├── Different complexity → add intermediate
│   └── Use rotation for meaning
│
├── SHAPE-TO-SHAPE → Geometric transforms
│   ├── Similar point count → linear interp
│   ├── Different counts → subdivision
│   └── Use path matching algorithm
│
├── BLOB/ORGANIC → Fluid shapes
│   ├── Low complexity → simple animation
│   ├── Continuous → loop animation
│   └── Add noise for organic feel
│
├── TEXT MORPH → Character transforms
│   ├── Per-character → stagger effect
│   ├── Word-to-word → full morph
│   └── Use glyph decomposition
│
└── LIQUID → Fluid transition
    ├── Low viscosity → fast, fluid
    └── High viscosity → slow, sticky

Patterns

Basic SVG Morph

Simple path morphing with CSS.

CSS (Animate attribute):

@keyframes morph {
  0% {
    d: path('M10,10 L90,10 L90,90 L10,90 Z'); /* Square */
  }
  50% {
    d: path('M50,10 L90,50 L50,90 L10,50 Z'); /* Diamond */
  }
  100% {
    d: path('M50,10 C80,10 90,50 50,90 C10,50 20,10 50,10 Z'); /* Oval-ish */
  }
}

.morphing-path {
  animation: morph 1.2s ease-in-out infinite;
  /* {motion.morphing.speed.slow} */
}

SMIL (SVG native):

<svg viewBox="0 0 100 100">
  <path fill="#3b82f6">
    <animate
      attributeName="d"
      dur="600ms"
      repeatCount="indefinite"
      values="
        M10,10 L90,10 L90,90 L10,90 Z;
        M50,10 L90,50 L50,90 L10,50 Z;
        M10,10 L90,10 L90,90 L10,90 Z
      "
      keyTimes="0; 0.5; 1"
      keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
      calcMode="spline"
    />
  </path>
</svg>

Icon Morph (Hamburger to X)

Classic menu icon transition.

CSS:

.hamburger {
  --transition: 400ms ease-out; /* {motion.morphing.icon-morph.transition.standard} */
}

.hamburger .line {
  transition:
    transform var(--transition),
    opacity var(--transition);
}

.hamburger.active .line:nth-child(1) {
  transform: translateY(8px) rotate(45deg);
}

.hamburger.active .line:nth-child(2) {
  opacity: 0;
  transform: scaleX(0);
}

.hamburger.active .line:nth-child(3) {
  transform: translateY(-8px) rotate(-45deg);
}

Framer Motion:

const hamburgerVariants = {
  closed: {
    rotate: 0,
    y: 0
  },
  open: {
    rotate: 45,
    y: 8
  }
};

const middleVariants = {
  closed: { opacity: 1, scaleX: 1 },
  open: { opacity: 0, scaleX: 0 }
};

<motion.div className="hamburger">
  <motion.span
    variants={hamburgerVariants}
    animate={isOpen ? 'open' : 'closed'}
    transition={{ duration: 0.4 }}
  />
  <motion.span
    variants={middleVariants}
    animate={isOpen ? 'open' : 'closed'}
    transition={{ duration: 0.4 }}
  />
  <motion.span
    variants={{ ...hamburgerVariants, open: { rotate: -45, y: -8 } }}
    animate={isOpen ? 'open' : 'closed'}
    transition={{ duration: 0.4 }}
  />
</motion.div>

Flubber Path Morph

Using Flubber library for complex morphs.

JavaScript:

import { interpolate } from 'flubber';

const fromPath = 'M0,0 L100,0 L100,100 L0,100 Z'; // Square
const toPath = 'M50,0 A50,50 0 1,1 50,100 A50,50 0 1,1 50,0'; // Circle

const interpolator = interpolate(fromPath, toPath, {
  maxSegmentLength: 10 // {motion.morphing.subdivision.low}
});

function animateMorph(element, duration = 600) {
  const startTime = performance.now();

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

    // Apply easing {motion.morphing.easing.smooth}
    const eased = progress < 0.5
      ? 2 * progress * progress
      : 1 - Math.pow(-2 * progress + 2, 2) / 2;

    element.setAttribute('d', interpolator(eased));

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

  requestAnimationFrame(update);
}

Blob Animation

Organic blob shape animation.

CSS:

.blob {
  border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
  animation: blob-morph 8s ease-in-out infinite;
  /* {motion.morphing.blob.speed.slow} */
}

@keyframes blob-morph {
  0%, 100% {
    border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
  }
  25% {
    border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%;
  }
  50% {
    border-radius: 50% 60% 30% 60% / 30% 40% 70% 60%;
  }
  75% {
    border-radius: 60% 30% 60% 50% / 60% 70% 40% 30%;
  }
}

JavaScript (SVG Blob):

class BlobMorph {
  constructor(svg, options = {}) {
    this.svg = svg;
    this.path = svg.querySelector('path');
    this.complexity = options.complexity || 6; // {motion.morphing.blob.complexity.normal}
    this.amplitude = options.amplitude || 0.3;  // {motion.morphing.blob.amplitude.moderate}
    this.speed = options.speed || 4000;         // {motion.morphing.blob.speed.normal}
  }

  generateBlobPath(time) {
    const points = [];
    const angleStep = (Math.PI * 2) / this.complexity;

    for (let i = 0; i < this.complexity; i++) {
      const angle = i * angleStep;
      const noise = Math.sin(time * 0.001 + i * 1.5) * this.amplitude;
      const radius = 40 + noise * 20;

      points.push({
        x: 50 + Math.cos(angle) * radius,
        y: 50 + Math.sin(angle) * radius
      });
    }

    return this.pointsToPath(points);
  }

  pointsToPath(points) {
    const n = points.length;
    let d = `M${points[0].x},${points[0].y}`;

    for (let i = 0; i < n; i++) {
      const p0 = points[(i - 1 + n) % n];
      const p1 = points[i];
      const p2 = points[(i + 1) % n];
      const p3 = points[(i + 2) % n];

      const cp1x = p1.x + (p2.x - p0.x) / 6;
      const cp1y = p1.y + (p2.y - p0.y) / 6;
      const cp2x = p2.x - (p3.x - p1.x) / 6;
      const cp2y = p2.y - (p3.y - p1.y) / 6;

      d += ` C${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
    }

    return d + 'Z';
  }

  animate() {
    const update = (time) => {
      this.path.setAttribute('d', this.generateBlobPath(time));
      requestAnimationFrame(update);
    };
    requestAnimationFrame(update);
  }
}

Liquid Morph

Fluid/liquid shape transition.

JavaScript (Canvas):

class LiquidMorph {
  constructor(canvas) {
    this.ctx = canvas.getContext('2d');
    this.width = canvas.width;
    this.height = canvas.height;

    this.tension = 0.5;    // {motion.morphing.liquid.tension.medium}
    this.viscosity = 0.5;  // {motion.morphing.liquid.viscosity.honey}

    this.points = this.createPoints(8);
    this.targetPoints = [...this.points];
  }

  createPoints(count) {
    const points = [];
    const angleStep = (Math.PI * 2) / count;
    const cx = this.width / 2;
    const cy = this.height / 2;
    const radius = Math.min(this.width, this.height) * 0.3;

    for (let i = 0; i < count; i++) {
      const angle = i * angleStep;
      points.push({
        x: cx + Math.cos(angle) * radius,
        y: cy + Math.sin(angle) * radius,
        vx: 0,
        vy: 0
      });
    }

    return points;
  }

  setTarget(shape) {
    // Generate target points for shape
    this.targetPoints = this.createPoints(8);
    // Apply shape transformation
  }

  update() {
    for (let i = 0; i < this.points.length; i++) {
      const point = this.points[i];
      const target = this.targetPoints[i];

      // Spring physics toward target
      const dx = target.x - point.x;
      const dy = target.y - point.y;

      point.vx += dx * this.tension;
      point.vy += dy * this.tension;

      // Apply viscosity (damping)
      point.vx *= (1 - this.viscosity);
      point.vy *= (1 - this.viscosity);

      point.x += point.vx;
      point.y += point.vy;
    }
  }

  draw() {
    this.ctx.clearRect(0, 0, this.width, this.height);
    this.ctx.beginPath();

    // Draw smooth curve through points
    const n = this.points.length;
    this.ctx.moveTo(this.points[0].x, this.points[0].y);

    for (let i = 0; i < n; i++) {
      const p0 = this.points[(i - 1 + n) % n];
      const p1 = this.points[i];
      const p2 = this.points[(i + 1) % n];

      const cp1x = p1.x + (p2.x - p0.x) * 0.2;
      const cp1y = p1.y + (p2.y - p0.y) * 0.2;

      this.ctx.quadraticCurveTo(p1.x, p1.y, cp1x, cp1y);
    }

    this.ctx.closePath();
    this.ctx.fill();
  }

  animate() {
    this.update();
    this.draw();
    requestAnimationFrame(() => this.animate());
  }
}

Text Morph

Character-by-character morphing.

GSAP + SplitText:

import gsap from 'gsap';
import { SplitText } from 'gsap/SplitText';

function morphText(fromElement, toElement, duration = 600) {
  const fromSplit = new SplitText(fromElement, { type: 'chars' });
  const toSplit = new SplitText(toElement, { type: 'chars' });

  const tl = gsap.timeline();

  // Morph out old text
  tl.to(fromSplit.chars, {
    opacity: 0,
    scale: 0.5,
    y: -20,
    stagger: 0.05, // {motion.morphing.text-morph.per-character}
    duration: duration / 2000,
    ease: 'power2.in'
  });

  // Morph in new text
  tl.from(toSplit.chars, {
    opacity: 0,
    scale: 0.5,
    y: 20,
    stagger: 0.05,
    duration: duration / 2000,
    ease: 'power2.out'
  }, `-=${duration / 4000}`); // Overlap {motion.morphing.text-morph.overlap}

  return tl;
}

GSAP MorphSVG

Using GSAP's MorphSVG plugin.

JavaScript:

import gsap from 'gsap';
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';

gsap.registerPlugin(MorphSVGPlugin);

// Basic morph
gsap.to('#shape1', {
  morphSVG: '#shape2',
  duration: 0.6, // {motion.morphing.speed.normal}
  ease: 'power2.inOut'
});

// With rotation
gsap.to('#icon', {
  morphSVG: {
    shape: '#targetIcon',
    shapeIndex: 'auto'
  },
  rotation: 180, // {motion.morphing.icon-morph.rotation.half}
  duration: 0.4,
  ease: 'power2.inOut'
});

// Sequence through multiple shapes
const shapes = ['#circle', '#square', '#triangle', '#star'];
let current = 0;

function morphNext() {
  current = (current + 1) % shapes.length;
  gsap.to('#morphTarget', {
    morphSVG: shapes[current],
    duration: 0.6,
    ease: 'power2.inOut',
    onComplete: () => setTimeout(morphNext, 1000)
  });
}

Reduced Motion

For prefers-reduced-motion: reduce:

Full MotionReduced Alternative
Animated morphInstant swap or crossfade
Blob animationStatic blob shape
Liquid effectsSimple opacity fade
Icon morphInstant icon swap

Implementation:

@media (prefers-reduced-motion: reduce) {
  .blob {
    animation: none;
  }

  .morphing-path {
    animation: none;
  }
}
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (prefersReducedMotion) {
  // Instant swap instead of morph
  element.setAttribute('d', targetPath);
} else {
  animateMorph(element, 600);
}

Cross-References

  • See stroke/ for stroke-based path animations
  • See masks/ for mask-based reveals
  • See easing/ for morph easing curves
  • See opacity/ for fade-based transitions

References

  • references/path-interpolation.md — Path matching algorithms

دانلود ZIP