SVG path morphing: how to make "coffee stains" elegantly diffuse
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:
- Path command types must match (e.g., both are cubic Bézier curves).
- 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
- 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)
);
- Hardware Acceleration: Add CSS properties
.stain-group {
will-change: transform, d;
transform: translateZ(0);
}
- 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