React Summit US talk
SVG egg
import React from 'react'; import { useSpring, animated } from 'react-spring'; import { getDistanceBetweenPoints, convertRadiansToDegrees, } from './utils'; import useMousePosition from './use-mouse-position'; import useBoundingBox from './use-bounding-box'; import styles from './LikeButton.module.css'; function LikeButton() { const mousePosition = useMousePosition(); const ref = React.useRef(); const boundingBox = useBoundingBox(ref); const bodyRotation = calculateBodyRotation( boundingBox, mousePosition ); const eyeTranslate = calculateEyePosition( boundingBox, mousePosition ); const bodyStyle = useSpring({ transform: `rotate(${bodyRotation}deg)`, }); const eyeTransform = useSpring({ transform: `translate( ${eyeTranslate.x} ${eyeTranslate.y} )`, }); return ( <button className={styles.wrapper}> <animated.svg xmlns="http://www.w3.org/2000/svg" ref={ref} width="222" height="264" fill="none" viewBox="0 0 222 264" style={bodyStyle} > {/* Egg body */} <path fill="#EEEDE7" d="M222 159c0 72.902-49.696 105-111 105S0 231.902 0 159 49.696 0 111 0s111 86.098 111 159z" /> {/* Eyes */} <animated.g {...eyeTransform}> <path fill="#000" d="M99 158.5c0 9.665-7.835 17.5-17.5 17.5S64 168.165 64 158.5 71.835 141 81.5 141 99 148.835 99 158.5z" /> <path stroke="#fff" strokeLinecap="round" strokeOpacity="0.5" strokeWidth="4" d="M71.317 154.198A8.501 8.501 0 0179.43 148" /> <path fill="#000" d="M158 158.5c0 9.665-7.835 17.5-17.5 17.5s-17.5-7.835-17.5-17.5 7.835-17.5 17.5-17.5 17.5 7.835 17.5 17.5z" /> <path stroke="#fff" strokeLinecap="round" strokeOpacity="0.5" strokeWidth="4" d="M130.318 154.198A8.497 8.497 0 01138.43 148" /> </animated.g> {/* Mouth */} <path stroke="#000" strokeLinecap="round" strokeWidth="8" d="M125 191a13.994 13.994 0 01-4.101 9.899 13.994 13.994 0 01-19.798 0A13.996 13.996 0 0197 191" /> {/* Reflections */} <g> <path stroke="#fff" strokeLinecap="round" strokeWidth="6" d="M45 71s0-10.5 15.038-24.897C72.757 33.928 79 33 79 33" /> </g> </animated.svg> </button> ); } const calculateBodyRotation = ( boundingBox, mousePosition ) => { if ( !boundingBox || typeof mousePosition.x !== 'number' || typeof mousePosition.y !== 'number' ) { return 0; } const areaOfEffectRadius = boundingBox.height * 2; const headCenterPoint = { x: boundingBox.left + boundingBox.width / 2, y: boundingBox.top + boundingBox.height / 2, }; const distanceToHead = getDistanceBetweenPoints( mousePosition, headCenterPoint ); if (distanceToHead > areaOfEffectRadius) { return 0; } const deltaX = headCenterPoint.x - mousePosition.x; const deltaY = headCenterPoint.y - mousePosition.y; const angleInRads = Math.atan2(deltaY, deltaX); const angleInDegrees = 180 + convertRadiansToDegrees(angleInRads); const rotationMax = 10; let a, b; if (angleInDegrees < 180) { a = (rotationMax / 90) * -1; b = rotationMax; } else { a = rotationMax / 90; b = rotationMax * -3; } return a * angleInDegrees + b; }; function calculateEyePosition( boundingBox, mousePosition ) { if ( !boundingBox || typeof mousePosition.x !== 'number' || typeof mousePosition.y !== 'number' ) { return { x: 0, y: 0 }; } const faceCenter = { x: boundingBox.left + boundingBox.width * 0.5, y: boundingBox.top + boundingBox.height * 0.625, }; const relativeMousePosition = { x: mousePosition.x - faceCenter.x, y: mousePosition.y - faceCenter.y, }; const x = Math.round(relativeMousePosition.x * 0.03); const y = Math.round(relativeMousePosition.y * 0.03); return { x, y }; } export default LikeButton;
3D Egg with react-three-fiber
import React from 'react'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { OrthographicCamera, OrbitControls, } from '@react-three/drei'; import { Canvas, useLoader, useFrame, } from '@react-three/fiber'; import { useSpring, animated } from '@react-spring/three'; import { Environment } from '@react-three/drei'; import { getDistanceBetweenPoints } from './utils'; import useMousePosition from './use-mouse-position'; import useBoundingBox from './use-bounding-box'; import styles from './LikeButton.module.css'; function LikeButton() { const model = useLoader(GLTFLoader, '/misc/egg.glb'); const mousePosition = useMousePosition(); const ref = React.useRef(); const boundingBox = useBoundingBox(ref); const bodyRotation = calculateBodyRotation( boundingBox, mousePosition ); const eyeTranslate = calculateEyePosition( boundingBox, mousePosition ); const bodySpring = useSpring({ rotation: [0, 0, bodyRotation], }); const eyeSpring = useSpring({ position: [eyeTranslate.x, eyeTranslate.y, 0.9], }); const lightSpring = useSpring({ position: [-15, 15, 5], config: { stiffness: 200, friction: 100, } }); return ( <button ref={ref} className={styles.wrapper}> <Canvas style={{ width: '100%', height: '100%' }}> <animated.group rotation={bodySpring.rotation}> <primitive object={model.nodes.Body} position={[0, 0, 0]} /> <animated.primitive object={model.nodes.Eyes} position={eyeSpring.position} /> <primitive object={model.nodes.Mouth} position={[0, -0.06, 0.8]} rotation={[Math.PI * 0.5, 0, 0]} /> </animated.group> <OrthographicCamera makeDefault position={[0, 0, 5]} zoom={100} /> <OrbitControls /> <animated.pointLight intensity={1} {...lightSpring} /> <Environment files="/misc/warehouse.hdr" /> </Canvas> </button> ); } const calculateBodyRotation = ( boundingBox, mousePosition ) => { if ( !boundingBox || typeof mousePosition.x !== 'number' || typeof mousePosition.y !== 'number' ) { return 0; } const areaOfEffectRadius = boundingBox.height * 2; const headCenterPoint = { x: boundingBox.left + boundingBox.width / 2, y: boundingBox.top + boundingBox.height / 2, }; const distanceToHead = getDistanceBetweenPoints( mousePosition, headCenterPoint ); if (distanceToHead > areaOfEffectRadius) { return 0; } const deltaX = headCenterPoint.x - mousePosition.x; const deltaY = headCenterPoint.y - mousePosition.y; const angleInRads = Math.atan2(deltaY, deltaX); const rotationMax = Math.PI * 0.025; let a = rotationMax / (Math.PI * 0.25); let b = rotationMax * -2; if (angleInRads < 0) { a *= -1; } return (a * angleInRads + b) * -1; }; function calculateEyePosition( boundingBox, mousePosition ) { if ( !boundingBox || typeof mousePosition.x !== 'number' || typeof mousePosition.y !== 'number' ) { return { x: 0, y: 0 }; } const faceCenter = { x: boundingBox.left + boundingBox.width * 0.5, y: boundingBox.top + boundingBox.height * 0.625, }; const relativeMousePosition = { x: mousePosition.x - faceCenter.x, y: mousePosition.y - faceCenter.y, }; const x = (relativeMousePosition.x * 0.03) / 150; const y = ((relativeMousePosition.y * 0.03) / 150) * -1; return { x, y }; } export default LikeButton;