艺术品展示
代码结构
- 组件传值接口
- 烟雾组件全局变量定义
- 烟雾位置移动函数
- 烟雾不同颜色改变函数
- 动画函数
- 初始化webgl
interface Iprops {
src: string;
opacity: number;
smokeSrc: string;
smokeOpacity: number;
height: string;
width: string;
}
const CanvasSmoke = () => {
const location = useLocation();
const props: Iprops = location.state || {};
const mount = useRef<HTMLDivElement>(null);
const clockRef = useRef(new THREE.Clock());
......
const evolveSmoke = useCallback(() => {
......
}, []);
const colorchange = useCallback(() => {
......
}, []);
const animate = useCallback(() => {
......
}, [evolveSmoke, colorchange]);
useEffect(() => {
......
}, [props.src, props.opacity, props.smokeSrc, props.smokeOpacity, animate]);
return (
<div
style={{
height: props.height || '100vh',
width: props.width || '100vw',
margin: '0'
}}
ref={mount}
/>
);
};
export default CanvasSmoke;
烟雾位置移动
- 获取粒子数量
- 循环给每个粒子围绕Z轴以及Y轴旋转,其中deltaRef是时间间隔
- 直到数量为0停止
evolveSmoke
const evolveSmoke = useCallback(() => {
let sp = smokeParticlesRef.current.length;
while (sp--) {
smokeParticlesRef.current[sp].rotation.z += deltaRef.current * 0.5;
smokeParticlesRef.current[sp].rotation.y += 0.02;
// smokeParticlesRef.current[sp].rotation.x += 0.02;
}
}, []);
烟雾设置不同颜色
- 设置随机颜色
- 关键说明
saturation 设为 1.0(最大饱和度) 0.0 = 灰色 0.5 = 中等饱和度 1.0 = 最大饱和度
// 高饱和度,较亮 const saturation = 1.0; const lightness = 0.6;
// 高饱和度,较暗 const saturation = 1.0; const lightness = 0.4;
colorchange
const colorchange = useCallback(() => {
timeRef.current += deltaRef.current;
const hue = (timeRef.current * 0.1 + 0.01) % 1;//色相
const saturation = 1.0; // 饱和度:1.0 表示最饱和
const lightness = 0.5; // 亮度:0.5 表示中等亮度
colorRef.current = new THREE.Color().setHSL(hue, saturation, lightness);
}, []);
动画
- 移动位置
- 设置颜色
- 更改文字材质颜色
- 渲染
const animate = useCallback(() => {
if (!rendererRef.current || !sceneRef.current || !cameraRef.current || !meshRef.current) return;
deltaRef.current = clockRef.current.getDelta();
frameIdRef.current = requestAnimationFrame(animate);
evolveSmoke();
colorchange();
meshRef.current.rotation.x += 0.005;
meshRef.current.rotation.y += 0.01;
cubeSineDriverRef.current += 0.01;
meshRef.current.position.z = 100 + Math.sin(cubeSineDriverRef.current) * 500;
if (textRef.current?.material instanceof THREE.MeshLambertMaterial) {
textRef.current.material.color = colorRef.current;
textRef.current.material.needsUpdate = true;
}
rendererRef.current.render(sceneRef.current, cameraRef.current);
}, [evolveSmoke, colorchange]);
初始化
初始setup
设置渲染区域大小,相机位置,场景内容,背景材质
第一个textMaterial主要渲染文字
- 创建一个 300x300 的平面几何体用于显示文字
- 加载纹理
- 创建材质
- 创建网格
- 设置文字的位置
- 添加到场景
- 保存文字网格的引用
重要特性解释:
- 材质特性:
-
color: 基础颜色(从 colorRef 获取)
-
opacity: 透明度(从 props 获取)
-
transparent: 启用透明度处理
-
blending: 使用加法混合模式,使重叠部分更亮
- AdditiveBlending(加法混合):
-
将前景色与背景色的RGB值相加
-
创造出发光的效果
-
适合创建霓虹、光效等视觉效果
- 位置设置:
-
text.position.z = 800 将文字放在场景较远处
-
这样文字会在烟雾效果的后方显示
- 引用保存:
-
使用 textRef 保存文字网格的引用
-
方便后续对文字进行动画或其他操作
第二个textMaterial主要渲染烟雾
设置300*300的烟雾切片
对每个切片循环设置不同的颜色 保存烟雾网格的引用
- 渲染
- 卸载动画以及渲染器
useEffect(() => {
const mountElement = mount.current;
if (!mountElement) return;
const width = mountElement.clientWidth;
const height = mountElement.clientHeight;
//Three.js setup
rendererRef.current = new THREE.WebGLRenderer();
rendererRef.current.setSize(width, height);
sceneRef.current = new THREE.Scene();
cameraRef.current = new THREE.PerspectiveCamera(75, width / height, 1, 10000);
cameraRef.current.position.z = 1000;
sceneRef.current.add(cameraRef.current);
let geometry = new THREE.BoxGeometry(200, 200, 200);
let material = new THREE.MeshLambertMaterial({
color: 0xaa6666,
wireframe: false
});
meshRef.current = new THREE.Mesh(geometry, material);
// 创建一个 300x300 的平面几何体用于显示文字
let textGeo = new THREE.PlaneGeometry(300, 300);
// 加载纹理
new THREE.TextureLoader().load(
props.src, // 纹理图片的源路径
// 成功加载后的回调函数
(textTexture) => {
// 创建材质
let textMaterial = new THREE.MeshLambertMaterial({
color: colorRef.current, // 使用引用的颜色
opacity: props.opacity, // 透明度
map: textTexture, // 加载的纹理
transparent: true, // 启用透明
blending: THREE.AdditiveBlending // 加法混合模式
});
// 创建网格(结合几何体和材质)
let text = new THREE.Mesh(textGeo, textMaterial);
// 设置文字在 z 轴上的位置(距离相机 800 单位)
text.position.z = 800;
// 添加到场景
sceneRef.current.add(text);
// 保存文字网格的引用
textRef.current = text;
},
undefined, // 加载进度回调(这里未使用)
(err) => { // 加载失败回调
console.log('load failed.');
console.log(err);
}
);
let light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(-1, 0, 1);
sceneRef.current.add(light);
new THREE.TextureLoader().load(
props.smokeSrc,
(smokeTexture) => {
for (let p = 0; p < 150; p++) {
const hue = Math.random();
const saturation = 1.0;
const lightness = 0.5;
const color = new THREE.Color().setHSL(hue, saturation, lightness);
let smokeMaterial = new THREE.MeshLambertMaterial({
color: color,
map: smokeTexture,
opacity: props.smokeOpacity,
transparent: true,
blending: THREE.AdditiveBlending
});
let smokeGeo = new THREE.PlaneGeometry(300, 300);
let particle = new THREE.Mesh(smokeGeo, smokeMaterial);
particle.position.set(
Math.random() * 500 - 250,
Math.random() * 500 - 250,
Math.random() * 1000 - 100
);
particle.rotation.z = Math.random() * 360;
sceneRef.current.add(particle);
smokeParticlesRef.current.push(particle);
}
},
undefined,
(err) => {
console.log('load failed.');
console.log(err);
}
);
mountElement.appendChild(rendererRef.current.domElement);
// remember these initial values
// let tanFOV = Math.tan(((Math.PI / 180) * this.camera.fov) / 2);
// let windowHeight = height;
animate();
return () => {
if (frameIdRef.current) {
cancelAnimationFrame(frameIdRef.current);
}
if (mountElement && rendererRef.current) {
mountElement.removeChild(rendererRef.current.domElement);
}
};
}, [props.src, props.opacity, props.smokeSrc, props.smokeOpacity, animate]);
完整内容
import React, {useCallback, useEffect, useRef} from 'react';
import {useLocation} from 'react-router-dom';
import * as THREE from 'three';
interface Iprops {
src: string;
opacity: number;
smokeSrc: string;
smokeOpacity: number;
height: string;
width: string;
}
const CanvasSmoke = () => {
const location = useLocation();
const props: Iprops = location.state || {};
const mount = useRef<HTMLDivElement>(null);
const clockRef = useRef(new THREE.Clock());
const rendererRef = useRef<THREE.WebGLRenderer>(new THREE.WebGLRenderer());
const sceneRef = useRef<THREE.Scene>(new THREE.Scene());
const cameraRef = useRef<THREE.PerspectiveCamera>(new THREE.PerspectiveCamera());
const meshRef = useRef<THREE.Mesh>(new THREE.Mesh());
const smokeParticlesRef = useRef<THREE.Mesh[]>([]);
const cubeSineDriverRef = useRef(0);
const frameIdRef = useRef<number | undefined>(undefined);
const deltaRef = useRef(0);
const colorRef = useRef<THREE.Color>(new THREE.Color());
const timeRef = useRef(0);
const textRef = useRef<THREE.Mesh>(new THREE.Mesh());
const evolveSmoke = useCallback(() => {
let sp = smokeParticlesRef.current.length;
while (sp--) {
smokeParticlesRef.current[sp].rotation.z += deltaRef.current * 0.5;
smokeParticlesRef.current[sp].rotation.y += 0.02;
// smokeParticlesRef.current[sp].rotation.x += 0.02;
}
}, []);
const colorchange = useCallback(() => {
timeRef.current += deltaRef.current;
const hue = (timeRef.current * 0.1 + 0.01) % 1;
const saturation = 1.0; // 饱和度:1.0 表示最饱和
const lightness = 0.5; // 亮度:0.5 表示中等亮度
colorRef.current = new THREE.Color().setHSL(hue, saturation, lightness);
}, []);
const animate = useCallback(() => {
if (!rendererRef.current || !sceneRef.current || !cameraRef.current || !meshRef.current) return;
deltaRef.current = clockRef.current.getDelta();
frameIdRef.current = requestAnimationFrame(animate);
evolveSmoke();
colorchange();
meshRef.current.rotation.x += 0.005;
meshRef.current.rotation.y += 0.01;
cubeSineDriverRef.current += 0.01;
meshRef.current.position.z = 100 + Math.sin(cubeSineDriverRef.current) * 500;
if (textRef.current?.material instanceof THREE.MeshLambertMaterial) {
textRef.current.material.color = colorRef.current;
textRef.current.material.needsUpdate = true;
}
rendererRef.current.render(sceneRef.current, cameraRef.current);
}, [evolveSmoke, colorchange]);
useEffect(() => {
const mountElement = mount.current;
if (!mountElement) return;
const width = mountElement.clientWidth;
const height = mountElement.clientHeight;
//Three.js setup
rendererRef.current = new THREE.WebGLRenderer();
rendererRef.current.setSize(width, height);
sceneRef.current = new THREE.Scene();
cameraRef.current = new THREE.PerspectiveCamera(75, width / height, 1, 10000);
cameraRef.current.position.z = 1000;
sceneRef.current.add(cameraRef.current);
let geometry = new THREE.BoxGeometry(200, 200, 200);
let material = new THREE.MeshLambertMaterial({
color: 0xaa6666,
wireframe: false
});
meshRef.current = new THREE.Mesh(geometry, material);
let textGeo = new THREE.PlaneGeometry(300, 300);
new THREE.TextureLoader().load(
props.src,
(textTexture) => {
let textMaterial = new THREE.MeshLambertMaterial({
color: colorRef.current,
opacity: props.opacity,
map: textTexture,
transparent: true,
blending: THREE.AdditiveBlending
});
let text = new THREE.Mesh(textGeo, textMaterial);
text.position.z = 800;
sceneRef.current.add(text);
textRef.current = text;
},
undefined,
(err) => {
console.log('load failed.');
console.log(err);
}
);
let light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(-1, 0, 1);
sceneRef.current.add(light);
new THREE.TextureLoader().load(
props.smokeSrc,
(smokeTexture) => {
for (let p = 0; p < 150; p++) {
const hue = Math.random();
const saturation = 1.0;
const lightness = 0.5;
const color = new THREE.Color().setHSL(hue, saturation, lightness);
let smokeMaterial = new THREE.MeshLambertMaterial({
color: color,
map: smokeTexture,
opacity: props.smokeOpacity,
transparent: true,
blending: THREE.AdditiveBlending
});
let smokeGeo = new THREE.PlaneGeometry(300, 300);
let particle = new THREE.Mesh(smokeGeo, smokeMaterial);
particle.position.set(
Math.random() * 500 - 250,
Math.random() * 500 - 250,
Math.random() * 1000 - 100
);
particle.rotation.z = Math.random() * 360;
sceneRef.current.add(particle);
smokeParticlesRef.current.push(particle);
}
},
undefined,
(err) => {
console.log('load failed.');
console.log(err);
}
);
mountElement.appendChild(rendererRef.current.domElement);
// remember these initial values
// let tanFOV = Math.tan(((Math.PI / 180) * this.camera.fov) / 2);
// let windowHeight = height;
animate();
return () => {
if (frameIdRef.current) {
cancelAnimationFrame(frameIdRef.current);
}
if (mountElement && rendererRef.current) {
mountElement.removeChild(rendererRef.current.domElement);
}
};
}, [props.src, props.opacity, props.smokeSrc, props.smokeOpacity, animate]);
return (
<div
style={{
height: props.height || '100vh',
width: props.width || '100vw',
margin: '0'
}}
ref={mount}
/>
);
};
export default CanvasSmoke;