

Buy iPhone 15 and iPhone 15 Plus - Apple (SG)
この記事は、「Appleのようなアニメーション:FigmaとJitterを使ってWebインターフェイス用の複雑なアニメーションをデザインする」の続きです。Framer Motion のuseAnimate関数を利用して、複雑なアニメーションを実行する方法について、特に複数の要素と SVG の連続アニメーションと同時アニメーションに焦点を当てて説明します。

前提条件
始める前に、以下を確認してください:
- Reactプロジェクトのセットアップ
- Tailwindの依存関係のインストール
npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p - Framer Motion パッケージをインストール
npm i framer-motion

SVG.svg <svg width="244" height="84" viewBox="0 0 244 84" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="121.5" cy="40.5" r="40.5" fill="#0171E2"/> <circle cx="121.5" cy="40.5" r="22.5" fill="#EFEFF2"/> <rect y="18" width="244" height="45" rx="22.5" fill="#EFEFF2"/> <g clip-path="url(#clip0_49_269)"> <circle cx="121.528" cy="40.4722" r="16.5278" fill="#0171E2"/> <path d="M129.083 39.5278H123.511V33.9556C123.511 33.1056 122.85 32.4444 122 32.4444C121.15 32.4444 120.583 33.1056 120.583 33.8611V39.4333H115.011C114.161 39.4333 113.5 40.0944 113.5 40.9444C113.5 41.7944 114.161 42.3611 114.917 42.3611H120.489V47.9333C120.489 48.7833 121.15 49.35 121.906 49.35C122.756 49.35 123.322 48.6889 123.322 47.9333V42.3611H128.894C129.744 42.3611 130.311 41.7 130.311 40.9444C130.5 40.1889 129.839 39.5278 129.083 39.5278Z" fill="white"/> </g> <defs> <clipPath id="clip0_49_269"> <rect width="34" height="34" fill="white" transform="translate(105 23)"/> </clipPath> </defs> </svg>
このデザインを作成し、FigmaからSVGとしてエクスポートしました。

このSVGは以下の要素で構成されています:
<circle cx="121.5" cy="40.5" r="40.5" fill="#0171E2"/>

<circle cx="121.5" cy="40.5" r="22.5" fill="#EFEFF2"/>

<rect y="18" width="244" height="45" rx="22.5" fill="#EFEFF2"/>

<g clip-path="url(#clip0_49_269)"> <circle cx="121.528" cy="40.4722" r="16.5278" fill="#0171E2"/> <path d="M129.083 39.5278H123.511V33.9556C123.511 33.1056 122.85 32.4444 122 32.4444C121.15 32.4444 120.583 33.1056 120.583 33.8611V39.4333H115.011C114.161 39.4333 113.5 40.0944 113.5 40.9444C113.5 41.7944 114.161 42.3611 114.917 42.3611H120.489V47.9333C120.489 48.7833 121.15 49.35 121.906 49.35C122.756 49.35 123.322 48.6889 123.322 47.9333V42.3611H128.894C129.744 42.3611 130.311 41.7 130.311 40.9444C130.5 40.1889 129.839 39.5278 129.083 39.5278Z" fill="white"/> </g>

前回の記事でJitterを使って各要素を別々にアニメーションさせたように、今回はコードを使って同じことをします。

Framer Motion の useAnimate 関数の理解
import { useAnimate } from "framer-motion"; import React, {useState} from "react"; const UseAnimateExample = () => { const [scope, animate] = useAnimate(); const startAnimation = async () => { // ピンクの円の半径を0に縮小 await animate("#pink-circle", { r: 0 }, { duration: 0.5 }); // ピンクの円の半径を22.5に拡大 await animate("#pink-circle", { r: 22.5 }, { duration: 0.5 }); // ピンクの円を水平方向にx座標400まで移動 await animate("#pink-circle", { cx: 400 }, { duration: 0.5 }); // ピンクの円を垂直方向にy座標400まで移動 await animate("#pink-circle", { cy: 400 }, { duration: 0.5 }); // ピンクの円を水平方向にx座標50まで移動 await animate("#pink-circle", { cx: 50 }, { duration: 0.5 }); // ピンクの円を垂直方向にy座標250まで移動 await animate("#pink-circle", { cy: 250 }, { duration: 0.5 }); // ピンクの円を水平方向にx座標250まで移動 await animate("#pink-circle", { cx: 250 }, { duration: 0.5 }); // ピンクの円の半径を50に拡大 await animate("#pink-circle", { r: 50 }, { duration: 0.5 }); // 複数のアニメーションを同時に実行 // ピンクの円を垂直方向に移動させながら半径を変更 const animation1 = animate("#pink-circle", { cy: 200 }, { duration: 0.5 }); const animation2 = animate("#pink-circle", { r: 40 }, { duration: 0.5 }); await Promise.all([animation1, animation2]); // 両方のアニメーションを同時に実行 // 最初のセットが完了した後の別のアニメーションセット // ピンクの円を元の位置に下げて半径を拡大 const animation3 = animate("#pink-circle", { cy: 250 }, { duration: 0.5 }); const animation4 = animate("#pink-circle", { r: 50 }, { duration: 0.5 }); }; return ( <div ref={scope} className="h-screen bg-black flex flex-col justify-center items-center"> <svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle id="pink-circle" cx="250" cy="250" r="50" fill="#F3B7B7"/> </svg> <button onClick={startAnimation} className="bg-white text-black mt-12 px-4 py-2 rounded-md">Animate</button> </div> ); }; export default UseAnimateExample;

この単純な例は、Framer Motion のuseAnimate関数がどのように機能するかを示しています。次に、このコンセプトをSVG内の複数の要素に適用し、包括的なアニメーションを作成します。
ステップ 1: SVG でコンポーネントを作成します。
AppleButton.jsx import { useAnimate } from "framer-motion"; import React, {useState} from "react"; const AppleButton = () => { const [scope, animate] = useAnimate(); const EntryAnimation = async () => { }; const ExitAnimation = async () => { }; return ( <div ref={scope} className="h-screen bg-black flex flex-col justify-center items-center"> <svg width="244" height="84" viewBox="0 0 244 84" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="121.5" cy="40.5" r="40.5" fill="#0171E2"/> <circle cx="121.5" cy="40.5" r="22.5" fill="#EFEFF2"/> <rect y="18" width="244" height="45" rx="22.5" fill="#EFEFF2"/> <g clip-path="url(#clip0_49_269)"> <circle cx="121.528" cy="40.4722" r="16.5278" fill="#0171E2"/> <path d="M129.083 39.5278H123.511V33.9556C123.511 33.1056 122.85 32.4444 122 32.4444C121.15 32.4444 120.583 33.1056 120.583 33.8611V39.4333H115.011C114.161 39.4333 113.5 40.0944 113.5 40.9444C113.5 41.7944 114.161 42.3611 114.917 42.3611H120.489V47.9333C120.489 48.7833 121.15 49.35 121.906 49.35C122.756 49.35 123.322 48.6889 123.322 47.9333V42.3611H128.894C129.744 42.3611 130.311 41.7 130.311 40.9444C130.5 40.1889 129.839 39.5278 129.083 39.5278Z" fill="white"/> </g> </svg> <button onClick={EntryAnimation} className="bg-white text-black mt-12 px-4 py-2 rounded-md">Entry</button> <button onClick={ExitAnimation} className="bg-white text-black mt-4 px-4 py-2 rounded-md">Exit</button> </div> ); }; export default AppleButton;

ステップ 2: 各 SVG 要素に id を割り当てます。
<svg width="244" height="84" viewBox="0 0 244 84" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle id="blue-circle" cx="121.5" cy="40.5" r="40.5" fill="#0171E2"/> <circle id="gray-circle" cx="121.5" cy="40.5" r="22.5" fill="#EFEFF2"/> <rect id="expanded-gray-circle" y="18" width="244" height="45" rx="22.5" fill="#EFEFF2"/> <g clip-path="url(#clip0_49_269)"> <circle id="blue-inner-circle" cx="121.528" cy="40.4722" r="16.5278" fill="#0171E2"/> <path id="plus-sign" d="M129.083 39.5278H123.511V33.9556C123.511 33.1056 122.85 32.4444 122 32.4444C121.15 32.4444 120.583 33.1056 120.583 33.8611V39.4333H115.011C114.161 39.4333 113.5 40.0944 113.5 40.9444C113.5 41.7944 114.161 42.3611 114.917 42.3611H120.489V47.9333C120.489 48.7833 121.15 49.35 121.906 49.35C122.756 49.35 123.322 48.6889 123.322 47.9333V42.3611H128.894C129.744 42.3611 130.311 41.7 130.311 40.9444C130.5 40.1889 129.839 39.5278 129.083 39.5278Z" fill="white"/> </g> </svg>
ステップ3:useAnimateを使ったアニメーションのコーディングの開始
SVGの初期状態は透明で、opacityとscaleが0に設定されているはずです。SVGが初期状態では見えないように、いくつかのプロパティを調整する必要があります。
さらに、Jitterファイルを参照して、各アニメーションセグメントの正確な継続時間を決定します。
矩形の SVG の x 座標を移動させるなど、useAnimate では直接実行できないアニメーションもあることに注意してください。これらのアクションについては、React のインライン スタイリングを使用してアニメーションを適用します。
AppleButton.jsx import { useAnimate } from "framer-motion"; import React, {useState} from "react"; const AppleButton = () => { const [scope, animate] = useAnimate(); const [expandedStyle, setExpandedStyle] = useState({width: '45px', x: '99.5', transition: 'all 0.5s cubic-bezier(0.5, 1.25, 0.3, 1)', fill: '#EFEFF2', opacity: 0}); const [gTransform, setGTransform] = useState(''); const [textPosition, setTextPosition] = useState({ x: 109.5, y: 41.5 }); const textContent = "Go deeper on design"; const EntryAnimation = async () => { const animation1 = animate("#blue-circle", { r: 40.5, opacity: 1 }, { duration: 0.5 }); const animation2 = animate("#gray-circle", { r: 22.5, opacity: 1 }, { duration: 0.5 }); await Promise.all([animation1, animation2]); await animate("#blue-circle", { r: 22.5, opacity: 1, cy: 40.5 }, { delay: 0.1, duration: 0.25, ease: "easeOut" }); const animation3 = animate("#blue-inner-circle", { r: "16.5278", opacity: 1, cy: "40.4722" }, { duration: 0.25, ease: "easeOut" }); const animation4 = animate("#plus-sign", { opacity: 1 }, { duration: 0.25, ease: "easeOut" }); await Promise.all([animation3, animation4]); setExpandedStyle({width: '244px', x: '0', transition: 'all 0.5s cubic-bezier(0.5, 1.25, 0.3, 1)', fill: '#EFEFF2', opacity: 1}); setGTransform('translateX(100px)'); await animate("#text", { opacity: 1 }, { duration: 0.5}); }; const ExitAnimation = async () => { const animation5 = animate("#plus-sign", { opacity: 0}, { duration: 0.25}); const animation6 = animate("#blue-inner-circle", { r: 0, cy: 40.5 }, {duration: 0.25, delay:0.1}); const animation7 = animate("#text", { opacity:0 }, { duration: 0.2 }); const animation8 = animate("#blue-circle", { r: 0, opacity: 0, cy: 40.5 }, {}); await Promise.all([animation5, animation6, animation7, animation8]); setExpandedStyle({width: '45px', x: '99.5', transition: '0.5s', fill: '#EFEFF2',}); const animation9 = animate("#gray-circle", { r: 0, opacity: 1 }, {delay: 0.5, duration: 0.25, ease: "easeOut"}); setGTransform('translateX(0px)'); const animation10 = animate("#expanded-gray-circle", { opacity: 0,}, {delay: 0.5}); await Promise.all([animation9, animation10]); }; return ( <div ref={scope} className="h-screen bg-black flex flex-col justify-center items-center"> <svg width="244" height="84" viewBox="0 0 244 84" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle id="blue-circle" cx="121.5" cy="40.5" r="0" fill="#0171E2"/> <circle id="gray-circle" cx="121.5" cy="40.5" r="0" fill="#EFEFF2"/> <rect id="expanded-gray-circle" x={expandedStyle.x} y="18" width={expandedStyle.width} height="45" rx="22.5" style={expandedStyle}/> <text id="text" x={textPosition.x} y={textPosition.y} fill="black" textAnchor="middle" alignmentBaseline="middle" fontWeight="600" opacity="0"> {textContent} </text> <g style={{transform: gTransform, transition: 'transform 0.5s cubic-bezier(0.5, 1.25, 0.3, 1)'}}> clip-path="url(#clip0_49_269)"> <circle id="blue-inner-circle" cx="121.528" cy="40.4722" r="0" fill="#0171E2" opacity="0"/> <path id="plus-sign" d="M129.083 39.5278H123.511V33.9556C123.511 33.1056 122.85 32.4444 122 32.4444C121.15 32.4444 120.583 33.1056 120.583 33.8611V39.4333H115.011C114.161 39.4333 113.5 40.0944 113.5 40.9444C113.5 41.7944 114.161 42.3611 114.917 42.3611H120.489V47.9333C120.489 48.7833 121.15 49.35 121.906 49.35C122.756 49.35 123.322 48.6889 123.322 47.9333V42.3611H128.894C129.744 42.3611 130.311 41.7 130.311 40.9444C130.5 40.1889 129.839 39.5278 129.083 39.5278Z" fill="white" opacity="0"/> </g> </svg> <button onClick={EntryAnimation} className="bg-white text-black mt-12 px-4 py-2 rounded-md">Entry</button> <button onClick={ExitAnimation} className="bg-white text-black mt-4 px-4 py-2 rounded-md">Exit</button> </div> ); }; export default AppleButton;

最後に
これで完成です!これで、このアニメーションボタンをウェブサイトに組み込むことができます。スクロール・トゥ・ビューのエフェクトでさらに強化したい場合は、ボタンが表示されるときにEntry Animationを、表示から離れるときにExit Animationをトリガーするようにスクロールイベントをリンクするだけです。
このプロジェクトの完全なコードをダウンロードするには、私のGitHubをご覧ください。
