three.js动画

93 阅读5分钟
// ImageAnimation.tsx


'use client'

import { useEffect, useRef } from 'react';

import * as THREE from 'three';

import { gsap } from 'gsap';

import { ScrollTrigger } from 'gsap/ScrollTrigger';

import { FontLoader } from 'three/addons/loaders/FontLoader.js';

import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';

interface AnimatedImagesProps {

images: string[]; // 图片URL数组

texts: string[]; // 文字数组

SecondTexts: string[]; // 文字数组

SecondTargetPositions: [number, number, number][]; // 第二目标位置数组 [[x,y,z], ...]

targetPositions: [number, number, number][]; // 目标位置数组 [[x,y,z], ...]

}

gsap.registerPlugin(ScrollTrigger);

const AnimatedImages: React.FC<AnimatedImagesProps> = ({ images, SecondTexts,targetPositions,texts ,SecondTargetPositions}) => {

const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {

if (!containerRef.current) return;

  


// 创建场景

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(

75,

window.innerWidth / window.innerHeight,

0.1,

1000

);

// camera.position.set(0, -10, 20);

// camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });

renderer.setSize(window.innerWidth, window.innerHeight);

containerRef.current.appendChild(renderer.domElement);

  


// 计算响应式尺寸

const calculateImageSize = () => {

const aspectRatio = window.innerWidth / window.innerHeight;

let imageWidth, imageHeight;

  


if (window.innerWidth <= 768) {

// 移动设备

imageWidth = 1.5;

imageHeight = 1.5;

} else if (window.innerWidth <= 1024) {

// 平板设备

imageWidth = 2;

imageHeight = 2;

} else {

// 桌面设备

imageWidth = 2.5;

imageHeight = 2.5;

}

  


return { width: imageWidth, height: imageHeight };

};

  
  


// 创建图片平面 32-46

const imageGroups: THREE.Group[] = [];

const textureLoader = new THREE.TextureLoader();

const { width: initialWidth, height: initialHeight } = calculateImageSize();

images.forEach((imageUrl, index) => {

const texture = textureLoader.load(imageUrl);

const geometry = new THREE.PlaneGeometry(initialWidth, initialHeight);

const material = new THREE.MeshBasicMaterial({

map: texture,

transparent: true,

opacity: 0.8, // 设置初始透明度为0

});

const mesh = new THREE.Mesh(geometry, material);

const group = new THREE.Group();

group.add(mesh);

// 计算屏幕底部的位置

const screenBottomY = -10; // 可以根据需要调整这个值

// 设置初始位置在屏幕正下方

group.position.set(0, screenBottomY, 0);

scene.add(group);

imageGroups.push(group);

});

  


// 调整相机位置以适应不同屏幕

const updateCameraPosition = () => {

const aspectRatio = window.innerWidth / window.innerHeight;

if (window.innerWidth <= 768) {

camera.position.z = 8; // 移动设备距离更远

} else if (window.innerWidth <= 1024) {

camera.position.z = 7; // 平板设备

} else {

camera.position.z = 5; // 桌面设备

}

camera.aspect = aspectRatio;

camera.updateProjectionMatrix();

};

  


updateCameraPosition();

  const addLights = () => {

// 环境光

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);

scene.add(ambientLight);

  


// 方向光

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);

directionalLight.position.set(5, 5, 5);

scene.add(directionalLight);

  


// 点光源(可选)

const pointLight = new THREE.PointLight(0xffffff, 0.5);

pointLight.position.set(0, 0, 5);

scene.add(pointLight);

};

  


// 在场景初始化时调用

addLights();


// 处理窗口大小变化

const handleResize = () => {

const { width: newWidth, height: newHeight } = calculateImageSize();

// 更新渲染器大小

renderer.setSize(window.innerWidth, window.innerHeight);

// 更新相机

camera.aspect = window.innerWidth / window.innerHeight;

camera.updateProjectionMatrix();

updateCameraPosition();

  


// 更新所有图片的大小

imageGroups.forEach(group => {

const mesh = group.children[0] as THREE.Mesh;

const newGeometry = new THREE.PlaneGeometry(newWidth, newHeight);

mesh.geometry.dispose();

mesh.geometry = newGeometry;

});

  


// 更新图片位置

imageGroups.forEach((group, index) => {

const targetPos = targetPositions[index];

// 根据屏幕大小调整位置

const scaleFactor = window.innerWidth <= 768 ? 0.7 : 1;

group.position.set(

targetPos[0] * scaleFactor,

targetPos[1] * scaleFactor,

targetPos[2]

);

});

};

  


window.addEventListener('resize', handleResize);

  


// 创建动画

let animationsCompleted = 0;

imageGroups.forEach((group, index) => {

const targetPos = targetPositions[index];

const scaleFactor = window.innerWidth <= 768 ? 0.7 : 1; // 移动设备缩放因子

// 初始动画

gsap.to(group.position, {

x: targetPos[0] * scaleFactor,

y: targetPos[1] * scaleFactor,

z: targetPos[2],

duration: 1.5,

delay: index * 0.2,

ease: 'power2.out',

onComplete: () => {

animationsCompleted++;

if (animationsCompleted === imageGroups.length) {

const textMeshes: THREE.Mesh[] = [];

const fontLoader = new FontLoader();

fontLoader.load('https://threejs.org/examples/fonts/helvetiker_regular.typeface.json', (font) => {

//第一组文字

texts.forEach((text, index) => {

const textGeometry = new TextGeometry(text, {

font: font,

size: 0.5,

depth: 0.2,

});

// 或者使用十六进制颜色字符串

const textColors = [

'#ff0000', // 红色

'#00ff00', // 绿色

'#0000ff', // 蓝色

'#ffff00', // 黄色

'#ff00ff', // 粉色

'#00ffff', // 青色

'#ffa500', // 橙色

'#800080' // 紫色

];

const textMaterial = new THREE.MeshStandardMaterial({

color: new THREE.Color(textColors[index] || '#ffffff'),

transparent: true, // 确保添加这个属性以支持透明度动画

metalness: 0.5, // 金属感

roughness: 0.5, // 粗糙度

emissive: new THREE.Color(textColors[index] || '#ffffff').multiplyScalar(0.2), // 发光效果

});

const textMesh = new THREE.Mesh(textGeometry, textMaterial);

// 居中文字

textGeometry.computeBoundingBox();

if (textGeometry.boundingBox) {

const centerOffset = -0.5 * (textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x);

textMesh.position.x = centerOffset;

}

// 初始位置在底部,每行错开位置

textMesh.position.y = -5;

scene.add(textMesh);

textMeshes.push(textMesh);

  


gsap.to(textMesh.position, {

y: -1 + index * 1, // 最终位置,每行错开1个单位

duration: 1.5,

delay: index * 0.3, // 错开动画开始时间

ease: "power2.out",

onComplete: () => {

// 滚动时向上移动并移出

gsap.to(textMesh.position, {

y: '+=15', // 向上移动10个单位

scrollTrigger: {

trigger: containerRef.current,

start: "top top", // 当容器顶部碰到视窗顶部时开始

end: "20%", // 滚动1000px后结束

scrub: 1, // 平滑滚动

// markers: true, // 调试用,可以看到触发位置

}

});

  


// 同时控制透明度

gsap.to(textMesh.material, {

opacity: 0,

scrollTrigger: {

trigger: containerRef.current,

start: "top top",

end: "20%",

scrub: 1,

}

});

}

})

});

  


// 第二组文字

const secondTextMeshes: THREE.Mesh[] = [];

SecondTexts.forEach((text, index) => {

const textGeometry = new TextGeometry(text, {

font: font,

size: 0.5,

depth: 0.2,

});

const textMaterial = new THREE.MeshPhongMaterial({

color: 0xffffff,

transparent: true,

opacity: 0, // 初始设置为透明

});

const textMesh = new THREE.Mesh(textGeometry, textMaterial);

// 居中文字

textGeometry.computeBoundingBox();

if (textGeometry.boundingBox) {

const centerOffset = -0.5 * (textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x);

textMesh.position.x = centerOffset;

}

// 初始位置设置在更低的位置

textMesh.position.y = -15;

scene.add(textMesh);

secondTextMeshes.push(textMesh);

// 第二组文字的动画,跟随滚动从底部进入

// 创建位置动画

gsap.fromTo(textMesh.position,

{

y: -15 // 起始位置

},

{

y: -1 + index * 1, // 目标位置

scrollTrigger: {

trigger: containerRef.current,

start: "30% top ", // 修改触发位置

end: "40%",

scrub: 1,

// markers: true, // 调试用

toggleActions: "play none none reverse"

}

}

);

// 同时控制透明度

// 创建独立的透明度动画

gsap.fromTo(textMaterial,

{

opacity: 0

},

{

opacity: 1,

scrollTrigger: {

trigger: containerRef.current,

start: "40% top", // 修改触发位置

end: "60%",

scrub: 1,

toggleActions: "play none none reverse"

}

}

);

});

});

  


// 鼠标滚动图片动画

imageGroups.forEach((group, i) => {

const startPos = targetPositions[i];

const endPos = SecondTargetPositions[i];

gsap.fromTo(group.position,

{

x: startPos[0],

y: startPos[1],

z: startPos[2]

},

{

x: endPos[0],

y: endPos[1],

z: endPos[2],

scrollTrigger: {

trigger: containerRef.current,

start: "top top",

end: "+=1000", // 滚动1000px后结束

scrub: 1, // 平滑过渡

markers: true, // 调试用

toggleActions: "play none none reverse"

}

}

);

});

}

}

})

});

  
  
  
  


// 动画循环

const animate = () => {

requestAnimationFrame(animate);

renderer.render(scene, camera);

};

animate();

  
  


// 清理函数

return () => {

window.removeEventListener('resize', handleResize);

containerRef.current?.removeChild(renderer.domElement);

scene.clear();

imageGroups.forEach(group => {

const mesh = group.children[0] as THREE.Mesh;

mesh.geometry.dispose();

(mesh.material as THREE.Material).dispose();

});

};

}, [images, targetPositions,texts,SecondTargetPositions,SecondTexts]);

  


return <div ref={containerRef} style={{ width: '100%', height: '100vh' }} />;

};

  


export default AnimatedImages;
// Image.tsx

import AnimatedImages from '../components/ImageAnimation';

  


export default function Home() {

const images = [

'/img0.jpg',//阳光

'/img1.jpg',

'/img2.jpg',

'/img3.jpg',

'/img4.jpg',

'/img5.jpg',

'/img6.jpg',

'/img7.jpg',// 葡萄

];

  


const targetPositions: [number, number, number][] = [

[-5, 3, 0], // 左上

[0, 2, 0], // 中上

[5, 3, 0], // 右上

[-7, 0, 0], // 左中

[1, 1, 0], // 右中

[-2, -1, 0], // 左下

[0, -1, 0], // 中下

[4, -2.5, 0], // 右下

];

const SecondTargetPositions: [number, number, number][] = [

[0, 2, 0], // 左上

[1, 1, 0], // 中上

[0, -1, 0], // 右上

[-2, 1, 0], // 左中

[1, -1, 0], // 右中

[-1, 1, 0], // 左下

[1, -1, 0], // 中下

[3, -1.5, 0], // 右下

];

const texts = [

'threejs----1',

'world',

'hello',

];

const SecondTexts = [

'threejs',

'world',

'hello'

];

  


return (

<div>

<AnimatedImages

images={images}

targetPositions={targetPositions}

SecondTargetPositions={SecondTargetPositions}

texts={texts}

SecondTexts={SecondTexts}

/>

</div>

);

}
// targetPositions的设置
// 假设我们的相机设置如下:
const camera = new THREE.PerspectiveCamera(
    75, // FOV (视野角度)
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);

// 相机位置设置
const updateCameraPosition = () => {
    if (window.innerWidth <= 768) {
        camera.position.z = 8; // 移动设备
    } else if (window.innerWidth <= 1024) {
        camera.position.z = 7; // 平板设备
    } else {
        camera.position.z = 5; // 桌面设备
    }
};

// 基于这个相机设置,以下是合理的 targetPositions 区间:
const targetPositions: [number, number, number][] = [
    // X轴范围:-8 到 8
    // Y轴范围:-4 到 4
    // Z轴范围:通常保持为 0
    
    [-8, 3, 0],     // 左上
    [-4, 3, 0],     // 左上偏中
    [0, 3, 0],      // 正上
    [4, 3, 0],      // 右上偏中
    [8, 3, 0],      // 右上
    [-6, -3, 0],    // 左下
    [0, -3, 0],     // 正下
    [6, -3, 0],     // 右下
];

// 第二个位置数组(滚动后的位置)
const SecondTargetPositions: [number, number, number][] = [
    [-6, 2, 0],     // 相对第一个位置略微集中
    [-3, 2, 0],
    [0, 2, 0],
    [3, 2, 0],
    [6, 2, 0],
    [-4, -2, 0],
    [0, -2, 0],
    [4, -2, 0],
];




// X轴的区间
// 水平方向
-88 之间:
- 负值:向左偏移
- 正值:向右偏移
- 0:屏幕中心

推荐区间:
- 外侧元素:±6 到 ±8
- 中间元素:±2 到 ±4
- 中心元素:0

// Y轴
// 垂直方向
-44 之间:
- 负值:向下偏移
- 正值:向上偏移
- 0:屏幕中心

推荐区间:
- 上方元素:24
- 中间元素:-11
- 下方元素:-2 到 -4

// Z轴

// 深度方向
通常保持为 0
- 负值:向屏幕内
- 正值:向屏幕外
- 0:默认平面

特殊效果可用范围:-22