vue3中实现3D地图——three.js

在这里插入图片描述

需求点

  • 地图区域大小随着父盒子大小变动,窗口缩放自动适配
  • 每个区域显示不同颜色和高度,描边
  • 每个区域显示名字label和icon
  • 点击区域改变其透明度,并且弹窗显示信息窗口
  • 点击点也可以
  • 可以自由放大缩小,360度旋转

在这里插入图片描述

npm install d3@^7.8.4
npm install lil-gui@^0.18.1
npm install three@^0.152.2

代码

<template><div style="width: 100%; height: 100vh; position: relative"><div id="map" style="width: 100%; height: 100%"></div><!-- 点击市或者点显示信息窗口 --><div v-if="city" class="infoPop" :style="`left:${left}px;top:${top}px`">我是{{ city }}</div></div>
</template><script setup>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import {CSS2DRenderer,CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer.js";
import { ref, onMounted, onUnmounted } from "vue";
import * as d3 from "d3";const mapWidth = ref(0);
const mapHeight = ref(0);
const mapElement = ref(null);
const left = ref(0);
const top = ref(0);
const city = ref("");onMounted(() => {// 获取map地图容器的宽高,监听窗口变化更新宽高mapElement.value = document.getElementById("map"); // 获取DOM元素引用mapWidth.value = mapElement.value.clientWidth;mapHeight.value = mapElement.value.clientHeight;console.log(mapWidth.value, mapHeight.value);const scene = new THREE.Scene();// const axesHelper = new THREE.AxesHelper(5);// scene.add(axesHelper);const ambientLight = new THREE.AmbientLight(0xd4e7fd, 4);scene.add(ambientLight);const directionalLight = new THREE.DirectionalLight(0xe8eaeb, 0.2);directionalLight.position.set(0, 10, 5);const directionalLight2 = directionalLight.clone();directionalLight2.position.set(0, 10, -5);const directionalLight3 = directionalLight.clone();directionalLight3.position.set(5, 10, 0);const directionalLight4 = directionalLight.clone();directionalLight4.position.set(-5, 10, 0);scene.add(directionalLight);scene.add(directionalLight2);scene.add(directionalLight3);scene.add(directionalLight4);const camera = new THREE.PerspectiveCamera(75,mapWidth.value / mapHeight.value,0.1,1000);camera.position.y = 8;camera.position.z = 8;const labelRenderer = new CSS2DRenderer();labelRenderer.domElement.style.position = "absolute";labelRenderer.domElement.style.top = "0px";labelRenderer.domElement.style.pointerEvents = "none";labelRenderer.setSize(mapWidth.value, mapHeight.value);document.getElementById("map").appendChild(labelRenderer.domElement);const renderer = new THREE.WebGLRenderer({ alpha: true });renderer.setSize(mapWidth.value, mapHeight.value);document.getElementById("map").appendChild(renderer.domElement);const controls = new OrbitControls(camera, renderer.domElement);controls.update();const animate = () => {requestAnimationFrame(animate);controls.update();renderer.render(scene, camera);labelRenderer.render(scene, camera);};animate();window.addEventListener("resize", () => {mapWidth.value = mapElement.value.clientWidth;mapHeight.value = mapElement.value.clientHeight;camera.aspect = mapWidth.value / mapHeight.value;camera.updateProjectionMatrix();renderer.setSize(mapWidth.value, mapHeight.value);labelRenderer.setSize(mapWidth.value, mapHeight.value);});const url = "https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json";fetch(url).then((res) => res.json()).then((data) => {const map = createMap(data);scene.add(map);let intersect = null;// 鼠标点击事件window.addEventListener("click", (event) => {const mouse = new THREE.Vector2();mouse.x = (event.clientX / mapWidth.value) * 2 - 1;mouse.y = -(event.clientY / mapHeight.value) * 2 + 1;const raycaster = new THREE.Raycaster();raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObjects(map.children).filter((item) => item.object.type !== "Line");if (intersects.length > 0) {// 点击市if (intersects[0].object.type === "Mesh") {// 打印鼠标点击的省市名称、adcode,显示信息窗口console.log(intersects[0].object.parent.name,intersects[0].object.parent.adcode);city.value = intersects[0].object.parent.name;left.value = event.clientX + 20;top.value = event.clientY + 20;if (intersect) isAplha(intersect, 1);intersect = intersects[0].object.parent;isAplha(intersect, 0.4);}// 点击iconif (intersects[0].object.type === "Sprite") {console.log(intersects[0].object);}} else {if (intersect) isAplha(intersect, 1);}function isAplha(intersect, opacity) {intersect.children.forEach((item) => {if (item.type === "Mesh") {item.material.opacity = opacity;}});}});});
});
onUnmounted(() => {window.removeEventListener("resize", resizeRenderer); // 移除窗口大小变化监听器
});
// 矫正坐标
const offsetXY = d3.geoMercator();// 根据省市的json数据创建地图
const createMap = (data) => {console.log(data, 11111);const map = new THREE.Object3D();const center = data.features[0].properties.centroid;offsetXY.center(center).translate([0, 0]);data.features.forEach((feature) => {const unit = new THREE.Object3D();const { centroid, center, name, adcode } = feature.properties;const { coordinates, type } = feature.geometry;const point = centroid || center || [0, 0];// 每个市随机颜色,随机深度const color = new THREE.Color(`hsl(${233},${Math.random() * 30 + 55}%,${Math.random() * 30 + 55}%)`).getHex();const depth = Math.random() * 0.3 + 0.3;// 绘制每个市的名称和图标const label = createLabel(name, point, depth);const icon = createIcon(center, depth);coordinates.forEach((coordinate) => {if (type === "MultiPolygon") coordinate.forEach((item) => fn(item));if (type === "Polygon") fn(coordinate);function fn(coordinate) {// 添加自定义属性,点击的时候可以打印出来unit.name = name;unit.adcode = adcode;// 绘制每个市的区域(传入颜色和深度)const mesh = createMesh(coordinate, color, depth);// 绘制每个市的边界const line = createLine(coordinate, depth);unit.add(mesh, ...line);}});// 在地图上添加标记 位置、名字、图标map.add(unit, label, icon);// 设置地图中心setCenter(map);});// ----添加任意点标记----const icon1 = createIcon([120.057576, 29.697459], 1.11);map.add(icon1);return map;
};
// 绘制每个市的区域
const createMesh = (data, color, depth, name) => {const shape = new THREE.Shape();data.forEach((item, idx) => {const [x, y] = offsetXY(item);if (idx === 0) shape.moveTo(x, -y);else shape.lineTo(x, -y);});const extrudeSettings = {depth: depth,bevelEnabled: false,};const materialSettings = {color: color,emissive: 0x000000,roughness: 0.45,metalness: 0.8,transparent: true,side: THREE.DoubleSide,};const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);const material = new THREE.MeshStandardMaterial(materialSettings);const mesh = new THREE.Mesh(geometry, material);return mesh;
};
// 绘制每个市的边界
const createLine = (data, depth) => {const points = [];data.forEach((item) => {const [x, y] = offsetXY(item);points.push(new THREE.Vector3(x, -y, 0));});const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);const uplineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });const downlineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });const upLine = new THREE.Line(lineGeometry, uplineMaterial);const downLine = new THREE.Line(lineGeometry, downlineMaterial);downLine.position.z = -0.0001;upLine.position.z = depth + 0.0001;return [upLine, downLine];
};
// 绘制每个市的名称
const createLabel = (name, point, depth) => {const div = document.createElement("div");div.style.color = "#fff";div.style.fontSize = "12px";div.style.textShadow = "1px 1px 2px #047cd6";div.textContent = name;const label = new CSS2DObject(div);label.scale.set(0.01, 0.01, 0.01);const [x, y] = offsetXY(point);label.position.set(x, -y, depth);return label;
};
// 绘制每个市的图标
const createIcon = (point, depth) => {const url = new URL("../assets/icon.png", import.meta.url).href;const map = new THREE.TextureLoader().load(url);const material = new THREE.SpriteMaterial({map: map,transparent: true,});const sprite = new THREE.Sprite(material);const [x, y] = offsetXY(point);sprite.scale.set(0.3, 0.3, 0.3);sprite.position.set(x, -y, depth + 0.2);sprite.renderOrder = 1;return sprite;
};
// 设置地图中心
const setCenter = (map) => {map.rotation.x = -Math.PI / 2;const box = new THREE.Box3().setFromObject(map);const center = box.getCenter(new THREE.Vector3());const offset = [0, 0];map.position.x = map.position.x - center.x - offset[0];map.position.z = map.position.z - center.z - offset[1];
};
</script><style>
* {padding: 0;margin: 0;box-sizing: border-box;
}
#map {background-color: #d4e7fd;
}
.infoPop {position: absolute;left: 0;top: 0;background: #fff;border: 1px solid #03c3fd;border-radius: 10px;color: #333;padding: 20px;z-index: 9999;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/30017.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

背景渐变动画登录页

b站视频演示效果: 效果图: 完整代码: <!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>背景…

大模型商业化:李开复向左,张亚勤向右

文&#xff5c;白 鸽 “零一万物坚决做To C&#xff0c;不做赔钱的To B&#xff0c;要做能赚钱的To B。”2024年6月14日&#xff0c;在智源大会上&#xff0c;零一万物创始人李开复如此坚定地说道。 而与之相对&#xff0c;中国工程院院士、清华大学智能产业研究院&…

2024/06/18--代码随想录算法8/17| 股票问题

121.买卖股票的最佳时机 力扣链接 动规五部曲 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 dp[i][0] 表示第i天持有股票所得最多现金,dp[i][1] 表示第i天不持有股票所得最多现金确定递推公式 dp[i][0] max(dp[i-1][0], -price[i]) dp[i][1]max(dp[i-1][1], …

QTimeEdit、QDateEdit、QDateTimeEdit、QCalendarWidget

实验 QTime和字符串相互转换 QDate和字符串相互转换 QDateTime和字符串相互转换 QCalendarWidget使用 year&#xff0c;month&#xff0c;day&#xff0c;minute&#xff0c;second&#xff0c;msec&#xff0c;dayOfWeek, dayto方法/属性的使用 布局 datetimeexample.cpp #inc…

智慧之选:Vatee万腾平台,引领未来的创新引擎

在数字化浪潮席卷全球的今天&#xff0c;我们身处一个信息爆炸、技术革新的时代。在这样的大背景下&#xff0c;选择一个能够引领我们走向未来的平台显得尤为重要。而Vatee万腾平台&#xff0c;正是这样一个不容错过的智慧之选。 Vatee万腾平台&#xff0c;作为一个集创新、科技…

PostgreSQL源码分析——口令认证

认证机制 对于数据库系统来说&#xff0c;其作为服务端&#xff0c;接受来自客户端的请求。对此&#xff0c;必须有对客户端的认证机制&#xff0c;只有通过身份认证的客户端才可以访问数据库资源&#xff0c;防止非法用户连接数据库。PostgreSQL支持认证方法有很多&#xff1…

Stable Diffusion 3 Medium 正式开源

Stable Diffusion 3 Medium 正式开源 Stability AI宣布Stable Diffusion 3 Medium现已开源&#xff0c;这是最新的文本生成图像AI模型&#xff0c;被官方声称为“迄今为止最先进的开源模型”&#xff0c;其性能超过了Midjourney 6。 这款Stable Diffusion 3 Medium模型拥有2…

【SpringBoot项目常见细化错误】(保姆级教程)Result Maps collection already contains value for

SpringBoot项目常见错误 1.当Mybatis报错 Result Maps collection already contains value for一、重复点击Mybatis-Generator导致配置文件重复生成XML二、正确配置Yml仔细检查有没有多了或者少了一个空格三、spring boot mybatis四、应该用resultMap来接收返回值&#xff0c;…

Blazor的SSR服务端渲染是不是交互式的

从.NET8开始&#xff0c;Blazor引入了SSR服务端渲染&#xff0c;归功于MVC和RazePage的沉淀&#xff0c;虽然来得晚&#xff0c;但一经发布&#xff0c;就将Blazor推向了新的高度。从今年开始&#xff0c;Youtube上关于Blazor的优质教学视频&#xff0c;以肉眼可见的速度在增加…

[保姆级教程]uniapp实现接口请求和请求方法二次封装

文章目录 新建文件配置方法发送请求使用 新建文件 现在src中新建一个api目录 新建index.js和request.js文件 配置方法发送请求 request.js中输入以下内容 const http {baseUrl: http://127.0.0.1:8000,request(config) {config beforeRequest(config)config.url this…

前端工具篇

在线工具 https://tool.lu/ 程序员工具箱 http://tool.pfan.cn/apitest 配色 https://webkul.github.io/coolhue/ 在线字符串和16进制互转 https://kw360.net/ox2str/ 代码美化截图 https://carbon.now.sh/?bgrgba 菜鸟工具 https://www.jyshare.com/ 文件格式转换 htt…

C++智能指针auto_ptr(有缺陷已废弃)

一、auto_ptr不能共享所有权 auto_ptr类没有拷贝构造函数。可以看出将Right对象的资源释放掉了。 在下面的操作中&#xff0c;ptr1的值给ptr2&#xff0c;那么此时ptr1的值将会为nullptr。在构造和赋值都发生了所有权的转移。调用函数的时候&#xff0c;接收参数的时候同样也会…

Rapidfuzz,一个高效的 Python 模糊匹配神器

目录 01初识 Rapidfuzz 什么是 Rapidfuzz? 为什么选择 Rapidfuzz? 安装 Rapidfuzz 配置 Rapidfuzz 02基本操作 简单比率计算 03高级功能 查找单个最佳匹配 查找多个最佳匹配 使用阈值优化性能 04实战案例…

IPython大师课:提升数据科学工作效率的终极工具

IPython是一个增强的Python交互式shell&#xff0c;它提供了丰富的功能和易用性改进&#xff0c;特别适合进行数据分析、科学计算和一般的Python开发。本文将全面介绍IPython的基本概念、使用方法、主要作用以及注意事项。 一、IPython简介 1. IPython的起源 IPython最初由Fe…

【Android】使用Binder(AIDL)实现利用自定义Bean进行的进程间通信(二)

项目前置 这是我之前写的关于Binder的一些知识点和使用基本数据类型在通信的文章&#xff0c;感兴趣的可以看一下: Binder&#xff08;一&#xff09;Binder的介绍和AIDL使用Binder的实例 项目目标 在两个APP之间进行数据传递&#xff0c;使用Android推荐的Binder通讯&#…

使用Flink接受kafka中的数据并对数据进行ETL

做这个开发是因为&#xff1a;在实际开发操作中&#xff0c;你的kafka主题中会有大量的数据但是需求并不需要所有数据&#xff0c;所有我们要对数据进行清洗&#xff0c;把需要的数据保存在flink流中&#xff0c;为下流的开发做好数据保障&#xff01; 首先创建工具类 再写一…

Golang | Leetcode Golang题解之第151题反转字符串中的单词

题目&#xff1a; 题解&#xff1a; import ("fmt" )func reverseWords(s string) string {//1.使用双指针删除冗余的空格slowIndex, fastIndex : 0, 0b : []byte(s)//删除头部冗余空格for len(b) > 0 && fastIndex < len(b) && b[fastIndex]…

视频与音频的交响:探索达摩院VideoLLaMA 2的技术创新

一、简介 文章&#xff1a;https://arxiv.org/abs/2406.07476 代码&#xff1a;https://github.com/DAMO-NLP-SG/VideoLLaMA2 VideoLLaMA 2是由阿里巴巴集团的DAMO Academy团队开发的视频大型语言模型&#xff08;Video-LLM&#xff09;&#xff0c;旨在通过增强空间-时间建模…

高考分数线一分一段统计汇总(熟练SQL窗口函数)

高考分数线一分一段统计汇总(使用SQL窗口函数) select 总分数&#xff0c; 一分一段人数&#xff0c; sum(一分一段人数) over( order by 总分数 desc) as 累计排名 from( select 总分数&#xff0c; count(考生号) as 一分一段人数 from &#xff08; select 考生号&…

大咖专栏 | AI 时代下,我们可以拥有怎样的数据库?

Hi&#xff0c;各位朋友们&#xff0c;我是 KaiwuDB 高级架构师赵衎衎。 KaiwuDB 始于万物互联时代下千万条数据洪流中&#xff0c;我们持续打磨构造了更加灵活兼容的分布式多模架构&#xff0c;实现了海量异构数据高性能、低成本的集中管理… …这些底层特性都在为后续提供更…