使用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; } } } }