我正在参加 #金石计划征文活动# ,希望各位点个赞,谢谢
效果图
绘制静态图
在threejs的场景中使用line
和Line2
绘制出外围由线段组成的部分,
外围虚线圆
下面的代码是其中一条圆形虚线的代码,其中材质用到的是LineMaterial
,属性包含color
颜色、linewidth
宽度、dashed
是否虚线、dashSize
虚线尺寸、gapSize
间隔尺寸、opacity
透明度、transparent
是否支持透明。
ts
const createRoundDashed1 = () => {
const geometry = new LineGeometry();
geometry.setPositions(getRoundPoints(50));
const matLine = new LineMaterial({
color: lineColor,
linewidth: 20,
dashed: true,
dashSize: 360 / 16,
gapSize: 360 / 16,
opacity: 0.1,
transparent: true
});
lineMaterials.push(matLine)
roundDashed1 = new Line2(geometry, matLine);
roundDashed1.computeLineDistances();
roundDashed1.scale.set(1, 1, 1);
scene.add(roundDashed1)
}
计算圆形的点位
getRoundPoints
方法是通过一个半径值生成一个圆弧并得到圆弧上的所有点位,供给LineGeometry
使用,作为position属性。内部通过三角函数中Math.sin
正弦和Math.cos
余弦计算得出。在之后所有的圆形绘制都使用这个方法获取圆
ts
const getRoundPoints = (R: number, N = 361): number[] => {
// 批量生成圆弧上的顶点数据
const vertices: number[] = []
for (var i = -N / 2; i < N / 2; i++) {
var angle = 2 * Math.PI / N * i;
var x = R * Math.sin(angle);
var y = R * Math.cos(angle);
vertices.push(x, y, 0)
}
return vertices
}
上面的虚线作为将来动图的第一组,也就是z坐标最接近0的位置,所以将上面创建的3个元素变成一个组,并在组的usedata里设置一个layer属性为1
围绕式字母
字母这里相对比较复杂一些,用到FontLoader
加载文字和InstancedFlow
流的实例化,InstancedFlow
主要作用就是将多个模型通过重复的方式展现,其提供的updateCurve
接受一个曲线路径,那么重复展现的物体将以这个曲线路径去排列,咱们这里使用到的就是加载一段文字并创建TextGeometry
,字体用的是threejs提供的字体json文件。
ts
const loader = new FontLoader();
loader.load('../node_modules/three/examples/fonts/droid/droid_sans_mono_regular.typeface.json', function (font) {
const letterSize = 2, offset = 1.5, letterRoundD = 28
const letterGeometry = new TextGeometry(' BLACKCATI XY ', {
font: font,
size: letterSize,
height: 0.05,
bevelThickness: 0.02,
bevelSize: 0.01,
});
letterGeometry.rotateX(Math.PI * 0.5);
const material = new THREE.MeshStandardMaterial({
color: lineColor
});
const numberOfInstances = 8;
let flow = new InstancedFlow(numberOfInstances, 1, letterGeometry, material);
const points: THREE.Vector3[] = []
const n = getRoundPoints(letterRoundD)
for (let i = 0; i < n.length / 3; i++) {
points.push(new THREE.Vector3().fromArray(n, i * 3))
}
const curve = new THREE.CatmullRomCurve3(points, true, 'centripetal');
flow.updateCurve(0, curve);
letterGroup.add(flow.object3D);
flow.setCurve(0, 0);
for (let i = 0; i < numberOfInstances; i++) {
const curveIndex = i % 1;
flow.setCurve(i, curveIndex);
flow.moveIndividualAlongCurve(i, i * 1 / numberOfInstances);
// flow.object3D.setColorAt( i, new THREE.Color( 0xffffff * Math.random() ) );
}
const createRound = (d: number) => {
const roundGeometry1 = new LineGeometry();
roundGeometry1.setPositions(getRoundPoints(d));
const matLine = new LineMaterial({
color: lineColor,
linewidth: 4,
});
lineMaterials.push(matLine)
let round1 = new Line2(roundGeometry1, matLine);
round1.computeLineDistances();
round1.scale.set(1, 1, 1);
letterGroup.add(round1)
}
createRound(letterRoundD - offset)
createRound(letterRoundD + letterSize + offset)
效果如下:
后面的矩形部分这里就不展示代码了,和前面绘制虚线的方法一样。
三角形和logo
三角形的点位坐标也是通过getRoundPoints
方法得到的,由三个点位组成的,第二个参数传3,意味着取得组成这个半径的圆的三个点,但是得到的图形并不是闭合的
那么就需要第四个点,第四个点和第一个点是相同的,所以将getRoundPoints
方法改造一下
ts
const getRoundPoints = (R: number, N = 361): number[] => {
// 批量生成圆弧上的顶点数据
const vertices: number[] = []
const start: number[] = []
for (var i = -N / 2; i < N / 2; i++) {
var angle = 2 * Math.PI / N * i;
var x = R * Math.sin(angle);
var y = R * Math.cos(angle);
if (i === -N / 2) {
// 将第一个点存下
start.push(x, y, 0)
}
vertices.push(x, y, 0)
}
// 将第一个点放在点位数组的最后一位
return [...vertices, ...start]
}
获取到三角形的定点信息后,利用创建圆形的方法创建由三个点位信息组成的Line2
。
logo用的是上一篇文章中所用到的logo,具体的点位信息获取这里就不赘述,感兴趣的同学可以看一下# THREE.JS------让你的logo切割出高级感。同样在获取点位信息后,创建
Line2
至此所有静态图形都绘制完成了,接下来要制作动效。
动效
图形结构
目前图形分为6层,每一层都单独制作效果,并根据层级的不同,改变每个组的z值,从而实现立体的效果
ts
// 最底层虚线的组
roundGroup.userData.layer = 1
scene.add(roundGroup)
// 上一层矩形的组
rectGroup.userData.layer = 2
scene.add(rectGroup)
// 字母和字母周围圆圈的组
letterGroup.userData.layer = 3
scene.add(letterGroup)
// 三组相同三角形的组
triangleGroup1.userData.layer = 4
scene.add(triangleGroup1)
// 独立三角形的组
triangleGroup2.userData.layer = 5
scene.add(triangleGroup2)
// logo 的组
girlGroup.userData.layer = 6
scene.add(girlGroup)
动效
tween动画
动效的实现方法使用的是tween,通过缩放各个元素的比例来实现显示的过程,在创建好元素后,将动画绑定到一个动画合集中,在所有元素加载完以后,调用tween的start方法来执行动画,以某个圆圈为例,比例从0放大到1的代码
ts
const createRoundDashed1 = () => {
...
roundDashed1 = new Line2(geometry, matLine);
roundDashed1.computeLineDistances();
roundDashed1.scale.set(0, 0, 0);
roundDashed1.name = 'roundDashed1'
animatList.push(new TWEEN.Tween(roundDashed1.scale)
.delay(300)
.to(new THREE.Vector3(1, 1, 1), 1000)
.onUpdate((value, a) => {
if (roundDashed1) {
roundDashed1.scale.copy(value)
}
}))
roundGroup.add(roundDashed1)
}
这个动画延迟300毫秒,并在1秒内执行完由new THREE.Vector3(0,0,0)
到new THREE.Vector3(1, 1, 1)
之间的变化,再将这个动画收集到animatList
中,我在画面中添加了一个按钮,在点击按钮的时候,循环遍历animatList
的所有动画,并调用动画的start
方法。
ts
const btn = document.querySelector('button')
if (btn) {
btn.onclick = () => {
console.log('gres');
animatList.forEach((animate: any) => {
animate.start()
})
}
}
这里tween就举着一个例子,其他基本都是一样的,下面代码是元素的旋转,还是以roundDashed1
为例,在render
中修改他的routation.z
ts
if (roundDashed1) {
// 速度 * 旋转范围
roundDashed1.rotation.z = Math.cos(Date.now() * 0.0005) * 0.5
}
以以上方法继续将其他的元素做好动画,控制好延迟执行和执行持续的时间,就会出现有层次的动画效果
除了缩放动画的部分,其他部分是根据路径更新来显示的
路径增加动画
之前我们做的三角形只有四个顶点(起点两个)。那么在做路径增长动画显然不是很合适,所以需要将newTriangle
方法改一下,使用LineCurve3
将每两个点之间添加一些顶点,pointCounts
这个就是原有顶点的数量,将第i个和第i+1个顶点作为线段的两段,通过getPoints
ts
const linePoints: THREE.Vector3[] = []
let pointArr:number[] = []
for (let i = 0; i < pointCounts; i++) {
const line = new THREE.LineCurve3(new THREE.Vector3().fromArray(points, i * 3), new THREE.Vector3().fromArray(points, (i + 1) * 3))
const ps = line.getPoints(10)
ps.forEach((v3: THREE.Vector3) => {
pointArr.push(...v3.toArray())
linePoints.push(v3)
})
}
这样就获得到每条线段11个点,每个三角形一共33个点位
有了这些点位就可以通过tweenjs继续做补间动画,对于LineGeometry
的更新,一般都是使用geometry.setPositions()
,不过有一点问题,就是顶点缓冲区数量要相同,不然会报错
[.WebGL-0x100016da900] GL_INVALID_OPERATION: Vertex buffer is not big enough for the draw call
那么比如一个三角形有33个点,在补间动画的过程中也要保持有33个点,但是又不能让这些点显示出来,就需要一个方法,将补间动画未经过的点设置为最后一个点的坐标,这样既能保证动画的完整性,也可以保证不报错
ts
/**
*
* @param arr 所有点位信息
* @param r 当前经过的百分比 0-1浮点数
* @returns 修正过的点位信息
*/
const filterPoint = (arr: THREE.Vector3[], r: number): Array<number> => {
let l = Math.ceil(arr.length * r)
if (l > arr.length) l -= arr.length
let v3: number[] = []
arr.forEach((n, i) => {
if (i <= l) v3.push(...n.toArray())
else v3.push(...arr[l].toArray())
})
return v3
}
具体的使用方法,就是用0-1做过渡,代表当前路径经过的进度。
ts
animatList.push(new TWEEN.Tween({ index: 0 })
.delay(delay)
.to({ index: 1 }, 1000)
.onUpdate(({ index }) => {
triangle.geometry.setPositions(filterPoint(linePoints, index))
})
)
路径动画效果
后面再用同样的方法把前面的图标也做成路径动画即可,将所有元素和动画都展示出来,大概就是这个效果,接下来要做的就是旋转并进行立体化
立体化
记得之前给每个元素都编组了吧而且每个组都加了layer属性,那么就可以在动画中根据每个组的layer修改对应的z轴的值,并且将摄像头沿着x轴向右移动
ts
const scale = 0.65
animatList.push(new TWEEN.Tween({
cameraPosition: camera.position,
layerZIndex: 0
})
.delay(1000)
.to({
cameraPosition: new THREE.Vector3(90, 0, 140),
layerZIndex: 3.5
}, 2000)
.onUpdate(({ cameraPosition, layerZIndex }) => {
scene.traverse((child: any) => {
if(child.name === 'flow') {
if(child) {
child.scale.set(scale,scale,scale)
}
}
if (child.userData.layer && child.isGroup) {
const layer = child.userData.layer;
console.log(layer);
child.position.z = layer * layerZIndex - layerZIndex * 0.5
}
})
})
)
gif图比较大,可以下载下来后查看