阿里云主机折上折
  • 微信号
Current Site:Index > SVG path morphing: how to make "coffee stains" elegantly diffuse

SVG path morphing: how to make "coffee stains" elegantly diffuse

Author:Chuan Chen 阅读数:15988人阅读 分类: 前端综合

SVG path morphing can bring static graphics to life with dynamic transition effects, envision the form of a coffee stain slowly spreading on paper—through path interpolation and keyframe animation, we can precisely control this visual effect with code.

SVG Path Basics and Morphing Principles

SVG paths are defined by the d attribute of the <path> element, with Bézier curve commands at their core. To achieve path morphing, two conditions must be met:

  1. Path command types must match (e.g., both are cubic Bézier curves).
  2. The number of path nodes must be identical.
<!-- Initial state: Circle -->
<path id="coffee-stain" d="M20,50 a30,30 0 1,1 60,0 a30,30 0 1,1 -60,0"/>

<!-- Target state: Spread shape -->
<path d="M20,50 C20,20 80,20 80,50 C80,80 20,80 20,50" style="opacity:0"/>

Path Interpolation Implementation

Option 1: SMIL Animation (Native but deprecated)

<path fill="#6F4E37">
  <animate attributeName="d" 
           dur="3s"
           values="M20,50 a30,30 0 1,1 60,0 a30,30 0 1,1 -60,0;
                   M20,50 C20,20 80,20 80,50 C80,80 20,80 20,50"
           fill="freeze"/>
</path>

Option 2: GSAP for Advanced Easing

import { gsap } from "gsap";

const stain = document.getElementById('coffee-stain');
const morphPaths = [
  "M20,50 a30,30 0 1,1 60,0 a30,30 0 1,1 -60,0",
  "M15,45 C15,15 85,15 85,45 C85,75 15,75 15,45",
  "M10,40 C10,10 90,10 90,40 C90,70 10,70 10,40"
];

gsap.to(stain, {
  duration: 2,
  morphSVG: morphPaths,
  ease: "sine.inOut",
  repeat: -1,
  yoyo: true
});

Enhancing Dynamic Spread Effects

Irregular Edge Handling

Add random variance for realism:

function generateOrganicPath(basePath, variance = 5) {
  return basePath.replace(/(\d+)/g, (match) => {
    return parseInt(match) + (Math.random() * variance * 2 - variance);
  });
}

Multi-Layer Technique

<g class="stain-group">
  <!-- Main shape -->
  <path class="main-stain" fill="#6F4E37" d="..."/>
  
  <!-- Edge watermarks -->
  <path class="edge-stain" fill="#8B6B4D" d="..." opacity="0.7">
    <animate attributeName="d" dur="4s" values="..." repeatCount="indefinite"/>
  </path>
  
  <!-- Highlight layer -->
  <path class="highlight" fill="white" d="..." opacity="0.3"/>
</g>

Performance Optimization Strategies

  1. Path Simplification: Use svg-pathdata to reduce nodes
import { parsePath, serializePath } from 'svg-pathdata';

const simplified = serializePath(
  parsePath(complexPath).filter((cmd, index) => index % 2 === 0)
);
  1. Hardware Acceleration: Add CSS properties
.stain-group {
  will-change: transform, d;
  transform: translateZ(0);
}
  1. Segmented Rendering: Animate in steps
function animateInSteps(steps) {
  let step = 0;
  function next() {
    if (step >= steps.length) return;
    stain.setAttribute('d', steps[step++]);
    requestAnimationFrame(next);
  }
  next();
}

Browser Compatibility Solutions

// Detect SMIL support
const smilSupported = document.createElementNS(
  'http://www.w3.org/2000/svg', 
  'animate'
).toString().includes('SVGAnimateElement');

// Fallback
if (!smilSupported) {
  const snap = Snap("#coffee-stain");
  snap.animate({ d: targetPath }, 2000, mina.easeinout);
}

Creative Extensions

Interactive Spread Effects

document.addEventListener('mousemove', (e) => {
  const tiltX = (e.clientX / window.innerWidth - 0.5) * 20;
  const tiltY = (e.clientY / window.innerHeight - 0.5) * 20;
  
  gsap.to(".main-stain", {
    duration: 0.5,
    morphSVG: generateTiltedPath(tiltX, tiltY),
    ease: "power1.out"
  });
});

Dynamic Texture Generation

Combine with Canvas noise:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

function generateNoise() {
  const imgData = ctx.createImageData(200, 200);
  for (let i = 0; i < imgData.data.length; i += 4) {
    const v = Math.random() * 255;
    imgData.data[i] = 111 + v * 0.2;  // R
    imgData.data[i+1] = 78 + v * 0.1; // G
    imgData.data[i+2] = 55 + v * 0.1; // B
    imgData.data[i+3] = 200;          // A
  }
  ctx.putImageData(imgData, 0, 0);
  return canvas.toDataURL();
}

document.querySelector('.main-stain').style.fill = `url(#noise)`;

Advanced Physics Simulation

Implement fluid dynamics-based path changes:

class FluidSimulation {
  constructor(pathElement) {
    this.points = this.parsePath(pathElement);
    this.velocities = this.points.map(() => ({ x: 0, y: 0 }));
  }

  update() {
    this.points.forEach((p, i) => {
      // Simulate surface tension
      const prev = this.points[(i - 1 + this.points.length) % this.points.length];
      const next = this.points[(i + 1) % this.points.length];
      const tension = {
        x: (prev.x + next.x) / 2 - p.x,
        y: (prev.y + next.y) / 2 - p.y
      };

      // Update velocity
      this.velocities[i].x += tension.x * 0.01;
      this.velocities[i].y += tension.y * 0.01;

      // Apply damping
      this.velocities[i].x *= 0.98;
      this.velocities[i].y *= 0.98;

      // Update position
      p.x += this.velocities[i].x;
      p.y += this.velocities[i].y;
    });

    this.updatePath();
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.