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

基于Three.js Shader的React组件:多卡片图片重复与hover全局触发问题

解决Three.js Shader React图片组件的两个问题

问题分析

  1. 所有卡片显示同一张图片:HoverImageShader的uniforms是全局共享对象,多个AnimatedImage实例复用同一组uniforms,后加载的纹理会覆盖之前的实例纹理。
  2. 所有图片同时触发hover效果:父组件使用单一全局hover状态,所有组件的playAnim都绑定这个状态,导致hover任意卡片都会触发全部效果。

修复方案

1. 修复图片重复问题

修改AnimatedImage组件,每次实例化时复制一份uniforms,避免共享引用,同时补充图片宽高比的计算(修复Shader中比例错误导致的拉伸问题):

import { useMemo, useState, useEffect } from 'react';
import { TextureLoader } from 'three';
import { Canvas } from '@react-three/fiber';
import { useSpring, animated, config } from '@react-spring/three';

import { HoverImageShader } from 'shader/HoverImageShader';

const AnimatedImage = ({
  src,
  playAnim = false,
  width = '100%',
  height = '100%',
  textureWidth = 10.8,
  textureHeight = 10.8,
}) => {
  const imgTexture = useMemo(() => {
    const loader = new TextureLoader();
    return loader.load(src);
  }, [src]);

  // 计算图片自身的宽高比
  const [imageAspect, setImageAspect] = useState(1);
  useEffect(() => {
    const img = new Image();
    img.src = src;
    img.onload = () => setImageAspect(img.width / img.height);
  }, [src]);

  // 计算画布容器的宽高比
  const canvasAspect = useMemo(() => {
    const w = typeof width === 'number' ? width : 100;
    const h = typeof height === 'number' ? height : 100;
    return w / h;
  }, [width, height]);

  const { hoverValue } = useSpring({
    hoverValue: playAnim ? 1 : 0,
    config: config.molasses,
  });

  // 复制Shader的uniforms,避免全局共享
  const shaderUniforms = useMemo(() => ({
    ...HoverImageShader.uniforms,
    tDiffuse: { ...HoverImageShader.uniforms.tDiffuse, value: imgTexture },
    imageAspectRatio: { ...HoverImageShader.uniforms.imageAspectRatio, value: imageAspect },
    aspectRatio: { ...HoverImageShader.uniforms.aspectRatio, value: canvasAspect },
    hover: { ...HoverImageShader.uniforms.hover, value: hoverValue },
  }), [imgTexture, imageAspect, canvasAspect, hoverValue]);

  return (
    <Canvas
      pixelratio={window.devicePixelRatio || 1}
      style={{ width, height }}
      camera={{ fov: 75, position: [0, 0, 7] }}
    >
      <animated.mesh>
        <planeBufferGeometry
          attach='geometry'
          args={[textureWidth, textureHeight]}
        />
        <animated.shaderMaterial
          attach='material'
          transparent
          args={[
            {
              ...HoverImageShader,
              uniforms: shaderUniforms,
            },
          ]}
        />
      </animated.mesh>
    </Canvas>
  );
};

export default AnimatedImage;

2. 修复hover效果全局触发问题

将全局hover状态改为每个卡片独立的状态,用索引跟踪当前hover的卡片:

// 父组件中替换原hover状态定义
const [hoveredIndex, setHoveredIndex] = useState(-1);

// 映射渲染代码修改
<div ref={trackRef} className={styles.cardsTrack}>
  {cardsData?.map((card, index) => (
    <div
      key={card.id} // 用卡片ID代替索引作为key,避免列表重排时的状态混乱
      className={styles.cardContainer}
    >
      <Link to={`/cards/${card.id}`} className={styles.link}>
        <div
          className={styles.cardImageContainer}
          onMouseEnter={() => setHoveredIndex(index)}
          onMouseLeave={() => setHoveredIndex(-1)}
        >
          <AnimatedImage src={card.image} playAnim={hoveredIndex === index} />
        </div>
      </Link>
    </div>
  ))}
</div>

内容的提问来源于stack exchange,提问作者Oliver

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 22:15:30