Files

366 lines
11 KiB
JavaScript
Raw Permalink Normal View History

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 typeChrome 需要)
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('');
});