<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<script type="module" src="./main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
index.html
global.css
main.js
App.vue
HelloBicool.vue
package.json
现在支持上传本地图片了!
index.html
global.css
body {
background-color: #fefefe;
}
编辑器加载中
main.js
import { createApp } from 'vue'
import './global.css'
import App from './App.vue'
createApp(App).mount('#app')
console.log(["Hello 笔.COOL 控制台"])
编辑器加载中
App.vue
<script setup>
import HelloBicool from './HelloBicool.vue'
import { ref } from "vue"
</script>
<template>
<div class="container">
<HelloBicool :title="title" :descr="descr" />
</div>
</template>
<style lang="scss" scoped>
$bg: #f0f2f5;
.container {
background: $bg;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.logo {
height: 40px;
width: 40px;
margin-top: 20px;
}
</style>
编辑器加载中
HelloBicool.vue
<template>
<div ref="container" class="canvas-container">
<div class="control-panel">
<button @click="toggleHeatmap">{{ heatmapText }}</button>
<button @click="toggleSpheres">{{ sphereText }}</button>
<button @click="toggleSensors">{{ sensorText }}</button>
<div class="interpolation-control">
<h4>地形控制点插值</h4>
<div>
<label>权重指数:</label>
<input type="range" min="1" max="4" step="0.1" v-model.number="interpolationParams.power">
<span>{{ interpolationParams.power }}</span>
</div>
<div>
<label>影响半径:</label>
<input type="range" min="50" max="150" step="5" v-model.number="interpolationParams.radius">
<span>{{ interpolationParams.radius }}</span>
</div>
<div>
<label>噪声强度:</label>
<input type="range" min="0" max="1" step="0.1" v-model.number="interpolationParams.noiseScale">
<span>{{ interpolationParams.noiseScale }}</span>
</div>
</div>
<div class="alpha-control">
<label>地形透明度:</label>
<input type="range" min="0" max="1" step="0.1" v-model.number="terrainAlpha">
<span>{{ (terrainAlpha * 100).toFixed(0) }}%</span>
</div>
<button @click="resetView">重置视角</button>
</div>
<div id="device-info" class="info-panel"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = ref(null);
const labels = ref([]);
// 响应式状态
const heatmapVisible = ref(true);
const spheresVisible = ref(true);
const sensorsVisible = ref(true);
const labelsVisible = ref(true);
const heatmapText = ref('隐藏热力图');
const sphereText = ref('隐藏噪声球体');
const sensorText = ref('隐藏监测点');
const terrainAlpha = ref(0.8);
const interpolationParams = ref({
power: 2,
radius: 100,
noiseScale: 0.3
});
// 控制器设置
function setupControls() {
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = true;
controls.maxPolarAngle = Math.PI / 2;
}
// 灯光设置
function setupLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
}
function setupCamera() {
camera.position.set(0, 50, 50);
camera.lookAt(0, 0, 0);
}
// Three.js 相关变量
let scene, camera, renderer, controls,raycaster;
const noiseSpheres = [];
const sensorLabels = [];
const deviceLabels = [];
const sensors = [];
const controlPoints = ref([
{ x: -80, y: -80, z: 15 }, // 西北角高峰
{ x: 0, y: 0, z: 30 }, // 中心主峰
{ x: 80, y: 80, z: 20 }, // 东南角次峰
{ x: 50, y: -30, z: 18 },
{ x: -60, y: 40, z: 22 },
{ x: 30, y: 60, z: 16 },
{ x: -40, y: -60, z: 14 },
{ x: 70, y: -20, z: 17 },
{ x: -20, y: 70, z: 19 },
{ x: 60, y: 40, z: 21 },
{ x: -70, y: 20, z: 13 },
]);
// 地形着色器
// const terrainShader = {
// uniforms: {
// time: { value: 0 },
// alpha: { value: 0.8 }
// },
// vertexShader: `
// uniform float time;
// varying vec3 vPosition;
// void main() {
// vec3 pos = position;
// float wave = sin(pos.x * 0.2 + time) * cos(pos.y * 0.2 + time) * 2.0;
// pos.z += wave;
// vPosition = pos;
// gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
// }
// `,
// fragmentShader: `
// varying vec3 vPosition;
// uniform float alpha;
// void main() {
// float heightFactor = smoothstep(-10.0, 10.0, vPosition.z);
// vec3 color = mix(vec3(0,1,0), vec3(1,0,0), heightFactor);
// gl_FragColor = vec4(color, alpha);
// }
// `
// };
const terrainShader = {
uniforms: {
time: { value: 0 },
alpha: { value: 0.8 },
minHeight: { value: 0 }, // 实际地形最小高度
maxHeight: { value: 30 } , // 实际地形最大高度
waveAmplitude: { value: 1.5 }, // 降低波浪幅度
},
vertexShader: `
uniform float time;
uniform float waveAmplitude;
varying vec3 vPosition;
void main() {
vec3 pos = position;
// 优化后的波浪函数
float wave = sin(pos.x * 0.15 + time) *
cos(pos.y * 0.15 + time) *
waveAmplitude;
pos.z += wave * smoothstep(0.0, 10.0, pos.z); // 根据高度衰减波浪
vPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec3 vPosition;
uniform float alpha;
uniform float minHeight;
uniform float maxHeight;
void main() {
// 标准化高度到0-100范围
float normalizedHeight = (vPosition.z - minHeight) / (maxHeight - minHeight) * 100.0;
vec3 color;
if(normalizedHeight < 50.0) {
// 绿色区间 (0,1,0)
color = vec3(0.0, 1.0, 0.0);
} else if(normalizedHeight < 65.0) {
// 黄色区间 (1,1,0)
color = vec3(1.0, 1.0, 0.0);
} else if(normalizedHeight < 80.0) {
// 橙色区间 (1,0.5,0)
color = vec3(1.0, 0.5, 0.0);
} else {
// 红色区间 (1,0,0)
color = vec3(1.0, 0.0, 0.0);
}
// 添加平滑过渡
color = mix(
color,
vec3(1.0, 1.0, 0.0),
smoothstep(48.0, 52.0, normalizedHeight)
);
color = mix(
color,
vec3(1.0, 0.5, 0.0),
smoothstep(63.0, 67.0, normalizedHeight)
);
color = mix(
color,
vec3(1.0, 0.0, 0.0),
smoothstep(78.0, 82.0, normalizedHeight)
);
gl_FragColor = vec4(color, alpha);
}
`
};
// 创建设备几何体
function createDeviceGeometry(device) {
switch (device.type) {
case 'transformer':
return new THREE.CylinderGeometry(4, 4, 6, 16);
case 'switchroom':
return new THREE.BoxGeometry(12, 5, 8);
default:
return new THREE.BoxGeometry(5, 5, 5);
}
}
// 初始化场景
function initScene() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
container.value.appendChild(renderer.domElement);
setupCamera();
setupControls();
setupLights();
createTerrain();
createSubstation();
setupRaycaster();
}
// 地形生成相关函数
// function generateHeight(x, y) {
// let total = 0, weightSum = 0;
// controlPoints.value.forEach(point => {
// const dx = x - point.x, dy = y - point.y;
// const distance = Math.sqrt(dx*dx + dy*dy);
// if (distance < interpolationParams.value.radius) {
// const weight = 1 / Math.pow(distance, interpolationParams.value.power);
// total += weight * point.z;
// weightSum += weight;
// }
// });
// return weightSum > 0 ? total / weightSum + Math.random() * interpolationParams.value.noiseScale : 0;
// }
const WIDTH = 200;
const SEGMENTS = 150; // 提高网格细分度
function createTerrain() {
// const geometry = new THREE.PlaneGeometry(200, 200, 100, 100);
// const positions = geometry.attributes.position.array;
// for (let i = 0; i < positions.length; i += 3) {
// positions[i + 2] = generateHeight(positions[i], positions[i + 1]);
// }
// geometry.computeVertexNormals();
// const material = new THREE.ShaderMaterial({
// uniforms: terrainShader.uniforms,
// vertexShader: terrainShader.vertexShader,
// fragmentShader: terrainShader.fragmentShader,
// wireframe: false,
// side: THREE.DoubleSide,
// transparent: true
// });
// const terrain = new THREE.Mesh(geometry, material);
// terrain.rotation.x = -Math.PI / 2;
// scene.add(terrain);
const geometry = new THREE.PlaneGeometry(WIDTH, WIDTH, SEGMENTS, SEGMENTS);
const positions = geometry.attributes.position.array;
// 预计算高度范围
let minHeight = Infinity;
let maxHeight = -Infinity;
// 生成基础高度
for (let i = 0; i < positions.length; i += 3) {
const height = generateHeight(positions[i], positions[i + 1]);
positions[i + 2] = height;
minHeight = Math.min(minHeight, height);
maxHeight = Math.max(maxHeight, height);
}
// 平滑处理
smoothTerrain(geometry, 2); // 增加平滑迭代次数
// 设置着色器参数
terrainShader.uniforms.minHeight.value = minHeight;
terrainShader.uniforms.maxHeight.value = maxHeight;
// 创建几何体
const material = new THREE.ShaderMaterial({
// ...保持原有参数
});
// 边缘保护处理
protectTerrainEdges(geometry, WIDTH);
}
// 新增平滑算法
function smoothTerrain(geometry, iterations = 1) {
const positions = geometry.attributes.position.array;
const original = new Float32Array(positions);
for (let n = 0; n < iterations; n++) {
for (let i = 0; i < positions.length; i += 3) {
if (i % (SEGMENTS * 3) === 0) continue; // 跳过边缘
// 获取相邻顶点高度
const neighbors = [
original[i - 3], // 左
original[i + 3], // 右
original[i - SEGMENTS * 3], // 上
original[i + SEGMENTS * 3] // 下
].filter(v => !isNaN(v));
// 计算平均高度
const avg = neighbors.reduce((a, b) => a + b, 0) / neighbors.length;
positions[i + 2] = (original[i + 2] + avg) * 0.5;
}
}
}
// 边缘保护函数
function protectTerrainEdges(geometry, width) {
const positions = geometry.attributes.position.array;
const center = width / 2;
for (let i = 0; i < positions.length; i += 3) {
const dx = Math.abs(positions[i] - center);
const dy = Math.abs(positions[i + 1] - center);
const distance = Math.sqrt(dx * dx + dy * dy);
// 边缘衰减系数
const edgeFactor = Math.min(1, distance / (center * 0.8));
positions[i + 2] *= 1 - edgeFactor * 0.3; // 降低边缘高度
}
}
// 优化后的高度生成
function generateHeight(x, y) {
let total = 0, weightSum = 0;
const BASE_HEIGHT = 5; // 基础高度
controlPoints.value.forEach(point => {
const dx = x - point.x;
const dy = y - point.y;
const distance = Math.sqrt(dx*dx + dy*dy);
// 动态影响半径
const dynamicRadius = interpolationParams.value.radius *
(1 + 0.1 * Math.sin(x * 0.05) * Math.cos(y * 0.05));
if (distance < dynamicRadius) {
const weight = 1 / (1 + Math.pow(distance, interpolationParams.value.power));
total += weight * point.z;
weightSum += weight;
}
});
// 确保最低高度
return weightSum > 0 ?
(total / weightSum) + BASE_HEIGHT +
Math.random() * interpolationParams.value.noiseScale :
BASE_HEIGHT + Math.random();
}
// 变电站设备创建(保留原有逻辑)
function createSubstation() {
const devices = [
{
type: 'transformer',
name: '主变压器1',
position: { x: -10, y: 0, z: 0 },
noiseLevel: 70,
size: { width: 10, height: 4, depth: 6 },
rotation: { y: Math.PI },
},
{
type: 'transformer',
name: '主变压器2',
position: { x: 10, y: 0, z: 0 },
noiseLevel: 75,
size: { width: 10, height: 4, depth: 6 },
rotation: { y: Math.PI },
},
{
type: 'switchroom',
name: '配电房',
position: { x: -30, y: 0, z: -5 },
noiseLevel: 38,
size: { width: 12, height: 5, depth: 8 },
},
{
type: 'switchroom',
name: '配电房',
position: { x: 30, y: 0, z: -5 },
noiseLevel: 38,
size: { width: 12, height: 5, depth: 8 },
},
{
type: 'controlroom',
name: '值班室',
position: { x: -60, y: 0, z: -30 },
noiseLevel: 38,
size: { width: 20, height: 10, depth: 25 },
},
{
type: 'capacitor',
name: '电容器组1',
position: { x: -25, y: 0, z: -15 },
noiseLevel: 38,
size: { width: 10, height: 10, depth: 10 },
},
// type: 'reactor',
// name: '电抗器1',
// position: { x: 0, y: 0, z: -15 },
// noiseLevel: 62,
// size: { width: 2, height: 3, depth: 2 }
// },
]
devices.forEach((device) => {
const geometry = createDeviceGeometry(device);
const material = new THREE.MeshPhongMaterial({
color: getDeviceColor(device.type),
shininess: 100,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(device.position.x, device.position.y, device.position.z);
mesh.castShadow = true;
scene.add(mesh);
addLabel(mesh, device.type);
});
}
// 响应式更新
watch(interpolationParams, () => updateTerrain(), { deep: true });
watch(terrainAlpha, (val) => {
scene.traverse(child => {
if(child.material?.uniforms?.alpha) child.material.uniforms.alpha.value = val;
});
});
// 其他原有功能(热力图、噪声球体等)
// ...
onMounted(() => {
initScene();
animate();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
renderer.dispose();
window.removeEventListener('resize', handleResize);
});
// 辅助函数和动画循环
function handleResize() {
camera.aspect = container.value.clientWidth / container.value.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
}
// 交互系统
function setupRaycaster() {
raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('click', (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
showObjectInfo(intersects[0].object);
}
});
}
function animate() {
requestAnimationFrame(animate);
controls.update();
updateLabels();
updateTerrainWaves();
renderer.render(scene, camera);
}
// 辅助函数
function getDeviceColor(type) {
const colors = {
transformer: 0x888888,
switchroom: 0x4682b4,
};
return colors[type] || 0x666666;
}
function addLabel(obj, text) {
const label = document.createElement('div');
label.className = 'object-label';
label.textContent = text;
container.value.appendChild(label);
labels.value.push({ element: label, object: obj });
}
function updateTerrainWaves() {
scene.traverse(child => {
if(child.material?.uniforms?.time) {
child.material.uniforms.time.value = performance.now() / 1000;
}
});
}
// 更新标签位置
function updateLabels() {
labels.value.forEach(({ element, object }) => {
const vector = object.position.clone().project(camera);
element.style.transform = `translate(
${(vector.x * 0.5 + 0.5) * 100}%,
${(-vector.y * 0.5 + 0.5) * 100}%
)`;
});
}
</script>
<style>
.canvas-container {
width: 100vw;
height: 100vh;
position: relative;
}
.control-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
z-index: 100;
}
.control-panel button {
display: block;
margin: 5px;
padding: 8px 15px;
background: #2196f3;
border: none;
color: white;
cursor: pointer;
border-radius: 3px;
}
.info-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px;
border-radius: 5px;
max-width: 250px;
display: none;
}
.object-label {
position: absolute;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 5px 10px;
border-radius: 3px;
pointer-events: none;
transform: translate(-50%, -50%);
}
.alpha-control {
margin-top: 10px;
color: white;
}
.alpha-control input[type='range'] {
vertical-align: middle;
width: 120px;
}
.terrain-control {
color: #fff;
}
.interpolation-control {
h4 {
color: #fff;
}
label {
color: #fff;
}
span {
color: #fff;
}
}
</style>
编辑器加载中
package.json
注意:新添加的依赖包首次加载可能会报错,稍后再次刷新即可
{
"dependencies": {
"vue": "3.5.13",
"three": "^0.167.1",
"gsap": "^3.12.7"
}
}
编辑器加载中
预览页面