newechoes/src/components/WorldHeatmap.tsx

1058 lines
37 KiB
TypeScript
Raw Normal View History

2025-03-28 02:27:42 +08:00
import React, { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
import worldData from '@/assets/world.zh.json';
import chinaData from '@/assets/china.json';
interface WorldHeatmapProps {
visitedPlaces: string[];
}
const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
2025-03-28 02:27:42 +08:00
const containerRef = useRef<HTMLDivElement>(null);
const [hoveredCountry, setHoveredCountry] = useState<string | null>(null);
const sceneRef = useRef<{
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;
renderer: THREE.WebGLRenderer;
labelRenderer: CSS2DRenderer;
controls: OrbitControls;
earth: THREE.Mesh;
bgSphere: THREE.Mesh;
countries: Map<string, THREE.Object3D>;
raycaster: THREE.Raycaster;
mouse: THREE.Vector2;
animationId: number | null;
lastCameraPosition: THREE.Vector3 | null;
lastMouseEvent: MouseEvent | null;
lastClickedCountry: string | null;
} | null>(null);
useEffect(() => {
2025-03-28 02:27:42 +08:00
if (!containerRef.current) return;
2025-03-28 02:27:42 +08:00
// 清理之前的场景
if (sceneRef.current) {
if (sceneRef.current.animationId !== null) {
cancelAnimationFrame(sceneRef.current.animationId);
}
sceneRef.current.renderer.dispose();
sceneRef.current.labelRenderer.domElement.remove();
sceneRef.current.scene.clear();
containerRef.current.innerHTML = '';
2025-03-27 21:40:41 +08:00
}
2025-03-28 02:27:42 +08:00
// 检查当前是否为暗色模式
const isDarkMode = document.documentElement.classList.contains('dark');
// 根据当前模式设置颜色
const getColors = () => {
return {
background: 'transparent', // 改为透明背景
earthBase: isDarkMode ? '#1f2937' : '#e5e7eb',
visited: isDarkMode ? '#059669' : '#10b981',
highlight: isDarkMode ? '#f59e0b' : '#f59e0b',
border: isDarkMode ? '#4b5563' : '#9ca3af',
visitedBorder: isDarkMode ? '#059669' : '#10b981',
chinaBorder: isDarkMode ? '#ef4444' : '#ef4444', // 中国边界使用红色
text: isDarkMode ? '#ffffff' : '#374151',
bgSphere: isDarkMode ? '#111827' : '#f3f4f6', // 背景球体颜色
};
};
const colors = getColors();
// 创建场景
const scene = new THREE.Scene();
// 将背景设置为透明
scene.background = null;
// 创建不透明材质的辅助函数
const createOpaqueMaterial = (color: string, side: THREE.Side = THREE.FrontSide, renderOrder: number = 0) => {
const material = new THREE.MeshBasicMaterial({
color: color,
side: side,
transparent: false,
opacity: 1.0,
depthTest: true,
depthWrite: true
});
return material;
};
// 创建地球前,先创建一个背景球体 - 设置为对应主题的背景色
const bgSphereGeometry = new THREE.SphereGeometry(2.1, 64, 64);
const bgSphereMaterial = createOpaqueMaterial(colors.bgSphere, THREE.BackSide);
const bgSphere = new THREE.Mesh(bgSphereGeometry, bgSphereMaterial);
bgSphere.renderOrder = 0;
scene.add(bgSphere);
// 创建一个中间层球体,防止看透 - 设置为对应主题的背景色
const midSphereGeometry = new THREE.SphereGeometry(2.0, 64, 64);
const midSphereMaterial = createOpaqueMaterial(colors.bgSphere);
const midSphere = new THREE.Mesh(midSphereGeometry, midSphereMaterial);
midSphere.renderOrder = 1;
scene.add(midSphere);
// 在地球内部添加一个实心球体 - 设置为对应主题的背景色
const innerSphereGeometry = new THREE.SphereGeometry(1.97, 32, 32);
const innerSphereMaterial = createOpaqueMaterial(colors.bgSphere);
const innerSphere = new THREE.Mesh(innerSphereGeometry, innerSphereMaterial);
innerSphere.renderOrder = 1.5;
scene.add(innerSphere);
// 创建相机
const camera = new THREE.PerspectiveCamera(
45,
containerRef.current.clientWidth / containerRef.current.clientHeight,
0.1,
1000
);
camera.position.z = 8;
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true, // 启用alpha通道
logarithmicDepthBuffer: true,
preserveDrawingBuffer: true,
precision: "highp"
2025-03-27 21:40:41 +08:00
});
2025-03-28 02:27:42 +08:00
renderer.sortObjects = true;
// 设置透明背景
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
containerRef.current.appendChild(renderer.domElement);
// 创建CSS2D渲染器用于标签
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0';
labelRenderer.domElement.style.pointerEvents = 'none';
containerRef.current.appendChild(labelRenderer.domElement);
// 添加控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.rotateSpeed = 0.5;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
controls.minDistance = 5;
controls.maxDistance = 15;
// 限制上下旋转角度,避免相机翻转
controls.minPolarAngle = Math.PI * 0.1;
controls.maxPolarAngle = Math.PI * 0.9;
// 添加控制器事件监听
controls.addEventListener('change', () => {
if (sceneRef.current) {
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
}
});
// 创建地球几何体注意减小尺寸防止Z-fighting
const earthGeometry = new THREE.SphereGeometry(1.95, 64, 64);
// 使用不透明的基础材质
const earthMaterial = createOpaqueMaterial(colors.earthBase, THREE.FrontSide);
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
earth.matrixAutoUpdate = false;
earth.updateMatrix();
earth.renderOrder = 2;
scene.add(earth);
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(5, 3, 5);
scene.add(directionalLight);
// 创建国家边界
const countries = new Map<string, THREE.Object3D>();
const countryGroup = new THREE.Group();
earth.add(countryGroup);
// 创建一个辅助函数,用于将经纬度转换为三维坐标
const latLongToVector3 = (lat: number, lon: number, radius: number): THREE.Vector3 => {
// 调整经度范围,确保它在[-180, 180]之间
while (lon > 180) lon -= 360;
while (lon < -180) lon += 360;
const phi = (90 - lat) * Math.PI / 180;
const theta = (lon + 180) * Math.PI / 180;
const x = -radius * Math.sin(phi) * Math.cos(theta);
const y = radius * Math.cos(phi);
const z = radius * Math.sin(phi) * Math.sin(theta);
return new THREE.Vector3(x, y, z);
};
// 省份边界和中心点数据结构
const provinceBoundaries = new Map<string, THREE.Vector3[][]>();
const provinceCenters = new Map<string, THREE.Vector3>();
// 创建一个通用函数,用于处理地理特性(国家或省份)
const processGeoFeature = (
feature: any,
parent: THREE.Group,
options: {
regionType: 'country' | 'province',
parentName?: string,
scale?: number,
borderColor?: string,
visitedBorderColor?: string
}
) => {
const { regionType, parentName, scale = 2.01, borderColor, visitedBorderColor } = options;
const regionName = regionType === 'province' && parentName
? `${parentName}-${feature.properties.name}`
: feature.properties.name;
const isRegionVisited = visitedPlaces.includes(regionName);
// 为每个地区创建一个组
const regionObject = new THREE.Group();
regionObject.userData = { name: regionName, isVisited: isRegionVisited };
// 计算地区中心点
let centerLon = 0;
let centerLat = 0;
let pointCount = 0;
let largestPolygonArea = 0;
let largestPolygonCenter = { lon: 0, lat: 0 };
// 首先检查GeoJSON特性中是否有预定义的中心点
let hasPreDefinedCenter = false;
let centerVector;
if (feature.properties.cp && Array.isArray(feature.properties.cp) && feature.properties.cp.length === 2) {
const [cpLon, cpLat] = feature.properties.cp;
hasPreDefinedCenter = true;
centerVector = latLongToVector3(cpLat, cpLon, scale + 0.005);
centerLon = cpLon;
centerLat = cpLat;
// 保存预定义中心点
provinceCenters.set(regionName, centerVector);
}
// 存储区域边界
const boundaries: THREE.Vector3[][] = [];
// 计算多边形面积的辅助函数
const calculatePolygonArea = (coords: number[][]) => {
let area = 0;
for (let i = 0, j = coords.length - 1; i < coords.length; j = i++) {
area += coords[i][0] * coords[j][1];
area -= coords[i][1] * coords[j][0];
}
return Math.abs(area / 2);
};
// 处理多边形坐标
const processPolygon = (polygonCoords: any) => {
const points: THREE.Vector3[] = [];
// 收集多边形的点
polygonCoords.forEach((point: number[]) => {
const lon = point[0];
const lat = point[1];
centerLon += lon;
centerLat += lat;
pointCount++;
// 使用辅助函数将经纬度转换为3D坐标
points.push(latLongToVector3(lat, lon, scale));
});
// 计算当前多边形的面积和中心
if (regionType === 'country') {
const area = calculatePolygonArea(polygonCoords);
if (area > largestPolygonArea) {
largestPolygonArea = area;
// 计算多边形中心
let polyLon = 0;
let polyLat = 0;
polygonCoords.forEach((point: number[]) => {
polyLon += point[0];
polyLat += point[1];
});
largestPolygonCenter = {
lon: polyLon / polygonCoords.length,
lat: polyLat / polygonCoords.length
};
}
}
// 保存边界多边形
if (points.length > 2) {
boundaries.push(points);
}
// 创建边界线
if (points.length > 1) {
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const lineMaterial = new THREE.LineBasicMaterial({
color: isRegionVisited
? (visitedBorderColor || colors.visitedBorder)
: (borderColor || colors.border),
linewidth: isRegionVisited ? 1.5 : 1,
transparent: true,
opacity: isRegionVisited ? 0.9 : 0.7,
depthTest: false, // 禁用深度测试解决Z-fighting问题
polygonOffset: true, // 启用多边形偏移
polygonOffsetFactor: isRegionVisited ? -2 : -1, // 已访问区域的边界线偏移更多,确保在上层
polygonOffsetUnits: 1
});
const line = new THREE.Line(lineGeometry, lineMaterial);
line.userData = {
name: regionName,
isVisited: isRegionVisited,
originalColor: isRegionVisited
? (visitedBorderColor || colors.visitedBorder)
: (borderColor || colors.border)
};
2025-03-28 02:27:42 +08:00
// 设置已访问区域的边界线渲染顺序更高,确保它们始终绘制在未访问区域边界线的上方
line.renderOrder = isRegionVisited ? 3 : 2;
regionObject.add(line);
// 如果是已访问的地区,为这个边界创建一个填充面
if (isRegionVisited && points.length >= 3) {
try {
// 使用ShapeGeometry创建一个平面填充
// 首先创建一个2D平面上的形状
const center = new THREE.Vector3(0, 0, 0);
// 将3D点投影到以该点为中心的平面上
const projectedPoints = points.map(p => {
// 计算从地球中心到点的方向向量
const dir = p.clone().normalize();
// 计算投影平面的法向量(就是该点的方向)
const normal = dir.clone();
// 创建一个与地球表面近似切线的平面
const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, p);
// 计算从中心到各点的投影坐标
const v1 = new THREE.Vector3();
const v2 = new THREE.Vector3();
// 获取平面上的两个相互垂直的方向作为UV坐标系
let u = new THREE.Vector3(1, 0, 0);
if (Math.abs(normal.dot(u)) > 0.9) {
u = new THREE.Vector3(0, 1, 0);
}
const v = new THREE.Vector3().crossVectors(normal, u).normalize();
u = new THREE.Vector3().crossVectors(v, normal).normalize();
// 计算点在该平面上的UV坐标
const projected = p.clone().sub(center);
v1.set(projected.dot(u), projected.dot(v), 0);
return v1;
});
// 创建一个Shape对象
const shape = new THREE.Shape();
// 移动到第一个点
shape.moveTo(projectedPoints[0].x, projectedPoints[0].y);
// 连接所有其他点
for (let i = 1; i < projectedPoints.length; i++) {
shape.lineTo(projectedPoints[i].x, projectedPoints[i].y);
}
// 关闭形状
shape.closePath();
// 创建一个面材质
const faceMaterial = new THREE.MeshBasicMaterial({
color: colors.visited,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
depthWrite: false
});
// 根据中心点确定方向
const centerPoint = points.reduce((acc, point) => acc.add(point), new THREE.Vector3()).multiplyScalar(1 / points.length);
const direction = centerPoint.clone().normalize();
// 根据曲面方向创建网格
const geometry = new THREE.ShapeGeometry(shape);
const mesh = new THREE.Mesh(geometry, faceMaterial);
// 放置并旋转网格以匹配多边形位置
mesh.position.copy(centerPoint);
mesh.lookAt(center);
// 设置网格属性
mesh.userData = {
name: regionName,
isVisited: isRegionVisited,
originalColor: colors.visited
};
mesh.renderOrder = 1;
// 添加到区域对象
regionObject.add(mesh);
} catch (error) {
console.error("填充区域时出错:", error);
// 如果填充区域出错,回退到简单的圆盘标记
const centerPoint = points.reduce((acc, point) => acc.add(point), new THREE.Vector3()).multiplyScalar(1 / points.length);
const diskGeometry = new THREE.CircleGeometry(0.1, 32);
const diskMaterial = new THREE.MeshBasicMaterial({
color: colors.visited,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide
});
const disk = new THREE.Mesh(diskGeometry, diskMaterial);
disk.position.copy(centerPoint);
disk.lookAt(0, 0, 0);
disk.rotateX(Math.PI / 2);
disk.userData = { name: regionName, isVisited: true };
disk.renderOrder = 1;
regionObject.add(disk);
}
}
}
2025-03-28 02:27:42 +08:00
};
// 处理不同类型的几何体
if (feature.geometry && (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon')) {
if (feature.geometry.type === 'Polygon') {
feature.geometry.coordinates.forEach((ring: any) => {
processPolygon(ring);
});
} else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach((polygon: any) => {
polygon.forEach((ring: any) => {
processPolygon(ring);
});
});
}
// 保存省份边界
if (regionType === 'province' && boundaries.length > 0) {
provinceBoundaries.set(regionName, boundaries);
}
if (pointCount > 0 && !hasPreDefinedCenter) {
// 如果是国家且有最大多边形
if (regionType === 'country' && largestPolygonArea > 0) {
centerLon = largestPolygonCenter.lon;
centerLat = largestPolygonCenter.lat;
} else {
// 回退到平均中心点
centerLon /= pointCount;
centerLat /= pointCount;
}
// 将中心点经纬度转换为3D坐标
centerVector = latLongToVector3(centerLat, centerLon, scale + 0.005);
// 保存计算的中心点
if (regionType === 'province') {
provinceCenters.set(regionName, centerVector);
}
2025-03-28 02:27:42 +08:00
}
if (pointCount > 0) {
// 添加地区对象到父组
parent.add(regionObject);
countries.set(regionName, regionObject);
}
}
return regionObject;
};
2025-03-28 02:27:42 +08:00
// 处理世界GeoJSON数据
worldData.features.forEach((feature: any) => {
const countryName = feature.properties.name;
// 跳过中国,因为我们将使用更详细的中国地图数据
if (countryName === '中国') return;
processGeoFeature(feature, countryGroup, {
regionType: 'country',
scale: 2.01
});
});
// 处理中国的省份
const chinaObject = new THREE.Group();
chinaObject.userData = { name: '中国', isVisited: visitedPlaces.includes('中国') };
chinaData.features.forEach((feature: any) => {
processGeoFeature(feature, chinaObject, {
regionType: 'province',
parentName: '中国',
scale: 2.015,
borderColor: colors.chinaBorder,
visitedBorderColor: colors.visitedBorder
});
});
2025-03-28 02:27:42 +08:00
// 添加中国对象到国家组
countryGroup.add(chinaObject);
countries.set('中国', chinaObject);
2025-03-27 21:40:41 +08:00
2025-03-28 02:27:42 +08:00
// 创建射线投射器用于鼠标交互
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 添加节流函数,限制鼠标移动事件的触发频率
const throttle = (func: Function, limit: number) => {
let inThrottle: boolean = false;
return function(this: any, ...args: any[]) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
2025-03-27 21:40:41 +08:00
};
};
2025-03-28 02:27:42 +08:00
// 计算鼠标射线和检测交互的通用函数
const calculateMouseRay = (event: MouseEvent) => {
if (!containerRef.current || !sceneRef.current) return null;
// 计算鼠标在画布中的归一化坐标
const rect = containerRef.current.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// 应用坐标,确保值在合理范围内
mouse.x = Math.max(-1, Math.min(1, x));
mouse.y = Math.max(-1, Math.min(1, y));
// 设置射线投射器的精度
raycaster.params.Line = { threshold: 0.2 };
raycaster.setFromCamera(mouse, sceneRef.current.camera);
return raycaster;
};
2025-03-27 21:40:41 +08:00
2025-03-28 02:27:42 +08:00
// 通用函数,查找与射线相交的国家
const findIntersectedCountry = () => {
if (!sceneRef.current) return null;
// 首先检测是否与地球相交
const sphereCenter = new THREE.Vector3(0, 0, 0);
const sphereRadius = 2; // 地球半径
const sphere = new THREE.Sphere(sphereCenter, sphereRadius);
// 检测射线与球体是否相交,并获取交点
const earthIntersectionPoint = new THREE.Vector3();
const hasEarthIntersection = raycaster.ray.intersectSphere(sphere, earthIntersectionPoint);
// 如果射线没有与地球相交返回null
if (!hasEarthIntersection) return null;
// 计算射线与地球交点的距离
const distanceToEarthIntersection = earthIntersectionPoint.distanceTo(sceneRef.current.camera.position);
// 检测与国家组的交叉
let intersects = raycaster.intersectObject(countryGroup, true);
// 过滤出只在地球前表面的交点
const tolerance = 0.1;
intersects = intersects.filter(intersect => {
return intersect.distance <= (distanceToEarthIntersection + tolerance);
});
if (intersects.length === 0) return null;
// 寻找相交的国家
// 先尝试找到面对象Mesh类型
for (const intersect of intersects) {
if (intersect.object instanceof THREE.Mesh &&
intersect.object.userData &&
intersect.object.userData.name) {
return intersect.object.userData.name;
}
}
// 如果没有找到面对象,尝试查找线对象
for (const intersect of intersects) {
let countryObject = intersect.object;
// 如果对象本身有userData优先使用
if (countryObject.userData && countryObject.userData.name) {
return countryObject.userData.name;
}
// 向上遍历对象层次结构找到有userData的父对象
while (countryObject.parent) {
countryObject = countryObject.parent as THREE.Object3D;
if (countryObject.userData && countryObject.userData.name) {
return countryObject.userData.name;
}
2025-03-28 02:27:42 +08:00
// 如果已经到达地球对象,则停止遍历
if (countryObject === earth) break;
}
}
return null;
};
// 鼠标移动事件 - 使用节流函数包装
const onMouseMove = throttle((event: MouseEvent) => {
if (!containerRef.current || !sceneRef.current) return;
// 如果是通过点击选中的国家,不要通过移动鼠标改变它
if (sceneRef.current.lastClickedCountry !== null) return;
// 保存最后的鼠标事件,用于相机变化时重新检测
sceneRef.current.lastMouseEvent = event;
// 计算射线
calculateMouseRay(event);
// 查找相交的国家
const countryName = findIntersectedCountry();
if (countryName) {
setHoveredCountry(countryName);
controls.autoRotate = false;
} else {
setHoveredCountry(null);
controls.autoRotate = true;
}
}, 60); // 60毫秒的节流时间
// 添加清除选择的函数
const clearSelection = () => {
setHoveredCountry(null);
if (sceneRef.current) {
sceneRef.current.lastClickedCountry = null;
}
controls.autoRotate = true;
};
2025-03-28 02:27:42 +08:00
// 添加鼠标点击事件处理
const onClick = (event: MouseEvent) => {
if (!containerRef.current || !sceneRef.current) return;
// 计算射线
calculateMouseRay(event);
// 查找相交的国家
const countryName = findIntersectedCountry();
if (countryName) {
setHoveredCountry(countryName);
controls.autoRotate = false;
sceneRef.current.lastClickedCountry = countryName;
} else {
clearSelection();
}
};
// 添加鼠标双击事件处理
const onDoubleClick = () => {
clearSelection();
};
containerRef.current.addEventListener('mousemove', onMouseMove);
containerRef.current.addEventListener('click', onClick);
containerRef.current.addEventListener('dblclick', onDoubleClick);
// 创建一个辅助函数,强制所有球体使用完全不透明的材质
const enforceOpaqueMaterials = () => {
// 修复地球材质
if (earth && earth.material) {
const mat = earth.material as THREE.MeshBasicMaterial;
mat.transparent = false;
mat.opacity = 1.0;
mat.depthTest = true;
mat.depthWrite = true;
mat.side = THREE.FrontSide;
mat.needsUpdate = true;
}
// 修复所有背景球体材质
[bgSphere, midSphere, innerSphere].forEach(sphere => {
if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) {
sphere.material.transparent = false;
sphere.material.opacity = 1.0;
sphere.material.depthTest = true;
sphere.material.depthWrite = true;
sphere.material.needsUpdate = true;
}
});
};
2025-03-27 21:40:41 +08:00
2025-03-28 02:27:42 +08:00
// 应用一次以确保初始状态正确
enforceOpaqueMaterials();
2025-03-27 21:40:41 +08:00
2025-03-28 02:27:42 +08:00
// 动画循环
const animate = () => {
if (!sceneRef.current) return;
// 获取当前帧计数
const frameCount = sceneRef.current.animationId || 0;
// 每5帧检查一次材质状态
if (frameCount % 5 === 0) {
// 使用统一函数更新材质状态 - 仅处理球体
[bgSphere, midSphere, innerSphere, earth].forEach(sphere => {
if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) {
sphere.material.transparent = false;
sphere.material.opacity = 1.0;
sphere.material.depthTest = true;
sphere.material.depthWrite = true;
sphere.material.needsUpdate = true;
}
});
}
// 更新控制器
sceneRef.current.controls.update();
// 相机变化检测
const cameraPosition = sceneRef.current.camera.position.clone();
let cameraChanged = false;
if (sceneRef.current.lastCameraPosition) {
const distance = cameraPosition.distanceTo(sceneRef.current.lastCameraPosition);
cameraChanged = distance > 0.35;
}
// 只有当相机移动且有最后鼠标事件时,才重新触发鼠标事件
if (cameraChanged && sceneRef.current.lastMouseEvent && !sceneRef.current.lastClickedCountry) {
onMouseMove(sceneRef.current.lastMouseEvent);
// 确保已访问区域的边界线始终在未访问区域之上
sceneRef.current.countries.forEach((object) => {
object.traverse((child) => {
if (child instanceof THREE.Line) {
const childIsVisited = child.userData?.isVisited === true;
if (childIsVisited) {
child.renderOrder = 3;
if (child.material instanceof THREE.LineBasicMaterial) {
child.material.depthTest = false;
child.material.polygonOffset = true;
child.material.polygonOffsetFactor = -2;
child.material.polygonOffsetUnits = 1;
child.material.needsUpdate = true;
}
}
}
});
});
}
// 降低相机位置保存频率
if (frameCount % 20 === 0) {
sceneRef.current.lastCameraPosition = cameraPosition.clone();
}
// 渲染
sceneRef.current.renderer.render(scene, camera);
sceneRef.current.labelRenderer.render(scene, camera);
// 请求下一帧
sceneRef.current.animationId = requestAnimationFrame(animate);
};
// 覆盖渲染器的render方法确保每次渲染前重设材质状态
const originalRender = renderer.render;
renderer.render = function(scene, camera) {
// 在渲染前仅对球体对象强制更新材质状态
[bgSphere, midSphere, innerSphere, earth].forEach(sphere => {
if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) {
sphere.material.transparent = false;
sphere.material.opacity = 1.0;
sphere.material.needsUpdate = true;
}
});
// 调用原始渲染方法
originalRender.call(this, scene, camera);
};
// 保存场景引用,添加中间层球体引用
sceneRef.current = {
scene,
camera,
renderer,
labelRenderer,
controls,
earth,
bgSphere,
countries,
raycaster,
mouse,
animationId: null,
lastCameraPosition: null,
lastMouseEvent: null,
lastClickedCountry: null
};
// 将视图旋转到中国位置
const positionCameraToFaceChina = () => {
// 中国的中心点经纬度
const centerLon = 104.195397;
const centerLat = 35.86166;
// 检查是否为小屏幕
const isSmallScreen = containerRef.current && containerRef.current.clientWidth < 640;
// 根据屏幕大小设置不同的相机初始位置
let fixedPosition;
if (isSmallScreen) {
// 小屏幕显示距离更远,以便看到更多地球
fixedPosition = new THREE.Vector3(-2.10, 3.41, -7.5);
} else {
// 大屏幕使用原来的位置
fixedPosition = new THREE.Vector3(-2.10, 3.41, -6.08);
}
// 应用位置
camera.position.copy(fixedPosition);
camera.lookAt(0, 0, 0);
controls.update();
// 禁用自动旋转一段时间
controls.autoRotate = false;
// 保存相机位置
const chinaCameraInitialPosition = camera.position.clone();
// 3秒后恢复旋转
setTimeout(() => {
if (sceneRef.current) {
sceneRef.current.controls.autoRotate = true;
}
}, 3000);
// 渲染
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
// 将初始相机位置保存在sceneRef中
if (sceneRef.current) {
sceneRef.current.lastCameraPosition = chinaCameraInitialPosition;
2025-03-27 21:40:41 +08:00
}
};
2025-03-28 02:27:42 +08:00
// 应用初始相机位置
positionCameraToFaceChina();
// 确保已访问区域的边界线优先显示
setTimeout(() => {
ensureVisitedBordersOnTop();
}, 100); // 稍微延迟确保所有元素都已创建完成
// 开始动画
sceneRef.current.animationId = requestAnimationFrame(animate);
// 处理窗口大小变化
const handleResize = () => {
if (!containerRef.current || !sceneRef.current) return;
const { camera, renderer, labelRenderer } = sceneRef.current;
const width = containerRef.current.clientWidth;
const height = containerRef.current.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
labelRenderer.setSize(width, height);
// 在调整大小后立即渲染一次,以防止闪烁或偏移
renderer.render(sceneRef.current.scene, camera);
labelRenderer.render(sceneRef.current.scene, camera);
// 确保已访问区域的边界线总是显示在顶层
ensureVisitedBordersOnTop();
};
2025-03-27 21:40:41 +08:00
window.addEventListener('resize', handleResize);
2025-03-28 02:27:42 +08:00
// 监听暗色模式变化
2025-03-27 21:40:41 +08:00
const darkModeObserver = new MutationObserver(() => {
2025-03-28 02:27:42 +08:00
if (!sceneRef.current) return;
const isDark = document.documentElement.classList.contains('dark');
const newColors = getColors();
// 保持场景背景透明
sceneRef.current.scene.background = null;
// 设置透明背景
sceneRef.current.renderer.setClearColor(0x000000, 0);
// 更新材质颜色函数
const updateMeshColor = (obj: THREE.Object3D, color: string) => {
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshBasicMaterial) {
obj.material.color.set(color);
obj.material.transparent = false;
obj.material.opacity = 1.0;
obj.material.needsUpdate = true;
}
};
// 更新背景相关球体颜色
const bgColor = newColors.bgSphere;
// 直接更新三个球体对象,而不是遍历整个场景
[bgSphere, midSphere, innerSphere].forEach(sphere => {
if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) {
updateMeshColor(sphere, bgColor);
}
});
// 更新地球颜色
if (sceneRef.current.earth && sceneRef.current.earth.material) {
const earthMat = sceneRef.current.earth.material as THREE.MeshBasicMaterial;
earthMat.color.set(new THREE.Color(newColors.earthBase));
earthMat.side = THREE.DoubleSide;
earthMat.needsUpdate = true;
}
// 更新国家颜色
sceneRef.current.countries.forEach((object, name) => {
const isVisited = visitedPlaces.includes(name);
object.traverse((child) => {
if (child instanceof THREE.Line) {
const childIsVisited = child.userData?.isVisited === true;
let color;
if (name.startsWith('中国-')) {
color = childIsVisited ? newColors.visitedBorder : newColors.chinaBorder;
} else if (name === '中国') {
color = newColors.chinaBorder;
} else {
color = isVisited ? newColors.visitedBorder : newColors.border;
2025-03-27 21:40:41 +08:00
}
2025-03-28 02:27:42 +08:00
if (child.material instanceof THREE.LineBasicMaterial) {
child.material.color.set(new THREE.Color(color));
child.userData.originalColor = color;
2025-03-27 21:40:41 +08:00
}
2025-03-28 02:27:42 +08:00
}
});
});
// 暗色模式变化后重新应用渲染优先级
setTimeout(() => {
ensureVisitedBordersOnTop();
}, 50);
});
2025-03-27 21:40:41 +08:00
darkModeObserver.observe(document.documentElement, { attributes: true });
2025-03-28 02:27:42 +08:00
// 添加一个函数来确保已访问区域的边界线总是显示在顶层
const ensureVisitedBordersOnTop = () => {
if (!sceneRef.current) return;
// 优化边界线的渲染优先级和材质属性
const updateLineMaterial = (child: THREE.Object3D) => {
if (!(child instanceof THREE.Line)) return;
const childIsVisited = child.userData?.isVisited === true;
child.renderOrder = childIsVisited ? 3 : 2;
if (child.material instanceof THREE.LineBasicMaterial) {
child.material.depthTest = false;
if (childIsVisited) {
child.material.polygonOffset = true;
child.material.polygonOffsetFactor = -2;
child.material.polygonOffsetUnits = 1;
}
child.material.needsUpdate = true;
}
};
// 遍历所有国家对象
sceneRef.current.countries.forEach(object => {
object.traverse(updateLineMaterial);
});
// 立即渲染一次以应用更改
sceneRef.current.renderer.render(sceneRef.current.scene, sceneRef.current.camera);
sceneRef.current.labelRenderer.render(sceneRef.current.scene, sceneRef.current.camera);
};
return () => {
2025-03-28 02:27:42 +08:00
// 清理资源和事件监听器
if (sceneRef.current) {
// 取消动画帧
if (sceneRef.current.animationId !== null) {
cancelAnimationFrame(sceneRef.current.animationId);
}
// 处理渲染器的处理
sceneRef.current.renderer.dispose();
sceneRef.current.renderer.forceContextLoss();
sceneRef.current.renderer.domElement.remove();
// 移除标签渲染器
if (sceneRef.current.labelRenderer) {
sceneRef.current.labelRenderer.domElement.remove();
}
// 释放控制器
if (sceneRef.current.controls) {
sceneRef.current.controls.dispose();
}
}
// 移除事件监听器
if (containerRef.current) {
containerRef.current.removeEventListener('mousemove', onMouseMove);
containerRef.current.removeEventListener('click', onClick);
containerRef.current.removeEventListener('dblclick', onDoubleClick);
2025-03-27 21:40:41 +08:00
}
2025-03-28 02:27:42 +08:00
// 移除窗口事件监听器
2025-03-27 21:40:41 +08:00
window.removeEventListener('resize', handleResize);
2025-03-28 02:27:42 +08:00
// 移除观察器
2025-03-27 21:40:41 +08:00
darkModeObserver.disconnect();
};
}, [visitedPlaces]);
2025-03-27 21:40:41 +08:00
return (
2025-03-28 02:27:42 +08:00
<div className="relative">
<div
ref={containerRef}
className="w-full h-[400px] sm:h-[450px] md:h-[500px] lg:h-[600px] xl:h-[700px]"
/>
{hoveredCountry && (
<div className="absolute bottom-5 left-0 right-0 text-center">
<div className="inline-block bg-white dark:bg-gray-800 px-4 py-2 rounded-lg shadow-md">
<p className="text-gray-800 dark:text-white font-medium text-lg">
{hoveredCountry}
{hoveredCountry && visitedPlaces.includes(hoveredCountry) ?
' ✓ 已去过' : ' 尚未去过'}
</p>
</div>
</div>
)}
</div>
2025-03-27 21:40:41 +08:00
);
};
export default WorldHeatmap;