前言
最近在学Three.js.,对着文档看了一周多,正好赶上码上掘金的活动,就顺便写了一个小demo,手搓一个罗盘特效。
太极
先来看一下太极的实现方式,这里我们使用CircleGeometry,将其分解开来可以看出是由圆形和半圆形组成 。
CircleGeometry
CircleGeometry | 官网案例 |
---|---|
radius | 半径 |
segments | 分段(三角面)的数量 |
thetaStart | 第一个分段的起始角度 |
thetaLength | 圆形扇区的中心角 |
这里不需要用到segments,但是需要颜色,所以定义一个函数传入半径、颜色、起始角度、中心角。
const createCircle = (r, color, thetaStart, thetaLength) => {
const material = new THREE.MeshBasicMaterial({
color: color,
side: THREE.DoubleSide
});
const geometry = new THREE.CircleGeometry(r, 64, thetaStart, thetaLength);
const circle = new THREE.Mesh(geometry, material);
return circle;
};
我们只需要通过传参生产不同大小的圆或半圆,再进行位移就可以实现其效果。
参考代码/73-96行 还有一些需要注意的地方写在注释里了。
罗盘
接下来看罗盘的实现,罗盘由一个个圆环组成,一个圆环又由内圈、外圈、分隔线、文字、八卦构成。
内外圈
内外圈我们使用两个RingGeometry
RingGeometry | 官网案例 |
---|---|
innerRadius | 内部半径 |
outerRadius | 外部半径 |
thetaSegments | 圆环的分段数 |
phiSegments | 圆环的分段数 |
thetaStart | 起始角度 |
thetaLength | 圆心角 |
通过circle控制内外圆圈的尺寸,circleWidth控制圆圈的线宽
const circleWidth = [0.1, 0.1]
const circle = [0, 1];
circle.forEach((i, j) => {
const RingGeo = new THREE.RingGeometry(
innerRing + i,
innerRing + i + circleWidth[j],
64,
1
);
const Ring = new THREE.Mesh(RingGeo, material);
RingGroup.add(Ring);
});
分隔线
分隔线使用的是PlaneGeometry
PlaneGeometry | 官网案例 |
---|---|
width | 宽度 |
height | 高度 |
widthSegments | 宽度分段数 |
heightSegments | 高度分段数 |
关于分隔线,它的长度就是内外圈的差值,所以这里使用外圈的数值,确定与圆心的距离就要使用内圈的数值加上自身长度除2。除此之外,还需要计算分隔线与圆心的夹角。
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
const planeGeo = new THREE.PlaneGeometry(lineWidth, circle[1]);
const line = new THREE.Mesh(planeGeo, material);
line.position.set(x, y, 0);
line.rotation.set(0, 0, rad + Math.PI / 2);
RingGroup.add(line);
}
文字
文字使用的是TextGeometry,定位与分隔线一致,只需要交错开来。
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
var txtGeo = new THREE.TextGeometry(text[i % text.length], {
font: font,
size: size,
height: 0.001,
curveSegments: 12,
});
txtGeo.translate(offsetX, offsetY, 0);
var txt = new THREE.Mesh(txtGeo, material);
txt.position.set(x, y, 0);
txt.rotation.set(0, 0, rad + -Math.PI / 2);
RingGroup.add(txtMesh);
不过TextGeometry的使用有一个得注意得前提,我们需要引入字体文件。
const fontLoader = new THREE.FontLoader();
const fontUrl =
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/fonts.json";
let font;
const loadFont = new Promise((resolve, reject) => {
fontLoader.load(
fontUrl,
function (loadedFont) {
font = loadedFont;
resolve();
},
undefined,
function (err) {
reject(err);
}
);
});
八卦
圆环中除了文字之外,还能展示八卦,通过传递baguaData给createBagua生成每一个符号。
const baguaData = [
[1, 1, 1],
[0, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0],
[1, 0, 1],
[1, 1, 0],
];
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
RingGroup.add(
createBagua(baguaData[i % 8], x, y, 0 , rad + Math.PI / 2, text[0]),
);
}
createBagua参考代码/114-146行 ,和分隔线是一样的,使用了PlaneGeometry只是做了一些位置的设置。
视频贴图
在罗盘外,还有一圈视频,这里是用到了VideoTexture,实现也很简单。唯一得注意的是视频的跨域问题,需要配置video.crossOrigin = "anonymous"
const videoSrc = [
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/yAC65vN6.mp4",
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/6Z5VZdZM.mp4",
];
video.src = videoSrc[Math.floor(Math.random() * 2)];
video.crossOrigin = "anonymous";
const texture = new THREE.VideoTexture(video);
...
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
map: texture,
});
动画
动画总共分为三个部分,一块是旋转动画,一块是分解动画和入场动画,我们使用gsap实现。
旋转动画
gsap.to(videoGroup.rotation, {
duration: 30,
y: -Math.PI * 2,
repeat: -1,
ease: "none",
});
分解动画
.to(RingGroup.position, {
duration: 1,
ease: "ease.inOut",
y: Math.random() * 10 - 5,
delay: 5,
})
.to(RingGroup.position, {
duration: 1,
ease: "ease.inOut",
delay: 5,
y: 0,
})
}
入场动画
item.scale.set(1.2, 1.2, 1.2);
gsap.to(item.scale, {
duration: 0.8,
x: 1,
y: 1,
repeat: 0,
ease: "easeInOut",
});
旋转动画与分解动画可以写在生成函数内,也可以写在添加scene时,但是入场动画只能写到scene后,因为在生成时,动画就添加上了,当我们点击开始的时候才会将其加入场景中,而这时动画可能已经执行了。
结尾
结合文档与chatpgt还是写完了,中间还是遇到蛮多坑的,视频贴图的跨域问题,font需要引入对应的字体文件。如果再加上着色器的话,效果可能会更不错,不过目前只是看了一点皮毛。