安阳网站制作 网络服务,广东涂料网站建设,手机软件制作网站平台,网站贴子推广怎么做设计概述
本次分享将介绍如何使用Three.js库创建一个沉浸式的 星云探索 交互应用。这个项目通过WebGL构建可视化的星系模型#xff0c;结合自定义着色器实现动态星云效果#xff0c;构建了一个包含星系生成、交互探索、状态管理的完整应用#xff0c;展现了Web…设计概述本次分享将介绍如何使用Three.js库创建一个沉浸式的 星云探索 交互应用。这个项目通过WebGL构建可视化的星系模型结合自定义着色器实现动态星云效果构建了一个包含星系生成、交互探索、状态管理的完整应用展现了WebGL在创意交互领域的强大潜力。效果概览应用运行后呈现以下核心效果1.开始探索打开应用后点击 开始探索 按钮进入星云探索模式2.基本操作移动鼠标可旋转星系视角点击星云区域开始扫描持续 7 秒有动态脉冲效果3.决策流程扫描完成后显示星系生命形式选择1再次点击星云→摧毁星系选择2点击 放弃摧毁→进入拯救流程拯救流程需在倒计时结束前点击 紧急终止摧毁 确认4.数据统计左上角实时显示摧毁/拯救星系数量星云-演示视频文件结构项目包含三个核心文件星云.html——定义页面结构、引入外部js库等style.css——样式设置文件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 idexploreButton style...开始探索/button !-- 交互流程所需DOM元素 -- div idlog style.../div div idinstruction style.../div div idtimeline style.../div div idgood-person style...放弃摧毁拯救星系/div div idabort classmetal style...紧急终止摧毁/div !-- 统计面板 -- div style... div摧毁星系span iddestroyedresult0/span/div div拯救星系span idsavedresult0/span/div /div1.2着色器定义包含顶点着色器 (vShader) 和片段着色器 (fShader)负责星云的视觉呈现!-- 顶点着色器 -- script idvShader typex-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 idfShader typex-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)); } /script1.3资源引入引入项目所需的外部资源和脚本!-- 先加载依赖库 -- script srcjs/three.min.js/script script srcjs/TweenMax.min.js/script script srcjs/stat.js defer/script !-- 最后加载业务逻辑 -- script srcjs/index.js defer/script星云.html完整版代码!DOCTYPE html html langen head meta charsetUTF-8 title星云/title meta charsetutf-8 meta namedescription contentWebGL galaxy with shaders / link relstylesheet hrefcss/style.css /head body !-- 开始按钮 -- button idexploreButton 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 idlog style position:absolute; top: 20px; left: 20px; color: white; font-size: 14px; z-index: 9999; /div div idinstruction 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 idtimeline style position:absolute; bottom: 40px; left: 50%; transform: translateX(-50%); width: 80%; height: 5px; background: #333; z-index: 9999; /div div idgood-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 idabort classmetal 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 styleposition:absolute; top: 60px; left: 20px; color: white; z-index: 9999; div摧毁星系span iddestroyedresult0/span/div div拯救星系span idsavedresult0/span/div /div !-- 着色器脚本-- script idvShader typex-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,b0.; void main(){ vPositionposition; alength(position); if(t0.)bmax(0.,(cos(a/20.-t*.02)-.99)*3./a); if(z0.)bmax(0.,cos(a/40.-z*.012.)); mPositionposition*(1.b*4.); vec4 mvPositionmodelViewMatrix*vec4(mPosition,1.); gl_PositionprojectionMatrix*mvPosition; gasmax(.0,sin(-a/20.)); gl_PointSizepixelRatio*size*(1.gas*2.)/length(mvPosition.xyz); } /script script idfShader typex-fragment/x-shader uniform float z; varying vec3 vPosition; varying vec3 mPosition; varying float gas; void main(){ float adistance(mPosition,vPosition); if(a0.)a1.; float bmax(.32,.0065*length(vPosition)); float cdistance(gl_PointCoord,vec2(.5)); float starlook-(c-.5)*1.2*gas; float gaslook(1.-gas)/(c*10.); float texturestarlookgaslook; gl_FragColorvec4(.32,.28,b,1.)*texture*(1.-a*.35); if(z0.)gl_FragColor*cos(1.57*z/322.)*(1.-.001*length(mPosition)); } /script !-- 先加载依赖库 -- script srcjs/three.min.js/script script srcjs/TweenMax.min.js/script script srcjs/stat.js defer/script !-- 最后加载业务逻辑 -- script srcjs/index.js defer/script /body /html2.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 ! weve finally found them !, the Ancients ! they were not a legend ! , damned, Oris !, sleeping Wraiths !, Reapers waiting here !, Gallifreys 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?statusI%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 iddestroyedresult0/span/div div拯救星系span idsavedresult0/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.jsThree.js核心库提供WebGL封装和3D渲染能力4.2TweenMax.min.js动画库实现星系切换的平滑过渡效果4.3stat.js性能统计库用于监控应用运行性能可选补充可以尝试修改以下参数来获得不同效果可修改newGalaxy函数参数调整星系形态调整着色器中的颜色值vec4(.32,.28,b,1.)改变星系色调修改animate函数中的旋转速度.001调整星系自转速度