agentleFS
soilmasssoilmass/motion-design-agent/skills/morphing/SKILL.md

morphing

Skill7 starsChanged 10 months agoMITView on GitHub

Shape morphing and path interpolation animations for transforming between shapes, icons, blobs, and text. Use when creating icon transitions, liquid effects, organic shape animations, or any shape-to-shape transformations.

The file

Files
SKILL.md

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.