threejs饼图
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Cake</title> <style> #cake { height: 500px; width: 500px; background-color: black; } </style> </head> <body> <div id="cake"></div> <script type="module"> import * as THREE from './node_modules/three/build/three.module.js'; import ThreeBase from './ThreeBase.js'; import { getShadowColor, getBasicMaterial, getTextArraySprite, getDrawColors } from './util.js'; class Cake extends ThreeBase { constructor() { super(); this.isStats = false; this.isAxis = false; this.rotateAngle = 0; this.count = 0; this.time = 0; this.currentTextMesh = null; } createChart(that) { this.that = that; if (this.group) { this.cleanObj(this.group); this.group = null; } if (that.data.length == 0) { return; } this.cLen = 4; //获取渐变色 this.colors = getDrawColors(that.colors, this.cLen); // this.colors = that.colors; console.log('[ colors ] >', this.colors, this.cLen); //从小到大排序 that.data = that.data.sort((a, b) => a.value - b.value); let { baseHeight, radius, perHeight, maxHeight, fontColor, fontSize } = that; let sum = 0; let min = Number.MAX_SAFE_INTEGER; let max = 0; for (let i = 0; i < that.data.length; i++) { let item = that.data[i]; sum += item.value; if (min > item.value) { min = item.value; } if (max < item.value) { max = item.value; } } let startRadius = 0; let valLen = max - min; let allHeight = maxHeight - baseHeight; let axis = new THREE.Vector3(1, 0, 0); let group = new THREE.Group(); this.group = group; this.scene.add(group); for (let idx = 0; idx < that.data.length; idx++) { let objGroup = new THREE.Group(); objGroup.name = 'group' + idx; let item = that.data[idx]; //角度范围 let angel = (item.value / sum) * Math.PI * 2; //高度与值的映射 let h = baseHeight + ((item.value - min) / valLen) * allHeight; //每个3D组成块组成:扇形柱体加两片矩形面 if (item.value) { //创建渐变色材质组 let cs = this.colors[idx % this.colors.length]; let geometry = new THREE.CylinderGeometry( radius, radius, h, 24, 24, false, startRadius, //开始角度 angel, //扇形角度占有范围 ); let ms = []; for (let k = 0; k < this.cLen - 1; k++) { ms.push(getBasicMaterial(THREE, cs[k])); } //给不同面的设定对应的材质索引 geometry.faces.forEach((f, fIdx) => { if (f.normal.y == 0) { //上面和底面 geometry.faces[fIdx].materialIndex = 0; } else { //侧面 geometry.faces[fIdx].materialIndex = 1; } }); //扇形圆柱 let mesh = new THREE.Mesh(geometry, ms); mesh.position.y = h * 0.5; mesh.name = 'p' + idx; objGroup.add(mesh); const g = new THREE.PlaneGeometry(radius, h); let m = getBasicMaterial(THREE, cs[this.cLen - 1]); //注意图形开始角度和常用的旋转角度差90度 //封口矩形1 let r1 = startRadius + Math.PI * 0.5; const plane = new THREE.Mesh(g, m); plane.position.y = h * 0.5; plane.position.x = 0; plane.position.z = 0; plane.name = 'c' + idx; plane.rotation.y = r1; plane.translateOnAxis(axis, -radius * 0.5); objGroup.add(plane); //封口矩形2 let r2 = startRadius + angel + Math.PI * 0.5; const plane1 = new THREE.Mesh(g, m); plane1.position.y = h * 0.5; plane1.position.x = 0; plane1.position.z = 0; plane1.name = 'b' + idx; plane1.rotation.y = r2; plane1.translateOnAxis(axis, -radius * 0.5); objGroup.add(plane1); //显示label if (that.isLabel) { let textList = [ { text: item.name, color: fontColor }, { text: item.value + that.suffix, color: fontColor }, ]; const { mesh: textMesh } = getTextArraySprite(THREE, textList, fontSize); textMesh.name = 'f' + idx; //y轴位置 textMesh.position.y = maxHeight + baseHeight; //x,y轴位置 let r = startRadius + angel * 0.5 + Math.PI * 0.5; textMesh.position.x = -Math.cos(r) * radius; textMesh.position.z = Math.sin(r) * radius; if (this.that.isAnimate) { if (idx == 0) { textMesh.visible = true; } else { textMesh.visible = false; } } objGroup.add(textMesh); } group.add(objGroup); } startRadius = angel + startRadius; } //图形居中,视角设置 this.setModelCenter(group, that.viewControl); } animateAction() { if (this.that?.isAnimate && this.group) { this.time++; this.rotateAngle += 0.01; //物体自旋转 this.group.rotation.y = this.rotateAngle; //标签显隐切换 if (this.time > 90) { if (this.currentTextMesh) { this.currentTextMesh.visible = false; } let textMesh = this.scene.getObjectByName('f' + (this.count % this.that.data.length)); textMesh.visible = true; this.currentTextMesh = textMesh; this.count++; this.time = 0; } if (this.rotateAngle > Math.PI * 2) { this.rotateAngle = 0; } } } } let cakeChart = new Cake(); cakeChart.initThree(document.getElementById('cake')); cakeChart.createChart({ //颜色 colors: ['#fcc02a', '#f16b91', '#187bac'], //数据 data: [ { name: '小学', value: 100 }, { name: '中学', value: 200 }, { name: '高中', value: 300 }, // { name: '大学', value: 300 }, ], //是否显示标签 isLabel: false, //最大高度 maxHeight: 20, //基础高度 baseHeight: 10, //半径 radius: 30, //单位后缀 suffix: '', //字体大小 fontSize: 10, //字体颜色 fontColor: 'rgba(255,255,255,1)', //开启动画 isAnimate: false, //视角控制 viewControl: { autoCamera: true, width: 1, height: 1.6, depth: 1, centerX: 1, centerY: 1, centerZ: 1, }, }); </script> </body> </html>
import * as THREE from './node_modules/three/build/three.module.js'; import { OrbitControls } from './node_modules/three/examples/jsm/controls/OrbitControls.js'; import Stats from './node_modules/three/examples/jsm/libs/stats.module.js'; export default class ThreeBase { constructor() { this.isModelRGB = false; this.isStats = false; this.isAxis = false; this.isRaycaster = false; this.initCameraPos = [0, 100, 0]; } initRaycaster() { this.raycaster = new THREE.Raycaster(); this.mouseHover(); this.mouseClick(); } mouseClick() { this.mouse = new THREE.Vector2(); this.container.style.cursor = 'pointer'; this.container.addEventListener('pointerdown', (event) => { console.log('click'); event.preventDefault(); this.mouse.x = ((event.offsetX - this.container.offsetLeft) / this.container.offsetWidth) * 2 - 1; this.mouse.y = -((event.offsetY - this.container.offsetTop) / this.container.offsetHeight) * 2 + 1; let vector = new THREE.Vector3(this.mouse.x, this.mouse.y, 1).unproject(this.camera); this.raycaster.set(this.camera.position, vector.sub(this.camera.position).normalize()); this.raycaster.setFromCamera(this.mouse, this.camera); this.raycasterAction(); }); } mouseHover() { this.mouse1 = new THREE.Vector2(); this.container.addEventListener('pointermove', (event) => { event.preventDefault(); this.mouse1.x = ((event.offsetX - this.container.offsetLeft) / this.container.offsetWidth) * 2 - 1; this.mouse1.y = -((event.offsetY - this.container.offsetTop) / this.container.offsetHeight) * 2 + 1; let vector = new THREE.Vector3(this.mouse1.x, this.mouse1.y, 1).unproject(this.camera); this.raycaster.set(this.camera.position, vector.sub(this.camera.position).normalize()); this.raycaster.setFromCamera(this.mouse1, this.camera); this.mouseHoverAction(); }); } mouseHoverAction() {} raycasterAction() {} createChart(that) {} getCameraControl() { console.log(this.camera.position); console.log(this.controls.target); } initThree(el) { window.ThreeBase = this; this.container = el; THREE.Cache.enabled = true; this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true, logarithmicDepthBuffer: this.isDepthBuffer || false }); this.renderer.setClearColor(0x000000, 0); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight); if (this.isModelRGB) { this.renderer.outputEncoding = THREE.sRGBEncoding; } this.renderer.shadowMap.enable = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.container.appendChild(this.renderer.domElement); this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera( 40, this.container.offsetWidth / this.container.offsetHeight, 1, 100000 ); this.camera.position.set(...this.initCameraPos); if (this.isAxis) { const axesHelper = new THREE.AxesHelper(500); this.scene.add(axesHelper); } if (this.isStats) { this.stats = new Stats(); this.stats.domElement.style.position = 'absolute'; this.stats.domElement.style.top = '0px'; this.container.appendChild(this.stats.domElement); } if (this.isRaycaster) { this.initRaycaster(); } this.controls = new OrbitControls(this.camera, this.renderer.domElement); window.addEventListener('resize', this.onResize.bind(this)); window.addEventListener('unload', this.cleanAll.bind(this)); this.animateRender(); } saveImage() { let image = this.renderer.domElement.toDataURL('image/png'); let parts = image.split(';base64,'); let contentType = parts[0].split(':')[1]; let raw = window.atob(parts[1]); let rawLength = raw.length; let uInt8Array = new Uint8Array(rawLength); for (let i = 0; i < rawLength; i++) { uInt8Array[i] = raw.charCodeAt(i); } const fileName = new Date().getTime() + '.png'; const file = new File([uInt8Array], fileName, { type: contentType }); const link = document.createElement('a'); link.href = window.URL.createObjectURL(file); link.download = fileName; link.target = '_blank'; link.style.display = 'none'; document.body.appendChild(link); link.click(); window.URL.revokeObjectURL(link.href); document.body.removeChild(link); } animateRender() { if (this.isStats && this.stats) { this.stats.update(); } if (this.controls) { this.controls.update(); } this.animateAction(); this.renderer.render(this.scene, this.camera); this.threeAnim = requestAnimationFrame(this.animateRender.bind(this)); } //执行动画动作 animateAction() {} cleanNext(obj, idx) { if (idx < obj.children.length) { this.cleanElmt(obj.children[idx]); } if (idx + 1 < obj.children.length) { this.cleanNext(obj, idx + 1); } } setView(cameraPos, controlPos) { this.camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z); this.controls.target.set(controlPos.x, controlPos.y, controlPos.z); } getView() { console.log('camera', this.camera.position); console.log('controls', this.controls.target); } cleanElmt(obj) { if (obj) { if (obj.children && obj.children.length > 0) { this.cleanNext(obj, 0); obj.remove(...obj.children); } if (obj.geometry) { obj.geometry.dispose && obj.geometry.dispose(); } if (obj.material) { if (Array.isArray(obj.material)) { obj.material.forEach((m) => { this.cleanElmt(m); }); } else { for (const v of Object.values(obj.material)) { if (v instanceof THREE.Texture) { v.dispose && v.dispose(); } } obj.material.dispose && obj.material.dispose(); } } obj.dispose && obj.dispose(); obj.clear && obj.clear(); } } setModelCenter(object, viewControl) { if (!object) { return; } if (object.updateMatrixWorld) { object.updateMatrixWorld(); } // 获得包围盒得min和max const box = new THREE.Box3().setFromObject(object); let objSize = box.getSize(); // 返回包围盒的中心点 const center = box.getCenter(new THREE.Vector3()); object.position.x += object.position.x - center.x; object.position.y += object.position.y - center.y; object.position.z += object.position.z - center.z; let width = objSize.x; let height = objSize.y; let depth = objSize.z; let centroid = new THREE.Vector3().copy(objSize); centroid.multiplyScalar(0.5); if (viewControl.autoCamera) { this.camera.position.x = centroid.x * (viewControl.centerX || 0) + width * (viewControl.width || 0); this.camera.position.y = centroid.y * (viewControl.centerY || 0) + height * (viewControl.height || 0); this.camera.position.z = centroid.z * (viewControl.centerZ || 0) + depth * (viewControl.depth || 0); } else { this.camera.position.set( viewControl.cameraPosX || 0, viewControl.cameraPosY || 0, viewControl.cameraPosZ || 0 ); } this.camera.lookAt(0, 0, 0); } cleanObj(obj) { this.cleanElmt(obj); obj?.parent?.remove && obj.parent.remove(obj); } cleanAll() { cancelAnimationFrame(this.threeAnim); window.removeEventListener('resize', this.onResize.bind(this)); if (this.stats) { this.container.removeChild(this.stats.domElement); this.stats = null; } this.cleanObj(this.scene); this.controls && this.controls.dispose(); this.renderer.renderLists && this.renderer.renderLists.dispose(); this.renderer.dispose && this.renderer.dispose(); this.renderer.forceContextLoss(); let gl = this.renderer.domElement.getContext('webgl'); gl && gl.getExtension('WEBGL_lose_context').loseContext(); this.renderer.setAnimationLoop(null); this.renderer.domElement = null; this.renderer.content = null; console.log('清空资源', this.renderer.info); this.renderer = null; THREE.Cache.clear(); } setModelCenter(object, viewControl) { if (!object) { return; } if (object.updateMatrixWorld) { object.updateMatrixWorld(); } // 获得包围盒得min和max const box = new THREE.Box3().setFromObject(object); let objSize = box.getSize(); // 返回包围盒的中心点 const center = box.getCenter(new THREE.Vector3()); object.position.x += object.position.x - center.x; object.position.y += object.position.y - center.y; object.position.z += object.position.z - center.z; let width = objSize.x; let height = objSize.y; let depth = objSize.z; let centroid = new THREE.Vector3().copy(objSize); centroid.multiplyScalar(0.5); if (viewControl.autoCamera) { this.camera.position.x = centroid.x * (viewControl.centerX || 0) + width * (viewControl.width || 0); this.camera.position.y = centroid.y * (viewControl.centerY || 0) + height * (viewControl.height || 0); this.camera.position.z = centroid.z * (viewControl.centerZ || 0) + depth * (viewControl.depth || 0); } else { this.camera.position.set( viewControl.cameraPosX || 0, viewControl.cameraPosY || 0, viewControl.cameraPosZ || 0 ); } this.camera.lookAt(0, 0, 0); } onResize() { if (this.container) { this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight); } } }