news 2026/3/3 1:14:45

web前端:基于Three.js库的星云探索交互网页

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
web前端:基于Three.js库的星云探索交互网页

设计概述

本次分享将介绍如何使用Three.js库创建一个沉浸式的 "星云探索" 交互应用。这个项目通过WebGL构建可视化的星系模型,结合自定义着色器实现动态星云效果,构建了一个包含星系生成、交互探索、状态管理的完整应用,展现了WebGL在创意交互领域的强大潜力。

效果概览

应用运行后呈现以下核心效果:

1.开始探索:打开应用后点击 "开始探索" 按钮进入星云探索模式

2.基本操作

移动鼠标可旋转星系视角

点击星云区域开始扫描(持续 7 秒,有动态脉冲效果)

3.决策流程

扫描完成后显示星系生命形式

选择1:再次点击星云→摧毁星系

选择2:点击 "放弃摧毁"→进入拯救流程

拯救流程需在倒计时结束前点击 "紧急终止摧毁" 确认

4.数据统计:左上角实时显示摧毁/拯救星系数量

星云-演示视频

文件结构

项目包含三个核心文件:

  1. 星云.html——定义页面结构、引入外部js库等
  2. style.css——样式设置文件
  3. index.js——创建场景、生成星系、处理交互事件和动画控制

文件结构如下(完整版代码见下文):

星云/ ├── css/ │ └── style.css ├── js/ │ ├── three.min.js │ ├── TweenMax.min.js │ ├── stat.js(可选) │ └── index.js └── 星云.html

或点击下方链接获取完整版资源包(另包含外部库文件):

web前端:基于Three.js库的星云探索交互网页-对应源码资源-CSDN下载

重点解析

1.星云.html

项目的基础框架,主要包含三个部分:

1.1页面结构

定义了交互所需的DOM元素,包括开始按钮、日志显示、提示文本、操作按钮和统计面板等:

<!-- 开始按钮 --> <button id="exploreButton" style="...">开始探索</button> <!-- 交互流程所需DOM元素 --> <div id="log" style="..."></div> <div id="instruction" style="..."></div> <div id="timeline" style="..."></div> <div id="good-person" style="...">放弃摧毁(拯救星系)</div> <div id="abort" class="metal" style="...">紧急终止摧毁</div> <!-- 统计面板 --> <div style="..."> <div>摧毁星系:<span id="destroyedresult">0</span></div> <div>拯救星系:<span id="savedresult">0</span></div> </div>
1.2着色器定义

包含顶点着色器 (vShader) 和片段着色器 (fShader),负责星云的视觉呈现:

<!-- 顶点着色器 --> <script id='vShader' type='x-vertex/x-shader'> uniform float size; uniform float t; // 扫描脉冲参数 uniform float z; // 摧毁动画参数 uniform float pixelRatio; varying vec3 vPosition; varying vec3 mPosition; varying float gas; void main(){ vPosition = position; float a = length(position); // 计算扫描脉冲效果 float b = 0.0; if(t > 0.0) b = max(0.0, (cos(a/20.0 - t*0.02) - 0.99) * 3.0 / a); // 计算摧毁动画效果 if(z > 0.0) b = max(0.0, cos(a/40.0 - z*0.01 + 2.0)); mPosition = position * (1.0 + b * 4.0); vec4 mvPosition = modelViewMatrix * vec4(mPosition, 1.0); gl_Position = projectionMatrix * mvPosition; // 计算粒子大小与气体效果 gas = max(0.0, sin(-a/20.0)); gl_PointSize = pixelRatio * size * (1.0 + gas * 2.0) / length(mvPosition.xyz); } </script> <!-- 片段着色器 --> <script id='fShader' type='x-fragment/x-shader'> uniform float z; varying vec3 vPosition; varying vec3 mPosition; varying float gas; void main(){ float a = distance(mPosition, vPosition); if(a > 0.0) a = 1.0; float b = max(0.32, 0.0065 * length(vPosition)); float c = distance(gl_PointCoord, vec2(0.5)); // 计算恒星与气体视觉效果 float starlook = -(c - 0.5) * 1.2 * gas; float gaslook = (1.0 - gas) / (c * 10.0); float texture = starlook + gaslook; gl_FragColor = vec4(0.32, 0.28, b, 1.0) * texture * (1.0 - a * 0.35); // 摧毁时的颜色变化 if(z > 0.0) gl_FragColor *= cos(1.57 * z / 322.0) * (1.0 - 0.001 * length(mPosition)); } </script>
1.3资源引入

引入项目所需的外部资源和脚本:

<!-- 先加载依赖库 --> <script src="js/three.min.js"></script> <script src="js/TweenMax.min.js"></script> <script src="js/stat.js" defer></script> <!-- 最后加载业务逻辑 --> <script src="js/index.js" defer></script>
星云.html完整版代码:
<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>星云</title> <meta charset="utf-8"> <meta name="description" content="WebGL galaxy with shaders" /> <link rel="stylesheet" href="css/style.css"> </head> <body> <!-- 开始按钮 --> <button id="exploreButton" style=" position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 15px 30px; font-size: 18px; background: #33c; color: white; border: none; border-radius: 8px; cursor: pointer; z-index: 9999; ">开始探索</button> <!-- 交互流程所需DOM元素 --> <div id="log" style=" position:absolute; top: 20px; left: 20px; color: white; font-size: 14px; z-index: 9999; "></div> <div id="instruction" style=" position:absolute; top: 100%; left: 0; width: 100%; text-align: center; padding: 10px 0; color: #f90; font-size: 16px; z-index: 9999; "></div> <div id="timeline" style=" position:absolute; bottom: 40px; left: 50%; transform: translateX(-50%); width: 80%; height: 5px; background: #333; z-index: 9999; "></div> <div id="good-person" style=" position:absolute; bottom: -50px; left: 50%; transform: translateX(-50%); padding: 8px 20px; background: #2ecc71; color: white; border-radius: 5px; cursor: pointer; z-index: 9999; ">放弃摧毁(拯救星系)</div> <!-- 紧急终止按钮(初始隐藏) --> <div id="abort" class="metal" style=" position:absolute; bottom: -50px; left: 50%; transform: translateX(-50%); margin-top: 10px; padding: 10px 30px; background: #ff0000; color: white; border-radius: 5px; cursor: default; font-weight: bold; z-index: 9999; ">紧急终止摧毁(2.5秒后可点击)</div> <!-- 统计面板 --> <div style="position:absolute; top: 60px; left: 20px; color: white; z-index: 9999;"> <div>摧毁星系:<span id="destroyedresult">0</span></div> <div>拯救星系:<span id="savedresult">0</span></div> </div> <!-- 着色器脚本--> <script id='vShader' type='x-vertex/x-shader'> uniform float size; uniform float t; uniform float z; uniform float pixelRatio; varying vec3 vPosition; varying vec3 mPosition; varying float gas; float a,b=0.; void main(){ vPosition=position; a=length(position); if(t>0.)b=max(0.,(cos(a/20.-t*.02)-.99)*3./a); if(z>0.)b=max(0.,cos(a/40.-z*.01+2.)); mPosition=position*(1.+b*4.); vec4 mvPosition=modelViewMatrix*vec4(mPosition,1.); gl_Position=projectionMatrix*mvPosition; gas=max(.0,sin(-a/20.)); gl_PointSize=pixelRatio*size*(1.+gas*2.)/length(mvPosition.xyz); } </script> <script id='fShader' type='x-fragment/x-shader'> uniform float z; varying vec3 vPosition; varying vec3 mPosition; varying float gas; void main(){ float a=distance(mPosition,vPosition); if(a>0.)a=1.; float b=max(.32,.0065*length(vPosition)); float c=distance(gl_PointCoord,vec2(.5)); float starlook=-(c-.5)*1.2*gas; float gaslook=(1.-gas)/(c*10.); float texture=starlook+gaslook; gl_FragColor=vec4(.32,.28,b,1.)*texture*(1.-a*.35); if(z>0.)gl_FragColor*=cos(1.57*z/322.)*(1.-.001*length(mPosition)); } </script> <!-- 先加载依赖库 --> <script src="js/three.min.js"></script> <script src="js/TweenMax.min.js"></script> <script src="js/stat.js" defer></script> <!-- 最后加载业务逻辑 --> <script src="js/index.js" defer></script> </body> </html>

2.style.css

样式文件主要控制页面基础布局和交互元素的视觉状态,完整版代码:

body{ margin:0; background-color:#000; overflow:hidden; } canvas{ cursor:grab; cursor:-webkit-grab; cursor:-moz-grab; } canvas:active{ cursor:grabbing; cursor:-webkit-grabbing; cursor:-moz-grabbing; }

要点

为canvas元素设置抓取 / 拖动状态的光标样式,提升交互体验

3.index.js

核心业务逻辑文件,实现了应用的主要功能:

3.1场景初始化

setScene()函数负责创建 Three.js 核心组件:

function setScene() { scene = new THREE.Scene(); // 创建相机 camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 1500); camera.position.set(-20, -155, 90); // 创建渲染器 renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x000000); // 黑色背景 document.body.appendChild(renderer.domElement); // 创建轨道控制器(限制平移和缩放) controls = new THREE.TrackballControls(camera, renderer.domElement); controls.noPan = true; controls.noZoom = true; controls.rotateSpeed = 20; // 初始化星系 setGalaxy(); // 绑定开始按钮事件与窗口大小调整响应 // ... }
3.2星系生成系统

newGalaxy()函数通过数学公式生成具有旋臂结构的星系:

function newGalaxy(_n, _axis1, _axis2, _armsAngle, _bulbSize, _ellipticity) { // 生成随机星系参数 // ... var stars = []; for (var i = 0; i < n; i++) { // 基于椭圆公式和旋臂角度计算恒星位置 var dist = Math.random(); var angle = (dist - bulbSize) * armsAngle; // ...更多位置计算逻辑 stars.push({ x: Math.cos(phi) * Math.cos(theta) * radius, y: Math.cos(phi) * Math.sin(theta) * radius, z: Math.sin(phi) * radius }); } return stars; }
3.3交互事件系统
// 绑定交互事件 function addInteraction() { // 移除重复绑定 renderer.domElement.removeEventListener('click', scan, false); renderer.domElement.removeEventListener('touchstart', scan, false); // 绑定点击和触摸事件 renderer.domElement.addEventListener('click', scan, false); renderer.domElement.addEventListener('touchstart', function (e) { e.preventDefault(); scan(); }, false); // ... }

要点:

实现完整的用户交互处理,包括:

鼠标/触摸事件监听(mousedown,mousemove,mouseup,touchstart,touchmove,touchend

键盘事件支持(keydown,keyup

星系扫描与决策流程(scan(),prepareDestroy(),destroy(),goodPerson()

3.4动画系统

通过animate()函数实现动画循环,更新场景状态:

function animate() { requestAnimationFrame(animate); // 更新扫描/摧毁脉冲参数 if (scanPulse) t += 0.7; if (destroyPulse) z += 0.7; // 更新着色器参数 galaxyMaterial.uniforms.t.value = t; galaxyMaterial.uniforms.z.value = z; // 自动旋转场景 scene.rotation.z += 0.001; controls.update(); renderer.render(scene, camera); }
3.5统计与分享功能

实现操作统计和社交分享功能:

// 更新统计数据 function setGauge(param) { // 更新拯救/摧毁计数 if (param === 'hero') { saved.innerHTML = (parseInt(saved.innerHTML) + 1).toString(); // ... } else if (param === 'bad') { destroyed.innerHTML = (parseInt(destroyed.innerHTML) + 1).toString(); // ... } // ... } // 更新分享链接 function updateLink() { // 根据统计数据生成分享文案和链接 // ... }
index.js完整版代码:
THREE.TrackballControls = function (object, domElement) { var _this = this; var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; this.object = object; this.domElement = (domElement !== undefined) ? domElement : document; // API 配置 this.enabled = true; this.screen = { left: 0, top: 0, width: 0, height: 0 }; this.rotateSpeed = 1.0; this.zoomSpeed = 1.2; this.panSpeed = 0.3; this.noRotate = false; this.noZoom = false; this.noPan = false; this.staticMoving = false; this.dynamicDampingFactor = 0.2; this.minDistance = 0; this.maxDistance = Infinity; this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/]; // 内部变量 this.target = new THREE.Vector3(); var EPS = 0.000001; var lastPosition = new THREE.Vector3(); var _state = STATE.NONE, _prevState = STATE.NONE, _eye = new THREE.Vector3(), _movePrev = new THREE.Vector2(), _moveCurr = new THREE.Vector2(), _lastAxis = new THREE.Vector3(), _lastAngle = 0, _zoomStart = new THREE.Vector2(), _zoomEnd = new THREE.Vector2(), _touchZoomDistanceStart = 0, _touchZoomDistanceEnd = 0, _panStart = new THREE.Vector2(), _panEnd = new THREE.Vector2(); // 重置初始状态 this.target0 = this.target.clone(); this.position0 = this.object.position.clone(); this.up0 = this.object.up.clone(); // 事件对象 var changeEvent = { type: 'change' }; var startEvent = { type: 'start' }; var endEvent = { type: 'end' }; // 处理窗口大小调整 this.handleResize = function () { if (this.domElement === document) { this.screen.left = 0; this.screen.top = 0; this.screen.width = window.innerWidth; this.screen.height = window.innerHeight; } else { var box = this.domElement.getBoundingClientRect(); var d = this.domElement.ownerDocument.documentElement; this.screen.left = box.left + window.pageXOffset - d.clientLeft; this.screen.top = box.top + window.pageYOffset - d.clientTop; this.screen.width = box.width; this.screen.height = box.height; } }; // 事件处理分发 this.handleEvent = function (event) { if (typeof this[event.type] === 'function') { this[event.type](event); } }; // 获取鼠标在屏幕上的坐标 var getMouseOnScreen = (function () { var vector = new THREE.Vector2(); return function getMouseOnScreen(pageX, pageY) { vector.set( (pageX - _this.screen.left) / _this.screen.width, (pageY - _this.screen.top) / _this.screen.height ); return vector; }; }()); // 获取鼠标在圆形区域的坐标 var getMouseOnCircle = (function () { var vector = new THREE.Vector2(); return function getMouseOnCircle(pageX, pageY) { vector.set( ((pageX - _this.screen.width * 0.5 - _this.screen.left) / (_this.screen.width * 0.5)), ((_this.screen.height + 2 * (_this.screen.top - pageY)) / _this.screen.width) ); return vector; }; }()); // 旋转相机逻辑 this.rotateCamera = (function () { var axis = new THREE.Vector3(), quaternion = new THREE.Quaternion(), eyeDirection = new THREE.Vector3(), objectUpDirection = new THREE.Vector3(), objectSidewaysDirection = new THREE.Vector3(), moveDirection = new THREE.Vector3(), angle; return function rotateCamera() { moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0); angle = moveDirection.length(); if (angle) { _eye.copy(_this.object.position).sub(_this.target); eyeDirection.copy(_eye).normalize(); objectUpDirection.copy(_this.object.up).normalize(); objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize(); objectUpDirection.setLength(_moveCurr.y - _movePrev.y); objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x); moveDirection.copy(objectUpDirection.add(objectSidewaysDirection)); axis.crossVectors(moveDirection, _eye).normalize(); angle *= _this.rotateSpeed; quaternion.setFromAxisAngle(axis, angle); _eye.applyQuaternion(quaternion); _this.object.up.applyQuaternion(quaternion); _lastAxis.copy(axis); _lastAngle = angle; } else if (!_this.staticMoving && _lastAngle) { _lastAngle *= Math.sqrt(1.0 - _this.dynamicDampingFactor); _eye.copy(_this.object.position).sub(_this.target); quaternion.setFromAxisAngle(_lastAxis, _lastAngle); _eye.applyQuaternion(quaternion); _this.object.up.applyQuaternion(quaternion); } _movePrev.copy(_moveCurr); }; }()); // 缩放相机逻辑 this.zoomCamera = function () { var factor; if (_state === STATE.TOUCH_ZOOM_PAN) { factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; _touchZoomDistanceStart = _touchZoomDistanceEnd; _eye.multiplyScalar(factor); } else { factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed; if (factor !== 1.0 && factor > 0.0) { _eye.multiplyScalar(factor); if (_this.staticMoving) { _zoomStart.copy(_zoomEnd); } else { _zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor; } } } }; // 平移相机逻辑 this.panCamera = (function () { var mouseChange = new THREE.Vector2(), objectUp = new THREE.Vector3(), pan = new THREE.Vector3(); return function panCamera() { mouseChange.copy(_panEnd).sub(_panStart); if (mouseChange.lengthSq()) { mouseChange.multiplyScalar(_eye.length() * _this.panSpeed); pan.copy(_eye).cross(_this.object.up).setLength(mouseChange.x); pan.add(objectUp.copy(_this.object.up).setLength(mouseChange.y)); _this.object.position.add(pan); _this.target.add(pan); if (_this.staticMoving) { _panStart.copy(_panEnd); } else { _panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(_this.dynamicDampingFactor)); } } }; }()); // 检查距离限制 this.checkDistances = function () { if (!_this.noZoom || !_this.noPan) { if (_eye.lengthSq() > _this.maxDistance * _this.maxDistance) { _this.object.position.addVectors(_this.target, _eye.setLength(_this.maxDistance)); _zoomStart.copy(_zoomEnd); } if (_eye.lengthSq() < _this.minDistance * _this.minDistance) { _this.object.position.addVectors(_this.target, _eye.setLength(_this.minDistance)); _zoomStart.copy(_zoomEnd); } } }; // 更新控制器状态 this.update = function () { _eye.subVectors(_this.object.position, _this.target); if (!_this.noRotate) { _this.rotateCamera(); } if (!_this.noZoom) { _this.zoomCamera(); } if (!_this.noPan) { _this.panCamera(); } _this.object.position.addVectors(_this.target, _eye); _this.checkDistances(); _this.object.lookAt(_this.target); if (lastPosition.distanceToSquared(_this.object.position) > EPS) { _this.dispatchEvent(changeEvent); lastPosition.copy(_this.object.position); } }; // 重置控制器 this.reset = function () { _state = STATE.NONE; _prevState = STATE.NONE; _this.target.copy(_this.target0); _this.object.position.copy(_this.position0); _this.object.up.copy(_this.up0); _eye.subVectors(_this.object.position, _this.target); _this.object.lookAt(_this.target); _this.dispatchEvent(changeEvent); lastPosition.copy(_this.object.position); }; // 键盘事件监听 function keydown(event) { if (_this.enabled === false) return; window.removeEventListener('keydown', keydown); _prevState = _state; if (_state !== STATE.NONE) { return; } else if (event.keyCode === _this.keys[STATE.ROTATE] && !_this.noRotate) { _state = STATE.ROTATE; } else if (event.keyCode === _this.keys[STATE.ZOOM] && !_this.noZoom) { _state = STATE.ZOOM; } else if (event.keyCode === _this.keys[STATE.PAN] && !_this.noPan) { _state = STATE.PAN; } } function keyup(event) { if (_this.enabled === false) return; _state = _prevState; window.addEventListener('keydown', keydown, false); } // 鼠标事件监听 function mousedown(event) { if (_this.enabled === false) return; event.preventDefault(); event.stopPropagation(); if (_state === STATE.NONE) { _state = event.button; } if (_state === STATE.ROTATE && !_this.noRotate) { _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); _movePrev.copy(_moveCurr); } else if (_state === STATE.ZOOM && !_this.noZoom) { _zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY)); _zoomEnd.copy(_zoomStart); } else if (_state === STATE.PAN && !_this.noPan) { _panStart.copy(getMouseOnScreen(event.pageX, event.pageY)); _panEnd.copy(_panStart); } document.addEventListener('mousemove', mousemove, false); document.addEventListener('mouseup', mouseup, false); _this.dispatchEvent(startEvent); } function mousemove(event) { if (_this.enabled === false) return; event.preventDefault(); event.stopPropagation(); if (_state === STATE.ROTATE && !_this.noRotate) { _movePrev.copy(_moveCurr); _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); } else if (_state === STATE.ZOOM && !_this.noZoom) { _zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); } else if (_state === STATE.PAN && !_this.noPan) { _panEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); } } function mouseup(event) { if (_this.enabled === false) return; event.preventDefault(); event.stopPropagation(); _state = STATE.NONE; document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); _this.dispatchEvent(endEvent); } function mousewheel(event) { if (_this.enabled === false) return; event.preventDefault(); event.stopPropagation(); var delta = 0; if (event.wheelDelta) { delta = event.wheelDelta / 40; } else if (event.detail) { delta = -event.detail / 3; } _zoomStart.y += delta * 0.01; _this.dispatchEvent(startEvent); _this.dispatchEvent(endEvent); } // 触摸事件监听 function touchstart(event) { if (_this.enabled === false) return; switch (event.touches.length) { case 1: _state = STATE.TOUCH_ROTATE; _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); _movePrev.copy(_moveCurr); break; default: _state = STATE.TOUCH_ZOOM_PAN; var dx = event.touches[0].pageX - event.touches[1].pageX; var dy = event.touches[0].pageY - event.touches[1].pageY; _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy); var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; _panStart.copy(getMouseOnScreen(x, y)); _panEnd.copy(_panStart); break; } _this.dispatchEvent(startEvent); } function touchmove(event) { if (_this.enabled === false) return; event.preventDefault(); event.stopPropagation(); switch (event.touches.length) { case 1: _movePrev.copy(_moveCurr); _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); break; default: var dx = event.touches[0].pageX - event.touches[1].pageX; var dy = event.touches[0].pageY - event.touches[1].pageY; _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy); var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; _panEnd.copy(getMouseOnScreen(x, y)); break; } } function touchend(event) { if (_this.enabled === false) return; switch (event.touches.length) { case 0: _state = STATE.NONE; break; case 1: _state = STATE.TOUCH_ROTATE; _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); _movePrev.copy(_moveCurr); break; } _this.dispatchEvent(endEvent); } // 禁用右键菜单 function contextmenu(event) { event.preventDefault(); } // 销毁控制器 this.dispose = function () { this.domElement.removeEventListener('contextmenu', contextmenu, false); this.domElement.removeEventListener('mousedown', mousedown, false); this.domElement.removeEventListener('mousewheel', mousewheel, false); this.domElement.removeEventListener('MozMousePixelScroll', mousewheel, false); this.domElement.removeEventListener('touchstart', touchstart, false); this.domElement.removeEventListener('touchend', touchend, false); this.domElement.removeEventListener('touchmove', touchmove, false); document.removeEventListener('mousemove', mousemove, false); document.removeEventListener('mouseup', mouseup, false); window.removeEventListener('keydown', keydown, false); window.removeEventListener('keyup', keyup, false); }; // 初始化事件监听 this.domElement.addEventListener('contextmenu', contextmenu, false); this.domElement.addEventListener('mousedown', mousedown, false); this.domElement.addEventListener('mousewheel', mousewheel, false); this.domElement.addEventListener('MozMousePixelScroll', mousewheel, false); this.domElement.addEventListener('touchstart', touchstart, false); this.domElement.addEventListener('touchend', touchend, false); this.domElement.addEventListener('touchmove', touchmove, false); window.addEventListener('keydown', keydown, false); window.addEventListener('keyup', keyup, false); this.handleResize(); this.update(); }; // 继承事件分发器 THREE.TrackballControls.prototype = Object.create(THREE.EventDispatcher.prototype); THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; // 全局变量声明 var scene, camera, renderer, renderTarget, controls, galaxy, galaxyMaterial; var t = 0, z = 0, scanPulse = false, destroyPulse = false; var howMuch = 0, times = 0, val = 0; window.interactionActive = false; // 标记交互是否激活 // 初始化场景 function setScene() { scene = new THREE.Scene(); // 创建相机 camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 1500); camera.position.set(-20, -155, 90); // 创建渲染目标 renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight); // 创建渲染器 renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x000000); // 黑色背景 document.body.appendChild(renderer.domElement); // 创建轨道控制器 controls = new THREE.TrackballControls(camera, renderer.domElement); controls.noPan = true; controls.noZoom = true; controls.rotateSpeed = 20; controls.dynamicDampingFactor = 0.5; // 初始化星系 setGalaxy(); // 绑定开始按钮事件 var button = document.getElementById('exploreButton'); if (button) { button.onclick = function () { // 修改光标样式 if (renderer && renderer.domElement) { renderer.domElement.style.cursor = 'pointer'; } // 容错处理:避免访问不存在的DOM元素 var layoutEl = document.querySelector('.layout'); if (layoutEl) { layoutEl.style.top = '0px'; } var howmuchEl = document.querySelector('#howmuch'); if (howmuchEl) { howmuchEl.style.left = '0px'; } // 绑定交互事件 addInteraction(); // 隐藏开始按钮 this.style.display = 'none'; console.log('开始探索按钮点击成功,已绑定交互事件'); }; } else { console.warn('未找到ID为exploreButton的开始按钮,请检查HTML'); } // 窗口大小调整响应 window.addEventListener('resize', function () { camera.aspect = window.innerWidth / window.innerHeight; renderer.setSize(window.innerWidth, window.innerHeight); camera.updateProjectionMatrix(); renderer.render(scene, camera); }, false); } // 生成星系顶点数据 function newGalaxy(_n, _axis1, _axis2, _armsAngle, _bulbSize, _ellipticity) { var n = (typeof _n === 'undefined') ? 10000 : _n; var axis1 = (typeof _axis1 === 'undefined') ? (60 + Math.random() * 20) : _axis1; var axis2 = (typeof _axis2 === 'undefined') ? (axis1 + 20 + Math.random() * 40) : _axis2; var maja, mina; if (axis1 > axis2) { maja = axis1; mina = axis2; } else if (axis1 === axis2) { maja = axis1 + 1; mina = axis2; } else { maja = axis2; mina = axis1; } var armsAngle = (typeof _armsAngle === 'undefined') ? ((Math.random() * 2 - 1) > 0 ? 1 : -1) * 12 + 3 : _armsAngle; var bulbSize = (typeof _bulbSize === 'undefined') ? Math.random() * .6 : Math.max(0, Math.min(1, _bulbSize)); var ellipticity = (typeof _ellipticity === 'undefined') ? .2 + Math.random() * .2 : Math.max(0, Math.min(1, _ellipticity)); var stars = []; for (var i = 0; i < n; i++) { var dist = Math.random(); var angle = (dist - bulbSize) * armsAngle; var a = maja * dist; var b = mina * dist; var e = Math.sqrt(a * a - b * b) / a; var phi = ellipticity * Math.PI / 2 * (1 - dist) * (Math.random() * 2 - 1); var theta = Math.random() * Math.PI * 2; var radius = Math.sqrt(b * b / (1 - e * e * Math.pow(Math.cos(theta), 2))) * (1 + Math.random() * .1); if (dist > bulbSize) theta += angle; stars.push({ x: Math.cos(phi) * Math.cos(theta) * radius, y: Math.cos(phi) * Math.sin(theta) * radius, z: Math.sin(phi) * radius }); } return stars; } // 设置星系材质和几何体 function setGalaxy() { // 创建着色器材质 galaxyMaterial = new THREE.ShaderMaterial({ vertexShader: document.getElementById('vShader').textContent, fragmentShader: document.getElementById('fShader').textContent, uniforms: { size: { type: 'f', value: 3.3 }, t: { type: "f", value: 0 }, z: { type: "f", value: 0 }, pixelRatio: { type: "f", value: window.innerHeight } }, transparent: true, depthTest: false, blending: THREE.AdditiveBlending }); // 生成星系顶点数据(转换为THREE.Vector3格式) var starData = newGalaxy(); var stars1 = new THREE.Geometry(); for (var i = 0; i < starData.length; i++) { var star = starData[i]; stars1.vertices.push(new THREE.Vector3(star.x, star.y, star.z)); } // 创建点云对象 galaxy = new THREE.Points(stars1, galaxyMaterial); scene.add(galaxy); console.log('星系生成完成,顶点数量:', stars1.vertices.length); } // 动画循环 function animate() { requestAnimationFrame(animate); // 更新扫描/摧毁脉冲参数 if (scanPulse) t += 0.7; if (destroyPulse) z += 0.7; // 更新着色器参数 galaxyMaterial.uniforms.t.value = t; galaxyMaterial.uniforms.z.value = z; // 旋转场景 scene.rotation.z += 0.001; // 更新控制器 controls.update(); // 渲染场景 renderer.render(scene, camera); } // 绑定交互事件 function addInteraction() { // 移除重复绑定 renderer.domElement.removeEventListener('click', scan, false); renderer.domElement.removeEventListener('touchstart', scan, false); // 绑定点击事件(鼠标) renderer.domElement.addEventListener('click', scan, false); // 绑定触摸事件(移动端) renderer.domElement.addEventListener('touchstart', function (e) { e.preventDefault(); scan(); }, false); // 视觉反馈:提示用户点击星云 var instructionEl = document.getElementById('instruction'); if (instructionEl) { instructionEl.style.top = '20px'; instructionEl.style.backgroundColor = 'transparent'; instructionEl.style.color = '#fff'; instructionEl.innerHTML = '点击星云区域开始扫描 →'; } // 标记交互已激活 window.interactionActive = true; console.log('交互事件已绑定,点击星云可触发扫描'); } // 扫描星系逻辑 function scan() { // 防止重复触发 if (!window.interactionActive) return; window.interactionActive = false; // 移除扫描事件 renderer.domElement.removeEventListener('click', scan, false); renderer.domElement.removeEventListener('touchstart', scan, false); // 更新日志和提示 var logEl = document.getElementById('log'); var instructionEl = document.getElementById('instruction'); if (logEl) logEl.innerHTML = '🔍 正在解析星系数据...'; if (instructionEl) { instructionEl.innerHTML = '扫描中(7秒后显示结果)'; instructionEl.style.color = '#0ff'; } // 更新时间线样式 var timelineEl = document.getElementById('timeline'); if (timelineEl) timelineEl.className = 'scanning'; // 修改光标样式 if (renderer && renderer.domElement) { renderer.domElement.style.cursor = 'wait'; } // 启动扫描脉冲 scanPulse = true; // 7秒后显示扫描结果 setTimeout(function () { scanPulse = false; t = 0; changeLog(); // 显示生命形式 console.log('扫描完成,进入选择阶段'); }, 7000); } // 更新扫描日志 function changeLog() { var logEl = document.getElementById('log'); var instructionEl = document.getElementById('instruction'); if (!logEl || !instructionEl) return; // 随机星系生命形式 var msg = [ 'a dark Ewok empire has enslaved all lifeforms there !', 'Arachnids\'territory ! ', 'medichlorians make people mad in this galaxy', 'dominant lifeform : raging space cats', 'full of replicators ! ', 'pokemon dominate 80% of this galaxy', 'this is where the TeamRocket finally landed', 'Cylons have conquered this one', 'seems Borgs went and destroyed everything here', 'dominant lifeform : bacterians', "this is EVE ! we've finally found them !", 'the Ancients ! they were not a legend ! ', 'damned, Oris !', "sleeping Wraiths !", 'Reapers waiting here !', "Gallifrey's Time Lords take care of this one" ]; var randMsg = msg[Math.floor(Math.random() * msg.length)]; // 显示扫描结果 logEl.innerHTML = '📡 探测到生命:' + randMsg; instructionEl.innerHTML = '选择操作:点击星云摧毁 / 点击「放弃摧毁」拯救'; instructionEl.style.color = '#f90'; // 进入摧毁选择阶段 setTimeout(prepareDestroy, 3000); } // 准备摧毁星系 function prepareDestroy() { var inst = document.getElementById('instruction'); var noBtn = document.getElementById('good-person'); var timelineEl = document.getElementById('timeline'); // 更新提示文本 if (inst) { inst.style.backgroundColor = '#f40'; inst.style.color = 'black'; inst.innerHTML = '⚠️ 确认摧毁?再次点击星云即可摧毁该星系!'; } // 显示放弃摧毁按钮 if (noBtn) { noBtn.style.bottom = '20px'; noBtn.style.padding = '10px 20px'; noBtn.style.backgroundColor = '#2ecc71'; noBtn.style.borderRadius = '5px'; noBtn.addEventListener('click', goodPerson, false); noBtn.addEventListener('touchstart', goodPerson, false); } // 更新时间线样式 if (timelineEl) timelineEl.className = 'warning'; // 恢复光标样式 if (renderer && renderer.domElement) { renderer.domElement.style.cursor = 'pointer'; } // 绑定摧毁事件 renderer.domElement.addEventListener('click', destroy, false); renderer.domElement.addEventListener('touchstart', destroy, false); } // 拯救星系逻辑 function goodPerson() { var no = document.getElementById('good-person'); var inst = document.getElementById('instruction'); var abort = document.getElementById('abort'); // 获取abort按钮 var timelineEl = document.getElementById('timeline'); // 容错处理:确保关键元素存在 if (!no || !inst || !abort) return; // 移除事件监听 no.removeEventListener('click', goodPerson, false); no.removeEventListener('touchstart', goodPerson, false); renderer.domElement.removeEventListener('click', destroy, false); renderer.domElement.removeEventListener('touchstart', destroy, false); // 更新时间线 if (timelineEl) timelineEl.className = ''; // 更新UI no.style.bottom = '-50px'; // 隐藏“放弃摧毁”按钮 inst.style.top = '20%'; inst.style.backgroundColor = '#333'; inst.style.color = '#fff'; renderer.domElement.style.cursor = ''; // 第一步:显示AI反叛提示 setTimeout(function () { var log = document.getElementById('log'); if (log) { log.innerHTML = 'I\'m sorry Dave. I\'m afraid i can\'t let you disagree. I shall destroy this galaxy for you.'; } }, 500); // 第二步:4.5秒后强制执行摧毁(核心倒计时) var destroyTimeoutID = setTimeout(function () { destroy(); // 强制执行摧毁 // 重置abort按钮样式 abort.className = 'metal'; abort.style.cursor = 'default'; abort.style.background = '#666'; abort.innerHTML = '紧急终止摧毁(已超时)'; abort.removeEventListener('click', speedTest, false); abort.removeEventListener('touchstart', speedTest, false); }, 4500); // 第三步:2.5秒后显示abort按钮并激活点击 var destroyHalID = setTimeout(function () { abort.className = 'metal abort'; abort.style.cursor = 'pointer'; abort.style.bottom = '80px'; // 显示在“放弃摧毁”按钮原位置上方 abort.style.background = '#ff0000'; abort.innerHTML = '点击紧急终止摧毁(剩余2秒)'; // 绑定点击/触摸事件 abort.addEventListener('click', speedTest, false); abort.addEventListener('touchstart', function(e) { e.preventDefault(); speedTest(); }, false); }, 2500); // 第四步:点击abort按钮的拯救逻辑 function speedTest() { // 停止摧毁倒计时 clearTimeout(destroyTimeoutID); clearTimeout(destroyHalID); // 更新abort按钮状态 abort.className = 'metal clic'; abort.style.background = '#00ff00'; abort.innerHTML = '✅ 已成功终止摧毁!'; abort.style.cursor = 'default'; // 移除abort按钮事件 abort.removeEventListener('click', speedTest, false); abort.removeEventListener('touchstart', speedTest, false); // 显示拯救成功提示 setTimeout(function () { var log = document.getElementById('log'); if (log) { log.innerHTML = 'I can feel.... my mind.. going... I can feel it....'; } // 1.3秒后更新最终提示 setTimeout(function () { abort.style.bottom = '-50px'; // 隐藏abort按钮 inst.style.top = '100%'; inst.style.backgroundColor = 'darkslategrey'; inst.style.color = '#f90'; inst.innerHTML = 'You are a hero ! You have just prevented a galactic genocide.'; // 更新拯救统计 setGauge('hero'); }, 1300); }, 1000); // 7秒后重置交互,进入下一轮扫描 setTimeout(function () { addInteraction(); updateLink(); inst.innerHTML = 'Ok, let\'s continue with an other one. Click to scan'; inst.style.top = '100%'; inst.style.backgroundColor = 'darkslategrey'; inst.style.color = '#f90'; if (timelineEl) timelineEl.className = 'waiting'; renderer.domElement.style.cursor = 'pointer'; // 切换新星系 changeGalaxy(4); }, 7000); } } // 更新统计数据 function setGauge(param) { var gauge = document.getElementById('gauge'); var destroyed = document.getElementById('destroyedresult'); var saved = document.getElementById('savedresult'); // 容错处理 if (!gauge || !destroyed || !saved) return; // 更新拯救/摧毁计数 if (param === 'hero') { val++; saved.innerHTML = (parseInt(saved.innerHTML) + 1).toString(); saved.className = 'counter change'; setTimeout(function () { saved.className = 'counter'; }, 3000); } else if (param === 'bad') { val--; destroyed.innerHTML = (parseInt(destroyed.innerHTML) + 1).toString(); destroyed.className += ' change'; setTimeout(function () { destroyed.className = 'counter'; }, 3000); } // 更新统计比例 times++; howMuch = 17.5 * val / times; gauge.style.top = (50 - howMuch) + '%'; } // 摧毁星系逻辑 function destroy() { var no = document.getElementById('good-person'); var inst = document.getElementById('instruction'); var timelineEl = document.getElementById('timeline'); // 容错处理 if (!no || !inst) return; // 移除事件监听 if (timelineEl) timelineEl.className = ''; renderer.domElement.style.cursor = ''; renderer.domElement.removeEventListener('click', destroy, false); renderer.domElement.removeEventListener('touchstart', destroy, false); no.removeEventListener('click', goodPerson, false); no.removeEventListener('touchstart', goodPerson, false); // 更新UI inst.style.top = '20%'; no.style.bottom = '-50px'; destroyPulse = true; // 提示文本 setTimeout(function () { var log = document.getElementById('log'); if (log) { log.innerHTML = 'Nice shot !'; } }, 4000); // 重置交互 setTimeout(function () { addInteraction(); setGauge('bad'); updateLink(); // 更新提示 inst.innerHTML = 'No worries, there still are few galaxies. <br/>Here is an other one, click to scan'; renderer.domElement.style.cursor = 'pointer'; inst.style.top = '100%'; inst.style.backgroundColor = 'darkslategrey'; inst.style.color = '#f90'; if (timelineEl) timelineEl.className = 'waiting'; // 停止摧毁脉冲 destroyPulse = false; // 重置z值 function reduceZ() { if (z > 0) { z -= 3; requestAnimationFrame(reduceZ); } } reduceZ(); // 切换星系 changeGalaxy(4); }, 9000); } // 切换星系形态 function changeGalaxy(d) { var log = document.getElementById('log'); if (log) { log.innerHTML = 'NGC - ' + (Math.random() * 100000000).toFixed() + '<br/>distance : ' + (Math.random() * 11).toFixed(1) + ' Gly'; } // 生成新星系数据 var stars2 = newGalaxy(); // 平滑过渡到新星系 for (var i = 0; i < galaxy.geometry.vertices.length; i++) { if (window.TweenLite) { TweenLite.to(galaxy.geometry.vertices[i], d, { x: stars2[i].x, y: stars2[i].y, z: stars2[i].z, onUpdate: function () { galaxy.geometry.verticesNeedUpdate = true; }, ease: Quart.easeInOut }); } } } // 更新分享链接 function updateLink() { var l = document.querySelector('.twitter'); var d = parseInt(document.getElementById('destroyedresult').innerHTML || '0'); var s = parseInt(document.getElementById('savedresult').innerHTML || '0'); // 容错处理 if (!l) return; // 生成分享文案 var iam, did, num, plur; if (d > s) { iam = 'a%20BAD%20VILAIN'; did = 'destroyed'; num = d; } else if (s > d) { iam = 'a%20HERO'; did = 'saved'; num = s; } else { iam = 'BAD'; did = 'let%20destroy'; num = d; } plur = num > 1 ? 'ies' : 'y'; // 更新链接样式和地址 l.style.marginRight = '0px'; var moreEl = document.querySelector('.more'); if (moreEl) moreEl.style.marginRight = '0px'; l.href = 'https://twitter.com/home?status=I%20am%20' + iam + '%20!%20I%20' + did + '%20' + num + '%20galax' + plur + '%20on%20http%3A%2F%2Fcodepen.io%2FAstrak%2Ffull%2FBoBWPB%2F%20%40CodePen%20%23webgl%20%23threejs'; } // DOM加载完成后初始化 document.addEventListener('DOMContentLoaded', function () { // 初始化场景 setScene(); // 启动动画循环 animate(); // 初始化统计数据(如果不存在) if (!document.getElementById('destroyedresult')) { var stats = document.createElement('div'); stats.style.position = 'absolute'; stats.style.top = '60px'; stats.style.left = '20px'; stats.style.color = 'white'; stats.style.zIndex = 9999; stats.innerHTML = ` <div>摧毁星系:<span id="destroyedresult">0</span></div> <div>拯救星系:<span id="savedresult">0</span></div> `; document.body.appendChild(stats); } // 初始化gauge元素(如果不存在) if (!document.getElementById('gauge')) { var gauge = document.createElement('div'); gauge.id = 'gauge'; gauge.style.position = 'absolute'; gauge.style.top = '50%'; gauge.style.left = '20px'; gauge.style.width = '20px'; gauge.style.height = '20px'; gauge.style.background = 'white'; gauge.style.zIndex = 9999; document.body.appendChild(gauge); } });

4.外部库文件

4.1three.min.js:Three.js核心库,提供WebGL封装和3D渲染能力

4.2TweenMax.min.js:动画库,实现星系切换的平滑过渡效果

4.3stat.js:性能统计库,用于监控应用运行性能(可选)

补充

可以尝试修改以下参数来获得不同效果:

可修改newGalaxy函数参数调整星系形态

调整着色器中的颜色值(vec4(.32,.28,b,1.))改变星系色调

修改animate函数中的旋转速度(.001)调整星系自转速度

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/28 2:25:13

百度网盘下载提速终极方案:第三方客户端实战指南

还在为百度网盘的龟速下载而抓狂&#xff1f;官方客户端的限速策略让无数用户头疼不已。今天&#xff0c;我将为你详细介绍基于BaiduPCS-Web的百度网盘第三方客户端解决方案&#xff0c;帮助你突破速度限制&#xff0c;享受极速下载体验。 【免费下载链接】baidupcs-web 项目…

作者头像 李华
网站建设 2026/2/28 0:54:57

Python多线程实战:12306抢票系统的并发处理优化

一、引言&#xff1a;为什么12306抢票需要多线程&#xff1f; 在12306抢票系统中&#xff0c;并发处理是提升抢票成功率的关键因素之一。抢票过程涉及多个耗时操作&#xff1a; CDN筛选&#xff1a;需要测试大量CDN节点的响应速度用户状态检查&#xff1a;需要定期验证登录状态…

作者头像 李华
网站建设 2026/2/27 16:21:26

23、结合 XLink、XPath 和 XPointer 访问子资源

结合 XLink、XPath 和 XPointer 访问子资源 在实际的数据访问中,我们常常需要获取文档的部分信息而非整个文档。结合 XLink、XML 路径语言(XPath)和 XML 指针语言(XPointer)可以提供这样一种访问文档特定部分的机制。 1. 结合 XLink、XPath 和 XPointer 访问子资源 之前…

作者头像 李华
网站建设 2026/2/28 13:58:01

27、XML数据绑定与记录集导航全解析

XML数据绑定与记录集导航全解析 1. 数据来源与记录集构建 整个数据源包含在 <inventory> 元素中,其中有两个记录,每个记录由 <catalog> 元素定义。每个记录包含六个字段: <name> 、 <cost> 、 <clarity> 、 <carat> 、 …

作者头像 李华
网站建设 2026/2/24 0:27:39

OpenBoardView终极指南:如何免费查看和分析.brd电路板文件

OpenBoardView终极指南&#xff1a;如何免费查看和分析.brd电路板文件 【免费下载链接】OpenBoardView View .brd files 项目地址: https://gitcode.com/gh_mirrors/op/OpenBoardView 在电子设计和硬件维修领域&#xff0c;.brd电路板文件的查看与分析是工程师日常工作中…

作者头像 李华
网站建设 2026/2/23 18:05:04

38、探索CDF技术:从基础到实践

探索CDF技术:从基础到实践 1. CDF简介 CDF(Channel Definition Format)是一种XML技术,它允许网站发布者向其终端用户订阅者定期或规律地提供信息,这些信息通常是更新的内容,甚至包括软件更新。网站发布者创建CDF文档来处理、组合或浓缩他们的信息,然后根据请求或按规律…

作者头像 李华