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);
    }
  }
}

 

posted @ 2024-05-20 09:29  wangmeihao  阅读(6)  评论(0编辑  收藏  举报