366 lines
11 KiB
JavaScript
366 lines
11 KiB
JavaScript
|
|
const express = require('express');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const app = express();
|
|||
|
|
const PORT = process.env.PORT || 8080;
|
|||
|
|
|
|||
|
|
const ROOT = __dirname;
|
|||
|
|
|
|||
|
|
// 首页 —— 全屏启动页面
|
|||
|
|
// 必须在静态文件中间件之前注册,否则 express.static 会优先返回 index.html
|
|||
|
|
app.get('/', (req, res) => {
|
|||
|
|
res.send(`<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|||
|
|
<title>运动伤害现场急救虚拟仿真实验</title>
|
|||
|
|
<link rel="shortcut icon" href="TemplateData/favicon.ico">
|
|||
|
|
<style>
|
|||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, "Microsoft YaHei", "PingFang SC", sans-serif;
|
|||
|
|
background: #231F20;
|
|||
|
|
color: #fff;
|
|||
|
|
overflow: hidden;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
height: 100dvh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ========== 启动封面 ========== */
|
|||
|
|
#landing {
|
|||
|
|
position: fixed; inset: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: radial-gradient(ellipse at center, #2a2526 0%, #1a1516 70%, #0d0a0b 100%);
|
|||
|
|
z-index: 100;
|
|||
|
|
transition: opacity 0.5s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#landing.hidden {
|
|||
|
|
opacity: 0;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#landing .logo-area {
|
|||
|
|
margin-bottom: 60px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#landing .logo-area .icon {
|
|||
|
|
width: 120px;
|
|||
|
|
height: 120px;
|
|||
|
|
margin: 0 auto 30px;
|
|||
|
|
background: url('TemplateData/unity-logo-light.png') no-repeat center;
|
|||
|
|
background-size: contain;
|
|||
|
|
filter: drop-shadow(0 0 30px rgba(255,255,255,0.15));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#landing h1 {
|
|||
|
|
font-size: clamp(24px, 4vw, 42px);
|
|||
|
|
font-weight: 700;
|
|||
|
|
letter-spacing: 0.08em;
|
|||
|
|
color: #fff;
|
|||
|
|
text-shadow: 0 2px 20px rgba(255,255,255,0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#landing .subtitle {
|
|||
|
|
margin-top: 12px;
|
|||
|
|
font-size: clamp(14px, 1.8vw, 18px);
|
|||
|
|
color: rgba(255,255,255,0.45);
|
|||
|
|
letter-spacing: 0.15em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#btn-start {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
padding: 16px 56px;
|
|||
|
|
font-size: clamp(16px, 2vw, 20px);
|
|||
|
|
font-weight: 600;
|
|||
|
|
letter-spacing: 0.1em;
|
|||
|
|
color: #fff;
|
|||
|
|
background: linear-gradient(135deg, #4a90d9 0%, #357abd 100%);
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 50px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|||
|
|
box-shadow: 0 8px 32px rgba(74, 144, 217, 0.35);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#btn-start:hover {
|
|||
|
|
transform: translateY(-2px) scale(1.03);
|
|||
|
|
box-shadow: 0 12px 40px rgba(74, 144, 217, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#btn-start:active {
|
|||
|
|
transform: translateY(0) scale(0.98);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#btn-start .arrow {
|
|||
|
|
display: inline-block;
|
|||
|
|
font-size: 1.2em;
|
|||
|
|
transition: transform 0.2s ease;
|
|||
|
|
}
|
|||
|
|
#btn-start:hover .arrow {
|
|||
|
|
transform: translateX(4px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#landing .hint {
|
|||
|
|
margin-top: 40px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: rgba(255,255,255,0.25);
|
|||
|
|
letter-spacing: 0.05em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ========== Unity 容器 (默认隐藏) ========== */
|
|||
|
|
#unity-container {
|
|||
|
|
position: fixed;
|
|||
|
|
inset: 0;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
height: 100dvh;
|
|||
|
|
display: none;
|
|||
|
|
background: #231F20;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#unity-container.active {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#unity-canvas {
|
|||
|
|
width: 100% !important;
|
|||
|
|
height: 100% !important;
|
|||
|
|
background: #231F20;
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ========== 加载进度条 ========== */
|
|||
|
|
#unity-loading-bar {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 50%;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
display: none;
|
|||
|
|
text-align: center;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
#unity-loading-bar.show { display: block; }
|
|||
|
|
|
|||
|
|
#unity-logo {
|
|||
|
|
width: 154px; height: 130px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
background: url('TemplateData/unity-logo-dark.png') no-repeat center;
|
|||
|
|
}
|
|||
|
|
#unity-progress-bar-empty {
|
|||
|
|
width: 141px; height: 18px;
|
|||
|
|
margin: 10px auto 0;
|
|||
|
|
background: url('TemplateData/progress-bar-empty-dark.png') no-repeat center;
|
|||
|
|
}
|
|||
|
|
#unity-progress-bar-full {
|
|||
|
|
width: 0%; height: 18px;
|
|||
|
|
background: url('TemplateData/progress-bar-full-dark.png') no-repeat center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#unity-footer {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 16px;
|
|||
|
|
left: 16px;
|
|||
|
|
right: 16px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
#unity-webgl-logo {
|
|||
|
|
width: 204px; height: 38px;
|
|||
|
|
background: url('TemplateData/webgl-logo.png') no-repeat center;
|
|||
|
|
}
|
|||
|
|
#unity-build-title {
|
|||
|
|
color: rgba(255,255,255,0.5);
|
|||
|
|
font-family: arial, sans-serif;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
#unity-fullscreen-button {
|
|||
|
|
cursor: pointer;
|
|||
|
|
width: 38px; height: 38px;
|
|||
|
|
background: url('TemplateData/fullscreen-button.png') no-repeat center;
|
|||
|
|
}
|
|||
|
|
#unity-warning {
|
|||
|
|
position: absolute; left: 50%; top: 5%;
|
|||
|
|
transform: translate(-50%);
|
|||
|
|
background: white; color: #333;
|
|||
|
|
padding: 10px; display: none; z-index: 20;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
|
|||
|
|
<!-- ===== 启动封面 ===== -->
|
|||
|
|
<div id="landing">
|
|||
|
|
<div class="logo-area">
|
|||
|
|
<div class="icon"></div>
|
|||
|
|
<h1>运动伤害现场急救虚拟仿真实验</h1>
|
|||
|
|
<p class="subtitle">VIRTUAL SIMULATION EXPERIMENT</p>
|
|||
|
|
</div>
|
|||
|
|
<button id="btn-start" onclick="enterFullscreen()">
|
|||
|
|
进入全屏实验
|
|||
|
|
<span class="arrow">→</span>
|
|||
|
|
</button>
|
|||
|
|
<p class="hint">点击按钮进入全屏沉浸式实验环境</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- ===== Unity 容器 ===== -->
|
|||
|
|
<div id="unity-container">
|
|||
|
|
<canvas id="unity-canvas" tabindex="-1"></canvas>
|
|||
|
|
<div id="unity-loading-bar">
|
|||
|
|
<div id="unity-logo"></div>
|
|||
|
|
<div id="unity-progress-bar-empty">
|
|||
|
|
<div id="unity-progress-bar-full"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div id="unity-warning"></div>
|
|||
|
|
<div id="unity-footer">
|
|||
|
|
<div id="unity-webgl-logo"></div>
|
|||
|
|
<div id="unity-fullscreen-button"></div>
|
|||
|
|
<div id="unity-build-title">运动伤害现场急救虚拟仿真实验</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
var container = document.querySelector("#unity-container");
|
|||
|
|
var canvas = document.querySelector("#unity-canvas");
|
|||
|
|
var loadingBar = document.querySelector("#unity-loading-bar");
|
|||
|
|
var progressBarFull = document.querySelector("#unity-progress-bar-full");
|
|||
|
|
var fullscreenButton = document.querySelector("#unity-fullscreen-button");
|
|||
|
|
var warningBanner = document.querySelector("#unity-warning");
|
|||
|
|
var landing = document.querySelector("#landing");
|
|||
|
|
|
|||
|
|
function unityShowBanner(msg, type) {
|
|||
|
|
function updateBannerVisibility() {
|
|||
|
|
warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
|
|||
|
|
}
|
|||
|
|
var div = document.createElement('div');
|
|||
|
|
div.innerHTML = msg;
|
|||
|
|
warningBanner.appendChild(div);
|
|||
|
|
if (type == 'error') div.style = 'background: red; padding: 10px;';
|
|||
|
|
else {
|
|||
|
|
if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
|
|||
|
|
setTimeout(function() {
|
|||
|
|
warningBanner.removeChild(div);
|
|||
|
|
updateBannerVisibility();
|
|||
|
|
}, 5000);
|
|||
|
|
}
|
|||
|
|
updateBannerVisibility();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var buildUrl = "Build";
|
|||
|
|
var loaderUrl = buildUrl + "/Build.loader.js";
|
|||
|
|
var config = {
|
|||
|
|
dataUrl: buildUrl + "/Build.data",
|
|||
|
|
frameworkUrl: buildUrl + "/Build.framework.js",
|
|||
|
|
codeUrl: buildUrl + "/Build.wasm",
|
|||
|
|
streamingAssetsUrl: "StreamingAssets",
|
|||
|
|
companyName: "DefaultCompany",
|
|||
|
|
productName: "运动伤害现场急救虚拟仿真实验",
|
|||
|
|
productVersion: "0.1.0",
|
|||
|
|
showBanner: unityShowBanner,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
var unityInstance = null;
|
|||
|
|
|
|||
|
|
// ========== 进入全屏 ==========
|
|||
|
|
function enterFullscreen() {
|
|||
|
|
var el = document.documentElement;
|
|||
|
|
|
|||
|
|
var requestFS = el.requestFullscreen || el.webkitRequestFullscreen
|
|||
|
|
|| el.msRequestFullscreen || el.mozRequestFullScreen;
|
|||
|
|
|
|||
|
|
if (requestFS) {
|
|||
|
|
requestFS.call(el).then(function() {
|
|||
|
|
startUnity();
|
|||
|
|
}).catch(function(err) {
|
|||
|
|
// 全屏被拒绝(例如用户取消),仍然启动 Unity
|
|||
|
|
console.warn('全屏请求失败,以普通模式启动:', err);
|
|||
|
|
startUnity();
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 不支持全屏 API 的浏览器
|
|||
|
|
startUnity();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 启动 Unity ==========
|
|||
|
|
function startUnity() {
|
|||
|
|
// 隐藏封面
|
|||
|
|
landing.classList.add('hidden');
|
|||
|
|
|
|||
|
|
// 显示 Unity 容器
|
|||
|
|
container.classList.add('active');
|
|||
|
|
loadingBar.classList.add('show');
|
|||
|
|
|
|||
|
|
// 加载 Unity
|
|||
|
|
var script = document.createElement("script");
|
|||
|
|
script.src = loaderUrl;
|
|||
|
|
script.onload = function() {
|
|||
|
|
createUnityInstance(canvas, config, function(progress) {
|
|||
|
|
progressBarFull.style.width = 100 * progress + "%";
|
|||
|
|
}).then(function(instance) {
|
|||
|
|
unityInstance = instance;
|
|||
|
|
loadingBar.classList.remove('show');
|
|||
|
|
|
|||
|
|
// 全屏按钮
|
|||
|
|
fullscreenButton.onclick = function() {
|
|||
|
|
unityInstance.SetFullscreen(1);
|
|||
|
|
};
|
|||
|
|
}).catch(function(message) {
|
|||
|
|
alert('加载失败: ' + message);
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
document.body.appendChild(script);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 监听全屏退出 ==========
|
|||
|
|
document.addEventListener('fullscreenchange', function() {
|
|||
|
|
if (!document.fullscreenElement && unityInstance) {
|
|||
|
|
canvas.style.width = '100%';
|
|||
|
|
canvas.style.height = '100%';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ========== 防止意外滚动 ==========
|
|||
|
|
document.addEventListener('gesturestart', function(e) { e.preventDefault(); });
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 静态文件服务
|
|||
|
|
app.use(express.static(ROOT, {
|
|||
|
|
setHeaders: (res, filePath) => {
|
|||
|
|
// 确保 .wasm 有正确的 MIME type(Chrome 需要)
|
|||
|
|
if (filePath.endsWith('.wasm')) {
|
|||
|
|
res.setHeader('Content-Type', 'application/wasm');
|
|||
|
|
}
|
|||
|
|
if (filePath.endsWith('.js')) {
|
|||
|
|
res.setHeader('Content-Type', 'application/javascript');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 启动服务
|
|||
|
|
app.listen(PORT, () => {
|
|||
|
|
console.log('');
|
|||
|
|
console.log(' ╔══════════════════════════════════════════════════╗');
|
|||
|
|
console.log(' ║ 运动伤害现场急救虚拟仿真实验 - WebGL Server ║');
|
|||
|
|
console.log(' ║ ║');
|
|||
|
|
console.log(' ║ Server running at: http://localhost:' + PORT + ' ║');
|
|||
|
|
console.log(' ║ Press Ctrl+C to stop ║');
|
|||
|
|
console.log(' ╚══════════════════════════════════════════════════╝');
|
|||
|
|
console.log('');
|
|||
|
|
});
|