לקוח ביקש מאיתנו להוסיף מורפינג חלק של אייקונים לאתר שלו: עיגול הופך לכוכב, הכוכב ללב. העיצוב היה מוכן, אבל מורפינג SVG מהקופסה לא עבד — הצורות קפצו ונשברו. הסיבה: לצורות ההתחלה והסיום היה מספר שונה של צמתים. פתרנו את זה עם GSAP MorphSVGPlugin מאת GreenSock. הנה איך להגדיר מורפינג בלי כאבי ראש ולחסוך בתקציב על דיבוג. במשך יותר מ-5 שנים, סיפקנו יותר מ-50 פרויקטים עם אנימציית SVG, ומורפינג הוא אחת המשימות הנפוצות ביותר. להלן כלים מוכחים, קטעי קוד מוכנים, ומלכודות טיפוסיות.
בעיות שאנחנו פותרים
הקושי העיקרי הוא אי-התאמה במספר הנקודות באלמנטים של path. SMIL ו-clip-path של CSS עובדים רק עם מבני נתיב זהים. הבעיה השנייה היא ביצועים במכשירים ישנים: האנימציה יורדת מתחת ל-30 FPS. השלישית היא מורכבות האינטגרציה בפריימוורקים מודרניים, במיוחד עם SSR/SSG, שבהם אנימציות בצד הלקוח דורשות טיפול זהיר. לדוגמה, בפרויקט אחד נתקלנו במורפינג לוגו בקטלוג — נתיבים לא אופטימליים גרמו לגמגום באייפון 8. הפתרון שלנו: להעביר הכל ל-GSAP עם MorphSVG, וההמרה לכרטיס המוצר עלתה ב-15%.
איך לבחור כלי למורפינג
| כלי | מורכבות | עלות | ביצועים | יישור נקודות |
|---|---|---|---|---|
| GSAP MorphSVG | בינונית | בתשלום | גבוהים | אוטומטי |
| Flubber.js | נמוכה | חינם | בינוניים | ידני בלבד |
| SMIL | גבוהה | חינם | נמוכים | דורש נתיבים זהים |
חשבו את עלות הפרויקט שלכם — צרו קשר.
למה GSAP MorphSVG עדיף על חלופות
GSAP MorphSVGPlugin מהיר פי 3 בהגדרה לנתיבים מורכבים מאשר Flubber.js. הוא מיישר אוטומטית את מספר הצמתים ותומך בטיימליינים לרצפים. בפרויקט קטלוג מוצרים, יישמנו מורפינג לוגו של מותג באמצעות React 18 ו-GSAP. אנימציית ההובר הגדילה את ההמרה לכרטיס המוצר ב-15%.
יישום ב-React עם GSAP
אנחנו משתמשים ב-React 18, Next.js 14, TypeScript. למורפינג — GSAP עם MorphSVGPlugin. דוגמת יישום:
// lib/gsap-morph.ts
import { gsap } from 'gsap'
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin'
gsap.registerPlugin(MorphSVGPlugin)
export { gsap }
// components/MorphingIcon.tsx
import { useEffect, useRef, useState } from 'react'
import { gsap } from '../lib/gsap-morph'
const shapes = {
circle: 'M 50,10 A 40,40 0 1,1 49.9,10',
star: 'M50,5 L61,35 L95,35 L68,57 L79,91 L50,70 L21,91 L32,57 L5,35 L39,35 Z',
heart: 'M50,85 C10,60 5,30 25,15 C35,7 45,10 50,18 C55,10 65,7 75,15 C95,30 90,60 50,85 Z',
arrow: 'M 10,50 L 40,20 L 40,35 L 90,35 L 90,65 L 40,65 L 40,80 Z',
}
type ShapeKey = keyof typeof shapes
export function MorphingIcon() {
const pathRef = useRef<SVGPathElement>(null)
const [current, setCurrent] = useState<ShapeKey>('circle')
const morphTo = (target: ShapeKey) => {
if (!pathRef.current || target === current) return
gsap.to(pathRef.current, {
morphSVG: {
shape: shapes[target],
origin: '50% 50%',
type: 'rotational',
},
duration: 0.8,
ease: 'power2.inOut',
})
setCurrent(target)
}
return (
<div>
<svg viewBox="0 0 100 100" className="w-32 h-32">
<path ref={pathRef} d={shapes.circle} fill="none" stroke="#3b82f6" strokeWidth="2" />
</svg>
<div className="flex gap-2 mt-4">
{(Object.keys(shapes) as ShapeKey[]).map(key => (
<button
key={key}
onClick={() => morphTo(key)}
className={`px-3 py-1 text-sm rounded ${
current === key ? 'bg-blue-500 text-white' : 'bg-gray-100'
}`}
>
{key}
</button>
))}
</div>
</div>
)
} כלים חלופיים: Flubber.js ו-SMIL
למורפים פשוטים של שני מצבים, Flubber.js עובד היטב. הוא חינמי אבל דורש יישור נקודות ידני. התקנה עם // lib/gsap-morph.ts import { gsap } from 'gsap' import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin' gsap.registerPlugin(MorphSVGPlugin) export { gsap } // components/MorphingIcon.tsx import { useEffect, useRef, useState } from 'react' import { gsap } from '../lib/gsap-morph' const shapes = { circle: 'M 50,10 A 40,40 0 1,1 49.9,10', star: 'M50,5 L61,35 L95,35 L68,57 L79,91 L50,70 L21,91 L32,57 L5,35 L39,35 Z', heart: 'M50,85 C10,60 5,30 25,15 C35,7 45,10 50,18 C55,10 65,7 75,15 C95,30 90,60 50,85 Z', arrow: 'M 10,50 L 40,20 L 40,35 L 90,35 L 90,65 L 40,65 L 40,80 Z', } type ShapeKey = keyof typeof shapes export function MorphingIcon() { const pathRef = useRef<SVGPathElement>(null) const [current, setCurrent] = useState<ShapeKey>('circle') const morphTo = (target: ShapeKey) => { if (!pathRef.current || target === current) return gsap.to(pathRef.current, { morphSVG: { shape: shapes[target], origin: '50% 50%', type: 'rotational', }, duration: 0.8, ease: 'power2.inOut', }) setCurrent(target) } return ( <div> <svg viewBox="0 0 100 100" className="w-32 h-32"> <path ref={pathRef} d={shapes.circle} fill="none" stroke="#3b82f6" strokeWidth="2" /> </svg> <div className="flex gap-2 mt-4"> {(Object.keys(shapes) as ShapeKey[]).map(key => ( <button key={key} onClick={() => morphTo(key)} className={`px-3 py-1 text-sm rounded ${ current === key ? 'bg-blue-500 text-white' : 'bg-gray-100' }`} > {key} </button> ))} </div> </div> ) } . דוגמת שימוש להלן:
// components/FlubberMorph.tsx
import { useEffect, useRef, useState } from 'react'
import { interpolate } from 'flubber'
export function FlubberMorph() {
const pathRef = useRef<SVGPathElement>(null)
const animFrameRef = useRef<number | null>(null)
const morphBetween = (
startPath: string,
endPath: string,
durationMs = 800
) => {
const interpolator = interpolate(startPath, endPath, { maxSegmentLength: 2 })
const start = performance.now()
const animate = (now: number) => {
const elapsed = now - start
const t = Math.min(elapsed / durationMs, 1)
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
if (pathRef.current) {
pathRef.current.setAttribute('d', interpolator(ease))
}
if (t < 1) {
animFrameRef.current = requestAnimationFrame(animate)
}
}
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current)
animFrameRef.current = requestAnimationFrame(animate)
}
const squarePath = 'M 20,20 L 80,20 L 80,80 L 20,80 Z'
const blobPath = 'M 50,10 C 80,10 90,30 90,50 C 90,70 70,90 50,90 C 30,90 10,70 10,50 C 10,30 20,10 50,10 Z'
const [isBlob, setIsBlob] = useState(false)
useEffect(() => {
if (pathRef.current) {
pathRef.current.setAttribute('d', squarePath)
}
}, [])
const toggle = () => {
morphBetween(
isBlob ? blobPath : squarePath,
isBlob ? squarePath : blobPath
)
setIsBlob(!isBlob)
}
return (
<div>
<svg viewBox="0 0 100 100" className="w-48 h-48">
<path ref={pathRef} fill="#8b5cf6" />
</svg>
<button onClick={toggle} className="mt-4 px-4 py-2 bg-purple-500 text-white rounded">
Морфинг
</button>
</div>
)
}אם לשתי הצורות יש אותו מספר נקודות, אפשר להשתמש ב-SMIL בלי ספריות. דוגמה: ריבוע הופך למעוין ובחזרה. ראו תיעוד MDN.
export function SMILMorph() {
return (
<svg viewBox="0 0 100 100" className="w-32 h-32">
<path fill="#f59e0b">
<animate
attributeName="d"
dur="1.5s"
repeatCount="indefinite"
values="
M 20,20 L 80,20 L 80,80 L 20,80 Z;
M 50,10 L 90,50 L 50,90 L 10,50 Z;
M 20,20 L 80,20 L 80,80 L 20,80 Z
"
keyTimes="0; 0.5; 1"
calcMode="spline"
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
/>
</path>
</svg>
);
} איך להכין נתיבי SVG למורפינג?
למורפינג מוצלח, יש לייעל את הצורות. השתמשו בכלים:
- SVGO — מזעור ונרמול נתיבים.
- Inkscape → Extensions > Modify Path > Add Nodes — יישור מספר נקודות.
-
GSAP MorphSVGPlugin.convertToPath() — ממיר
npm install flubber,// components/FlubberMorph.tsx import { useEffect, useRef, useState } from 'react' import { interpolate } from 'flubber' export function FlubberMorph() { const pathRef = useRef<SVGPathElement>(null) const animFrameRef = useRef<number | null>(null) const morphBetween = ( startPath: string, endPath: string, durationMs = 800 ) => { const interpolator = interpolate(startPath, endPath, { maxSegmentLength: 2 }) const start = performance.now() const animate = (now: number) => { const elapsed = now - start const t = Math.min(elapsed / durationMs, 1) const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t if (pathRef.current) { pathRef.current.setAttribute('d', interpolator(ease)) } if (t < 1) { animFrameRef.current = requestAnimationFrame(animate) } } if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current) animFrameRef.current = requestAnimationFrame(animate) } const squarePath = 'M 20,20 L 80,20 L 80,80 L 20,80 Z' const blobPath = 'M 50,10 C 80,10 90,30 90,50 C 90,70 70,90 50,90 C 30,90 10,70 10,50 C 10,30 20,10 50,10 Z' const [isBlob, setIsBlob] = useState(false) useEffect(() => { if (pathRef.current) { pathRef.current.setAttribute('d', squarePath) } }, []) const toggle = () => { morphBetween( isBlob ? blobPath : squarePath, isBlob ? squarePath : blobPath ) setIsBlob(!isBlob) } return ( <div> <svg viewBox="0 0 100 100" className="w-48 h-48"> <path ref={pathRef} fill="#8b5cf6" /> </svg> <button onClick={toggle} className="mt-4 px-4 py-2 bg-purple-500 text-white rounded"> Морфинг </button> </div> ) }וכו' ל-export function SMILMorph() { return ( <svg viewBox="0 0 100 100" className="w-32 h-32"> <path fill="#f59e0b"> <animate attributeName="d" dur="1.5s" repeatCount="indefinite" values=" M 20,20 L 80,20 L 80,80 L 20,80 Z; M 50,10 L 90,50 L 50,90 L 10,50 Z; M 20,20 L 80,20 L 80,80 L 20,80 Z " keyTimes="0; 0.5; 1" calcMode="spline" keySplines="0.4 0 0.2 1; 0.4 0 0.2 1" /> </path> </svg> ) }.
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin' MorphSVGPlugin.convertToPath('#my-circle, #my-rect') טיפים נוספים להכנה
לפני המורפינג, בדקו את מספר הצמתים עם `path.getTotalLength()`. ההפרש צריך להיות מינימלי. לצורות מורכבות, השתמשו ב-GSAP — הוא מיישר צמתים אוטומטית. אם התקציב מוגבל, בחרו ב-Flubber.js והכינו נתיבים ידנית.תהליך וטיימליינים
- ניתוח — הסכמה על עיצוב ואימות צורות מקור.
- אופטימיזציה — ניקוי נתיבים עם SVGO, יישור צמתים.
- בחירת ספרייה — המלצה על GSAP לפרויקטים מורכבים.
- יישום — כתיבת אנימציה, בדיקה במכשירים ניידים.
- אינטגרציה — שילוב באפליקציה, בדיקת Core Web Vitals (LCP, CLS).
| סוג עבודה | טיימליין |
|---|---|
| מורפינג פשוט של שני מצבים | מ-4 שעות |
| מורפינג אינטראקטיבי עם בחירת צורה | 1–2 ימים |
| רצף מורכב עם צבע ואינטראקטיביות | עד 5 ימים |
המחיר כולל תיעוד, גישה לריפוזיטורי עם דמו, הדרכה לצוות הלקוח, ותמיכה ל-30 יום. אנחנו מבטיחים 60 FPS חלקים בכל המכשירים. הזמינו מורפינג לאתר שלכם — קבלו ייעוץ והערכת פרויקט תוך יום אחד. צרו קשר.
טעויות טיפוסיות
- אי-התאמה במספר הצמתים — בדקו עם
<circle>. אפילו צומת אחד נוסף שובר את האנימציה. - התעלמות מפונקציות ease — בלעדיהן האנימציה נראית לא טבעית. השתמשו ב-
<rect>או<path>. - אין טיפול בשינוי גודל חלון — צורות עשויות לזוז אם הקואורדינטות לא מחושבות מחדש. קשרו את האנימציה ל-resize דרך ResizeObserver.
אנחנו מבטיחים שעם הניסיון שלנו (5+ שנים, 50+ פרויקטים) המורפינג שלכם ירוץ בלי גמגום. צרו קשר לייעוץ.







