首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

使用threejs实现3D卡片菜单

编程知识
2024年07月25日 16:43

 

成品效果:

 

 

 

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

template

<template>
  <div></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><div data-url="${item.appHomePage}"><div></div><div>${item.name}</div><div>点击进入<i></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;
      }
    }
  }
}

 

From:https://www.cnblogs.com/luobiao/p/18323802
本文地址: http://shuzixingkong.net/article/430
0评论
提交 加载更多评论
其他文章 面试官:聊聊你对分库分表的理解?
在 MySQL 集群架构中有两种主流的集群实现,一种是读写分离,而另外一种则是数据分片。所谓的数据分片其实就是今天要聊的分库分表技术。 分库分表技术不但是日常工作中用于解决数据库中的数据量会急剧增长,解决单库单表性能瓶颈的一种方案,更是面试中的高频知识点。 在阿里巴巴的《Java 开发手册》中规定:
面试官:聊聊你对分库分表的理解? 面试官:聊聊你对分库分表的理解? 面试官:聊聊你对分库分表的理解?
用.Net实现GraphRag:从零开始构建智能知识图谱
近来,大模型技术日新月异,使得与其相关的研发项目也层出不穷。其中一个备受关注的技术便是RAG(Retrieval Augmented Generation)。今天,我要跟大家分享一个出色的项目:GraphRag。出于对该技术的浓厚兴趣,我决定利用.Net框架自己实现一个GraphRag.Net,并将
用.Net实现GraphRag:从零开始构建智能知识图谱 用.Net实现GraphRag:从零开始构建智能知识图谱 用.Net实现GraphRag:从零开始构建智能知识图谱
ThinkPHP一对一关联模型的运用(ORM)
一、序言 最近在写ThinkPHP关联模型的时候一些用法总忘,我就想通过写博客的方式复习和整理下一些用法。 具体版本: topthink/framework:6.1.4 topthink/think-orm:2.0.61 二、实例应用 1、一对一关联 1.1、我先设计了两张表,分别为用户表(user
ThinkPHP一对一关联模型的运用(ORM) ThinkPHP一对一关联模型的运用(ORM) ThinkPHP一对一关联模型的运用(ORM)
RIME:用交叉熵 loss 大小分辨 preference 是否正确 + 内在奖励预训练 reward model
① 假设正确样本的 CELoss 上限是 ρ,可推出错误样本相对 P_ψ(x) 分布的 KL 散度上限,从而筛出可信样本、翻转不可信样本;② 用归一化到 (-1,1) 的 intrinsic reward 预训练 reward model。
iOS开发基础142-广告归因
IDFA IDFA是苹果为iOS设备提供的一个唯一标识符,专门用于广告跟踪和相关的营销用途。与之对应的,在Android平台的是谷歌广告ID(Google Advertising ID)。 IDFA的工作原理: IDFA是分配给每个设备的唯一标识符,广告商和开发者可以利用IDFA跟踪用户对广告的点击
从DDPM到DDIM(三) DDPM的训练与推理
从DDPM到DDIM(三) DDPM的训练与推理 前情回顾 首先还是回顾一下之前讨论的成果。 扩散模型的结构和各个概率模型的意义。下图展示了DDPM的双向马尔可夫模型。 其中\(\mathbf{x}_T\)代表纯高斯噪声,\(\mathbf{x}_t, 0 &lt; t &lt; T\) 代表中间的
从DDPM到DDIM(三) DDPM的训练与推理 从DDPM到DDIM(三) DDPM的训练与推理 从DDPM到DDIM(三) DDPM的训练与推理
巧用 QLineF 从 QTransform 提取角度
我们在对 QGraphicsItem 进行变换时,QT 提供了很多便捷的方法。但当我们想获取当前变换的角度时却有些困难,因为 QTransform 没有提供获取角度的方法。在文章Qt 从 QTransform 逆向解出 Translate/Scale/Rotate(平移/缩放/旋转)分析 分析过,使
Llama 3.1 - 405B、70B 和 8B 的多语言与长上下文能力解析
Llama 3.1 发布了!今天我们迎来了 Llama 家族的新成员 Llama 3.1 进入 Hugging Face 平台。我们很高兴与 Meta 合作,确保在 Hugging Face 生态系统中实现最佳集成。Hub 上现有八个开源权重模型 (3 个基础模型和 5 个微调模型)。 Llama