three.js实现3d球体树状结构布局——树状结构的实现
本文建议先看系列文章第一篇树状结构的实现,下文内容也将衔接第一篇内容。
// 球形背景几何体的材质
const createMeshPhongMaterialTexture = (data) => {
let materialData = {
map: new THREE.TextureLoader().load(data.url)
}
if(data.side){
materialData.side = THREE.BackSide
}
return new THREE.MeshPhysicalMaterial(materialData);
}
const bjMeshStyle = {
geometry: {
radius: 'auto',
widthSegments: 320,
heightSegments: 160
},
material: {
url: "./sphere-bg2.jpg",
side: true
}
}
const bjGeometry = createSphereGeometry(bjMeshStyle.geometry);
const bjMaterial = createMeshPhongMaterialTexture(bjMeshStyle.material);
const bjMesh = createMesh(bjGeometry, bjMaterial);
bjMesh.position.set(
cameraStyle.lookAt[0],
cameraStyle.lookAt[1],
cameraStyle.lookAt[2]
);
scene.add(bjMesh);
pointLight = createPointLight({ color: "#fff", intensity: 1 });
let pointLightL = R * 10
pointLight.rotateSpeed = Math.PI / 100;
pointLight.lightAngle = Math.atan(1);
pointLight.arcR = Math.sqrt(pointLightL * pointLightL * 2);
pointLight.init_position = {
x: pointLightL,
y: pointLightL,
z: pointLightL,
}
pointLight.position.set(pointLightL, pointLightL, pointLightL);
scene.add(pointLight);
// 渲染
const render = () => {
rotate()
//循环调用
requestAnimationFrame(render);
renderer.render(scene, camera);
};
const resetPointLightPosition = () => {
pointLight.position.set(pointLight.init_position.x, pointLight.init_position.y, pointLight.init_position.z);
pointLight.lightAngle = Math.atan(1);
}
const rotate = (angle) => {
if(pointLight.rotateFlag){
return
}
pointLight.lightAngle += angle || pointLight.rotateSpeed
pointLight.lightAngle = pointLight.lightAngle % (Math.PI * 2)
pointLight.position.set(Math.sin(pointLight.lightAngle) * pointLight.arcR, pointLight.position.y, Math.cos(pointLight.lightAngle) * pointLight.arcR);
}
const computedPointPosition = (
data,
style,
ArcRArr,
startAngle = 0,
endAngle = Math.PI * 2,
deep = 0
) => {
let totalWight = 0;
for (let i = 0; i < data.length; i++) {
totalWight += data[i].weight;
}
let AngleScope = endAngle - startAngle;
let curAngle = startAngle;
let randTranslate = ArcRArr[deep] * 10 || 1000
for (let i = 0; i < data.length; i++) {
let item = data[i];
let ratioAngle = (item.weight / totalWight) * AngleScope;
item.targetPostion = {
x: Math.sin(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[0],
y: style.startPositionY - deep * (style.yr || 0) + style.centerXYZ[1],
z: Math.cos(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[2]
}
item.translatePostion = {
y: rand(-randTranslate, randTranslate),
x: rand(-randTranslate, randTranslate),
z: rand(-randTranslate, ArcRArr[deep])
}
if (item?.children?.length) {
computedPointPosition(
item?.children,
style,
ArcRArr,
curAngle,
curAngle + ratioAngle,
deep + 1
);
}
curAngle += ratioAngle;
}
};
const render = () => {
rotate()
pointAnimation()
cameraAnimation()
//循环调用
requestAnimationFrame(render);
renderer.render(scene, camera);
};
const pointAnimation = () => {
if(animationIndex === targetAnimationIndex){
return
}
animationIndex++
let scale = (targetAnimationIndex - animationIndex) / targetAnimationIndex
let scale2 = animationIndex / targetAnimationIndex
sphereGeometrys.forEach((item, index) => {
let data = {
x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale,
y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale,
z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale
}
item.position.set(data.x, data.y, data.z);
textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
})
old_sphereGeometrys.forEach((item, index) => {
let data = {
x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale2,
y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale2,
z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale2
}
item.position.set(data.x, data.y, data.z);
old_textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
})
if(animationIndex === targetAnimationIndex){
old_sphereGeometrys.forEach((item,index) => {
scene.remove(item);
scene.remove(old_textGeometrys[index]);
})
old_sphereGeometrys = []
old_textGeometrys = []
if(lineGeometrys.length){
scene.add(...lineGeometrys);
}
}
}
const cameraAnimation = () => {
if(cameraAnimationIndex === targetCameraAnimationIndex){
return
}
cameraAnimationIndex++
let scale = (targetCameraAnimationIndex - cameraAnimationIndex) / targetCameraAnimationIndex
camera.position.set(
camera.camera_position.x + camera.translate_camera_position.x * scale,
camera.camera_position.y + camera.translate_camera_position.y * scale,
camera.camera_position.z + camera.translate_camera_position.z * scale,
);
camera.lookAt(
cameraStyle.lookAt[0],
cameraStyle.lookAt[1],
cameraStyle.lookAt[2]
);
}
treeDom.value.addEventListener( 'click', meshOnClick );
// 点击回调
const meshOnClick = (event) => {
const pointer = new THREE.Vector2();
pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
let intersects = [];
let raycaster = new THREE.Raycaster();
raycaster.setFromCamera( pointer, camera );
intersects = raycaster.intersectObjects( sphereGeometrys, true );
if ( intersects.length > 0 ) {
meshOnClickCb(intersects[0].object)
}
}
const meshOnClickCb = (mesh) => {
let dx = mesh.position.x - cameraStyle.lookAt[0]
let dy = mesh.position.y - cameraStyle.lookAt[1]
let dz = mesh.position.z - cameraStyle.lookAt[2]
if(Math.abs(dx) < 0.001){
dx = 0
}
if(Math.abs(dy) < 0.001){
dy = 0
}
if(Math.abs(dz) < 0.001){
dz = 0
}
let targetPositon = {}
if(dx === 0 && dy === 0 && dz === 0){
targetPositon = {
x: mesh.position.x + treeStyle.pointInterval,
y: mesh.position.y + treeStyle.pointInterval,
z: mesh.position.z + treeStyle.pointInterval,
}
}else if(dx === 0 && dy === 0){
let dzPointInterval = dz / Math.abs(dz) * treeStyle.pointInterval * 2
targetPositon = {
x: mesh.position.x + dzPointInterval,
y: mesh.position.y + dzPointInterval,
z: mesh.position.z + dzPointInterval,
}
}else if(dy === 0 && dz === 0){
let dxPointInterval = dx / Math.abs(dx) * treeStyle.pointInterval * 2
targetPositon = {
x: mesh.position.x + dxPointInterval,
y: mesh.position.y + dxPointInterval,
z: mesh.position.z + dxPointInterval,
}
}else if(dx === 0 && dz === 0){
let dyPointInterval = dy / Math.abs(dy) * treeStyle.pointInterval * 2
targetPositon = {
x: mesh.position.x + dyPointInterval,
y: mesh.position.y + dyPointInterval,
z: mesh.position.z + dyPointInterval,
}
}else{
let dd = null
if(dx !== 0){
dd = dx
}else if(dy !== 0){
dd = dy
}else if(dz !== 0){
dd = dz
}
let translateL = treeStyle.pointInterval * dd / Math.abs(dd)
let dxdd = dx / dd
let dydd = dy / dd
let dzdd = dz / dd
let maxdd = dxdd > dydd ? dxdd : dydd
maxdd = maxdd > dzdd ? maxdd : dzdd
if(Math.abs(maxdd) > 1){
let scale = Math.abs(maxdd) / 1
dxdd = dxdd / scale
dydd = dydd / scale
dzdd = dzdd / scale
}
targetPositon = {
x: mesh.position.x + translateL * dxdd,
y: mesh.position.y + translateL * dydd,
z: mesh.position.z + translateL * dzdd,
}
}
camera.translate_camera_position = {
x: camera.position.x - targetPositon.x,
y: camera.position.y - targetPositon.y,
z: camera.position.z - targetPositon.z,
}
camera.camera_position = JSON.parse(JSON.stringify(targetPositon))
let length = Math.sqrt(Math.pow(camera.translate_camera_position.x, 2) + Math.pow(camera.translate_camera_position.y, 2) + Math.pow(camera.translate_camera_position.z, 2))
cameraAnimationIndex = length > treeStyle.pointInterval ? 0 : Math.floor(length / treeStyle.pointInterval * targetCameraAnimationIndex)
}
three.js实现3d球体树状结构布局博客演示视频2
<script setup>
import { onMounted, ref, onBeforeUnmount, computed, reactive } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import SpriteText from "three-spritetext";
import { MeshLine, MeshLineMaterial } from "three.meshline";
// 图片
import cameraSvg from '@/assets/images/camera.svg'
import openSvg from '@/assets/images/open.svg'
import closeSvg from '@/assets/images/close.svg'
import startSvg from '@/assets/images/start.svg'
import stopSvg from '@/assets/images/stop.svg'
import dataSvg from '@/assets/images/data.svg'
// 渲染dom
let treeDom = ref(null);
// 场景
const createScene = () => {
return new THREE.Scene();
};
// 相机(透视投影相机)
const createPerspectiveCamera = ({ fov, aspect, near, far }) => {
/*
fov — 摄像机视锥体垂直视野角度
aspect — 摄像机视锥体长宽比
near — 摄像机视锥体近端面
far — 摄像机视锥体远端面
*/
return new THREE.PerspectiveCamera(fov, aspect, near, far);
};
// 渲染器
const createWebGLRenderer = ({ dom, width, height }) => {
/*
renderDom — dom
width — 渲染宽度 一般取domclientWidth
height — 渲染高度 一般取clientHeight
*/
if (width === undefined) {
width = dom.clientWidth;
}
if (height === undefined) {
height = dom.clientHeight;
}
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
dom.appendChild(renderer.domElement);
return renderer;
};
// 辅助线
const createAxesHelper = (length) => {
return new THREE.AxesHelper(length);
};
// 环境光
const createAmbientLight = ({ color, intensity }) => {
// color - (可选参数)) 十六进制光照颜色。 缺省值 0xffffff (白色)。
// intensity - (可选参数) 光照强度。 缺省值 1。
return new THREE.AmbientLight(color, intensity);
};
// 点光
const createPointLight = ({ color, intensity, distance, decay }) => {
/*
color - (可选参数)) 十六进制光照颜色。 缺省值 0xffffff (白色)。
intensity - (可选参数) 光照强度。 缺省值 1。
distance - 这个距离表示从光源到光照强度为0的位置。 当设置为0时,光永远不会消失(距离无穷大)。缺省值 0.
decay - 沿着光照距离的衰退量。缺省值 2。
*/
return new THREE.PointLight(color, intensity, distance, decay);
};
// 球形几何体
const createSphereGeometry = ({
radius,
widthSegments,
heightSegments,
phiStart,
phiLength,
thetaStart,
thetaLength,
}) => {
/*
radius — 球体半径,默认为1。
widthSegments — 水平分段数(沿着经线分段),最小值为3,默认值为32。
heightSegments — 垂直分段数(沿着纬线分段),最小值为2,默认值为16。
phiStart — 指定水平(经线)起始角度,默认值为0。。
phiLength — 指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2。
thetaStart — 指定垂直(纬线)起始角度,默认值为0。
thetaLength — 指定垂直(纬线)扫描角度大小,默认值为 Math.PI。
*/
return new THREE.SphereGeometry(
radius,
widthSegments,
heightSegments,
phiStart,
phiLength,
thetaStart,
thetaLength
);
};
// 球形几何体的材质
const createMeshLambertMaterial = (data) => {
return new THREE.MeshLambertMaterial(data);
};
// 球形背景几何体的材质
const createMeshPhongMaterialTexture = (data) => {
let materialData = {
map: new THREE.TextureLoader().load(data.url)
}
if(data.side){
materialData.side = THREE.BackSide
}
return new THREE.MeshPhysicalMaterial(materialData);
}
// 线几何体
const createLineGeometry = (points) => {
const pointsVector3 = [];
for (let i = 0; i < points.length; i++) {
pointsVector3.push(
new THREE.Vector3(points[i].x, points[i].y, points[i].z)
);
}
const geometry = new THREE.BufferGeometry().setFromPoints(pointsVector3);
const line = new MeshLine();
line.setGeometry(geometry);
return line;
};
// 线几何体的材质
const createMeshLineMaterial = (data) => {
return new MeshLineMaterial({
lineWidth: data.linewidth,
color: data.color || "white",
dashArray: data.dashArray || 0,
transparent: true,
});
};
// 物体
const createMesh = (geometry, materialBasic) => {
return new THREE.Mesh(geometry, materialBasic);
};
// 文本
const createText = ({ text, size, color }) => {
let textClass = new SpriteText(text, size);
textClass.color = color;
return textClass;
};
// 轨道控制
const createControl = (camera, dom) => {
return new OrbitControls(camera, dom);
};
// 3d树布局算法 灵感来源 梯形/三角形转换为圆锥
const computedDataWeight = (data) => {
let weight = 0;
for (let i = 0; i < data.length; i++) {
let item = data[i];
if (item?.children?.length) {
item.weight = computedDataWeight(item.children);
weight += item.weight;
} else {
item.weight = 1;
weight += 1;
}
}
return weight;
};
const computedArcRArr = (data, pointInterval) => {
let ArcRArr = [];
let ArcData = [];
let ArcWeight = [];
formatTreeToArcData(data, ArcData);
for (let i = 0; i < ArcData.length; i++) {
let item = ArcData[i];
let weight = 0;
for (let j = 0; j < item.length; j++) {
weight += item[j].weight;
}
ArcWeight.push(weight);
}
let R = computedArcR(pointInterval, ArcWeight[0]);
// 半径算法
for (let i = 0; i < ArcData.length; i++) {
let item = ArcData[i];
if (ArcWeight[i] < ArcWeight[0]) {
// 不是完全层
ArcRArr.push(R);
} else {
if (item.length > 1) {
// 完全层
let DValue = 0;
item.forEach((weight) => {
DValue += Math.floor(weight.weight / 2);
});
ArcRArr.push(((ArcWeight[i] - DValue) / ArcWeight[i]) * R);
} else {
ArcRArr.push(0);
}
}
}
return { ArcRArr, R };
};
const formatTreeToArcData = (data, ArcData, deep = 0) => {
data.forEach((element) => {
if (!ArcData[deep]) {
ArcData[deep] = [];
}
ArcData[deep].push({
label: element.label,
point_uuid: element.point_uuid,
weight: element.weight,
});
if (element?.children?.length) {
formatTreeToArcData(element?.children, ArcData, deep + 1);
}
});
};
const computedArcR = (pointInterval, points) => {
if (points === 1) {
return pointInterval * 2;
}
let arcR =
pointInterval /
2 /
Math.cos((Math.PI / 180) * (((points - 2) * 180) / points / 2));
if (arcR < pointInterval) {
arcR = pointInterval * 2;
}
return arcR;
};
const computedTreeStyleAuto = (style, ArcRArr, R) => {
if (style.yr === "auto") {
style.yr = ArcRArr.length === 1 ? R : R / (ArcRArr.length - 1);
}
style.startPositionY =
((ArcRArr.length - 1) / 2) * style.yr + style.centerXYZ[1];
};
const computedPointPosition = (
data,
style,
ArcRArr,
startAngle = 0,
endAngle = Math.PI * 2,
deep = 0
) => {
let totalWight = 0;
for (let i = 0; i < data.length; i++) {
totalWight += data[i].weight;
}
let AngleScope = endAngle - startAngle;
let curAngle = startAngle;
let randTranslate = ArcRArr[deep] * 10 || 1000
for (let i = 0; i < data.length; i++) {
let item = data[i];
let ratioAngle = (item.weight / totalWight) * AngleScope;
item.targetPostion = {
x: Math.sin(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[0],
y: style.startPositionY - deep * (style.yr || 0) + style.centerXYZ[1],
z: Math.cos(curAngle + ratioAngle / 2) * ArcRArr[deep] + style.centerXYZ[2]
}
item.translatePostion = {
y: rand(-randTranslate, randTranslate),
x: rand(-randTranslate, randTranslate),
z: rand(-randTranslate, ArcRArr[deep])
}
if (item?.children?.length) {
computedPointPosition(
item?.children,
style,
ArcRArr,
curAngle,
curAngle + ratioAngle,
deep + 1
);
}
curAngle += ratioAngle;
}
};
// 计算camera初始位置
const computedCameraStyle = (style, dom, treeStyle, R) => {
if (style.position === "auto") {
style.position = {
x: 0,
y: treeStyle.yr * 1.5,
z: R * 3,
};
}
if (style.data === "auto") {
style.data = {
fov: 45,
aspect: dom.clientWidth / dom.clientHeight,
near: 0.1,
far: R * 1000,
};
}
if (style.lookAt === "auto") {
style.lookAt = JSON.parse(JSON.stringify(treeStyle.centerXYZ));
}
};
// 计算
const computedBjMeshStyleAuto = (style, R) => {
style.geometry.radius = R
}
// 随机数
const rand = (n, m) => {
const c = m - n + 1
return Math.floor(Math.random() * c + n)
}
// randTree
const randTree = (deep = 0, maxDeep = 5, parentName) => {
let tree = []
let length = rand(1,3)
for(let i = 0; i < length; i++){
let data = {
name: parentName ? parentName + '-' + i : i + ''
}
if(rand(0,2) && deep < maxDeep){
data.children = randTree(deep + 1, maxDeep, data.name)
}
tree.push(data)
}
return tree
}
const originTreeStyle = {
centerXYZ: [0, 0, 0],
yr: "auto",
pointInterval: 10,
};
let treeStyle = null
const originCameraStyle = {
position: "auto",
data: "auto",
lookAt: "auto",
};
let cameraStyle = null
const bjMeshStyle = {
geometry: {
radius: 'auto',
widthSegments: 320,
heightSegments: 160
},
material: {
url: "./sphere-bg2.jpg",
side: true
}
}
const sphereMeshStyle = {
geometry: {
radius: 1,
widthSegments: 320,
heightSegments: 160,
},
material: {
color: "#ffffff",
wireframe: false, //是否将几何体渲染为线框,默认值为false(即渲染为平面多边形)
},
};
const lineMeshStyle = {
material: {
color: "#ffffff",
linewidth: 0.2,
},
};
const textMeshStyle = {
material: {
size: 0.5,
color: "#ffffff",
},
};
let scene = null;
let camera = null;
let renderer = null;
let control = null;
let bjMesh = null;
let axes = null;
let ambientLight = null;
let pointLight = null;
let animationIndex = 0
let targetAnimationIndex = 50
let sphereGeometrys = [];
let textGeometrys = [];
let lineGeometrys = [];
let old_sphereGeometrys = [];
let old_textGeometrys = [];
let cameraAnimationIndex = 0
let targetCameraAnimationIndex = 50
const init = (rendererDom, data) => {
treeStyle = JSON.parse(JSON.stringify(originTreeStyle))
cameraStyle = JSON.parse(JSON.stringify(originCameraStyle))
computedDataWeight(data);
let { ArcRArr, R } = computedArcRArr(data, treeStyle.pointInterval);
computedTreeStyleAuto(treeStyle, ArcRArr, R);
computedPointPosition(data, treeStyle, ArcRArr);
computedCameraStyle(cameraStyle, rendererDom, treeStyle, R);
if(!scene){
scene = createScene();
}
if(!camera){
camera = createPerspectiveCamera(cameraStyle.data);
camera.position.set(
cameraStyle.position.x,
cameraStyle.position.y,
cameraStyle.position.z
);
}
camera.translate_camera_position = {
x: camera.position.x - cameraStyle.position.x,
y: camera.position.y - cameraStyle.position.y,
z: camera.position.z - cameraStyle.position.z,
}
camera.camera_position = cameraStyle.position
camera.init_camera_position = JSON.parse(JSON.stringify(cameraStyle.position))
if(!renderer){
renderer = createWebGLRenderer({
dom: rendererDom,
});
}
if(!control){
control = createControl(camera, rendererDom);
}
if(!bjMesh){
computedBjMeshStyleAuto(bjMeshStyle, R * 12);
const bjGeometry = createSphereGeometry(bjMeshStyle.geometry);
const bjMaterial = createMeshPhongMaterialTexture(bjMeshStyle.material);
bjMesh = createMesh(bjGeometry, bjMaterial);
bjMesh.position.set(
cameraStyle.lookAt[0],
cameraStyle.lookAt[1],
cameraStyle.lookAt[2]
);
scene.add(bjMesh);
}
if(!axes){
axes = createAxesHelper(R);
scene.add(axes);
}
if(!ambientLight){
ambientLight = createAmbientLight({ color: "#fff", intensity: 0.2 });
scene.add(ambientLight);
}
if(!pointLight){
pointLight = createPointLight({ color: "#fff", intensity: 1 });
let pointLightL = R * 10
pointLight.rotateSpeed = Math.PI / 100;
pointLight.lightAngle = Math.atan(1);
pointLight.arcR = Math.sqrt(pointLightL * pointLightL * 2);
pointLight.init_position = {
x: pointLightL,
y: pointLightL,
z: pointLightL,
}
pointLight.position.set(pointLightL, pointLightL, pointLightL);
scene.add(pointLight);
}
animationIndex = 0
cameraAnimationIndex = 0
old_sphereGeometrys = [...sphereGeometrys];
old_textGeometrys = [...textGeometrys];
lineGeometrys.forEach(item => {
scene.remove(item);
})
sphereGeometrys = [];
textGeometrys = [];
lineGeometrys = [];
initGeometrys(data, sphereGeometrys, textGeometrys, lineGeometrys);
scene.add(...sphereGeometrys);
scene.add(...textGeometrys);
render();
};
const initGeometrys = (data, sphereGeometrys, textGeometrys, lineGeometrys, parentPosition) => {
for (let i = 0; i < data.length; i++) {
let item = data[i];
const geometry = createSphereGeometry(sphereMeshStyle.geometry);
const material = createMeshLambertMaterial(sphereMeshStyle.material);
const mesh = createMesh(geometry, material);
mesh.position.set(item.targetPostion.x + item.translatePostion.x, item.targetPostion.y + item.translatePostion.y, item.targetPostion.z + item.translatePostion.z);
mesh.originData = item
sphereGeometrys.push(mesh);
geometry.dispose();
material.dispose();
const text = createText({
text: item.name,
size: textMeshStyle.material.size,
color: textMeshStyle.material.color,
});
text.position.x = item.targetPostion.x + item.translatePostion.x;
text.position.y = item.targetPostion.y + item.translatePostion.y + sphereMeshStyle.geometry.radius * 2;
text.position.z = item.targetPostion.z + item.translatePostion.z;
textGeometrys.push(text);
if (parentPosition) {
const lineGeometry = createLineGeometry([
parentPosition,
{ x: item.targetPostion.x, y: item.targetPostion.y, z: item.targetPostion.z },
]);
const lineMaterial = createMeshLineMaterial(lineMeshStyle.material);
const lineMesh = createMesh(lineGeometry, lineMaterial);
lineGeometrys.push(lineMesh);
lineGeometry.dispose();
lineMaterial.dispose();
}
if (item?.children?.length) {
initGeometrys(
item.children,
sphereGeometrys,
textGeometrys,
lineGeometrys,
{ x: item.targetPostion.x, y: item.targetPostion.y, z: item.targetPostion.z }
);
}
}
};
// 渲染
const render = () => {
rotate()
pointAnimation()
cameraAnimation()
//循环调用
requestAnimationFrame(render);
renderer.render(scene, camera);
};
const resetPointLightPosition = () => {
pointLight.position.set(pointLight.init_position.x, pointLight.init_position.y, pointLight.init_position.z);
pointLight.lightAngle = Math.atan(1);
}
const rotate = (angle) => {
if(pointLight.rotateFlag){
return
}
pointLight.lightAngle += angle || pointLight.rotateSpeed
pointLight.lightAngle = pointLight.lightAngle % (Math.PI * 2)
pointLight.position.set(Math.sin(pointLight.lightAngle) * pointLight.arcR, pointLight.position.y, Math.cos(pointLight.lightAngle) * pointLight.arcR);
}
const pointAnimation = () => {
if(animationIndex === targetAnimationIndex){
return
}
animationIndex++
let scale = (targetAnimationIndex - animationIndex) / targetAnimationIndex
let scale2 = animationIndex / targetAnimationIndex
sphereGeometrys.forEach((item, index) => {
let data = {
x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale,
y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale,
z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale
}
item.position.set(data.x, data.y, data.z);
textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
})
old_sphereGeometrys.forEach((item, index) => {
let data = {
x: item.originData.targetPostion.x + item.originData.translatePostion.x * scale2,
y: item.originData.targetPostion.y + item.originData.translatePostion.y * scale2,
z: item.originData.targetPostion.z + item.originData.translatePostion.z * scale2
}
item.position.set(data.x, data.y, data.z);
old_textGeometrys[index].position.set(data.x, data.y + sphereMeshStyle.geometry.radius * 2, data.z)
})
if(animationIndex === targetAnimationIndex){
old_sphereGeometrys.forEach((item,index) => {
scene.remove(item);
scene.remove(old_textGeometrys[index]);
})
old_sphereGeometrys = []
old_textGeometrys = []
if(lineGeometrys.length){
scene.add(...lineGeometrys);
}
}
}
const cameraAnimation = () => {
if(cameraAnimationIndex === targetCameraAnimationIndex){
return
}
cameraAnimationIndex++
let scale = (targetCameraAnimationIndex - cameraAnimationIndex) / targetCameraAnimationIndex
camera.position.set(
camera.camera_position.x + camera.translate_camera_position.x * scale,
camera.camera_position.y + camera.translate_camera_position.y * scale,
camera.camera_position.z + camera.translate_camera_position.z * scale,
);
camera.lookAt(
cameraStyle.lookAt[0],
cameraStyle.lookAt[1],
cameraStyle.lookAt[2]
);
}
// 点击回调
const meshOnClick = (event) => {
const pointer = new THREE.Vector2();
pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
let intersects = [];
let raycaster = new THREE.Raycaster();
raycaster.setFromCamera( pointer, camera );
intersects = raycaster.intersectObjects( sphereGeometrys, true );
if ( intersects.length > 0 ) {
meshOnClickCb(intersects[0].object)
}
}
const meshOnClickCb = (mesh) => {
let dx = mesh.position.x - cameraStyle.lookAt[0]
let dy = mesh.position.y - cameraStyle.lookAt[1]
let dz = mesh.position.z - cameraStyle.lookAt[2]
if(Math.abs(dx) < 0.001){
dx = 0
}
if(Math.abs(dy) < 0.001){
dy = 0
}
if(Math.abs(dz) < 0.001){
dz = 0
}
let targetPositon = {}
if(dx === 0 && dy === 0 && dz === 0){
targetPositon = {
x: mesh.position.x + treeStyle.pointInterval,
y: mesh.position.y + treeStyle.pointInterval,
z: mesh.position.z + treeStyle.pointInterval,
}
}else if(dx === 0 && dy === 0){
let dzPointInterval = dz / Math.abs(dz) * treeStyle.pointInterval * 2
targetPositon = {
x: mesh.position.x + dzPointInterval,
y: mesh.position.y + dzPointInterval,
z: mesh.position.z + dzPointInterval,
}
}else if(dy === 0 && dz === 0){
let dxPointInterval = dx / Math.abs(dx) * treeStyle.pointInterval * 2
targetPositon = {
x: mesh.position.x + dxPointInterval,
y: mesh.position.y + dxPointInterval,
z: mesh.position.z + dxPointInterval,
}
}else if(dx === 0 && dz === 0){
let dyPointInterval = dy / Math.abs(dy) * treeStyle.pointInterval * 2
targetPositon = {
x: mesh.position.x + dyPointInterval,
y: mesh.position.y + dyPointInterval,
z: mesh.position.z + dyPointInterval,
}
}else{
let dd = null
if(dx !== 0){
dd = dx
}else if(dy !== 0){
dd = dy
}else if(dz !== 0){
dd = dz
}
let translateL = treeStyle.pointInterval * dd / Math.abs(dd)
let dxdd = dx / dd
let dydd = dy / dd
let dzdd = dz / dd
let maxdd = dxdd > dydd ? dxdd : dydd
maxdd = maxdd > dzdd ? maxdd : dzdd
if(Math.abs(maxdd) > 1){
let scale = Math.abs(maxdd) / 1
dxdd = dxdd / scale
dydd = dydd / scale
dzdd = dzdd / scale
}
targetPositon = {
x: mesh.position.x + translateL * dxdd,
y: mesh.position.y + translateL * dydd,
z: mesh.position.z + translateL * dzdd,
}
}
camera.translate_camera_position = {
x: camera.position.x - targetPositon.x,
y: camera.position.y - targetPositon.y,
z: camera.position.z - targetPositon.z,
}
camera.camera_position = JSON.parse(JSON.stringify(targetPositon))
let length = Math.sqrt(Math.pow(camera.translate_camera_position.x, 2) + Math.pow(camera.translate_camera_position.y, 2) + Math.pow(camera.translate_camera_position.z, 2))
cameraAnimationIndex = length > treeStyle.pointInterval ? 0 : Math.floor(length / treeStyle.pointInterval * targetCameraAnimationIndex)
}
// 操作
let openOperateFlag = ref(true)
const changeOpenOperateFlag = () => {
openOperateFlag.value = !openOperateFlag.value
}
const resetCameraPosition = () => {
camera.translate_camera_position = {
x: camera.position.x - camera.init_camera_position.x,
y: camera.position.y - camera.init_camera_position.y,
z: camera.position.z - camera.init_camera_position.z,
}
camera.camera_position = JSON.parse(JSON.stringify(camera.init_camera_position))
let length = Math.sqrt(Math.pow(camera.translate_camera_position.x, 2) + Math.pow(camera.translate_camera_position.y, 2) + Math.pow(camera.translate_camera_position.z, 2))
cameraAnimationIndex = length > treeStyle.pointInterval ? 0 : Math.floor(length / treeStyle.pointInterval * targetCameraAnimationIndex)
}
let pointLightFlag = ref(false)
const changePointLightFlag = () => {
pointLight.rotateFlag = !pointLight.rotateFlag
resetPointLightPosition()
pointLightFlag.value = pointLight.rotateFlag
}
const changeData = () => {
let data = randTree();
let rendererDom = treeDom.value;
init(rendererDom, data);
}
onMounted(() => {
changeData()
treeDom.value.addEventListener( 'click', meshOnClick );
});
</script>
<template>
<div class="tree-new-page">
<div class="operate">
<div class="open-operate frosted-glass" @click="changeOpenOperateFlag">
<img :src="openOperateFlag ? closeSvg : openSvg"/>
</div>
<div :class="{'operate-list': true, 'frosted-glass': true, 'close': !openOperateFlag}">
<div class="operate-item camera" @click="resetCameraPosition">
<img :src="cameraSvg"/>
<div class="label">返回正视角</div>
</div>
<div class="operate-item point-light" @click="changePointLightFlag">
<img :src="pointLightFlag ? stopSvg : startSvg"/>
<div class="label">{{pointLightFlag ? '停止灯光旋转' : '开启灯光旋转'}}</div>
</div>
<div class="operate-item data" @click="changeData">
<img :src="dataSvg"/>
<div class="label">更换数据</div>
</div>
</div>
</div>
<div class="tree-new" ref="treeDom"></div>
</div>
</template>
<style scoped lang="scss">
.tree-new-page {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #000;
position: relative;
.tree-new {
width: 100%;
height: 100%;
}
.operate{
position: absolute;
z-index: 10;
left: 0;
top: 50%;
transform: translateY(-50%);
.open-operate{
width: 22px;
position: relative;
padding: 6px 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
cursor: pointer;
&.frosted-glass{
border-radius: 0 4px 4px 0;
}
img{
height: 20px;
margin-left: 2px;
}
}
.operate-list{
position: relative;
padding: 10px;
&.frosted-glass{
border-radius: 0 4px 4px 0;
}
&.close{
.operate-item{
.label{
margin-left: 0px;
width: 0px;
}
}
}
.operate-item{
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
cursor: pointer;
&:last-child{
margin-bottom: 0px;
}
img{
height: 20px;
}
.label{
width: 100px;
white-space: nowrap;
overflow: hidden;
color: $defaultWhite;
font-size: 16px;
margin-left: 10px;
transition: all .3s;
}
}
.point-light{
img{
height: 24px;
margin-left: -2px;
}
}
}
}
.frosted-glass {
&::after {
position: absolute;
top: 0;
left: 0;
z-index: -1;
content: "";
display: block;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
box-shadow: 0 0 4px rgb(93, 93, 93);
border-radius: 4px;
}
}
}
</style>
下载地址:点击此处,如果打不开则在审核中,可以先搜藏博客,后面再来下载。
项目资源目录
public
sphere-bg2.jpg
src
assets
images
...其余图片