You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1395 lines
65 KiB
HTML

2 months ago
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>three</title>
<script src="./js/vue.global.min.js"></script>
<script src="./js/antd.min.js"></script>
<link rel="stylesheet" href="./css/antd.min.css">
<link rel="stylesheet" href="./css/element-plus.css">
<script src="./js/element-plus.js"></script>
<!-- 引入 Element Plus 中文语言包 -->
<script src="./js/zh-cn.min.js"></script>
<script src="./js/echarts.min.js"></script>
<script type="text/javascript" src="./js/axios.min.js"></script>
<!-- 引入three -->
<script type="text/javascript" src="./three/three.min.js"></script>
<script type="text/javascript" src="./three/OrbitControls.js"></script>
<script type="text/javascript" src="./three/BufferGeometryUtils.js"></script>
<script type="text/javascript" src="./three/Lut.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 10px;
background-color: #f5f5f5;
}
.content {
height: 98vh;
width: 100%;
background-color: #fff;
}
#tree-box {
width: 400px;
height: 350px;
}
</style>
</head>
<body>
<div id="app">
<div class="content">
<div ref="prpsBoxRef" id="tree-box"></div>
<a-button type="primary" @click="onStarPrps" style="margin-right: 12px;">开始 prps</a-button>
<div ref="prpdBoxRef" id="tree-box"></div>
<a-button type="primary" @click="onStarPrpd">开始 prpd</a-button>
</div>
<script>
const { createApp, ref, onMounted, unref, watch, reactive, computed, nextTick } = Vue;
const { message } = antd;
const fontStyles = [
0,
{
w: 55,
h: 25,
size: 16,
},
{
w: 55,
h: 25,
size: 16,
},
{
w: 75,
h: 25,
ch: 9,
cw: 5.7,
size: 20.3,
},
{
w: 85,
h: 32,
size: 22.3,
},
{
w: 110,
h: 40,
size: 25.3,
},
]
// 生成指定范围内的随机整数
function getRandomInt(min, max) {
const randomNumber = Math.random() * (max - min) + min;
const roundedNumber = randomNumber.toFixed(1);
return parseFloat(roundedNumber);
}
const generateArray = (period, phase) => {
let counter = 1, allArray = [];
for (let i = 0; i < period; i++) {
const newArray = [];
const step = 360 / phase;
for (let i = 1; i <= phase; i++) {
newArray.push([counter, parseInt(i * step), getRandomInt(10, 70)]);
}
counter = (counter % period) + 1;
allArray.push(newArray);
}
return allArray;
}
const formatNumber = (num, decimalPlaces = 1) => {
if (Number.isInteger(num)) {
// 如果是整数,直接返回
return num.toString();
} else {
// 如果是小数,使用 toFixed 方法
return num.toFixed(decimalPlaces);
}
}
const drawPlan = (obj) => {
const { size, color } = obj;
let geometry = new THREE.PlaneGeometry(size, 360); //创建一个Buffer类型几何体对象
geometry.rotateX(Math.PI / 2).translate(size / 2, 0, 180);
// 三角面(网格)渲染模式
let material = new THREE.MeshBasicMaterial({
color, //三角面颜色
transparent: true,//透明
opacity: 0.2,
side: THREE.DoubleSide, //两面可见
}); //材质对象
let mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
mesh.name = 'plan';
//scene.add(mesh);
return mesh
}
const drawLine = (obj) => {
let { line, color, position, margin, num } = obj
let geometry = new THREE.BufferGeometry(); //创建一个Buffer类型几何体对象
//3个为一组表示一个顶点的xyz坐标
geometry.setAttribute('position', new THREE.Float32BufferAttribute(line, 3))
// 线条渲染模式
let material = new THREE.LineBasicMaterial({
color, //线条颜色
});//材质对象
let LineList = [];
let modelLine = new THREE.Line(geometry, material);//线条模型对象
LineList.push(modelLine)
for (let i = 1; i < num; i++) {
let lineChildren = modelLine.clone()['translate' + position](i * margin)
LineList.push(lineChildren);
}
return LineList
};
const draw2DText = (obj) => {
let scale = 1;//window.devicePixelRatio;
let { content, w, h, color, font, xyz, size, cw, ch, rotate } = obj;
cw = cw ? cw : 6;
ch = ch ? ch : 8;
let canvas = document.createElement('canvas')
canvas.style.width = w * scale;
canvas.style.height = h * scale;
if (rotate) {
canvas.width = w * cw * scale * 3;
canvas.height = h * ch * scale * 2;
} else {
canvas.width = w * cw * scale * 2;
canvas.height = h * ch * scale * 3;
}
let ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.strokeStyle = '#000';
ctx.font = font;
ctx.direction = "ltr"; // 文本方向从左向右
ctx.textAlign = "left"; // 左对齐
ctx.textBaseline = 'middle'//基线对齐选项,决定文字垂直方向的对齐方式
if (rotate) {
ctx.translate(canvas.width / 2, canvas.height);
ctx.rotate(-Math.PI / 2);
ctx.fillText(content, h, 0);
ctx.strokeText(content, h, 0);
} else {
for (let i = 0; i < 1; i++) {
ctx.fillText(content[i], 0, canvas.height / 2);
ctx.strokeText(content[i], 0, canvas.height / 2);
}
}
let texture = new THREE.CanvasTexture(canvas)
// texture.needsUpdate = true//如果编码类型在纹理已被一个材质使用之后发生了改变, 你需要来设置Material.needsUpdate为true来使得材质重新编译
let geometry = new THREE.BufferGeometry(); //创建一个Buffer类型几何体对象
//3个为一组表示一个顶点的xyz坐标
geometry.setAttribute('position', new THREE.Float32BufferAttribute(xyz, 3));
const material = new THREE.PointsMaterial({
map: texture,
transparent: true,//材质透明
size: size ? size : 16.0, //点对象像素尺寸
sizeAttenuation: false
});
const pointsText = new THREE.Points(geometry, material);
pointsText.name = 'pointsText';
return pointsText;
}
const drawSinLine = (obj) => {
let { line, color } = obj, list = [];
line.forEach(item => {
list.push(new THREE.Vector3(...item));
})
let geometry = new THREE.BufferGeometry(); //声明一个几何体对象Geometry
// 三维样条曲线 Catmull-Rom算法
let curve = new THREE.CatmullRomCurve3(list);
//getPoints是基类Curve的方法返回一个vector3对象作为元素组成的数组
let points = curve.getPoints(200); //分段数100返回101个顶点
// setFromPoints方法从points中提取数据改变几何体的顶点属性vertices
geometry.setFromPoints(points);
//材质对象
let material = new THREE.LineBasicMaterial({
color,
linewidth: 1,
});
//线条模型对象
let modelLine = new THREE.Line(geometry, material);
return modelLine
//scene.add(line); //线条对象添加到场景中
};
const createAxisInfo = (size, minSize = 0, newPeriod = 50) => {
const textY = minSize ? (360 / (size + (-minSize))) * minSize - 30 : -30;
let data = {};
let prps = [
{
type: 'plan',
size: newPeriod,
color: 0x32494B,
},
{
type: 'curveLine',
line: [
[0, minSize ? 0 : size / 2, 0],
[0, minSize ? minSize : 0, 90],
[0, size, 270],
[0, minSize ? 0 : size / 2, 360],
],
color: 0xffffff,
},
{
type: 'line',
line: [
0, minSize, 0,
0, size, 0,
0, size, 360,
0, minSize, 360,
0, minSize, 0,
0, 0, 0,
newPeriod, 0, 0,
newPeriod, 0, 360,
0, 0, 360,
0, 0, 0,
],
color: 0xffffff,
},
{
type: 'line',
line: [
0, 0, 90,
newPeriod, 0, 90,
],
color: 0xffffff,
position: 'Z',
num: 3,
margin: 90,
},
{
type: 'line',
line: [
newPeriod / 4, 0, 0,
newPeriod / 4, 0, 360,
],
color: 0xffffff,
position: 'X',
num: 3,
margin: newPeriod / 4,
},
{
type: 'line',
line: [
0, (minSize ? minSize + (size / 2) : size / 4), 0,
0, (minSize ? minSize + (size / 2) : size / 4), 360,
],
color: 0xffffff,
position: 'Y',
num: 3,
margin: minSize ? size / 2 : size / 4,
},
{
type: 'line',
line: [
0, minSize, 90,
0, size, 90,
],
color: 0xffffff,
position: 'Z',
num: 3,
margin: 90,
},
{
type: 'text',
content: [formatNumber(newPeriod)],
w: 55,
h: 25,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [360, -40, 360],
},
{
type: 'text',
content: [formatNumber(newPeriod / 2)],
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [180, -40, 360],
},
{
type: 'text',
content: ['0'],
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, -40, 360],
},
{
type: 'text',
content: ['0'],
w: 55,
h: 25,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [390, -40, 330],
textAlign: 'right',
},
{
type: 'text',
content: ['90'],
w: 55,
h: 25,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [380, -40, 250],
},
{
type: 'text',
content: ['180'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 20.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [380, -40, 160],
},
{
type: 'text',
content: ['270'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 20.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [380, -40, 70],
},
{
type: 'text',
content: ['360'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 20.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [380, -40, -20],
},
]
let prpd = [
{
type: 'curveLine',
line: [
[0, minSize ? 0 : size / 2, 0],
[0, minSize ? minSize : 0, 90],
[0, size, 270],
[0, minSize ? 0 : size / 2, 360],
],
color: 0xffffff,
},
{
type: 'line',
line: [
0, minSize, 0,
0, size, 0,
0, size, 360,
0, minSize, 360,
0, minSize, 0,
],
color: 0xffffff,
},
{
type: 'line',
line: [
0, (minSize ? minSize + (size / 2) : size / 4), 0,
0, (minSize ? minSize + (size / 2) : size / 4), 360,
],
color: 0xffffff,
position: 'Y',
num: 3,
margin: minSize ? size / 2 : size / 4,
},
{
type: 'line',
line: [
0, minSize, 90,
0, size, 90,
],
color: 0xffffff,
position: 'Z',
num: 3,
margin: 90,
},
{
type: 'text',
content: ['0'],
w: 40,
h: 25,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, textY, 340],
},
{
type: 'text',
content: ['90'],
w: 47,
h: 25,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, textY, 270],
},
{
type: 'text',
content: ['180'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 22.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, textY - 2, 180],
},
{
type: 'text',
content: ['270'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 22.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, textY - 2, 90],
},
{
type: 'text',
content: ['360'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 22.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, textY - 2, 0],
},
];
let prpd3d = [
{
type: 'plan',
size,
color: 0x32494B,
},
{
type: 'curveLine',
line: [
[minSize ? 0 : size / 2, 0, 0],
[size, 0, 90],
[minSize, 0, 270],
[minSize ? 0 : size / 2, 0, 360],
],
color: 0xffffff,
},
{
type: 'line',
line: [
minSize, 0, 0,
size, 0, 0,
size, 0, 360,
minSize, 0, 360,
minSize, 0, 0,
0, 0, 0,
0, newPeriod, 0,
0, newPeriod, 360,
0, 0, 360,
0, 0, 0,
],
color: 0xffffff,
},
{
type: 'line',
line: [
minSize, 0, 90,
size, 0, 90,
],
color: 0xffffff,
position: 'Z',
num: 3,
margin: 90,
},
{
type: 'line',
line: [
(minSize ? minSize + (size / 2) : size / 4), 0, 0,
(minSize ? minSize + (size / 2) : size / 4), 0, 360,
],
color: 0xffffff,
position: 'X',
num: 3,
margin: (minSize ? size / 2 : size / 4),
},
{
type: 'line',
line: [
0, 0, 90,
0, newPeriod, 90,
],
color: 0xffffff,
position: 'Z',
num: 3,
margin: 90,
},
{
type: 'line',
line: [
0, newPeriod / 4, 0,
0, newPeriod / 4, 360,
],
color: 0xffffff,
position: 'Y',
num: 3,
margin: newPeriod / 4,
},
{
type: 'text',
content: ['0'],
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, 0, 380],
},
{
type: 'text',
content: [formatNumber(newPeriod / 2)],
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, 180, 400],
},
{
type: 'text',
content: [formatNumber(newPeriod)],
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, 360, 400],
},
{
type: 'text',
content: ['0'],
w: 40,
h: 25,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [minSize ? 200 : 400, -40, 330],
},
{
type: 'text',
content: ['90'],
w: 47,
h: 25,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [minSize ? 200 : 400, -40, 230],
},
{
type: 'text',
content: ['180'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 22.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [minSize ? 200 : 400, -40, 140],
},
{
type: 'text',
content: ['270'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 22.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [minSize ? 200 : 400, -40, 50],
},
{
type: 'text',
content: ['360'],
w: 75,
h: 25,
ch: 9,
cw: 5.7,
color: '#ffffff',
size: 22.3,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [minSize ? 200 : 400, -40, -40],
},
];
prps = prps.map(i => i.type !== 'text' ? i : { ...i, ...fontStyles[i.content[0].length] });
prpd3d = prpd3d.map(i => i.type !== 'text' ? i : { ...i, ...fontStyles[i.content[0].length] })
let set = [360 / newPeriod, 360 / (size + (-minSize)), 1];
let isGIS = size <= 80;
let YAxisText = minSize ? [
[size, 180],
[size / 2, 90],
[0, 0],
[minSize / 2, -90],
[minSize, -180],
] : [
[isGIS ? size == 80 ? 0 : `-${80 - size}` : size],
[isGIS ? `-${80 - (size * 0.75)}` : size * 0.75,],
[isGIS ? `-${80 - (size * 0.5)}` : size * 0.5,],
[isGIS ? `-${80 - (0.25 * size)}` : size * 0.25,],
[isGIS ? `-80` : 0,],
];
let infoNum = [
{
w: 40,
h: 20,
size: 16,
}, {
w: 55,
h: 25,
size: 22.3,
}, {
w: 70,
h: 25,
size: 22.3,
}, {
w: 100,
h: 30,
size: 26.3,
}, {
w: 115,
h: 32,
size: 28.3,
}
]
YAxisText.forEach((item, index) => {
item[0] = Math.ceil(item[0]).toString();
let length = item[0].length;
let info = {
type: 'text',
content: item,
w: infoNum[length - 1].w,
h: infoNum[length - 1].h,
cw: length >= 3 ? 5.7 : '',
ch: length >= 3 ? 9 : '',
size: infoNum[length - 1].size,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [index, minSize ? item[1] : 360 - (index * 90), length === 1 ? 410 : 430],
textAlign: 'right',
};
let infoPrpd = {
type: 'text',
content: item,
w: infoNum[length - 1].w,
h: infoNum[length - 1].h,
cw: length >= 3 ? 5.7 : '',
ch: length >= 3 ? 9 : '',
size: infoNum[length - 1].size,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [0, minSize ? item[1] : 360 - (index * 90), length === 1 ? 385 : 395],
textAlign: 'right',
};
let info3d = {
type: 'text',
content: item,
w: infoNum[length - 1].w,
h: infoNum[length - 1].h,
cw: length >= 3 ? 5.7 : '',
ch: length >= 3 ? 9 : '',
size: infoNum[length - 1].size,
color: '#ffffff',
font: 'normal Bold 500px Arial,sans-serif',
xyz: [minSize ? item[1] - 20 : 360 - (index * 90), -40, 380],
textAlign: 'right',
};
prps.push(info);
prpd.push(infoPrpd);
prpd3d.push(info3d);
})
data[size] = { set, prps, prpd, prpd3d }
return data;
}
createApp({
setup() {
const testPrpsData = ref([])
const testPrpdData = ref([])
const prpsBoxRef = ref();
const prpdBoxRef = ref();
const currentPrpsData = ref([])
const currentPrpdData = ref([])
const timer = ref([])
let renderer = null
let scene = null
let camera = null
let controls = null
let axis = new THREE.Group()
let meshList = new THREE.Group() //图例柱子
meshList.name = 'chartsMesh'
let pointsList = new THREE.Group()
pointsList.name = 'chartsPoint'
let geometry = '' //InstancedMesh的初始几何体
let initInstancedMesh = ''
let pointPositions = ''
let pointsColor = '' //Points实例的位置及颜色的集合
let dummy = new THREE.Object3D()
let lut = new THREE.Lut()
lut.addColorMap('axis', [
[0.0, 0x00ee00],
[0.25, 0xeeee00],
[0.75, 0xee0000],
[1.0, 0x4e0211],
])
lut.setColorMap('axis', 32)
const maxNum = 80
const minNum = 0
const range = 100
let count = 0
let period = 50
let phase = 128
// prpd相关参数
let count1 = 0
let geometry1 = ''
let pointPositions1 = ''
let pointsColor1 = ''
let initInstancedMesh1 = ''
let renderer1 = null
let scene1 = null
let camera1 = null
let controls1 = null
let axis1 = new THREE.Group()
let meshList1 = new THREE.Group() //图例柱子
meshList1.name = 'chartsMesh1'
let dummy1 = new THREE.Object3D()
const delay = (id, callBack, ms) => {
return new Promise((resolve) => {
timer.value[id] = setTimeout(() => {
callBack();
resolve();
}, ms);
});
};
const onStarPrps = async () => {
console.log('[ unref(testPrpsData) ] >', unref(testPrpsData))
const data = unref(testPrpsData);
for (let [index, item] of data.entries()) {
let callBack = () => {
currentPrpsData.value = item;
};
await delay(index, callBack, 20);
}
// 等待所有延迟完成后再次开始
onStarPrps();
};
const onStarPrpd = () => {
let index = 0;
// 每1秒执行一次
const intervalId = setInterval(() => {
currentPrpdData.value = unref(testPrpdData)[index];
index = (index + 1) % unref(testPrpdData).length; // 循环索引
}, 1000);
// 需要停止时调用 clearInterval(intervalId);
}
const drawAxis = (size, minNum, type) => {
if (type === 1) {
if (!scene) {
return
}
const options = ['prps', 'prpd', 'prpd3d']
let key = options[type - 1]
const data = createAxisInfo(size, minNum, 50)
let [x, y, z] = data[size]['set'] // 坐标比例
scene.remove(...scene.children.filter((item) => item.name === 'axisNumberText'))
if (axis.children.length) {
axis.remove(...axis.children)
}
let axisText = new THREE.Group()
axisText.name = 'axisNumberText'
scene.add(axisText)
axis.scale.set(x, y, z)
meshList.scale.set(x, y, z)
pointsList.scale.set(x, y, z)
data[size][key].forEach(async (item) => {
switch (item.type) {
case 'plan':
axis.add(drawPlan(item)) // 有可能默认有面
break
case 'line':
axis.add(...drawLine(item))
break
case 'curveLine':
axis.add(drawSinLine(item))
break
case 'text':
axisText.add(draw2DText(item))
}
})
} else {
if (!scene1) {
return
}
const options = ['prps', 'prpd', 'prpd3d']
let key = options[type - 1]
const data = createAxisInfo(size, minNum, 50)
let [x, y, z] = data[size]['set'] // 坐标比例
scene1.remove(...scene1.children.filter((item) => item.name === 'axisNumberText'))
if (axis1.children.length) {
axis1.remove(...axis1.children)
}
let axisText1 = new THREE.Group()
axisText1.name = 'axisNumberText'
scene1.add(axisText1)
axis1.scale.set(x, y, z)
meshList.scale.set(x, y, z)
pointsList.scale.set(x, y, z)
data[size][key].forEach(async (item) => {
switch (item.type) {
case 'plan':
axis1.add(drawPlan(item)) // 有可能默认有面
break
case 'line':
axis1.add(...drawLine(item))
break
case 'curveLine':
axis1.add(drawSinLine(item))
break
case 'text':
axisText1.add(draw2DText(item))
}
})
}
}
const initChart = () => {
count = period * phase
geometry = new THREE.BoxGeometry(0.6, 1, 2.5) // 高度初始化比例为1
initInstancedMesh = new THREE.InstancedMesh(geometry, new THREE.MeshBasicMaterial(), count)
let k = 0
let initList = generateArray(period, phase)
for (let j = 0; j < initList.length; j++) {
for (let i = 0; i < initList[j].length; i++) {
const [a, b, c] = initList[j][i]
dummy.position.set(j, 99999, b)
dummy.scale.y = c
dummy.updateMatrix()
initInstancedMesh.setMatrixAt(k, dummy.matrix)
const color = lut.getColor(Math.abs(c) / maxNum)
initInstancedMesh.setColorAt(k, color)
k++
}
}
initInstancedMesh.instanceMatrix.needsUpdate = true
initInstancedMesh.instanceColor.needsUpdate = true
const object = meshList.getObjectByName('initInstancedMesh')
if (!object) {
meshList.add(initInstancedMesh)
}
}
const draw2DText = (obj) => {
let scale = 1;//window.devicePixelRatio;
let { content, w, h, color, font, xyz, size, cw, ch, rotate } = obj;
cw = cw ? cw : 6;
ch = ch ? ch : 8;
let canvas = document.createElement('canvas')
canvas.style.width = w * scale;
canvas.style.height = h * scale;
if (rotate) {
canvas.width = w * cw * scale * 3;
canvas.height = h * ch * scale * 2;
} else {
canvas.width = w * cw * scale * 2;
canvas.height = h * ch * scale * 3;
}
let ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.strokeStyle = '#000';
ctx.font = font;
ctx.direction = "ltr"; // 文本方向从左向右
ctx.textAlign = "left"; // 左对齐
ctx.textBaseline = 'middle'//基线对齐选项,决定文字垂直方向的对齐方式
if (rotate) {
ctx.translate(canvas.width / 2, canvas.height);
ctx.rotate(-Math.PI / 2);
ctx.fillText(content, h, 0);
ctx.strokeText(content, h, 0);
} else {
for (let i = 0; i < 1; i++) {
ctx.fillText(content[i], 0, canvas.height / 2);
ctx.strokeText(content[i], 0, canvas.height / 2);
}
}
let texture = new THREE.CanvasTexture(canvas)
// texture.needsUpdate = true//如果编码类型在纹理已被一个材质使用之后发生了改变, 你需要来设置Material.needsUpdate为true来使得材质重新编译
let geometry = new THREE.BufferGeometry(); //创建一个Buffer类型几何体对象
//3个为一组表示一个顶点的xyz坐标
geometry.setAttribute('position', new THREE.Float32BufferAttribute(xyz, 3));
const material = new THREE.PointsMaterial({
map: texture,
transparent: true,//材质透明
size: size ? size : 16.0, //点对象像素尺寸
sizeAttenuation: false
});
const pointsText = new THREE.Points(geometry, material);
pointsText.name = 'pointsText';
return pointsText;
}
// 坐标轴绘制
const drawAxisText = (type) => {
const setTextMap = type === 1 ? {
amplitude: '幅值',
period: '周期',
phase: '相位',
} : {
amplitude: '幅值',
phase: '相位',
}
if (!scene) {
return
}
const isPrpd = type == 2
let axisText = [
{
type: 'text',
lang: true,
content: 'amplitude',
rotate: 90,
color: '#ffffff',
size: 30,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [
isPrpd ? 0 : -40,
isPrpd ? 165 : 120,
400,
],
},
{
type: 'text',
lang: true,
content: 'period',
xyz: [250, -120, 400],
color: '#ffffff',
size: 30,
font: 'normal Bold 500px Arial,sans-serif',
},
{
type: 'text',
lang: true,
content: 'phase',
color: '#ffffff',
size: 30,
font: 'normal Bold 500px Arial,sans-serif',
xyz: [
isPrpd ? 0 : 460,
isPrpd ? -75 : -100,
170,
],
},
]
let infoNum = [
{},
{
w: 60,
h: 32,
cw: 9,
ch: 8,
size: 26.3,
},
{},
{
w: 125,
h: 50,
size: 38.3,
},
{
w: 125,
h: 50,
size: 38.3,
},
{
w: 130,
h: 50,
size: 38.3,
},
{
w: 165,
h: 122,
size: 68.3,
},
{},
{
w: 165,
h: 122,
size: 68.3,
},
]
let setList = (list) => {
let newList = []
for (let i of list) {
let str = setTextMap[i.content]
if (str) {
i.content = [str]
let { w, h, cw, ch, size } = infoNum[str.length - 1]
i.w = i.rotate ? h : w
i.h = i.rotate ? w : h
i.cw = i.rotate ? ch : cw
i.ch = i.rotate ? cw : ch
i.size = size
if (str.length == 9 && !i.rotate) {
i.w = 200
i.h = 100
}
if (i.rotate) {
i.xyz[2] = 445 + size / 2
}
const mesh = draw2DText(i)
if (isPrpd && str.length > 2 && i.rotate) {
mesh.position.z += 40
}
newList.push(mesh)
}
}
return newList
}
if (isPrpd) {
let textList1 = scene1.getObjectByName('axisText')
if (textList1) {
textList1.children.map((i) => {
i.material.dispose()
i.geometry.dispose()
textList1.remove(i)
})
scene1.remove(textList1)
}
let axisTextGroup1 = new THREE.Group()
axisTextGroup1.name = 'axisText'
axisTextGroup1.add(...setList(axisText))
scene1.add(axisTextGroup1)
} else {
let textList = scene.getObjectByName('axisText')
if (textList) {
textList.children.map((i) => {
i.material.dispose()
i.geometry.dispose()
textList.remove(i)
})
scene.remove(textList)
}
let axisTextGroup = new THREE.Group()
axisTextGroup.name = 'axisText'
axisTextGroup.add(...setList(axisText))
scene.add(axisTextGroup)
}
}
const render = () => {
//获取摄像机的视锥体
const frustum = new THREE.Frustum()
const cameraViewProjectionMatrix = new THREE.Matrix4()
camera.updateMatrixWorld() // 确保摄像机的世界矩阵已更新
cameraViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
frustum.setFromProjectionMatrix(cameraViewProjectionMatrix)
// 遍历场景中的每个对象,并根据视锥体进行剔除
scene.traverse((object) => {
if (object.isMesh) {
object.visible = frustum.intersectsObject(object)
}
})
requestAnimationFrame(render)
renderer.render(scene, camera)
}
const render1 = () => {
const frustum = new THREE.Frustum()
const cameraViewProjectionMatrix = new THREE.Matrix4()
camera1.updateMatrixWorld() // 确保摄像机的世界矩阵已更新
cameraViewProjectionMatrix.multiplyMatrices(camera1.projectionMatrix, camera1.matrixWorldInverse)
frustum.setFromProjectionMatrix(cameraViewProjectionMatrix)
// 遍历场景中的每个对象,并根据视锥体进行剔除
scene1.traverse((object) => {
if (object.isMesh) {
object.visible = frustum.intersectsObject(object)
}
})
requestAnimationFrame(render1)
renderer1.render(scene1, camera1)
}
// 初始化prps
const initPrps = () => {
let box = prpsBoxRef.value
let width = box.clientWidth
let height = box.clientHeight //box.clientHeight
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.autoClear = false
renderer.localClippingEnabled = true
let constant = (360 / (maxNum + -minNum)) * maxNum + 1
const planes = [
new THREE.Plane(new THREE.Vector3(0, -1, 0), constant), //y轴正值截取
new THREE.Plane(new THREE.Vector3(0, 1, 0), constant), //y轴负值截取
]
renderer.clippingPlanes = planes
renderer.setSize(width, height) //设置渲染区域尺寸
renderer.setPixelRatio(window.devicePixelRatio) //设备像素比,优化渲染效果
box.append(renderer.domElement) //body元素中插入canvas对象
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000d13)
scene.add(axis, meshList)
drawAxis(maxNum, minNum, 1)
const setCamera = [500, 300, 500]
let k = width / height //窗口宽高比
let s = 420 //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 0.1, 1000)
camera.position.set(...setCamera) //设置相机位置
camera.lookAt(scene.position) //设置相机方向(指向的场景对象)
controls = new THREE.OrbitControls(camera, renderer.domElement)
// 设置左右方向的最大、最小角度限制为 90 度和 0 度
controls.minAzimuthAngle = 0
controls.maxAzimuthAngle = Math.PI / 2
// 设置上下方向的最大、最小角度限制为 90 度和 0 度
controls.minPolarAngle = 0
controls.maxPolarAngle = Math.PI / 2
//设置控制器中心点
controls.target.set(25, 0, 180)
controls.enableZoom = true //启用或禁用摄像机的缩放
controls.maxZoom = 2;
controls.minZoom = 0.5;
initChart()
drawAxisText(1)
render()
}
// 处理数据
const handlePrpsData = (data) => {
const isClear = data.length ? false : true
const max = maxNum * (range / 100)
if (isClear) {
let k = 0,
initList = generateArray(period, phase)
for (let j = 0; j < initList.length; j++) {
for (let i = 0; i < initList[j].length; i++) {
const [a, b, c] = initList[j][i]
dummy.position.set(j, 99999, b)
dummy.scale.y = c
dummy.updateMatrix()
initInstancedMesh.setMatrixAt(k, dummy.matrix)
k++
}
}
initInstancedMesh.instanceMatrix.needsUpdate = true
return
}
let k = 0
let list = JSON.parse(JSON.stringify(data))
list = list.reverse().map((i) => ({
h: i[2],
color: lut.getColor(Math.abs(i[2] / maxNum).toFixed(2)),
}))
for (let j = 0; j < period; j++) {
for (let i = 0; i < list.length; i++) {
initInstancedMesh.getMatrixAt(k, dummy.matrix)
dummy.position.setFromMatrixPosition(dummy.matrix)
dummy.scale.setFromMatrixScale(dummy.matrix)
if (dummy.position.x > 0) {
dummy.position.x -= 1
} else {
let { h, color } = list[i]
dummy.position.x = period - 1
dummy.position.y = Math.abs(h) / 2 // 居中
dummy.scale.y = Math.abs(h)
initInstancedMesh.setColorAt(k, color)
}
dummy.updateMatrix()
initInstancedMesh.setMatrixAt(k, dummy.matrix)
k++
}
}
initInstancedMesh.instanceMatrix.needsUpdate = true
initInstancedMesh.instanceColor.needsUpdate = true
data = null
}
const initChart1 = () => {
let maxYPoints = 800
count1 = maxYPoints * phase
geometry1 = new THREE.BufferGeometry()
pointPositions1 = new Float32Array(count1 * 3) // x, y, z
pointsColor1 = new Float32Array(count1 * 3) // r, g, b
// 初始化点的位置和颜色
for (let i = 0; i < count1; i++) {
const x = 30
const y = period
const z = period
pointPositions1[i * 3] = x
pointPositions1[i * 3 + 1] = y
pointPositions1[i * 3 + 2] = z
pointsColor1[i * 3] = 0
pointsColor1[i * 3 + 1] = 0
pointsColor1[i * 3 + 2] = 0
}
geometry1.setAttribute('position', new THREE.BufferAttribute(pointPositions1, 3))
geometry1.setAttribute('color', new THREE.BufferAttribute(pointsColor1, 3))
// 创建 ShaderMaterial
const material = new THREE.PointsMaterial({ size: 2, vertexColors: true }) //双面显示
// 创建 Points 对象
initInstancedMesh1 = new THREE.Points(geometry1, material)
const object = meshList1.getObjectByName('initInstancedPoints')
if (!object) {
meshList1.add(initInstancedMesh1)
}
}
const initPrpd = () => {
const box = prpdBoxRef.value
const width = box.clientWidth
const height = box.clientHeight //box.clientHeight
renderer1 = new THREE.WebGLRenderer({ antialias: true })
renderer1.autoClear = false
renderer1.localClippingEnabled = true
let constant = (360 / (maxNum + -minNum)) * maxNum + 1
const planes = [
new THREE.Plane(new THREE.Vector3(0, -1, 0), constant), //y轴正值截取
new THREE.Plane(new THREE.Vector3(0, 1, 0), constant), //y轴负值截取
]
renderer1.clippingPlanes = planes
renderer1.setSize(width, height) //设置渲染区域尺寸
renderer1.setPixelRatio(window.devicePixelRatio) //设备像素比,优化渲染效果
box.append(renderer1.domElement) //body元素中插入canvas对象
scene1 = new THREE.Scene()
scene1.background = new THREE.Color(0x000d13)
scene1.add(axis1, meshList1)
drawAxis(maxNum, minNum, 2)
const setCamera = [600, 0, 0]
let k = width / height //窗口宽高比
let s = 280 //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
camera1 = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 0.1, 1000)
camera1.position.set(...setCamera) //设置相机位置
const isPrpd = true
camera1.left = -s * k - (isPrpd ? 200 : 20)
camera1.right = s * k - (isPrpd ? 200 : 20)
camera1.top = s + (isPrpd ? (minNum ? -30 : 155) : isPrps3d ? 60 : minNum ? -80 : 60)
camera1.bottom = -s + (isPrpd ? (minNum ? -30 : 155) : isPrps3d ? 60 : minNum ? -80 : 60)
camera1.aspect = k
camera1.updateProjectionMatrix()
camera1.lookAt(scene1.position) //设置相机方向(指向的场景对象)
controls1 = new THREE.OrbitControls(camera1, renderer1.domElement)
// 设置左右方向的最大、最小角度限制为 90 度和 0 度
controls1.minAzimuthAngle = 0
controls1.maxAzimuthAngle = Math.PI / 2
// 设置上下方向的最大、最小角度限制为 90 度和 0 度
controls1.minPolarAngle = 0
controls1.maxPolarAngle = Math.PI / 2
//设置控制器中心点
controls1.target.set(25, 0, 180)
controls1.enableZoom = true //启用或禁用摄像机的缩放
controls1.maxZoom = 2;
controls1.minZoom = 0.5;
controls1.enableRotate = false //启用或禁用摄像机水平或垂直旋转
controls1.enableZoom = false //启用或禁用摄像机的缩放
controls1.enableZoom = false //启用或禁用摄像机的缩放
initChart1()
drawAxisText(2)
render1()
}
const handlePrpdData = (data) => {
const isClear = data.length ? false : true
const max = maxNum * (range / 100)
const colors = [
{
color: new THREE.Color(0x50a3ba),
num: 5,
},
{
color: new THREE.Color(0x9db578),
num: 7,
},
{
color: new THREE.Color(0xeac736),
num: 10,
},
{
color: new THREE.Color(0xe28b4a),
num: 14,
},
]
data.forEach((item, index) => {
// 更新颜色
const color =
item[2] <= 3
? new THREE.Color(0x0082df)
: item[2] >= 15
? new THREE.Color(0xd94e5d)
: colors.find((i) => item[2] <= i.num).color
pointPositions1[index * 3] = 1 // X
pointPositions1[index * 3 + 1] = item[0] //> max ? max : item[0] // Y
pointPositions1[index * 3 + 2] = 360 - item[1] // Z
pointsColor1[index * 3] = color.r //R
pointsColor1[index * 3 + 1] = color.g //G
pointsColor1[index * 3 + 2] = color.b //B
})
for (let i = data.length - 1; i < count1; i++) {
pointPositions1[i * 3 + 1] = 9999999 // Y//移到y轴高出隐藏多余的点
}
// 通知 Three.js 更新 BufferGeometry
geometry1.attributes.position.needsUpdate = true
geometry1.attributes.color.needsUpdate = true
geometry1.computeBoundingSphere() //重新计算边界球体 解决顶点的位置超出了原来的范围而默认的渲染器无法正确处理这种变化导致points不可见的bug
}
// 监听当前prps
watch(currentPrpsData, (data) => {
handlePrpsData(data)
}, { deep: true })
// 监听当前prpd
watch(currentPrpdData, (data) => {
console.log('[ data ] >', data)
handlePrpdData(data)
}, { deep: true })
onMounted(() => {
initPrps();
initPrpd();
// 加载prps数据
fetch('./three/prps.json') // JSON文件路径
.then(response => response.json()) // 解析为JSON对象
.then(data => {
testPrpsData.value = data;
})
.catch(error => console.error('加载JSON失败:', error));
// 加载prpd数据
fetch('./three/prpd.json') // JSON文件路径
.then(response => response.json()) // 解析为JSON对象
.then(data => {
console.log('[ data ] >', data)
testPrpdData.value = data
})
.catch(error => console.error('加载JSON失败:', error));
})
return {
prpsBoxRef,
prpdBoxRef,
testPrpsData,
testPrpdData,
currentPrpsData,
currentPrpdData,
onStarPrps,
onStarPrpd,
timer
};
}
})
.use(antd)
.use(ElementPlus, {
locale: ElementPlusLocaleZhCn // 使用中文语言包
})
.mount('#app');
</script>
</body>
</html>