You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Framer Motion 3D卡片初始渲染异常,hover后恢复求解决

3D卡片初始渲染异常问题排查与解决

问题描述

首次使用Framer Motion实现随鼠标移动动态旋转的3D卡片效果,页面加载时卡片渲染异常,仅当鼠标移入卡片再移出(触发mouseleave事件)后,卡片才会恢复到正确位置。尝试基于cardRef.current设置x、y初始值,但未能解决该问题。

原因分析

  1. 初始值设置错误:当前useEffect中把x、y初始值设为卡片的宽高,对应rotateY/rotateX转换范围的极值(20/-20),导致页面加载时卡片处于最大旋转角度的异常状态。
  2. 正确初始态应为中间值:mouseleave时设置的cardRect.width / 2和cardRect.height / 2才是对应旋转角度为0的中间状态,这才是卡片初始应该呈现的位置。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 19:43:10