Framer Motion 3D卡片初始渲染异常,hover后恢复求解决
3D卡片初始渲染异常问题排查与解决
问题描述
首次使用Framer Motion实现随鼠标移动动态旋转的3D卡片效果,页面加载时卡片渲染异常,仅当鼠标移入卡片再移出(触发mouseleave事件)后,卡片才会恢复到正确位置。尝试基于cardRef.current设置x、y初始值,但未能解决该问题。
原因分析
- 初始值设置错误:当前
useEffect中把x、y初始值设为卡片的宽高,对应rotateY/rotateX转换范围的极值(20/-20),导致页面加载时卡片处于最大旋转角度的异常状态。 - 正确初始态应为中间值:
mouseleave时设置的cardRect.width / 2和cardRect.height / 2才是对应旋转角度为0的中间状态,这才是卡片初始应该呈现的位置。 - DOM尺寸获取时机问题:组件挂载时的
getBoundingClientRect可能获取的是未完全渲染的DOM尺寸,导致初始值设置不准确。
解决方案
直接将x、y的初始值设置为对应卡片中心的中间值,同时优化初始状态的设置逻辑:
- 移除依赖DOM尺寸的初始设置,直接给
useMotionValue设置初始中心值 - 若卡片为响应式尺寸,可在
useEffect中用requestAnimationFrame确保DOM完全渲染后再设置中心值
修改后的代码
import React, { useRef, useEffect } from 'react'; import { motion, useMotionValue, useTransform, useSpring } from 'framer-motion'; const Card3D = () => { const cardRef = useRef(null); const glareRef = useRef(null); // 直接设置初始值为卡片中心对应值(适配固定宽高的场景) const x = useMotionValue(400); const y = useMotionValue(210); const glareX = useMotionValue(-2000); const glareY = useMotionValue(-2000); const rotateY = useTransform(x, [0, 800], [20, -20]) const rotateX = useTransform(y, [0, 420], [-20, 20]) const rotateYSpring = useSpring(rotateY) const rotateXSpring = useSpring(rotateX) // 响应式场景下:确保DOM完全渲染后设置中心值 useEffect(() => { const setInitialCenter = () => { if (cardRef.current) { const cardRect = cardRef.current.getBoundingClientRect(); x.set(cardRect.width / 2); y.set(cardRect.height / 2); } }; requestAnimationFrame(setInitialCenter); }, []); return ( <motion.div className='w-[800px] aspect-card2 rounded-32 relative overflow-hidden cursor-pointer' onMouseMove={(e) => { if (!cardRef.current || !glareRef.current) return; const cardRect = cardRef.current.getBoundingClientRect(); const glareRect = glareRef.current.getBoundingClientRect(); x.set(e.clientX - cardRect.left); y.set(e.clientY - cardRect.top); glareX.set(e.clientX - cardRect.left - glareRect.width / 2); glareY.set(e.clientY - cardRect.top - glareRect.height / 2); }} onMouseLeave={() => { if (!cardRef.current || !glareRef.current) return; const cardRect = cardRef.current.getBoundingClientRect(); x.set(cardRect.width / 2); y.set(cardRect.height / 2); glareX.set(-2000); glareY.set(-2000); }} ref={cardRef} style={{ rotateY: rotateYSpring, rotateX: rotateXSpring, }} > <motion.div className='bg-gradient-radial from-white from-20% to-white/0 to-70% opacity-25 absolute size-[600px]' whileHover={{scale: 1.1}} style={{ x: glareX, y: glareY, }} ref={glareRef} /> <div className='absolute inset-4 bg-white rounded-32 flex flex-col'> <div className='flex-1 border-8 border-gray-300 rounded-32 relative' style={{ backgroundImage: 'url(/progra.jpg)', backgroundSize: 'cover' }}> <img src="/pic1.jpg" alt="Profile picture" className='border-8 border-white rounded-32 absolute left-[3%] top-[3%] w-[15%]' /> <div className='bg-slate-800 shadow-[rgba(0,0,0,0.8)_1px_1px_15px] p-4 absolute bottom-0 inset-x-0 text-gray-500 text-xs rounded-2xl translate-y-1/2 flex space-x-3'> <span className='space-x-1 flex items-center'> <b className='text-white text-lg'>Title</b> </span> </div> </div> <div className='flex-1 text-black rounded-32 px-2 pb-2'> <div className='h-full rounded-32 px-4 shadow-[rgba(0,0,0,0.3)_1px_1px_1px] relative overflow-hidden'> <div className='pt-8'> <h1 className='font-bold text-2xl'>Username</h1> <div className='text-xs space-x-2'> <span>@user</span> <span className='text-gray-400'>Location</span> </div> </div> <hr className='my-4' /> <div className='text-xs flex flex-wrap gap-2'> {["#test1", "#test2", "#test3", "#test4", "#test5", "#test6", "#test7"].map((tag) => ( <span key={tag} className='border border-black px-2 py-1 rounded-lg'>{tag}</span> ))} </div> </div> </div> </div> </motion.div> ); } export default Card3D;
内容的提问来源于stack exchange,提问作者Heighties
相关产品推荐
相关产品推荐

