UNPKG

dxcr-waterfall

Version:

waterfall

1,112 lines (993 loc) 32.1 kB
/* * @Description: * @Author: Dxcr * @Date: 2024-06-26 15:06:00 * @LastEditTime: 2024-07-09 15:51:29 * @LastEditors: Dxcr */ import * as THREE from "three"; import DxcrWebWorker from "dxcr-webworker"; import { ThreejsUtill } from "dxcr-three"; import VisualMap from "./visualMap"; import Stats from "three/addons/libs/stats.module.js"; import dayjs from "dayjs"; import colors from "./colors"; class Waterfall { // 组件 dom; scene; camera; renderer; stats; webWorker; // 数据处理 geometry; material; attributesUpdateFlag = false; countBuffer = 0; // canvas; chartContext; canvasTexture; chartMesh; // 坐标轴 yAxis; yAxisData = []; font; tickNum = 0; scrollCount = 0; xAxis; xAxisData = []; xAxisOptions = { color: 0xffffff, showCount: 10, }; yAxisOptions = { color: 0xffffff, showCount: 10, }; //坐标轴指示器 xAxisPointer; yAxisPointer; xAxisPointerText; yAxisPointerText; currentCoordinate; //图表 rawData = []; chartData = []; rawDataCount; //选中区域 range = [0, 1]; rangeArea = [0, 0]; rangeAreaMesh; // 参数 dataCount = 1000; keepCount = 500; visualMapOptions = { height: 100, width: 20, paddingLeft: 20, paddingRight: 10, show: false, }; minValue = 0; maxValue = 1000; paddingLeft = 0; paddingRight = 10; paddingTop = 10; paddingBottom = 0; chartWidth = 1000; chartHeight = 500; axisWidth = 120; axisHeight = 20; tickInterval = 20; subTickInterval = 5; debugMode = false; width = 500; height = 500; constructor(dom, options = {}) { this.dom = dom; this.dom.style.position = "relative"; // this.width = // this.axisWidth + // this.chartWidth + // this.paddingLeft + // this.paddingRight + // 1; // this.height = // this.axisHeight + // this.chartHeight + // 1 + // this.paddingBottom + // this.paddingTop; this.paddingTop = options.paddingTop || this.paddingTop; this.paddingBottom = options.paddingBottom || this.paddingBottom; this.paddingLeft = options.paddingLeft || this.paddingLeft; this.paddingRight = options.paddingRight || this.paddingRight; this.width = dom.clientWidth; this.height = dom.clientHeight; this.chartWidth = this.width - this.axisWidth - this.paddingLeft - this.paddingRight; this.chartHeight = this.height - this.axisHeight - this.paddingTop - this.paddingBottom; this.dataCount = options.dataCount || this.dataCount; this.keepCount = options.keepCount || this.keepCount; // this.chartWidth = options.chartWidth || this.chartWidth; // this.chartHeight = options.chartHeight || this.chartHeight; this.debugMode = options.debugMode || this.debugMode; this.rawDataCount = options.rawDataCount || this.dataCount; this.minValue = options.minValue || this.minValue; this.maxValue = options.maxValue || this.maxValue; this.xAxisOptions = Object.assign(this.xAxisOptions, options.xAxisOptions); this.yAxisOptions = Object.assign(this.yAxisOptions, options.yAxisOptions); this.visualMapOptions = Object.assign(this.visualMapOptions, options.visualMapOptions); this.init(); } init() { if (this.visualMapOptions.show) { this.chartWidth -= this.visualMapOptions.width + this.visualMapOptions.paddingLeft + this.visualMapOptions.paddingRight; this.initVisualMap(); } this.initThree(); this.initWebWorker(); if (this.debugMode) { console.log("waterfall - Init"); this.initStats(); } // this.handleVisibilityChange(); } initWebWorker() { let c = this; this.webWorker = new DxcrWebWorker( "/dxcr-waterfall/Worker/worker.js", { type: "module" }, { onMessage: (event) => { // console.log('WebWorker Received:', event.data) let returnObject = event.data; let type = returnObject.type; let data = returnObject.data; if (type == 1) { c.updateDataAll(data); } else { c.updateData(data, returnObject.index); } }, onError: (event) => console.error("WebWorker error:", event), } ); } initVisualMap() { const visualMap = new VisualMap(this.dom); this.visualMap = visualMap // 将 RGB 数组转换为 CSS 颜色字符串 const rgbToCssColor = (rgb) => `rgb(${rgb.map((val) => Math.round(val * 255)).join(", ")})`; // 创建颜色渐变字符串 const gradient = colors.map(rgbToCssColor); visualMap.updateVisualMap(gradient, this.minValue, this.maxValue); visualMap.visualMapDom.style.position = "absolute"; visualMap.visualMapDom.style.bottom = "20px"; visualMap.visualMapDom.style.right = "20px"; visualMap.bindLabelDoubleClick((type, data) => { if (type == 0) { this.minValue = data; } else { this.maxValue = data; } this.setData(this.rawData); }); } addData(data, index) { this.webWorker.postMessage({ data: data, index: index, type: 0, minValue: this.minValue, maxValue: this.maxValue, dataCount: this.dataCount, }); } setData(data) { this.rawData = data; this.webWorker.postMessage({ data: data, type: 1, minValue: this.minValue, maxValue: this.maxValue, dataCount: this.dataCount, }); } setColorValue(minValue, maxValue) { this.minValue = minValue || this.minValue; this.maxValue = maxValue || this.maxValue; const rgbToCssColor = (rgb) => `rgb(${rgb.map((val) => Math.round(val * 255)).join(", ")})`; const gradient = colors.map(rgbToCssColor); this.visualMap.updateVisualMap(gradient, this.minValue, this.maxValue); } clearData() { this.chartData = []; this.updateChartAll(this.chartData); } handleVisibilityChange() { const c = this; // 处理页面可见性变化 document.addEventListener("visibilitychange", function () { if (document.visibilityState === "visible") { // 强制更新所有几何体属性 c.geometry.attributes.position.updateRange.offset = 0; c.geometry.attributes.position.updateRange.count = -1; c.geometry.attributes.position.needsUpdate = true; c.geometry.attributes.color.updateRange.offset = 0; c.geometry.attributes.color.updateRange.count = -1; c.geometry.attributes.color.needsUpdate = true; } }); } resizeChart() { this.range = [0, 1]; this.scaleChart(this.range); } scaleChart(range) { let startUV = range[0] > range[1] ? range[1] : range[0]; let endUV = range[0] > range[1] ? range[0] : range[1]; //改变uv坐标 let uv = this.geometry.attributes.uv.array; for (let i = 0; i < uv.length; i += 2) { if (i < uv.length / 2) { uv[i] = startUV; } else { uv[i] = endUV; } } this.geometry.attributes.uv.needsUpdate = true; let start = Math.floor(startUV * this.rawDataCount); let end = Math.floor(endUV * this.rawDataCount); this.xAxisData = this.generateXDatas(start, end); this.updateXAxis(); } scaleDatas(datas, range) { let count = datas[0].length; let startRange = range[0] || 0; let endRange = range[1] || count; const sliceData = datas.map((data) => { return data.slice(startRange, endRange); }); return sliceData; } scaleData(data, range) { let count = data.length; let startRange = range[0] || 0; let endRange = range[1] || count; return data.slice(startRange, endRange); } initXAxis() { this.xAxisData = this.generateXDatas(); this.updateXAxis(); } updateData(dataColors, index) { // console.log("dataColors", dataColors); let rollIndex = index % this.keepCount; this.chartData[rollIndex] = dataColors; this.updateChart(dataColors, rollIndex); this.updateYAxis(index); if (index >= this.keepCount) { this.scroll(); } } updateDataAll(datasArray) { this.clearTick(); this.updateAllYAxis(); this.chartData = datasArray; this.updateChartAll(datasArray); } updateChartAll(datasArray) { let c = this; this.clearChart(); let y = 0; let width = this.chartWidth / this.dataCount; let height = this.chartHeight / this.keepCount; for (let i = 0; i < datasArray.length; i++) { let colors = datasArray[i]; let x = 0; for (let j = 0; j < colors.length; j += 3) { const cssColor = `rgb(${Math.round(colors[j] * 255)},${Math.round( colors[j + 1] * 255 )},${Math.round(colors[j + 2] * 255)})`; this.chartContext.fillStyle = cssColor; // this.chartContext.fillStyle = "rgb(64, 150, 255)"; this.chartContext.fillRect( x, this.canvas.height - 1 - y, width, height ); x += width; } y += height; } // 创建纹理 const texture = new THREE.CanvasTexture(this.canvas); texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.magFilter = THREE.NearestFilter; texture.minFilter = THREE.NearestFilter; this.material.map = texture; this.material.needsUpdate = true; } updateChart(colors, index) { let c = this; // 绘制纹理图内容 let width = this.chartWidth / this.dataCount; let height = this.chartHeight / this.keepCount; let x = 0; let y = this.canvas.height - height - index * height; this.clearChart(0, y, this.canvas.width, height); for (let i = 0; i < colors.length; i += 3) { const cssColor = `rgb(${Math.round(colors[i] * 255)}, ${Math.round( colors[i + 1] * 255 )}, ${Math.round(colors[i + 2] * 255)})`; this.chartContext.fillStyle = cssColor; this.chartContext.fillRect(x, y, width, height); x += width; } // 创建纹理 const texture = new THREE.CanvasTexture(this.canvas); texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.magFilter = THREE.NearestFilter; texture.minFilter = THREE.NearestFilter; this.material.map = texture; this.material.needsUpdate = true; } updateChartGradient(colors, index) { // 创建线性渐变色对象 let gradient = this.chartContext.createLinearGradient( 0, 0, this.canvas.width, 0 ); // 绘制纹理图内容 for (let i = 0; i < colors.length; i += 3) { // 设置渐变色的起止颜色 // 假设colors数组是颜色值的线性索引或直接存储颜色值 const cssColor = `rgb(${Math.round(colors[i] * 255)}, ${Math.round( colors[i + 1] * 255 )}, ${Math.round(colors[i + 2] * 255)})`; gradient.addColorStop(i / (colors.length - 3), cssColor); } // 使用渐变色填充矩形 this.chartContext.fillStyle = gradient; this.chartContext.fillRect( 0, this.canvas.height - index, this.canvas.width, 1 ); // 创建纹理 const texture = new THREE.CanvasTexture(this.canvas); texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.magFilter = THREE.NearestFilter; texture.minFilter = THREE.NearestFilter; this.material.map = texture; this.material.needsUpdate = true; } clearChart( x = 0, y = 0, width = this.canvas.width, height = this.canvas.height ) { this.chartContext.clearRect(x, y, width, height); } scroll() { let c = this; // 关键代码 更新了边界球体 解决了超出一定范围不显示的bug if (this.scrollCount % (this.chartHeight / 2) < 1) { this.geometry.computeBoundingSphere(); this.yAxis.geometry.computeBoundingSphere(); } this.scrollCount++; this.updateAxis(this.yAxis); this.rollChart(this.scrollCount); } rollChart(index) { let texture = this.material.map; texture.offset.y = (index * (1 / this.keepCount)) % 1; } initThree() { // 创建场景和相机 this.scene = new THREE.Scene(); // 宽高要+1 防止第一条数据丢失 this.camera = new THREE.OrthographicCamera( -this.width / 2, // 左 this.width / 2, // 右 this.height / 2, // 上 -this.height / 2, // 下 1, 1000 ); this.camera.position.set(this.width / 2, this.height / 2, 100); // 创建渲染器 const renderer = new THREE.WebGLRenderer(); renderer.setClearColor(0xb9d3ff, 0.0); this.renderer = renderer; renderer.setSize(this.width, this.height); this.dom.appendChild(renderer.domElement); // 创建几何体 let geometry = new THREE.BufferGeometry(); this.geometry = geometry; //类型化数组创建顶点数据 const vertices = new Float32Array([ 0, 0, 0, //顶点1坐标 0, this.chartHeight, 0, //顶点2坐标 this.chartWidth, this.chartHeight, 0, //顶点3坐标 this.chartWidth, 0, 0, //顶点4坐标 ]); // 创建索引数组 const indices = new Uint16Array([ 0, 1, 2, // 三角形1 0, 2, 3, // 三角形2 ]); // 创建属性缓冲区对象 const attribue = new THREE.BufferAttribute(vertices, 3); geometry.setAttribute("position", attribue); geometry.setIndex(new THREE.BufferAttribute(indices, 1)); // 创建纹理坐标 const uv = new Float32Array([ 0, 0, // 顶点1的纹理坐标 0, 1, // 顶点2的纹理坐标 1, 1, // 顶点3的纹理坐标 1, 0, // 顶点4的纹理坐标 ]); geometry.setAttribute("uv", new THREE.BufferAttribute(uv, 2)); const material = new THREE.MeshBasicMaterial({ color: "#ffffff", //材质颜色 map: null, // side: THREE.FrontSide, //默认只有正面可见 side: THREE.DoubleSide, //两面可见 // side: THREE.BackSide, //设置只有背面可见 }); // material.onBeforeCompile = function (shader, renderer) { // console.log('shader',shader) // // shader.uniforms.diffuse.value = null // console.log('renderer',renderer) // }; this.material = material; const chartMesh = new THREE.Mesh(geometry, material); chartMesh.name = "chart"; this.chartMesh = chartMesh; chartMesh.position.set(this.axisWidth, this.axisHeight, 0); this.scene.add(chartMesh); this.initTicks(); this.initXAxis(); this.initCanvas(); this.initAxisPointer(); this.initRangeArea(); this.addEvent(); // 渲染 this.animate(); } addEvent() { let c = this; // 监听鼠标按下事件 let mouseDown = false; let startUV = new THREE.Vector2(); let endUV = new THREE.Vector2(); // 创建射线投射器 let dom = this.renderer.domElement; this.renderer.domElement.addEventListener("mousedown", (event) => { mouseDown = false; // 将鼠标位置转换为NDC坐标 let mouse = new THREE.Vector2(); mouse.x = (event.offsetX / dom.clientWidth) * 2 - 1; mouse.y = -(event.offsetY / dom.clientHeight) * 2 + 1; // 使用射线投射器和相机来检测点击的对象 const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, c.camera); let intersects = raycaster.intersectObjects(c.scene.children, true); if (intersects.length > 0) { // 获取第一个交点的对象 let clickedObject = intersects[0].object; // console.log("Clicked object:", clickedObject); let name = clickedObject.name; if (name == "chart") { let uv = intersects[0].uv; startUV = uv; c.range[0] = startUV.x; let point = intersects[0].point; c.rangeArea[0] = point.x; mouseDown = true; } } }); // 监听鼠标移动事件 this.renderer.domElement.addEventListener("mousemove", (event) => { // console.log("mousemove"); // 将鼠标位置转换为NDC坐标 let mouse = new THREE.Vector2(); mouse.x = (event.offsetX / dom.clientWidth) * 2 - 1; mouse.y = -(event.offsetY / dom.clientHeight) * 2 + 1; // 使用射线投射器和相机来检测点击的对象 const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, c.camera); let intersects = raycaster.intersectObjects(c.scene.children, true); if (intersects.length > 0) { // 获取第一个交点的对象 let clickedObject = intersects[0].object; let name = clickedObject.name; if (name == "chart") { if (mouseDown) { let uv = intersects[0].uv; endUV = uv; c.range[1] = endUV.x; let point = intersects[0].point; c.rangeArea[1] = point.x; c.updateRangeArea(); } let clickedPoint = intersects[0].point; let x = clickedPoint.x - clickedObject.position.x; let y = clickedPoint.y - clickedObject.position.y; c.currentCoordinate = { x, y }; } } }); // 监听鼠标释放事件 this.renderer.domElement.addEventListener("mouseup", (event) => { if (mouseDown) { // 将鼠标位置转换为NDC坐标 let mouse = new THREE.Vector2(); mouse.x = (event.offsetX / dom.clientWidth) * 2 - 1; mouse.y = -(event.offsetY / dom.clientHeight) * 2 + 1; // 使用射线投射器和相机来检测点击的对象 let raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, c.camera); let intersects = raycaster.intersectObjects(c.scene.children, true); if (intersects.length > 0) { // 获取第一个交点的对象 let clickedObject = intersects[0].object; // console.log("Clicked object:", clickedObject); let name = clickedObject.name; if (name == "chart") { //缩放 let uv = intersects[0].uv; endUV = uv; //缩放矩阵 if (startUV.x != endUV.x) { c.range[1] = endUV.x; //执行放大 c.scaleChart(c.range); } c.showRangeArea(false); } } mouseDown = false; } }); this.renderer.domElement.addEventListener("dblclick", () => { //还原矩阵 c.resizeChart(); }); // 监听鼠标移出事件 this.renderer.domElement.addEventListener("mouseout", function (event) { c.currentCoordinate = null; }); } initCanvas() { this.canvas = document.createElement("canvas"); // this.dom.appendChild(this.canvas); this.canvas.width = this.chartWidth; this.canvas.height = this.chartHeight; this.chartContext = this.canvas.getContext("2d"); this.canvasTexture = new THREE.CanvasTexture(this.canvas); this.canvasTexture.wrapS = THREE.RepeatWrapping; this.canvasTexture.wrapT = THREE.RepeatWrapping; this.canvasTexture.magFilter = THREE.NearestFilter; this.canvasTexture.minFilter = THREE.NearestFilter; this.material.map = this.canvasTexture; this.material.needsUpdate = true; } animate() { ThreejsUtill.animateDelta((deltaTime) => { if (this.stats) { this.stats.update(); } this.renderer.render(this.scene, this.camera); this.attributesUpdateFlag = false; }); } initStats() { // 创建stats对象 this.stats = new Stats(); // Stats.domElement:web页面上输出计算结果,一个div元素 this.dom.appendChild(this.stats.domElement); this.stats.domElement.style.position = "absolute"; this.stats.domElement.style.bottom = "0px"; this.stats.domElement.style.top = "auto"; } createAxis(length, color) { const material = new THREE.LineBasicMaterial({ color: color, linewidth: 10, }); const points = []; points.push(new THREE.Vector3(0, 0, 0)); points.push(new THREE.Vector3(length, 0, 0)); const geometry = new THREE.BufferGeometry().setFromPoints(points); return new THREE.Line(geometry, material); } createTick(position, color, length = 10) { const material = new THREE.LineBasicMaterial({ color: color }); const points = []; points.push(new THREE.Vector3(position, 0, 0)); points.push(new THREE.Vector3(position, length, 0)); const geometry = new THREE.BufferGeometry().setFromPoints(points); return new THREE.Line(geometry, material); } addTicks(axis, texts, color, tickInterval, subTickNum) { let arr = []; for (let i = 0; i < texts.length; i++) { let text = texts[i]; let tickPosition = i * tickInterval; if (i == 0 && this.xAxis == axis) { tickPosition += 1; } if (i % subTickNum == 0) { let tick = this.addTick(axis, tickPosition, text, color, 6, -6); arr.push(tick); } else { let tick = this.addTick(axis, tickPosition, text, color, 6, -6); arr.push(tick); } } return arr; } addTick(axis, tickPosition, text, color, length, textDistance) { const tick = this.createTick(tickPosition, color, length); axis.add(tick); const sprite = this.createTextSprite(text, 12, "#ffffff", null, [2, 4]); sprite.position.x += tickPosition; sprite.position.y += textDistance; tick.add(sprite); return tick; } removeTick() { let tick = this.yAxis.children[0]; this.yAxis.remove(tick); } initTicks() { const length = this.chartHeight; this.yAxis = this.createAxis(length, 0xffffff); this.yAxis.rotation.z = Math.PI / 2; this.yAxis.position.x = this.axisWidth + 1; this.yAxis.position.y = this.axisHeight; this.scene.add(this.yAxis); // 添加X轴 let xAxisOptions = this.xAxisOptions; const xAxisLength = this.chartWidth; let xAxis = this.createAxis(xAxisLength, xAxisOptions.color); // 使用相同的颜色创建X轴 this.xAxis = xAxis; this.xAxis.position.x = this.axisWidth; // 调整X轴的位置 this.xAxis.position.y = this.axisHeight; // 调整X轴的位置 this.scene.add(this.xAxis); } setYData(data) { this.yAxisData = data; } setXData(data) { this.xAxisData = data; } updateXAxis() { let xDatas = []; let showCount = this.xAxisOptions.showCount; for (let i = 0; i <= showCount; i++) { xDatas.push( this.xAxisData[ Math.floor(((this.xAxisData.length - 1) / showCount) * i) ] ); } this.xAxis.children = []; let ticks = this.addTicks( this.xAxis, xDatas, "#ffffff", this.chartWidth / (xDatas.length - 1), 0 ); for (const tick of ticks) { tick.position.y = -6; } } //生成x轴文字 generateXDatas(start = 0, end = this.rawDataCount) { let datas = []; for (let i = start; i <= end; i++) { datas.push(i); } return datas; } initRangeArea() { const rangeAreaMesh = this.createRangeArea({ width: this.chartWidth, height: this.chartHeight, }); rangeAreaMesh.position.x = this.chartWidth / 2; rangeAreaMesh.position.y = this.chartHeight / 2; this.chartMesh.add(rangeAreaMesh); rangeAreaMesh.visible = false; this.rangeAreaMesh = rangeAreaMesh; } updateRangeArea() { let AreaX = this.rangeArea[1] - this.rangeArea[0]; let scaleX = AreaX / this.chartWidth; this.rangeAreaMesh.scale.x = scaleX; this.rangeAreaMesh.position.x = this.rangeArea[0] - this.chartMesh.position.x + AreaX / 2; this.rangeAreaMesh.visible = true; } showRangeArea(isShow = true) { this.rangeAreaMesh.visible = isShow; this.rangeAreaMesh.visible = isShow; } createRangeArea({ width, height }) { let material = new THREE.MeshBasicMaterial({ color: 0xcccccc, // 设置材质颜色 transparent: true, // 启用透明度 opacity: 0.4, // 设置透明度为50% }); let geometry = new THREE.PlaneGeometry(width, height); let mesh = new THREE.Mesh(geometry, material); return mesh; } showAxisPointer(isShow = true) { this.xAxisPointer.visible = isShow; this.yAxisPointer.visible = isShow; } updateAxisPointer() { if (this.currentCoordinate) { this.showAxisPointer(); const { x, y } = this.currentCoordinate; this.xAxisPointer.position.x = x; this.yAxisPointer.position.y = y; this.updateAxisPointerText(x, y); } else { this.showAxisPointer(false); } } updateAxisPointerText(x, y) { let xIndex = Math.ceil((x / this.chartWidth) * this.rawDataCount); let yIndex = Math.floor((y / this.chartHeight) * this.chartHeight); xIndex = Math.min(xIndex, this.xAxisData.length - 1); yIndex = Math.min(yIndex, this.yAxisData.length - 1); let xText = this.xAxisData[xIndex] ?? ""; let yText = this.yAxisData[yIndex] ?? ""; const texture = this.createTextTexture( xText, 12, "#ffffff", "#333333", [2, 4] ); this.xAxisPointerText.material.map = texture; this.xAxisPointerText.scale.set( texture.image.width, texture.image.height, 1 ); const texture1 = this.createTextTexture( yText, 12, "#ffffff", "#333333", [2, 4] ); this.yAxisPointerText.material.map = texture1; this.yAxisPointerText.scale.set( texture1.image.width, texture1.image.height, 1 ); } initAxisPointer() { let c = this; let xAxisPointer = this.createXAxisPointer(); this.xAxisPointer = xAxisPointer; xAxisPointer.visible = false; this.chartMesh.add(xAxisPointer); let yAxisPointer = this.createYAxisPointer(); this.yAxisPointer = yAxisPointer; yAxisPointer.visible = false; this.chartMesh.add(yAxisPointer); const xAxisPointerText = this.createTextSprite( "", 12, "#ffffff", "#333333", [2, 4] ); xAxisPointerText.center = [0.5, 1]; this.xAxisPointerText = xAxisPointerText; xAxisPointerText.position.y = -4; xAxisPointerText.position.z = 1; this.xAxisPointer.add(xAxisPointerText); const yAxisPointerText = this.createTextSprite( "", 12, "#ffffff", "#333333", [2, 4] ); yAxisPointerText.center = [1, 0.5]; this.yAxisPointerText = yAxisPointerText; yAxisPointerText.position.x = -4; yAxisPointerText.position.z = 1; this.yAxisPointer.add(yAxisPointerText); ThreejsUtill.animate(() => { c.updateAxisPointer(); }); } createXAxisPointer() { const material = new THREE.LineBasicMaterial({ color: "#ffffff", linewidth: 10, }); const points = []; points.push(new THREE.Vector3(0, 0, 0)); points.push(new THREE.Vector3(0, this.chartHeight, 0)); const geometry = new THREE.BufferGeometry().setFromPoints(points); const line = new THREE.Line(geometry, material); return line; } createYAxisPointer() { const material = new THREE.LineBasicMaterial({ color: "#ffffff", linewidth: 10, }); const points = []; points.push(new THREE.Vector3(0, 0, 0)); points.push(new THREE.Vector3(this.chartWidth, 0, 0)); const geometry = new THREE.BufferGeometry().setFromPoints(points); const line = new THREE.Line(geometry, material); this.yAxisPointer = line; return line; } async initFont() { this.font = await ThreejsUtill.loadFont( "/fonts/Noto_Sans_SC/Noto Sans SC Light_Regular.json" ); return this.font; } createTextTexture( text, fontSize, fontColor, backgroundColor, padding = [0, 0] ) { let paddingV = padding[0]; let paddingH = padding[1]; const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); context.font = `${fontSize}px 宋体`; // 设置画布尺寸 const width = context.measureText(text).width; canvas.height = fontSize + paddingV * 2; canvas.width = width + paddingH * 2; //设置背景色 if (backgroundColor) { context.fillStyle = backgroundColor; context.fillRect(0, 0, canvas.width, canvas.height); } //写字 context.font = `${fontSize}px 宋体`; context.fillStyle = fontColor; // 绘制文字 context.fillText(text, paddingH, canvas.height / 2 + fontSize / 2 - 2); // 创建纹理 const texture = new THREE.CanvasTexture(canvas); texture.magFilter = THREE.NearestFilter; texture.minFilter = THREE.NearestFilter; return texture; } createTextSprite( text, fontSize, fontColor, backgroundColor, padding = [0, 0] ) { const texture = this.createTextTexture( text, fontSize, fontColor, backgroundColor, padding ); const material = new THREE.SpriteMaterial({ map: texture, }); const sprite = new THREE.Sprite(material); // 根据字体大小调整精灵的比例 sprite.scale.set(texture.image.width, texture.image.height, 1); return sprite; } updateAxis(yAxis) { let height = this.chartHeight / this.keepCount; // 改变原有坐标轴的点 后一个点坐标+1 const position = yAxis.geometry.attributes.position; const numPoints = position.count; // 坐标轴线上的点的总数 // 更新坐标轴线上每个点的位置 for (let i = 0; i < numPoints; i++) { // 将每个点的 x 坐标增加 1 position.setX(i, position.getX(i) + height); } position.needsUpdate = true; yAxis.position.y -= height; } updateYAxis(index, data) { data = data || dayjs().format("YYYY-MM-DD HH:mm:ss"); this.yAxisData[index] = data; let y = index * (this.chartHeight / this.keepCount); if (index % this.tickInterval == 0) { this.addTick(this.yAxis, y, data, 0xffffff, 6, 70); this.tickNum++; //删除老的 if (this.tickNum > this.keepCount / this.tickInterval) { this.removeTick(); this.tickNum--; } } } updateAllYAxis() { let datas = []; let showCount = this.yAxisOptions.showCount; for (let i = 0; i <= showCount; i++) { datas.push( this.yAxisData[ Math.floor(((this.yAxisData.length - 1) / showCount) * i) ] ); } this.yAxis.children = []; let ticks = this.addTicks( this.yAxis, datas, "#ffffff", this.chartHeight / (datas.length - 1), 0 ); for (const tick of ticks) { for (const children of tick.children) { children.position.y += 70; } } } clearTick() { this.yAxis.children = []; this.tickNum = 0; } } export default Waterfall;