案例效果截圖:
具體場景和功能,詳見b站影片:
https://www.bilibili.com/video/BV1Kb421q7c4/?vd_source=7d4ec9c9275b9c7d16afe9b4625f636c
案例邏輯程式碼:
<template> <div id="chinaMap"> <div id="threejs" ref="threejs"></div> <!-- 右側按鈕 --> <div class="rightButton" ref="rightButton"> <div v-for="(item, index) in rightButItem" :key="index" :value="item.value" :class="item.selected ? 'selected common' : 'common'" @click="rightButClick"> {{ item.name }} </div> </div> <!-- 地圖名稱元素 --> <div id="provinceName" style="display: none"></div> <!-- 光柱上方數值元素 --> <div id="cylinderValue" style="display: none"></div> <!-- 地圖示牌元素 --> <div id="mapTag" style="display: none"> <div class="content"> <div>旅客:</div> <div id="mapTag_value">1024萬</div> </div> <div class="arrow"></div> </div> <!-- 彈框元素 --> <div id="popup" style="display: none"> <div class="popup_line"></div> <div class="popup_Main"> <div class="popupMain_top"></div> <div class="popup_content"> <div class="popup_head"> <div class="popup_title"> <div class="title_icon"></div> <div id="popup_Name">湖北省</div> </div> <div class="close" @click="popupClose"></div> </div> <div class="popup_item"> <div>當前流入:</div> <div class="item_value">388萬人次</div> </div> <div class="popup_item"> <div>景區容量:</div> <div class="item_value">2688萬人次</div> </div> <div class="popup_item"> <div>交通資源利用率:</div> <div class="item_value">88.7%</div> </div> <div class="popup_item"> <div>省市熱搜指數:</div> <div class="item_value">88.7%</div> </div> </div> <div class="popupMain_footer"></div> </div> </div> </div> </template> <script setup> import { regionCode } from '@/assets/regionCode.js'; import { onMounted, reactive, computed, ref } from 'vue'; import * as THREE from 'three'; // 引入TWEENJS import TWEEN from '@tweenjs/tween.js'; import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'; import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js'; // threejs基礎配置,場景相機渲染器等 import { scene, camera, controls, renderer, css3DRenderer, css2DRenderer, outlinePass, composer, finalComposer } from './baseConfig/index.js'; // 載入地圖 import { initMap, nationMapModel, mapUf, projection, topMaterial, sideMaterial } from './initChinaMap/index.js'; // 地圖底部網格背景 import { gridHelper, meshPoint } from './backgroundMesh/index.js'; // 初始化滑鼠移入地圖浮動效果 import { initMapFloat } from './mapFloat/index.js'; // 外圈、內圈、擴散波紋、漸變平面 import { circleUf, outerCircle, innerCircle, diffuseCircle, gradientPlane } from './backgroundCircle/index.js'; // 光柱發光陣列、光圈動畫、建立光柱函式 import { cylinderGlowArr, apertureAnimation, createCylinder } from './cylinder/index.js'; // 建立地圖示牌函式 import { createMapTag } from './mapTag/index.js'; import { particlesUpdate, createParticles, particles } from './particles/index.js'; // 首次進入動畫 import { eventAnimation } from './eventAnimation/index.js'; // 右側按鈕選項 const rightButItem = reactive([ { value: 'cylinder', name: '光柱', selected: false }, { value: 'tag', name: '標牌', selected: false }, ]); // 描邊模型 let outlineModel = null; // 時鐘物件,用於獲取兩幀渲染之間的時間值 const clock = new THREE.Clock(); // 射線拾取中模型物件 let rayObj = null; // 彈框div元素 let popupDiv = null; // css2D彈框物件 let css2Dpopup = null; // 需要輝光的模型陣列 let glowArr = []; // 右側按鈕元素 const rightButton = ref(); // 地圖狀態,分為國省市三種,決定了點選事件等操作效果 const mapStatus = ref('國'); // 省份地圖模型 const provinceMapModel = new THREE.Group(); provinceMapModel.name = '省'; provinceMapModel.rotateX(-Math.PI / 2); // 城市級地圖模型 const cityMapModel = new THREE.Group(); cityMapModel.name = '市'; cityMapModel.rotateX(-Math.PI / 2); // 當前顯示模型 const currentShowModel = computed(() => { return mapStatus.value === '國' ? nationMapModel : mapStatus.value === '省' ? provinceMapModel : cityMapModel; }); // threejs容器元素 const threejs = ref(); onMounted(async () => { threejs.value.appendChild(renderer.domElement); threejs.value.appendChild(css3DRenderer.domElement); threejs.value.appendChild(css2DRenderer.domElement); // 初始化彈框 initCSS2DPopup(); // // 建立省份名稱物件 // createProvinceName(); // 建立粒子 createParticles(); // 載入中國地圖 await initMap(); // 初始化滑鼠移入地圖浮動效果 initMapFloat(camera, currentShowModel); // 初始化點選事件 initClickEvent(); scene.add(nationMapModel, gridHelper, meshPoint, outerCircle, innerCircle, diffuseCircle, gradientPlane, provinceMapModel, cityMapModel, particles); // 設定需要輝光物體 glowArr = [...cylinderGlowArr]; // 開始迴圈渲染 render(); // 需要展示全國則註釋掉regionSetMap函式呼叫 // regionSetMap('湖北省') // 首次進入動畫 eventAnimation(camera, controls); }); // 迴圈渲染 function render() { requestAnimationFrame(render); camera.updateProjectionMatrix(); controls.update(); // 兩幀渲染間隔 let deltaTime = clock.getDelta(); // 地圖模型側邊漸變效果 mapUf.uTime.value += deltaTime; if (mapUf.uTime.value >= 5) { mapUf.uTime.value = 0.0; } if (rightButItem[0].selected) apertureAnimation(); // 光圈縮放動畫 particlesUpdate(); // 粒子動畫 // 背景外圈內圈旋轉 outerCircle.rotation.z -= 0.003; innerCircle.rotation.z += 0.003; // 波紋擴散動畫 circleUf.uTime.value += deltaTime; if (circleUf.uTime.value >= 6) { circleUf.uTime.value = 0.0; } // TWEEN更新 TWEEN.update(); // 將場景內的物體材質設定為黑色 scene.traverse(darkenMaterial); // 渲染輝光 composer.render(); // 還原材質 scene.traverse(restoreMaterial); // 最終渲染 finalComposer.render(); css3DRenderer.render(scene, camera); css2DRenderer.render(scene, camera); } // 右側按鈕點選事件 function rightButClick(e, item) { // 當前按鈕點選項 let clickItem; // 程式碼中觸發的點選項 if (item) { clickItem = item; } // 使用者手動點選觸發的點選項 else { const value = e.target.getAttribute('value'); clickItem = rightButItem.filter((obj) => obj.value === value)[0]; clickItem.selected = !clickItem.selected; } if (clickItem.selected) { currentShowModel.value.traverse((item) => { if (item.name === clickItem.name) { item.traverse((item) => (item.visible = true)); } }); } else { currentShowModel.value.traverse((item) => { if (item.name === clickItem.name) { item.traverse((item) => (item.visible = false)); } }); } } // 彈框關閉事件 function popupClose() { // outlineModel存在 if (outlineModel) { outlinePass.selectedObjects = []; rayObj.remove(outlineModel); // 給彈框清除建立漸變動畫 new TWEEN.Tween({ opacity: 1 }) .to({ opacity: 0 }, 500) .onUpdate(function (obj) { //動態更新div元素透明度 popupDiv.style.opacity = obj.opacity; }) .onComplete(function () { // 清除彈框 rayObj.remove(css2Dpopup); }) .start(); } } // 將材質設定成黑色 function darkenMaterial(obj) { if (obj.visible) { if (obj instanceof THREE.Scene) { obj.background = null; } const material = obj.material; if (material && !glowArr.includes(obj) && !material.isShaderMaterial) { obj.originalMaterial = obj.material; const Proto = Object.getPrototypeOf(material).constructor; obj.material = new Proto({ color: new THREE.Color('#000') }); } } } // 還原材質 function restoreMaterial(obj) { if (obj.visible) { if (!obj.originalMaterial) return; obj.material = obj.originalMaterial; delete obj.originalMaterial; } } // 初始化CSS2D彈框 function initCSS2DPopup() { popupDiv = document.getElementById('popup'); const widthScale = window.innerWidth / 1920; const heightScale = window.innerHeight / 941; popupDiv.style.top += (37 * heightScale).toFixed(2) + 'px'; popupDiv.style.left += (390 * widthScale).toFixed(2) + 'px'; // 轉換為CSS2D物件 css2Dpopup = new CSS2DObject(popupDiv); css2Dpopup.name = '彈框'; // 設定一個較高的渲染順序,防止彈框被標牌等物體遮擋住 css2Dpopup.renderOrder = 99; } // 初始化點選事件 function initClickEvent() { // 左鍵點選定時器 let leftClickTimer = null; // 右鍵點選定時器 let rightClickTimer = null; // 左鍵跟蹤連續點選的次數 let leftClickCount = 0; // 右鍵跟蹤連續點選的次數 let rightClickCount = 0; // 延遲時間 const delay = 300; // 新增左鍵點選事件,雙擊進入下一層地圖(國=>省=>市) addEventListener('click', function (event) { // 每次點選時增加點選次數 leftClickCount++; // 如果已經有定時器在執行,重置定時器 if (leftClickTimer) { clearTimeout(leftClickTimer); } // 設定定時器,如果使用者停止點選,將根據點選次數決定觸發何種事件 leftClickTimer = setTimeout(() => { // 點選次數超過2次以上都視為雙擊 if (leftClickCount >= 2) { // 當為市級地圖時沒有下一層了,跳出不執行後續的程式碼 if (mapStatus.value === '市') return; // 射線檢測 const testResult = rayTest(event, currentShowModel.value); // 檢測到模型時 if (testResult.length) { // 雙擊處理 doubleClickHandle(testResult, 'left'); } } // 點選次數為1則為單擊事件 else { // 射線檢測 const testResult = rayTest(event, currentShowModel.value); // 檢測到模型在進行下一步處理 if (testResult.length) oneClickHandle(testResult); // 未檢測到則觸發彈框關閉事件 // else popupClose(); } // 重置計數器和定時器 leftClickTimer = null; leftClickCount = 0; }, delay); }); // 新增右鍵點選事件,雙擊返回上一層地圖(市=>省=>國) addEventListener('contextmenu', function (event) { // 每次點選時增加點選次數 rightClickCount++; // 如果已經有定時器在執行,重置定時器 if (rightClickTimer) { clearTimeout(rightClickTimer); } // 設定定時器,如果使用者停止點選,將根據點選次數決定觸發何種事件 rightClickTimer = setTimeout(() => { // 點選次數超過2次以上都視為雙擊 if (rightClickCount >= 2) { // 當為國級地圖時沒有上一層了,跳出不執行後續的程式碼 if (mapStatus.value === '國') return; // 雙擊處理 doubleClickHandle(null, 'right'); } // 重置計數器和定時器 rightClickTimer = null; rightClickCount = 0; }, delay); }); } // 射線檢測 function rayTest(event, model) { const px = event.offsetX; const py = event.offsetY; // 螢幕座標轉為標準裝置座標 const x = (px / window.innerWidth) * 2 - 1; const y = -(py / window.innerHeight) * 2 + 1; // 建立射線 const raycaster = new THREE.Raycaster(); // 設定射線引數 raycaster.setFromCamera(new THREE.Vector2(x, y), camera); // 射線交叉計算拾取模型 let intersects = raycaster.intersectObjects(model.children); // 檢測結果過濾 intersects = intersects.filter(function (intersect) { return intersect.object.name !== '邊線' && intersect.object.name !== '地圖名稱' && intersect.object.name !== '光圈' && intersect.object.name !== '光柱' && intersect.object.name !== '標牌'; }); return intersects; } // 地圖狀態切換 function mapStatusSwitch(e) { // 地圖狀態陣列 const mapStatusArr = ['國', '省', '市']; // 當前地圖狀態在mapStatusArr中的下標位置 let index = mapStatusArr.indexOf(mapStatus.value); // left表示滑鼠左鍵雙擊觸發,地圖狀態進入下一層,國=>省=>市 if (e === 'left') { index++; } // right表示滑鼠右鍵鍵雙擊觸發,地圖狀態返回上一層,市=>省=>國 else if (e === 'right') { index--; } // 重新定義地圖狀態 if (mapStatusArr[index]) mapStatus.value = mapStatusArr[index]; } // 單擊處理 function oneClickHandle(rel) { // 清除上一次新增的描邊模型 if (rayObj) { rayObj.remove(outlineModel); } // 射線選中模型 rayObj = rel[0].object.parent; // 新增彈框到選中模型rayObj中去 rayObj.add(css2Dpopup); // 獲取選中模型的位置 const center = rayObj.userData.center; // 設定彈框位置 css2Dpopup.visible = true; css2Dpopup.position.set(center[0], center[1], 0); // 彈框名稱元素 const popupNameDiv = css2Dpopup.element.querySelector('#popup_Name'); // 更換彈框名稱 popupNameDiv.innerHTML = rayObj.name; // 給彈框顯示建立漸變動畫 new TWEEN.Tween({ opacity: 0 }) .to({ opacity: 1.0 }, 500) .onUpdate(function (obj) { //動態更新div元素透明度 popupDiv.style.opacity = obj.opacity; }) .start(); // 地圖邊線資料 const mapLineData = rayObj.userData.mapData; // 建立shape物件 const shape = new THREE.Shape(); // 當資料為多個多邊形時 if (mapLineData.type === 'MultiPolygon') { // 遍歷資料,繪製shape物件資料 mapLineData.coordinates.forEach((coordinate, index) => { if (index === 0) { coordinate.forEach((rows) => { rows.forEach((row) => { const [x, y] = projection(row); if (index === 0) { shape.moveTo(x, y); } shape.lineTo(x, y); }); }); } }); } // 當資料為單個多邊形時 if (mapLineData.type === 'Polygon') { mapLineData.coordinates.forEach((coordinate) => { // 遍歷資料,繪製shape物件資料 mapLineData.coordinates.forEach((rows, index) => { if (index === 0) { rows.forEach((row) => { const [x, y] = projection(row); if (index === 0) { shape.moveTo(x, y); } shape.lineTo(x, y); }); } }); }); } // 建立形狀幾何體,shape物件作為引數 const geometry = new THREE.ShapeGeometry(shape); const material = new THREE.MeshBasicMaterial({ color: rayObj.children[0].material[0].color, map: rayObj.children[0].material[0].map, side: THREE.DoubleSide, }); let mesh = new THREE.Mesh(geometry, material); mesh.rotateX(-Math.PI); mesh.name = '描邊模型'; outlineModel = mesh; rayObj.add(outlineModel); // 設定描邊模型進行發光 outlinePass.selectedObjects = [outlineModel]; glowArr.pop(); glowArr.push(outlineModel); } function codeSetMapStatus(code) { const cityCode = code.substring(2, 4); const districtCode = code.substring(4, 6); if (code === "100000") { mapStatus.value = '國'; } else if (cityCode === "00" && districtCode === "00") { mapStatus.value = '省'; } else if (districtCode === "00") { mapStatus.value = '市'; } } function regionSetMap(regionName) { // 隱藏地圖狀態切換之前的模型 currentShowModel.value.traverse((item) => { item.visible = false; }); const code = regionCode[regionName]; codeSetMapStatus(code); let url = `http://211.143.122.110:18062/mapdata/geojson/areas_v3_full/all/${code}.json`; fetch(url) .then((response) => { if (!response.ok) { // 如果響應狀態碼不是2xx,丟擲錯誤 throw new Error('Network response was not ok: ' + response.statusText); } // 響應成功,返回解析的JSON資料 return response.json(); }) .then(async (data) => { await new Promise((resolve) => { // 處理地圖資料,繪製模型 handleMapData(data, 1, resolve); // 全國模型的包圍盒 const nationModelBox = new THREE.Box3().setFromObject(nationMapModel); // 當前射線選中模型的包圍盒 let currentModelbox; // 市級地圖切換到市級地圖時,市級地圖是經過了一次縮放的,需要還原縮放比例進行計算 if (mapStatus.value === '市') { const cloneModel = currentShowModel.value.clone(); cloneModel.scale.set(1, 1, 1); currentModelbox = new THREE.Box3().setFromObject(cloneModel); } else { currentModelbox = new THREE.Box3().setFromObject(currentShowModel.value); } // 計算寬度和高度 const widthA = nationModelBox.max.x - nationModelBox.min.x; const heightA = nationModelBox.max.z - nationModelBox.min.z; const widthB = currentModelbox.max.x - currentModelbox.min.x; const heightB = currentModelbox.max.z - currentModelbox.min.z; // 計算寬度和高度的比例 const widthRatio = widthA / widthB; const heightRatio = heightA / heightB; // 當前模型與全國模型大小的縮放值 const scale = (widthRatio + heightRatio) / 2; // 應用縮放值到切換後的模型上去 currentShowModel.value.scale.set(scale, scale, 1); currentShowModel.value.traverse(item => { if (item.name.includes('名稱物件')) { item.scale.set(0.2 / scale, 0.2 / scale, 0.2 / scale); } if (item.name == '光柱') { item.scale.set(1 / scale, 1, 1 / scale) } }) // 這時候計算省份模型,得出放大後的省份模型的中心點,並將其位置歸於原點 const scaledBox = new THREE.Box3().setFromObject(currentShowModel.value); const center = new THREE.Vector3(); // 獲取放大後模型的中心點 scaledBox.getCenter(center); // 將模型的位置調整,使縮放後的中心位於原點 currentShowModel.value.position.sub(center); // 高度不增加 currentShowModel.value.position.y += center.y; // 顯示地圖狀態切換之後的模型 currentShowModel.value.traverse((item) => { item.visible = true; }); glowArr = [...cylinderGlowArr]; // 觸發右側按鈕點選事件 rightButItem.map((item) => { rightButClick(null, item); }); }); }); } // 雙擊處理 function doubleClickHandle(rel, type) { console.log(rel, 'rel') // 隱藏地圖狀態切換之前的模型 currentShowModel.value.traverse((item) => { item.visible = false; }); // 左鍵雙擊進入地圖下一層 if (type === 'left') { // 地圖狀態切換,國=>省=>市 mapStatusSwitch('left'); // 射線檢測到的模型 const relModel = rel[0].object.parent; // 地圖code,用於獲取資料 const adcode = relModel.userData.adcode; let url = `http://211.143.122.110:18062/mapdata/geojson/areas_v3_full/all/${adcode}.json`; fetch(url) .then((response) => { if (!response.ok) { // 如果響應狀態碼不是2xx,丟擲錯誤 throw new Error('Network response was not ok: ' + response.statusText); } // 響應成功,返回解析的JSON資料 return response.json(); }) .then(async (data) => { await new Promise((resolve) => { // 全國模型的包圍盒 const nationModelBox = new THREE.Box3().setFromObject(nationMapModel); // 當前射線選中模型的包圍盒 let currentModelbox; // 市級地圖切換到市級地圖時,市級地圖是經過了一次縮放的,需要還原縮放比例進行計算 if (mapStatus.value === '市') { const cloneModel = relModel.clone(); cloneModel.scale.set(1, 1, 1); currentModelbox = new THREE.Box3().setFromObject(cloneModel); } else { currentModelbox = new THREE.Box3().setFromObject(relModel); } // 計算寬度和高度 const widthA = nationModelBox.max.x - nationModelBox.min.x; const heightA = nationModelBox.max.z - nationModelBox.min.z; const widthB = currentModelbox.max.x - currentModelbox.min.x; const heightB = currentModelbox.max.z - currentModelbox.min.z; // 計算寬度和高度的比例 const widthRatio = widthA / widthB; const heightRatio = heightA / heightB; // 當前模型與全國模型大小的縮放值 const scale = (widthRatio + heightRatio) / 2; // 應用縮放值到切換後的模型上去 currentShowModel.value.scale.set(scale, scale, 1); // 處理地圖資料,繪製模型 handleMapData(data, scale, resolve); // 這時候計算省份模型,得出放大後的省份模型的中心點,並將其位置歸於原點 const scaledBox = new THREE.Box3().setFromObject(currentShowModel.value); const center = new THREE.Vector3(); // 獲取放大後模型的中心點 scaledBox.getCenter(center); // 將模型的位置調整,使縮放後的中心位於原點 currentShowModel.value.position.sub(center); // 高度不增加 currentShowModel.value.position.y += center.y; // 顯示地圖狀態切換之後的模型 currentShowModel.value.traverse((item) => { item.visible = true; }); glowArr = [...cylinderGlowArr]; // 觸發右側按鈕點選事件 rightButItem.map((item) => { rightButClick(null, item); }); }); }); } // 右鍵雙擊返回地圖上一層 if (type === 'right') { // 全國地圖是固定的,省模型、市模型需要清除掉之前的子物件 if (mapStatus.value !== '國') { currentShowModel.value.traverse((item) => { // 彈框、標牌等這些為CSS2D和CSS3D物件,要清除其在頁面中的dom元素 if (item.element) { if (item.element.parentNode) item.element.parentNode.removeChild(item.element); } if (item.geometry && item.material) { // 從記憶體中銷燬幾何體資源 item.geometry.dispose(); // 從記憶體中銷燬材質資源 if (item.material.length) { item.material[0].dispose(); item.material[1].dispose(); } else { item.material.dispose(); } } }); currentShowModel.value.children = []; } // 地圖狀態切換,市=>省=>國 mapStatusSwitch('right'); if (mapStatus.value === '國') rightButton.value.style.display = 'block'; // 地圖狀態轉換完成後顯示對應的地圖 currentShowModel.value.traverse((item) => { item.visible = true; }); // 觸發右側按鈕點選事件 rightButItem.map((item) => { rightButClick(null, item); }); } } // 處理地圖資料,繪製模型 function handleMapData(data, scale, resolve) { // 全部資訊 const features = data.features; features.map((feature) => { const name = feature.properties.name; // 海南省三沙市範圍太廣不方便展示所以跳過 if (name === '三沙市') { return; } // 模型 const model = new THREE.Object3D(); model.name = name; model.userData.animationActive = false; // 新增屬性來跟蹤動畫狀態 model.userData.animationTimer = null; // 用於儲存定時器的引用 model.userData.adcode = feature.properties.adcode; // 用於儲存定時器的引用 currentShowModel.value.add(model); // 模型中心座標 const pos = feature.properties.centroid ? feature.properties.centroid : feature.properties.center; // 獲取地圖名稱dom const nameDom = document.getElementById('provinceName').cloneNode(); // 設定dom文字 nameDom.innerHTML = name; // 轉為CSS3D物件 const css3DObject = new CSS3DObject(nameDom); css3DObject.rotateX(Math.PI); css3DObject.name = '名稱物件'; // 這裡轉換完成後將元素pointerEvents屬性設定為none,防止阻礙相機旋轉縮放平移等操作 nameDom.style.pointerEvents = 'none'; // 設定名稱物件在模型中心位置 const [x, y] = projection(pos); css3DObject.position.set(x, -y, 0); css3DObject.rotateX(Math.PI); // 縮放一定大小 css3DObject.scale.set(0.2 / scale, 0.2 / scale, 0.2 / scale); const coordinates = feature.geometry.coordinates; // 繪製模型和邊界線 if (feature.geometry.type === 'MultiPolygon') { coordinates.forEach((coordinate) => { coordinate.forEach((rows) => { // 城市模型 const mesh = darwMapModel(rows); mesh.rotateX(Math.PI); model.add(mesh); // 邊線 const line = lineDraw(rows); line.name = '邊線'; line.position.z += 0.15; model.add(line, css3DObject); }); }); } // 繪製模型和邊界線 if (feature.geometry.type === 'Polygon') { coordinates.forEach((coordinate) => { // 選中省份模型的材質,將繼續應用到地級市模型上 // 城市模型 const mesh = darwMapModel(coordinate); mesh.rotateX(Math.PI); model.add(mesh); // 邊線 const line = lineDraw(coordinate); line.position.z += 0.15; line.name = '邊線'; model.add(line, css3DObject); }); } // 轉換成平面座標 const center = projection(pos); center[1] = -center[1]; // 儲存中心位置 model.userData.center = center; // 儲存地圖資料 model.userData.mapData = feature.geometry; // 建立地圖示牌 const mapTag = createMapTag( { name: name, x: center[0], y: center[1], }, Math.random() * 30 + 70 ); // 建立光柱 const cylinder = createCylinder( { name: name, x: center[0], y: center[1], }, Math.random() * 30000 + 70000, scale ); model.add(mapTag, cylinder); }); resolve(); } // 繪製地圖模型 function darwMapModel(polygon) { // 建立形狀 const shape = new THREE.Shape(); // 遍歷座標陣列,繪製形狀 polygon.forEach((row, i) => { // 座標點轉換 const [x, y] = projection(row); if (i === 0) { shape.moveTo(x, y); } shape.lineTo(x, y); }); // 將形狀進行拉伸 const geometry = new THREE.ExtrudeGeometry(shape, { depth: 10, bevelEnabled: true, bevelSegments: 10, bevelThickness: 0.1, }); // const topMaterial = materialArr[0].clone(); // const sideMaterial = materialArr[1]; const mesh = new THREE.Mesh(geometry, [topMaterial, sideMaterial]); return mesh; } // 繪製邊界線 function lineDraw(polygon) { const lineGeometry = new THREE.BufferGeometry(); const pointsArray = new Array(); polygon.forEach((row) => { const [x, y] = projection(row); // 建立三維點 pointsArray.push(new THREE.Vector3(x, -y, 0)); }); // 放入多個點 lineGeometry.setFromPoints(pointsArray); const lineMaterial = new THREE.LineBasicMaterial({ color: '#00ffff', // color: "#00C5CD", }); return new THREE.Line(lineGeometry, lineMaterial); } </script> <style lang="less"> body, html { font-size: 0.8vw; } /* 當視口寬度小於 600 畫素時,設定最小字型大小 */ @media (max-width: 1400px) { #mapTag { font-size: 12px !important; width: 80px !important; height: 30px !important; } } #chinaMap { width: 100%; height: 100%; position: absolute; overflow: hidden; } #threejs { width: 100%; height: 100%; } .rightButton { position: absolute; right: 1vw; bottom: 40vh; width: 4vw; .common { width: 100%; height: 3vh; border: 1px solid #00ffff; display: flex; justify-content: center; align-items: center; margin: 1.2vh 0; color: #fafafa; opacity: 0.5; font-size: 0.7vw; cursor: pointer; transition: 1s; } .selected { opacity: 1 !important; transition: 1s; } } #provinceName { pointer-events: none; position: absolute; left: 0; top: 0; color: #8ee5ee; padding: 10px; width: 200px; height: 20px; line-height: 20px; text-align: center; font-size: 13px; } #popup { z-index: 999; position: absolute; left: 0px; top: 0px; width: 41.66vw; height: 26.59vh; display: flex; .popup_line { margin-top: 4%; width: 24%; height: 26%; background: url('../../public/popup_line.png') no-repeat; background-size: 100% 100%; } .popup_Main { width: 35%; height: 80%; .popupMain_top { width: 100%; height: 10%; background: url('../../public/popupMain_head.png') no-repeat; background-size: 100% 100%; } .popupMain_footer { width: 100%; height: 10%; background: url('../../public/popupMain_footer.png') no-repeat; background-size: 100% 100%; } .popup_content { color: #fafafa; // background: rgba(47, 53, 121, 0.9); background-image: linear-gradient(to bottom, rgba(15, 36, 77, 1), rgba(8, 124, 190, 1)); border-radius: 10px; width: 100%; height: 70%; padding: 5% 0%; .popup_head { width: 100%; height: 12%; margin-bottom: 2%; display: flex; align-items: center; .popup_title { color: #8ee5ee; font-size: 1vw; letter-spacing: 5px; width: 88%; height: 100%; display: flex; align-items: center; .title_icon { width: 0.33vw; height: 100%; background: #2586ff; margin-right: 10%; } } .close { cursor: pointer; pointer-events: auto; width: 1.5vw; height: 1.5vw; background: url('../../public/close.png') no-repeat; background-size: 100% 100%; } } .popup_item { display: flex; align-items: center; width: 85%; padding-left: 5%; height: 18%; // background: rgb(160, 196, 221); border-radius: 10px; margin: 2.5% 0%; margin-left: 10%; div { line-height: 100%; margin-right: 10%; } .item_value { font-size: 0.9vw; color: #00ffff; font-weight: 600; letter-spacing: 2px; } } } } } #cylinderValue { position: absolute; top: 0; left: 0; color: #bbffff; } #mapTag { z-index: 997; position: absolute; top: 0; left: 0; font-size: 0.6vw; width: 4.2vw; height: 4.7vh; display: flex; flex-direction: column; justify-content: center; align-items: center; .content { width: 100%; height: calc(100% - 1vw); // background: #0e1937; background: #0e2346; border: 1px solid #6298a9; display: flex; align-items: center; justify-content: center; color: #fafafa; #mapTag_value { color: #ffd700; } } .content::before { content: ''; width: 100%; height: calc(100% - 1vw); position: absolute; background: linear-gradient(to top, #26aad1, #26aad1) left top no-repeat, //上左 linear-gradient(to right, #26aad1, #26aad1) left top no-repeat, linear-gradient(to top, #26aad1, #26aad1) right bottom no-repeat, //下右 linear-gradient(to left, #26aad1, #26aad1) right bottom no-repeat; //右下 background-size: 2px 10px, 16px 2px, 2px 10px, 16px 2px; pointer-events: none; } .arrow { background: url('../../public/arrow.png') no-repeat; background-size: 100% 100%; width: 1vw; height: 1vw; } } </style>