YYSuni
cover

A Dynamic Circle

A deformed circle, wavy curve, interactive button.

Practice

Reference website:https://yoyogipark.trunk-hotel.com/en/about

Notes

Figma + framer-motion

  • Compared to design software or specialized SVG editors, Figma offers sufficient functionality, user-friendly experience, smooth performance, and a certain level of frontend-friendliness.

  • framer-motion and react-spring are both similar in animating numbers.

Design SVG

  • It should use Figma to make circle with more than 4 points, as 4 points are not sufficient for natural transformations.

  • When transforming the cirle, be bold and allow for greater variations in order to create more dramatic changes in the design.

  • When exporting SVG, it is necessary to disable the “Simplify Stroke” option to prevent any alteration of the nodes in the path.

Code it

  • Process the d data first.
const ds = [
	'M100 40C118.483 40 139.5 48.5 152.5 71C164.5 91.7692 163.486 125.627 137.921 146.5C114.766 165.405 80 163.929 58.5 143.333C36 121.779 36 92 48 69.5C58.8389 49.1771 82.4725 40 100 40Z',
	'M100 39.5C125.213 42.7176 144.835 50.428 155.5 73.5C167.582 99.6363 151.581 116.939 130 136C103.485 159.419 71.6185 166.406 47 141C23.5889 116.84 18.5721 79.1434 42 55C58.3271 38.1742 76.7433 36.5321 100 39.5Z',
	'M97 36.5C125 36.5 150 48 162.5 68C175 88 163.565 120.627 138 141.5C112.435 162.373 61.3212 161.152 42 138.5C27.5 121.5 34.7505 86.8563 47.5 65C58 47 79.4725 36.5 97 36.5Z',
	'M102 31.5C120.483 31.5 138.063 46.5 151.5 68C164 88 166.565 128.627 141 149.5C117.846 168.405 78.5 167.096 57 146.5C34.5 124.946 41.4649 94.0209 53 71.5C63.5 51 84.4725 31.5 102 31.5Z'
]
const dArr = ds.map(item =>
	item
		.slice(1, -1)
		.split(/[C ]/)
		.filter(item => !!item)
		.map(item => +item)
)

Point positions may vary in the exported data, so manual adjustments are needed to ensure that the difference between the two data points following “M” is not significant (indicating the same starting point),svg tutorial: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths

  • Declare dynamic data

Since frame-motion/react-spring cannot manipulate arrays of numbers, we need to declare them in batches.

const dPathArr = dArr[0].map(item => useMotionValue(item))
  • Convert and use them

Use useTransform instead of useMotionTemplate because useTransform does not require writing a lot of repetitive code like useMotionTemplate.

const d = useTransform(
	dPathArr,
	() =>
		`M${dPathArr[0].get()} ${dPathArr[1].get()} ${new Array(5)
			.fill(0)
			.map(
				(_, i) =>
					'C' +
					dPathArr
						.slice(2 + i * 6, 2 + (i + 1) * 6)
						.map(item => item.get())
						.join(' ')
			)
			.join(' ')} Z`
)

return (
	<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200' fill='none'>
		<rect width='200' height='200' fill='#F0EDE7' />
		<motion.path d={d} stroke='#403D3C' />
	</svg>
)
  • Animation arrangement
    • Enter from 0 to 1 => 2 => 3 => 1 loop;
    • useRef is better here than useState.
const [scope, animate] = useAnimate()
const animationsRef = useRef<AnimationPlaybackControls[]>()

const animates = () => {
	animationsRef.current = dPathArr.map((item, index) => animate(item, dArr[1][index], { duration: 1 }))
	Promise.all(animationsRef.current).then(() => {
		animationsRef.current = dPathArr.map((item, index) =>
			animate(item, [dArr[1][index], dArr[2][index], dArr[3][index], dArr[1][index]], {
				duration: 3,
				repeat: Infinity
			})
		)
	})
}

useEffect(() => {
	animates()

	return () => animationsRef.current?.forEach(item => item.stop())
}, [])
  • Hover interaction
    • Add _count++ here to prevent animation conflicts.
<svg
	className=' cursor-pointer'
	onMouseEnter={() => {
		animationsRef.current = dPathArr.map((item, index) => animate(item, dArr[0][index], { duration: 0.5 }))
	}}
	onMouseLeave={() => {
		animates()
	}}