使用threejs实现3D卡片菜单

成品效果:

用到的技术:vue2、three.js、gsap.js

template

复制代码
<template>
  <div id="box" class="container"></div>
</template>

script

复制代码
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { CSS3DObject, CSS3DRenderer } from "three/examples/jsm/renderers/CSS3DRenderer.js";
import gsap from "gsap";
const httpMatcher = /http|https/;
export default {
  name: "3DMenu",
  components: {},
  data() {
    return {
      app: null,
      el: null,
      mesh: null,
      camera: null,
      scene: null,
      renderer: null,
      labelRenderer: null,
      controls: null,
      menuData: [
        {
          id: "1",
          parentId: "1537645492375449602",
          name: "用户中心",
          description: null,
          appKey: "xjt_user",
          appHomePage: "/auth-ui/",
        },
        {
          id: "1534774879700992002",
          parentId: "1537645492375449602",
          name: "人资系统",
          description: null,
          appKey: "xjt_hr",
          appHomePage: "/hr-ui/",
        },
        {
          id: "1536947570488430593",
          parentId: "1537645492375449602",
          name: "合同系统",
          description: null,
          appKey: "xjt_contract",
          appHomePage: "/contract-ui/",
        },
        {
          id: "1537733169730351105",
          parentId: "1537645492375449602",
          name: "OA系统",
          description: null,
          appKey: "xjt_oa",
          appHomePage: "/oa-ui/",
        },
        {
          id: "1551507637786374145",
          parentId: "1537645492375449602",
          name: "费报系统",
          description: null,
          appKey: "xjt_fb",
          appHomePage: "/feibao-ui/",
        },
        {
          id: "1613789365929680897",
          parentId: "1537645492375449602",
          name: "考试系统",
          description: null,
          appKey: "xjt_exam",
          appHomePage: "/exam-ui/",
        },
        {
          id: "1615265465629380610",
          parentId: "1537645492375449602",
          name: "培训系统",
          description: null,
          appKey: "xjt_px",
          appHomePage: "/px-ui/",
        },
        {
          id: "1669546339670454274",
          parentId: "1537645492375449602",
          name: "会议系统",
          description: null,
          appKey: "xjt_cloud_meeting",
          appHomePage: "/cloud-meeting-ui/",
        },
        {
          id: "1674596267673264130",
          parentId: "1537645492375449602",
          name: "资产系统",
          description: null,
          appKey: "xjt_property",
          appHomePage: "/property-ui/",
        },
      ],
      radius: 400,
      objects: [],
      spheres: [], //用来存放目标对象的位置
      isAnimationPaused: false,
    };
  },
  mounted() {
    this.initZThree();
    window.addEventListener("resize", this.handleResize);
  },
  beforeDestroy() {
    window.removeEventListener("resize", this.handleResize);
    this.destroyThree();
  },
  methods: {
    initZThree() {
      this.el = document.getElementById("box");
      const { offsetWidth, offsetHeight } = this.el;
      this.initScene();
      this.initCamera(offsetWidth, offsetHeight);
      this.initRenderer(offsetWidth, offsetHeight);
      this.initControl();
      this.initMenu();
    },
    initScene() {
      // 渲染场景
      this.scene = new THREE.Scene();
    },
    initCamera(offsetWidth, offsetHeight) {
      // 创建相机
      this.camera = new THREE.PerspectiveCamera(
        50,
        offsetWidth / offsetHeight,
        1,
        20000
      );
      this.camera.position.set(-1265, 798, -105); // 设置相机位置
      this.camera.lookAt(0, 0, 0); // 设置相机看先中心点
    },
    initRenderer(offsetWidth, offsetHeight) {
      // 创建渲染器
      this.renderer = new THREE.WebGLRenderer({
        antialias: true, // true/false表示是否开启反锯齿
        alpha: true, // true/false 表示是否可以设置背景色透明
      });
      this.renderer.setSize(offsetWidth, offsetHeight); // 设置渲染区域宽高
      this.renderer.shadowMap.enabled = true; // 允许渲染器产生阴影贴图
      this.renderer.setPixelRatio(window.devicePixelRatio);
      this.renderer.setClearColor(0x01dcc9, 0); // 设置背景颜色
      this.el.append(this.renderer.domElement);

      // 网页标签
      this.labelRenderer = new CSS3DRenderer();
      this.labelRenderer.domElement.style.zIndex = 2;
      this.labelRenderer.domElement.style.position = "absolute";
      this.labelRenderer.domElement.style.top = "0px";
      this.labelRenderer.domElement.style.left = "0px";
      this.labelRenderer.domElement.style.pointerEvents = "none"; // 避免HTML标签遮挡三维场景的鼠标事件
      this.labelRenderer.setSize(offsetWidth, offsetHeight);
      this.labelRenderer.domElement.addEventListener("mousemove", this.handleMousemove);
      this.labelRenderer.domElement.addEventListener("mouseout",  this.handleMouseout);
      this.el.appendChild(this.labelRenderer.domElement);
    },
    initControl() {
      // 初始化控制器
      let controls = new OrbitControls(this.camera, this.renderer.domElement);
      // controls.autoRotate = true; //为true时,相机自动围绕目标旋转,但必须在animation循环中调用update()
      controls.enableDamping = true; // 设置带阻尼的惯性
      controls.dampingFactor = 0.05; // 设置阻尼的系数
      // 避免鼠标滚轮放大缩小
      controls.minDistance = 1500;
      controls.maxDistance = 1500;
      this.controls = controls;
      this.controls.update();
    },
    initMenu() {
      this.objects = [];
      this.spheres = [];
      this.menuData.forEach((item, index) => {
        const cardLabel = this.addCss3dLabel(item, index + 1);
        cardLabel.element.addEventListener("click", this.handleClick);
        this.objects.push(cardLabel);
        this.scene.add(cardLabel);
      });
      const vector = new THREE.Vector3(20, 20, 20);
      for (let i = 0, l = this.objects.length; i < l; i++) {
        const phi = (i / l) * 2 * Math.PI; // 分配每个对象在圆上的角度
        const object = new THREE.Object3D();
        object.position.x = this.radius * Math.cos(phi);
        object.position.y = 0;
        object.position.z = this.radius * Math.sin(phi);
        // 设置对象朝向圆心
        vector.x = object.position.x;
        vector.y = object.position.y;
        vector.z = object.position.z;
        object.lookAt(vector);
        this.spheres.push(object);
      }
      this.transform();
      this.renderFun(); // 渲染
    },
    addCss3dLabel(item = {}, index) {
      const element = document.createElement("div");
      element.className = `sys-item-li sys-item-${index}`;
      element.innerHTML = `<div class="sys-item"><div class="sys-content" data-url="${item.appHomePage}"><div class="sys-bg ${item.appKey}"></div><div class="sys-name">${item.name}</div><div class="sys-btn">点击进入<i class="el-icon-arrow-right"></i></div></div></div>`;
      let textLabel = new CSS3DObject(element);
      textLabel.name = item.name;
      textLabel.userData = item;
      const position = Math.random() * this.radius + this.radius;
      textLabel.position.set(position, position, position);
      return textLabel;
    },
    renderFun() {
      this.objects.forEach((object) => {
        object.lookAt(this.camera.position);
      });
      if (!this.isAnimationPaused) {
        this.scene.rotation.y -= 0.005; // 旋转速度
      }
      this.renderer.render(this.scene, this.camera);
      this.labelRenderer.render(this.scene, this.camera);
      requestAnimationFrame(this.renderFun);
    },
    transform(duration = 2) {
      for (var i = 0; i < this.objects.length; i++) {
        let object = this.objects[i];
        let target = this.spheres[i];
        gsap.to(object.position, {
          x: target.position.x,
          y: target.position.y,
          z: target.position.z,
          duration: Math.random() * duration + duration,
          ease: "Linear.inOut",
        });
        gsap.to(object.rotation, {
          x: target.rotation.x,
          y: target.rotation.y,
          z: target.rotation.z,
          duration: Math.random() * duration + duration,
          ease: "Linear.inOut",
        });
      }
    },
    handleResize() {
      this.camera.aspect = this.el.offsetWidth / this.el.offsetHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(this.el.offsetWidth, this.el.offsetHeight);
      this.labelRenderer.setSize(this.el.offsetWidth, this.el.offsetHeight);
    },
    handleMousemove() {
      this.isAnimationPaused = true; // 暂停动画
    },
    handleMouseout() {
      this.isAnimationPaused = false; // 恢复动画
    },
    handleClick(e) {
      const { url } = e.target.dataset; 
      console.log("url", url);
       if (httpMatcher.test(url)) {
          window.location.href = url;
        } else {
          window.location.href = `${window.location.origin}${url}`;
        }
    },
    destroyThree() {
      this.scene.traverse((child) => {
        if (child.material) {
          child.material.dispose();
        }
        if (child.geometry) {
          child.geometry.dispose();
        }
        child = null;
      });
      this.renderer.forceContextLoss();
      this.renderer.dispose();
      this.scene.clear();
    },
  },
};

css

复制代码
.container {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: #f2f6fe;
  background-image: url(~@/assets/images/subsystem/switch-system-bg.jpg);
  background-size: cover;
  background-repeat: no-repeat;
  ::v-deep.sys-item {
    opacity: 1;
    width: 24vh;
    height: 24vh;
    text-align: center;
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.3);
    border-radius: 16px;
    overflow: hidden;
    color: #3768f5;
    transform: rotate(45deg);
    cursor: pointer;
    &::before {
      content: "";
      position: absolute;
      width: 100%;
      height: 100%;
      margin-left: -50%;
      z-index: 1;
      box-sizing: border-box;
      border-radius: 16px;
      border: 2px solid rgba(255, 255, 255, 0.5);
      background: linear-gradient(90deg, #f2efff 0%, #fff 100%);
      transition: all 0.25s ease;
    }
    &:hover {
      transform: rotate(45deg) scale(1.07);
      box-shadow: 0 2px 24px 16px rgba(0, 142, 255, 0.08);
      background: linear-gradient(135deg, #fff 0%, #cbe8ff 100%);
      &::before {
        opacity: 1;
        border: 2px solid #4a93ff;
        box-shadow: 0 2px 24px 12px rgba(0, 142, 255, 0.08);
        background: linear-gradient(135deg, #fff 0%, #cbe8ff 100%);
      }
      .sys-btn {
        color: #fff;
        background: rgba(55, 102, 245, 0.8);
      }
    }
    .sys-content {
      position: relative;
      width: 100%;
      height: 100%;
      transform: rotate(-45deg);
      z-index: 9;
    }
    .sys-bg {
      width: 55%;
      height: 55%;
      margin: auto;
      pointer-events: none;
      background-size: cover;
      background-repeat: no-repeat;
      background-image: url(~@/assets/images/subsystem/xjt_contract.png);
      &.xjt_user {
        background-image: url(~@/assets/images/subsystem/xjt_user.png);
      }
      &.xjt_hr {
        background-image: url(~@/assets/images/subsystem/xjt_hr.png);
      }
      &.xjt_fb,
      &.expense {
        background-image: url(~@/assets/images/subsystem/xjt_fb.png);
      }
      &.xjt_budget {
        background-image: url(~@/assets/images/subsystem/xjt_budget.png);
      }
      &.xjt_px {
        background-image: url(~@/assets/images/subsystem/xjt_px.png);
      }
      &.xjt_contract {
        background-image: url(~@/assets/images/subsystem/xjt_contract.png);
      }
      &.xjt_oa {
        background-image: url(~@/assets/images/subsystem/xjt_oa.png);
      }
      &.xjt_exam {
        background-image: url(~@/assets/images/subsystem/xjt_exam.png);
      }
      &.xjt_cloud_meeting {
        background-image: url(~@/assets/images/subsystem/xjt_cloud_meeting.png);
      }
    }
    .sys-name {
      font-size: 2.7vh;
      font-weight: 600;
      pointer-events: none;
    }
    .sys-btn {
      display: inline-block;
      height: 4vh;
      padding: 0 1.2vh;
      margin-top: 1vh;
      line-height: 4vh;
      font-size: 1.8vh;
      font-weight: 500;
      border-radius: 2vh;
      transition: all 0.2s ease;
      cursor: pointer;
      pointer-events: none;
      .el-icon-arrow-right {
        vertical-align: middle;
        margin-top: -1px;
      }
    }
  }
}
相关推荐
gis分享者2 天前
学习threejs,使用MeshBasicMaterial基本网格材质
threejs·basicmaterial·基本网格材质
gis分享者6 天前
学习threejs,使用PointLight点光源
threejs·点光源·pointlight
gis分享者10 天前
学习threejs,使用HemisphereLight半球光
threejs·hemispherelight·半球光
gis分享者15 天前
学习threejs,使用Lensflare模拟镜头眩光
threejs·lensflare·眩光
gis分享者17 天前
学习threejs,tga格式图片文件贴图
threejs·贴图·tga·tgaloader
gis分享者18 天前
学习threejs,pvr格式图片文件贴图
threejs·贴图·pvr
gis分享者1 个月前
学习threejs,使用OrbitControls相机控制器
threejs·相机·相机控制器·orbitcontrols
不系舟1781 个月前
threejs 实现镜面反射,只反射指定物体,背景透明
threejs
gis分享者1 个月前
学习threejs,使用RollControls相机控制器
threejs·相机控制器·rollcontrols
gis分享者1 个月前
学习threejs,使用FlyControls相机控制器
threejs·相机控制器·flycontrols