feat: 新增3个思想实验室项目,完善私有剪贴板授权与上传功能,添加配套测试脚本
1. 新增怀旧弹珠台、人性与投资、穿越三体、四维空间四个思想实验室项目 2. 为私有剪贴板添加业务层授权校验,优化上传进度显示与鉴权失败处理 3. 新增多个调试与单元测试脚本,覆盖三体积分、弹珠台物理、集成测试等场景 4. 补全穿越三体项目的完整HTML页面资源
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>思想实验室 · 穿越三体</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;500&display=swap');
|
||||
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #020205;
|
||||
color: #ddd;
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#stage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 顶部/底部叙事框 */
|
||||
#narrative-box {
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60%;
|
||||
max-width: 800px;
|
||||
background: rgba(10, 15, 25, 0.85);
|
||||
border: 1px solid rgba(100, 150, 255, 0.3);
|
||||
padding: 25px 35px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.8);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
letter-spacing: 1px;
|
||||
text-align: justify;
|
||||
min-height: 120px;
|
||||
text-shadow: 0 0 5px rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
/* 侧边信息栏 */
|
||||
.info-panel {
|
||||
top: 50px;
|
||||
width: 260px;
|
||||
background: rgba(5, 5, 10, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
backdrop-filter: blur(5px);
|
||||
font-family: sans-serif;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#science-panel { left: 30px; border-top: 3px solid #4a90e2; }
|
||||
#fiction-panel { right: 30px; border-top: 3px solid #e24a4a; }
|
||||
|
||||
.info-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
#science-panel h3 { color: #4a90e2; }
|
||||
#fiction-panel h3 { color: #e24a4a; }
|
||||
|
||||
.info-panel ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.info-panel li { margin-bottom: 12px; color: #ccc; }
|
||||
|
||||
/* 控件 */
|
||||
#controls {
|
||||
bottom: 50px;
|
||||
right: 50px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
transition: all 0.3s;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button:hover { background: rgba(255, 255, 255, 0.2); }
|
||||
button:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
|
||||
/* 场景标题 */
|
||||
#scene-title {
|
||||
top: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 4px;
|
||||
color: rgba(255,255,255,0.9);
|
||||
text-shadow: 0 0 10px rgba(255,255,255,0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#intro {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: #000;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
#intro h1 { font-size: 3rem; letter-spacing: 10px; margin-bottom: 20px; font-weight: 300;}
|
||||
#intro p { font-size: 1.2rem; color: #888; }
|
||||
#start-btn { margin-top: 40px; font-size: 1.2rem; padding: 10px 40px; pointer-events: auto;}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="stage"></canvas>
|
||||
|
||||
<div id="intro">
|
||||
<h1>思想实验室 · 穿越三体</h1>
|
||||
<p>一场基于物理与想象的第一人称视觉实验</p>
|
||||
<button id="start-btn">启程</button>
|
||||
</div>
|
||||
|
||||
<div id="scene-title" class="overlay"></div>
|
||||
|
||||
<div id="science-panel" class="overlay info-panel">
|
||||
<h3>真实科学</h3>
|
||||
<ul id="science-list"></ul>
|
||||
</div>
|
||||
|
||||
<div id="fiction-panel" class="overlay info-panel">
|
||||
<h3>小说想象</h3>
|
||||
<ul id="fiction-list"></ul>
|
||||
</div>
|
||||
|
||||
<div id="narrative-box" class="overlay">
|
||||
<span id="narrative-text"></span><span class="cursor">_</span>
|
||||
</div>
|
||||
|
||||
<div id="controls" class="overlay">
|
||||
<button id="prev-btn" disabled>上一跃迁</button>
|
||||
<button id="next-btn">下一跃迁</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 核心逻辑:画布与渲染
|
||||
*/
|
||||
const canvas = document.getElementById('stage');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let width, height;
|
||||
|
||||
function resize() {
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
// 宇宙背景星空
|
||||
const stars = Array.from({length: 300}, () => ({
|
||||
x: Math.random() * 2000,
|
||||
y: Math.random() * 2000,
|
||||
r: Math.random() * 1.5,
|
||||
alpha: Math.random(),
|
||||
speed: Math.random() * 0.05
|
||||
}));
|
||||
|
||||
function drawBackground() {
|
||||
ctx.fillStyle = '#020205';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
stars.forEach(star => {
|
||||
star.alpha += star.speed;
|
||||
let a = Math.abs(Math.sin(star.alpha));
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${a})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(star.x % width, star.y % height, star.r, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打字机效果管理器
|
||||
*/
|
||||
let typeWriterTimer = null;
|
||||
function typeWriter(text, elementId, speed = 40) {
|
||||
const el = document.getElementById(elementId);
|
||||
el.innerHTML = "";
|
||||
if(typeWriterTimer) clearInterval(typeWriterTimer);
|
||||
|
||||
let i = 0;
|
||||
typeWriterTimer = setInterval(() => {
|
||||
if(i < text.length) {
|
||||
el.innerHTML += text.charAt(i);
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(typeWriterTimer);
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景定义与逻辑
|
||||
*/
|
||||
const scenes = [
|
||||
{
|
||||
title: "第一幕:红岸的回声",
|
||||
narrative: "我凝视着夜空,那是一块深邃的黑天鹅绒。这里的海拔很高,风中带着寒意。红岸基地的巨型抛物面天线在缓慢转动。4.37光年外的星光刚刚抵达我的视网膜。我收到了一条无声的警告:『不要回答。』但我知道,文明的孤岛已被打破,我将化身幽灵,沿着电波的轨迹,去见证那场跨越数百个世纪的史诗。",
|
||||
science: [
|
||||
"半人马座α星是距离太阳系最近的恒星系统,距离约 4.37 光年(约 41.3 万亿公里)。",
|
||||
"电磁波在真空中的传播速度为光速(约 300,000 km/s),信号一来一回需近9年。"
|
||||
],
|
||||
fiction: [
|
||||
"太阳可以作为超级天线,利用恒星大气的等离子体将电波信号放大亿倍进行星际广播。",
|
||||
"三体文明截获了广播,其监听员发出了警告。"
|
||||
],
|
||||
state: { earthR: 200, alpha: 1 },
|
||||
init() { this.state.earthR = Math.min(width, height) * 0.4; this.state.alpha = 1; },
|
||||
draw(ctx, width, height) {
|
||||
// 画一个巨大的地球边缘
|
||||
let cx = width / 2;
|
||||
let cy = height + this.state.earthR * 0.5;
|
||||
|
||||
let grd = ctx.createRadialGradient(cx, cy, this.state.earthR * 0.8, cx, cy, this.state.earthR);
|
||||
grd.addColorStop(0, "rgba(10, 30, 80, 1)");
|
||||
grd.addColorStop(0.8, "rgba(40, 100, 200, 0.8)");
|
||||
grd.addColorStop(1, "rgba(0, 0, 0, 0)");
|
||||
|
||||
ctx.fillStyle = grd;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, this.state.earthR, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 雷达红点
|
||||
this.state.alpha = 0.5 + 0.5 * Math.sin(Date.now() / 300);
|
||||
ctx.fillStyle = `rgba(255, 50, 50, ${this.state.alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy - this.state.earthR * 0.95, 3, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 发射波纹
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${this.state.alpha * 0.5})`;
|
||||
ctx.lineWidth = 1;
|
||||
let time = Date.now() / 1000;
|
||||
for(let i=1; i<=3; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy - this.state.earthR * 0.95, (time * 50 % 100) * i, Math.PI*1.2, Math.PI*1.8);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "第二幕:乱纪元的炼狱 (三体问题)",
|
||||
narrative: "经过漫长的意识跃迁,我坠入了这片毫无规律的炼狱。三颗巨大的恒星像狂舞的巨兽,在牛顿引力定律的诅咒下做着永不重复的混沌运动。刚刚还是飞星不动的三日凌空,瞬间又被撕裂成极寒的漫漫长夜。这里的文明在恒纪元与乱纪元的交替中痛苦轮回,而它们唯一的生路,就是逃离。",
|
||||
science: [
|
||||
"三体问题(Three-body problem):三个质量、初始位置和初始速度都是任意的天体,在相互之间万有引力的作用下的运动规律问题。",
|
||||
"庞加莱已证明,三体问题没有一般解析解,其系统具备极端的混沌性对初始条件极度敏感。",
|
||||
"注:背景动画采用纯粹的 RK4 (龙格-库塔) 数值积分实时演算,并非预设动画。"
|
||||
],
|
||||
fiction: [
|
||||
"在一个如此极度不稳定的混沌星系中,居然存在一颗未被吞噬或甩出的行星。",
|
||||
"该行星上诞生了能够脱水复苏,并进化出高度发达科技的三体文明。"
|
||||
],
|
||||
state: { bodies: [], trails: [] },
|
||||
init() {
|
||||
// 初始化三个恒星 (位置,速度,质量,颜色)
|
||||
this.state.bodies = [
|
||||
{x: -150, y: 50, vx: 0, vy: -30, m: 3000, color: '#ff4d4d', r: 15},
|
||||
{x: 150, y: -50, vx: 0, vy: 30, m: 3000, color: '#fdb813', r: 15},
|
||||
{x: 0, y: 0, vx: 20, vy: 0, m: 2500, color: '#4da6ff', r: 12}
|
||||
];
|
||||
this.state.trails = [[], [], []];
|
||||
},
|
||||
// RK4 引力积分演算核心
|
||||
calculateAccelerations(bodies) {
|
||||
const G = 10; // 引力常数调节
|
||||
const softening = 20; // 软化参数防止奇点爆炸
|
||||
let accels = bodies.map(() => ({ax: 0, ay: 0}));
|
||||
for(let i=0; i<bodies.length; i++) {
|
||||
for(let j=0; j<bodies.length; j++) {
|
||||
if(i===j) continue;
|
||||
let dx = bodies[j].x - bodies[i].x;
|
||||
let dy = bodies[j].y - bodies[i].y;
|
||||
let distSq = dx*dx + dy*dy + softening*softening;
|
||||
let dist = Math.sqrt(distSq);
|
||||
let force = (G * bodies[j].m) / (distSq * dist);
|
||||
accels[i].ax += force * dx;
|
||||
accels[i].ay += force * dy;
|
||||
}
|
||||
}
|
||||
return accels;
|
||||
},
|
||||
draw(ctx, width, height) {
|
||||
const dt = 0.05; // 时间步长
|
||||
let b = this.state.bodies;
|
||||
|
||||
// RK4 步骤
|
||||
// k1
|
||||
let a1 = this.calculateAccelerations(b);
|
||||
let k1 = b.map((body, i) => ({vx: a1[i].ax*dt, vy: a1[i].ay*dt, x: body.vx*dt, y: body.vy*dt}));
|
||||
// k2
|
||||
let b2 = b.map((body, i) => ({x: body.x + k1[i].x/2, y: body.y + k1[i].y/2, m: body.m}));
|
||||
let a2 = this.calculateAccelerations(b2);
|
||||
let k2 = b.map((body, i) => ({vx: a2[i].ax*dt, vy: a2[i].ay*dt, x: (body.vx + k1[i].vx/2)*dt, y: (body.vy + k1[i].vy/2)*dt}));
|
||||
// k3
|
||||
let b3 = b.map((body, i) => ({x: body.x + k2[i].x/2, y: body.y + k2[i].y/2, m: body.m}));
|
||||
let a3 = this.calculateAccelerations(b3);
|
||||
let k3 = b.map((body, i) => ({vx: a3[i].ax*dt, vy: a3[i].ay*dt, x: (body.vx + k2[i].vx/2)*dt, y: (body.vy + k2[i].vy/2)*dt}));
|
||||
// k4
|
||||
let b4 = b.map((body, i) => ({x: body.x + k3[i].x, y: body.y + k3[i].y, m: body.m}));
|
||||
let a4 = this.calculateAccelerations(b4);
|
||||
let k4 = b.map((body, i) => ({vx: a4[i].ax*dt, vy: a4[i].ay*dt, x: (body.vx + k3[i].vx)*dt, y: (body.vy + k3[i].vy)*dt}));
|
||||
|
||||
// 更新状态
|
||||
for(let i=0; i<b.length; i++) {
|
||||
b[i].x += (k1[i].x + 2*k2[i].x + 2*k3[i].x + k4[i].x)/6;
|
||||
b[i].y += (k1[i].y + 2*k2[i].y + 2*k3[i].y + k4[i].y)/6;
|
||||
b[i].vx += (k1[i].vx + 2*k2[i].vx + 2*k3[i].vx + k4[i].vx)/6;
|
||||
b[i].vy += (k1[i].vy + 2*k2[i].vy + 2*k3[i].vy + k4[i].vy)/6;
|
||||
|
||||
this.state.trails[i].push({x: b[i].x, y: b[i].y});
|
||||
if(this.state.trails[i].length > 150) this.state.trails[i].shift();
|
||||
}
|
||||
|
||||
// 寻找中心点用于摄像机跟随
|
||||
let cx = width/2, cy = height/2;
|
||||
let sumX = 0, sumY = 0;
|
||||
b.forEach(body => { sumX += body.x; sumY += body.y; });
|
||||
let comX = sumX/3, comY = sumY/3;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx - comX, cy - comY);
|
||||
|
||||
// 绘制轨迹和恒星
|
||||
for(let i=0; i<b.length; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = b[i].color;
|
||||
ctx.lineWidth = 1.5;
|
||||
let tr = this.state.trails[i];
|
||||
for(let j=0; j<tr.length; j++) {
|
||||
ctx.globalAlpha = j / tr.length; // 轨迹渐隐
|
||||
if(j===0) ctx.moveTo(tr[j].x, tr[j].y);
|
||||
else ctx.lineTo(tr[j].x, tr[j].y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(b[i].x, b[i].y, b[i].r, 0, Math.PI*2);
|
||||
ctx.fillStyle = b[i].color;
|
||||
ctx.shadowColor = b[i].color;
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "第三幕:绝对光滑 (水滴)",
|
||||
narrative: "它静静地悬浮在太空中,完美得不包含任何瑕疵。这是一种无法用人类语言描述的美,表面绝对光滑,全反射着周围深邃的宇宙。它就像是一滴死神的眼泪,没有任何温度。我眼睁睁看着这颗长约三米的水滴,在瞬间撕碎了人类两千艘恒星级战舰。在物理学的铁壁面前,傲慢付出了最惨痛的代价。",
|
||||
science: [
|
||||
"强相互作用力:自然界四大基本力中最强的一种,负责将质子和中子结合在原子核中。作用距离极短(飞米级)。",
|
||||
"绝对零度与全反射:微观粒子热运动几乎停止,若表面粗糙度低于光波长,可呈现高反射率。"
|
||||
],
|
||||
fiction: [
|
||||
"水滴(探测器)的材料是由强相互作用力锁死的物质,这使得它的原子像被钉死般排列,坚不可摧。",
|
||||
"在宏观层面运用强相互作用力,使其硬度远超自然界任何常规物质,并能进行锐角转向无工质推进。"
|
||||
],
|
||||
state: { angle: 0 },
|
||||
init() { this.state.angle = 0; },
|
||||
draw(ctx, width, height) {
|
||||
this.state.angle += 0.01;
|
||||
let cx = width / 2;
|
||||
let cy = height / 2;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
|
||||
// 微弱的上下浮动
|
||||
let floatY = Math.sin(this.state.angle * 2) * 10;
|
||||
ctx.translate(0, floatY);
|
||||
|
||||
// 绘制水滴 (贝塞尔曲线)
|
||||
let w = 200;
|
||||
let h = 80;
|
||||
|
||||
// 建立金属渐变反射
|
||||
let grd = ctx.createLinearGradient(-w/2, -h, w/2, h);
|
||||
grd.addColorStop(0, "#fff");
|
||||
grd.addColorStop(0.2, "#88aaff");
|
||||
grd.addColorStop(0.5, "#112244");
|
||||
grd.addColorStop(0.8, "#5577aa");
|
||||
grd.addColorStop(1, "#000");
|
||||
|
||||
ctx.beginPath();
|
||||
// 左侧尾部
|
||||
ctx.moveTo(-w/2, 0);
|
||||
// 上半部分弧线
|
||||
ctx.bezierCurveTo(-w/4, -h/2, w/4, -h/2, w/2, 0);
|
||||
// 下半部分弧线
|
||||
ctx.bezierCurveTo(w/4, h/2, -w/4, h/2, -w/2, 0);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = grd;
|
||||
ctx.shadowColor = 'rgba(150, 180, 255, 0.5)';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.fill();
|
||||
|
||||
// 高光
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-w/2 + 20, -5);
|
||||
ctx.bezierCurveTo(-w/4, -h/2 + 10, w/4, -h/2 + 10, w/2 - 20, -5);
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.7)";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.filter = 'blur(2px)';
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "第四幕:梵高的星空 (二向箔)",
|
||||
narrative: "一张明信片大小的纸条,剥夺了整个太阳系的深度。我绝望地看着前方的空间本身开始坍塌。木星、地球、逃亡的飞船……所有宏大的3D结构无可挽回地跌落,被死死地压在了一个二维的平面上。信息的细节在降维展开中铺陈,星海失去了深邃,变成了一幅绚丽而死寂的无边画卷,宛如梵高的《星月夜》。毁灭你,与你何干。",
|
||||
science: [
|
||||
"维度与空间:弦理论预测宇宙包含更多维度,但宏观上我们只体验到三维空间加一维时间。",
|
||||
"热力学第二定律:孤立系统熵增(混乱度增加)。任何物理降维过程都会释放惊人的能量与信息。"
|
||||
],
|
||||
fiction: [
|
||||
"将降维(从三维降至二维)作为一种宇宙规律武器,使得卷曲的维度展开。",
|
||||
"逃逸降维打击的唯一方法是达到光速(即需要曲率驱动飞船)。二维化后的世界呈现出内部结构的完全平面展示。"
|
||||
],
|
||||
state: { radius: 10, planets: [] },
|
||||
init() {
|
||||
this.state.radius = 10;
|
||||
this.state.planets = [
|
||||
{x: 200, y: 100, r: 40, col: '#3498db'}, // 地球
|
||||
{x: -150, y: -80, r: 60, col: '#e67e22'}, // 木星
|
||||
{x: -50, y: 150, r: 25, col: '#bdc3c7'} // 飞船/月球
|
||||
];
|
||||
},
|
||||
draw(ctx, width, height) {
|
||||
let cx = width / 2;
|
||||
let cy = height / 2;
|
||||
|
||||
// 扩张的二维化光环
|
||||
this.state.radius += 2.5;
|
||||
let r = this.state.radius;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
|
||||
// 绘制尚未被吞噬的 3D 星球
|
||||
this.state.planets.forEach(p => {
|
||||
let dist = Math.sqrt(p.x*p.x + p.y*p.y);
|
||||
if(dist > r) {
|
||||
// 3D 效果
|
||||
let grd = ctx.createRadialGradient(p.x - p.r/3, p.y - p.r/3, p.r/10, p.x, p.y, p.r);
|
||||
grd.addColorStop(0, p.col);
|
||||
grd.addColorStop(1, '#000');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
} else {
|
||||
// 二维化效果:摊大、同心环状结构、纯色无光影
|
||||
ctx.strokeStyle = p.col;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.5;
|
||||
for(let i=1; i<=5; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r * 1.5 * i/3, 0, Math.PI*2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// 降维打击的边界 (二向箔边缘)
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, r, 0, Math.PI*2);
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.shadowColor = '#fff';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.stroke();
|
||||
|
||||
// 二维区内部网格 (失去深度的象征)
|
||||
ctx.clip();
|
||||
ctx.strokeStyle = 'rgba(0, 255, 255, 0.1)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.shadowBlur = 0;
|
||||
for(let i=-r; i<r; i+=20) {
|
||||
ctx.beginPath(); ctx.moveTo(i, -r); ctx.lineTo(i, r); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(-r, i); ctx.lineTo(r, i); ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "最终幕:时间闭环 (小宇宙)",
|
||||
narrative: "几百亿年,又或者只是一瞬。在宇宙大撕裂的边缘,我推开了那扇第647号小宇宙的门。智子在一旁静静地泡着茶。我留下了一封信,一个装有五公斤生态的小球。我知道,为了大宇宙的重生,必须归还所有的质量。伴随着引力的坍缩,一切都回到了起点。时间闭环,我又看到了那颗蓝色的水球。一切都结束了,一切又将重新开始。",
|
||||
science: [
|
||||
"宇宙终局理论:大坍缩(Big Crunch)或热寂(Heat Death)。如果宇宙总质量超过临界值,引力最终会使其坍缩为一个奇点。",
|
||||
"物质与质量守恒:在宏观闭合系统中,总质能是守恒的。"
|
||||
],
|
||||
fiction: [
|
||||
"超级文明能够创造独立于大宇宙之外的『小宇宙』以躲避大爆炸或大坍缩。",
|
||||
"回归运动声明书呼吁所有小宇宙归还质量,以便大宇宙能够收缩并引发新的大爆炸。"
|
||||
],
|
||||
state: { size: 0 },
|
||||
init() { this.state.size = 0; },
|
||||
draw(ctx, width, height) {
|
||||
let cx = width / 2;
|
||||
let cy = height / 2;
|
||||
this.state.size += (1 - this.state.size) * 0.02; // 缓动展开
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
|
||||
// 小宇宙的门
|
||||
let doorWidth = 120 * this.state.size;
|
||||
let doorHeight = 200 * this.state.size;
|
||||
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.shadowColor = "#fff";
|
||||
ctx.shadowBlur = 50;
|
||||
ctx.fillRect(-doorWidth/2, -doorHeight/2, doorWidth, doorHeight);
|
||||
|
||||
// 门中的微型新地球
|
||||
if (this.state.size > 0.8) {
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = "#3498db";
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 30, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
|
||||
// 生态球光晕
|
||||
ctx.strokeStyle = "rgba(100, 255, 100, 0.8)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 40, 0, Math.PI*2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
let currentSceneIndex = 0;
|
||||
|
||||
function updatePanels() {
|
||||
const scene = scenes[currentSceneIndex];
|
||||
document.getElementById('scene-title').innerText = scene.title;
|
||||
|
||||
// 渲染科学与想象列表
|
||||
const sList = document.getElementById('science-list');
|
||||
sList.innerHTML = scene.science.map(item => `<li>${item}</li>`).join('');
|
||||
|
||||
const fList = document.getElementById('fiction-list');
|
||||
fList.innerHTML = scene.fiction.map(item => `<li>${item}</li>`).join('');
|
||||
|
||||
// 打字机叙事
|
||||
typeWriter(scene.narrative, 'narrative-text');
|
||||
|
||||
// 按钮状态
|
||||
document.getElementById('prev-btn').disabled = currentSceneIndex === 0;
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
if (currentSceneIndex === scenes.length - 1) {
|
||||
nextBtn.innerText = "重新循环";
|
||||
} else {
|
||||
nextBtn.innerText = "下一跃迁";
|
||||
}
|
||||
|
||||
scene.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主循环
|
||||
*/
|
||||
function loop() {
|
||||
drawBackground();
|
||||
|
||||
// 渲染当前场景
|
||||
scenes[currentSceneIndex].draw(ctx, width, height);
|
||||
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 交互控制
|
||||
*/
|
||||
document.getElementById('start-btn').addEventListener('click', () => {
|
||||
document.getElementById('intro').style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
document.getElementById('intro').style.display = 'none';
|
||||
updatePanels();
|
||||
loop();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
document.getElementById('next-btn').addEventListener('click', () => {
|
||||
if (currentSceneIndex < scenes.length - 1) {
|
||||
currentSceneIndex++;
|
||||
} else {
|
||||
currentSceneIndex = 0; // 循环
|
||||
}
|
||||
updatePanels();
|
||||
});
|
||||
|
||||
document.getElementById('prev-btn').addEventListener('click', () => {
|
||||
if (currentSceneIndex > 0) {
|
||||
currentSceneIndex--;
|
||||
updatePanels();
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘支持
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if(document.getElementById('intro').style.display !== 'none') return;
|
||||
if(e.key === 'ArrowRight' || e.key === 'Space') {
|
||||
document.getElementById('next-btn').click();
|
||||
} else if(e.key === 'ArrowLeft') {
|
||||
document.getElementById('prev-btn').click();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user