<!-- 1. 结构 -->
<div id="app" class="ultra-rocket-demo">
<canvas ref="mainCanvas" class="main-canvas"></canvas>
<!-- 闪光层 -->
<div class="flash-overlay" ref="flashOverlay"></div>
<!-- UI -->
<div class="hud-layer">
<div class="altitude">ALT: {{ altitude.toFixed(0) }} M</div>
<div class="speed">VEL: {{ velocity.toFixed(0) }} KM/H</div>
<div class="status" :class="{ blink: isLaunching }">{{ statusText }}</div>
<button class="fire-btn" @click="startSequence" :disabled="isLaunching">
<span class="btn-text">{{ isLaunching ? 'IGNITION' : 'LAUNCH' }}</span>
<div class="btn-glitch"></div>
</button>
</div>
</div>
<!-- 2. 样式 -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
.ultra-rocket-demo {
position: relative;
width: 100vw;
height: 100vh;
background: #000;
overflow: hidden;
font-family: 'Orbitron', sans-serif;
cursor: crosshair;
}
.main-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.flash-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
opacity: 0;
pointer-events: none;
z-index: 100;
mix-blend-mode: overlay;
}
.hud-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 50;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.altitude, .speed {
font-size: 1.5rem;
color: #00ffcc;
text-shadow: 0 0 10px #00ffcc;
margin-bottom: 10px;
}
.status {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 4rem;
font-weight: 900;
color: rgba(255, 255, 255, 0.1);
letter-spacing: 10px;
white-space: nowrap;
}
.status.blink {
color: #ff3333;
text-shadow: 0 0 20px #ff0000;
animation: hud-blink 0.2s infinite;
}
@keyframes hud-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.fire-btn {
pointer-events: auto;
align-self: center;
padding: 20px 60px;
background: transparent;
border: 2px solid #ff3333;
color: #ff3333;
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
font-weight: bold;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
box-shadow: 0 0 20px rgba(255, 51, 51, 0.2);
}
.fire-btn:hover {
background: #ff3333;
color: #000;
box-shadow: 0 0 40px rgba(255, 51, 51, 0.6);
}
.fire-btn:disabled {
border-color: #555;
color: #555;
cursor: not-allowed;
box-shadow: none;
}
</style>
<!-- 3. 逻辑 -->
<script>
const { createApp, ref, onMounted, onBeforeUnmount } = Vue;
createApp({
setup() {
const mainCanvas = ref(null);
const flashOverlay = ref(null);
const isLaunching = ref(false);
const statusText = ref('SYSTEM READY');
const altitude = ref(0);
const velocity = ref(0);
let ctx;
let width, height;
let animationId;
// 游戏循环变量
let particles = [];
let stars = [];
let shockwaves = [];
let debris = [];
// 火箭状态
let rocket = {
x: 0,
y: 0,
vy: 0,
width: 40,
height: 100,
shake: 0,
thrust: 0
};
let camera = { y: 0, shake: 0 };
let state = 'IDLE'; // IDLE, IGNITION, LIFTOFF
let time = 0;
// --- 粒子系统 ---
class Particle {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
this.life = 1.0;
if (type === 'fire') {
const angle = Math.PI/2 + (Math.random()-0.5) * 0.5;
const speed = Math.random() * 15 + 10;
this.vx = Math.cos(angle) * speed * 0.3;
this.vy = Math.sin(angle) * speed;
this.size = Math.random() * 20 + 10;
this.decay = Math.random() * 0.05 + 0.02;
this.color = [255, Math.random()*200, 0]; // RGB
} else if (type === 'smoke') {
const angle = Math.PI/2 + (Math.random()-0.5) * 1.5;
const speed = Math.random() * 8 + 2;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.size = Math.random() * 40 + 20;
this.decay = Math.random() * 0.01 + 0.005;
this.color = [100, 100, 100];
} else if (type === 'spark') {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 20 + 10;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.size = Math.random() * 3 + 1;
this.decay = Math.random() * 0.05 + 0.05;
this.color = [255, 255, 200];
}
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life -= this.decay;
if (this.type === 'smoke') {
this.size += 0.5;
this.vx *= 0.95;
this.vy *= 0.95;
} else if (this.type === 'spark') {
this.vy += 0.5; // 重力
}
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y - camera.y, this.size, 0, Math.PI*2);
if (this.type === 'fire') {
// 核心发光
ctx.shadowBlur = 20;
ctx.shadowColor = `rgba(${this.color[0]}, ${this.color[1]}, ${this.color[2]}, 1)`;
ctx.fillStyle = `rgba(${this.color[0]}, ${this.color[1]}, ${this.color[2]}, ${this.life})`;
ctx.globalCompositeOperation = 'lighter'; // 叠加变亮
} else if (this.type === 'smoke') {
ctx.shadowBlur = 0;
ctx.fillStyle = `rgba(${this.color[0]}, ${this.color[1]}, ${this.color[2]}, ${this.life * 0.5})`;
ctx.globalCompositeOperation = 'source-over';
} else {
ctx.shadowBlur = 10;
ctx.shadowColor = '#fff';
ctx.fillStyle = `rgba(255, 255, 255, ${this.life})`;
ctx.globalCompositeOperation = 'lighter';
}
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
ctx.shadowBlur = 0;
}
}
class Shockwave {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = 10;
this.life = 1.0;
this.maxSize = 300;
}
update() {
this.size += 15;
this.life -= 0.05;
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y - camera.y, this.size, 0, Math.PI*2);
ctx.lineWidth = 20 * this.life;
ctx.strokeStyle = `rgba(255, 255, 255, ${this.life * 0.5})`;
ctx.stroke();
}
}
// --- 初始化 ---
const init = () => {
width = window.innerWidth;
height = window.innerHeight;
mainCanvas.value.width = width;
mainCanvas.value.height = height;
ctx = mainCanvas.value.getContext('2d');
rocket.x = width / 2;
rocket.y = height - 150;
// 初始化星空
for(let i=0; i<300; i++) {
stars.push({
x: Math.random() * width,
y: Math.random() * height * 2, // 星空更高
z: Math.random() * 2 + 0.5 // 深度
});
}
loop();
};
// --- 逻辑控制 ---
const startSequence = () => {
isLaunching.value = true;
state = 'IGNITION';
statusText.value = 'IGNITION';
// 倒计时震动
let count = 0;
const ignitionInterval = setInterval(() => {
rocket.shake = 5;
// 产生少量烟雾
particles.push(new Particle(rocket.x, rocket.y + 50, 'smoke'));
count++;
if (count > 60) { // 1秒后发射
clearInterval(ignitionInterval);
liftoff();
}
}, 16);
};
const liftoff = () => {
state = 'LIFTOFF';
statusText.value = 'MAX THRUST';
// 屏幕闪白
gsap.fromTo(flashOverlay.value,
{ opacity: 1 },
{ opacity: 0, duration: 1, ease: "power2.out" }
);
// 产生冲击波
shockwaves.push(new Shockwave(rocket.x, rocket.y + 50));
// 产生火花碎片
for(let i=0; i<20; i++) {
particles.push(new Particle(rocket.x, rocket.y + 50, 'spark'));
}
};
// --- 渲染循环 ---
const loop = () => {
// 1. 清空与背景
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, width, height);
// 2. 震动摄像机
let camShakeX = 0;
let camShakeY = 0;
if (state === 'IGNITION') {
camShakeX = (Math.random()-0.5) * 5;
camShakeY = (Math.random()-0.5) * 5;
} else if (state === 'LIFTOFF') {
camera.shake = Math.min(camera.shake + 0.5, 20); // 震动增强
camShakeX = (Math.random()-0.5) * camera.shake;
camShakeY = (Math.random()-0.5) * camera.shake;
}
ctx.save();
ctx.translate(camShakeX, camShakeY);
// 3. 绘制星空 (Warp Speed 效果)
ctx.fillStyle = '#fff';
stars.forEach(star => {
// 星星向下移动
let speed = 0.2 * star.z;
if (state === 'LIFTOFF') speed += rocket.vy * 0.5 * star.z;
star.y += speed;
if (star.y > height + camera.y) {
star.y = camera.y - 100;
star.x = Math.random() * width;
}
// 拉长效果
const length = Math.min(speed * 2, 50);
ctx.globalAlpha = Math.min(speed * 0.1 + 0.2, 1);
ctx.fillRect(star.x, star.y - camera.y - length, Math.max(1, star.z), length + 2);
});
ctx.globalAlpha = 1;
// 4. 更新火箭物理
if (state === 'LIFTOFF') {
rocket.thrust += 0.5;
rocket.vy += 0.2;
rocket.y -= rocket.vy;
camera.y -= rocket.vy; // 摄像机跟随
altitude.value += rocket.vy * 0.1;
velocity.value = rocket.vy * 30;
// 持续产生火焰和烟雾
for(let i=0; i<5; i++) particles.push(new Particle(rocket.x + (Math.random()-0.5)*20, rocket.y + 50, 'fire'));
for(let i=0; i<2; i++) particles.push(new Particle(rocket.x + (Math.random()-0.5)*30, rocket.y + 50, 'smoke'));
}
// 5. 绘制地面 (简单的发射台)
if (camera.y > -height) {
ctx.fillStyle = '#222';
ctx.fillRect(width/2 - 100, height - 100 - camera.y, 200, 100);
}
// 6. 绘制火箭
const rx = rocket.x + (Math.random()-0.5) * rocket.shake;
const ry = rocket.y - camera.y;
// 简单的 Canvas 绘制火箭
ctx.fillStyle = '#eee';
ctx.beginPath();
ctx.ellipse(rx, ry, 20, 60, 0, 0, Math.PI*2);
ctx.fill();
// 尾翼
ctx.fillStyle = '#ff3333';
ctx.beginPath();
ctx.moveTo(rx - 20, ry + 20);
ctx.lineTo(rx - 35, ry + 60);
ctx.lineTo(rx - 15, ry + 50);
ctx.fill();
ctx.beginPath();
ctx.moveTo(rx + 20, ry + 20);
ctx.lineTo(rx + 35, ry + 60);
ctx.lineTo(rx + 15, ry + 50);
ctx.fill();
// 7. 绘制粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.update();
p.draw(ctx);
if (p.life <= 0) particles.splice(i, 1);
}
// 8. 绘制冲击波
for (let i = shockwaves.length - 1; i >= 0; i--) {
const s = shockwaves[i];
s.update();
s.draw(ctx);
if (s.life <= 0) shockwaves.splice(i, 1);
}
ctx.restore();
animationId = requestAnimationFrame(loop);
};
const handleResize = () => {
width = window.innerWidth;
height = window.innerHeight;
mainCanvas.value.width = width;
mainCanvas.value.height = height;
};
onMounted(() => {
init();
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
cancelAnimationFrame(animationId);
window.removeEventListener('resize', handleResize);
});
return {
mainCanvas,
flashOverlay,
isLaunching,
startSequence,
statusText,
altitude,
velocity
};
}
}).mount('#app');
</script>