2026-01-20 13:40:04 +08:00
|
|
|
<!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>
|
|
|
|
|
:root {
|
|
|
|
|
--primary: #ff4757;
|
|
|
|
|
--secondary: #2f3542;
|
|
|
|
|
--bg: #1e272e;
|
|
|
|
|
--text: #f1f2f6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
background-color: var(--bg);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
user-select: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
text-shadow: 0 0 10px #ff6b6b;
|
|
|
|
|
font-size: 2.5rem;
|
|
|
|
|
letter-spacing: 2px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 转盘容器 */
|
|
|
|
|
.wheel-container {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 500px;
|
|
|
|
|
height: 500px;
|
|
|
|
|
filter: drop-shadow(0 0 20px rgba(0,0,0,0.5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
canvas#wheelCanvas {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
transform: rotate(0deg);
|
|
|
|
|
transition: transform 5s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 指针 */
|
|
|
|
|
.pointer {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -20px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 60px;
|
|
|
|
|
background: #ff4757;
|
|
|
|
|
clip-path: polygon(50% 100%, 0 0, 100% 0);
|
|
|
|
|
z-index: 10;
|
|
|
|
|
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 按钮 */
|
|
|
|
|
button#spinBtn {
|
|
|
|
|
margin-top: 40px;
|
|
|
|
|
padding: 15px 50px;
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
background: linear-gradient(45deg, #ff4757, #ff6b6b);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 50px;
|
|
|
|
|
color: white;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
box-shadow: 0 5px 15px rgba(255, 71, 87, 0.4);
|
|
|
|
|
transition: transform 0.1s, box-shadow 0.1s;
|
|
|
|
|
outline: none;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button#spinBtn:active {
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
box-shadow: 0 2px 5px rgba(255, 71, 87, 0.4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button#spinBtn:disabled {
|
|
|
|
|
background: #57606f;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 结果模态框 - 地狱火烤风格 */
|
|
|
|
|
#resultModal {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: rgba(0, 0, 0, 0.9);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
transition: opacity 0.5s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#resultModal.active {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
pointer-events: all;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lucky-title {
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
color: #ced6e0;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(-20px);
|
|
|
|
|
transition: all 0.5s 0.5s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#resultModal.active .lucky-title {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.victim-name {
|
|
|
|
|
font-size: 6rem;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
color: #fff;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
text-shadow: 0 0 20px #ff4757;
|
|
|
|
|
animation: shake 0.5s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 痛苦的表情 */
|
|
|
|
|
.face-emoji {
|
|
|
|
|
font-size: 4rem;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -60px;
|
|
|
|
|
right: -60px;
|
|
|
|
|
animation: bounce 1s infinite alternate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 火焰画布 */
|
|
|
|
|
canvas#fireCanvas {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 60%;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 弹幕吐槽 */
|
|
|
|
|
.danmaku {
|
|
|
|
|
position: absolute;
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
animation: fly linear forwards;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
text-shadow: 1px 1px 2px black;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes fly {
|
|
|
|
|
from { transform: translateX(100vw); }
|
|
|
|
|
to { transform: translateX(-100vw); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes shake {
|
|
|
|
|
0% { transform: translate(1px, 1px) rotate(0deg); }
|
|
|
|
|
10% { transform: translate(-1px, -2px) rotate(-1deg); }
|
|
|
|
|
20% { transform: translate(-3px, 0px) rotate(1deg); }
|
|
|
|
|
30% { transform: translate(3px, 2px) rotate(0deg); }
|
|
|
|
|
40% { transform: translate(1px, -1px) rotate(1deg); }
|
|
|
|
|
50% { transform: translate(-1px, 2px) rotate(-1deg); }
|
|
|
|
|
60% { transform: translate(-3px, 1px) rotate(0deg); }
|
|
|
|
|
70% { transform: translate(3px, 1px) rotate(-1deg); }
|
|
|
|
|
80% { transform: translate(-1px, -1px) rotate(1deg); }
|
|
|
|
|
90% { transform: translate(1px, 2px) rotate(0deg); }
|
|
|
|
|
100% { transform: translate(1px, -2px) rotate(-1deg); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes bounce {
|
|
|
|
|
from { transform: translateY(0); }
|
|
|
|
|
to { transform: translateY(-20px); }
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 13:48:35 +08:00
|
|
|
/* 枪击特效样式 */
|
|
|
|
|
.gun-wrapper {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: -300px; /* 初始在屏幕外 */
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
width: 300px;
|
|
|
|
|
height: 200px;
|
|
|
|
|
z-index: 50;
|
|
|
|
|
transition: right 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gun-wrapper.active {
|
|
|
|
|
right: 50px; /* 伸出来 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 枪身 SVG 容器 */
|
|
|
|
|
.gun-svg {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
filter: drop-shadow(5px 5px 5px rgba(0,0,0,0.5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 枪口火光 */
|
|
|
|
|
.muzzle-flash {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -40px;
|
|
|
|
|
top: 35px;
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
background: radial-gradient(circle, #fff, #ffff00, #ff0000, transparent);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
transform: scale(0);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
z-index: 51;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.muzzle-flash.bang {
|
|
|
|
|
animation: flash 0.1s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes flash {
|
|
|
|
|
0% { transform: scale(0); opacity: 1; }
|
|
|
|
|
50% { transform: scale(1.5); opacity: 1; }
|
|
|
|
|
100% { transform: scale(0.5); opacity: 0; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 开枪时的后坐力 */
|
|
|
|
|
.gun-wrapper.shoot .gun-svg {
|
|
|
|
|
animation: recoil 0.2s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes recoil {
|
|
|
|
|
0% { transform: translateX(0) rotate(0); }
|
|
|
|
|
10% { transform: translateX(50px) rotate(10deg); }
|
|
|
|
|
100% { transform: translateX(0) rotate(0); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 砰文字 */
|
|
|
|
|
.bang-text {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -80px;
|
|
|
|
|
top: 0;
|
|
|
|
|
font-size: 4rem;
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
text-shadow: 0 0 10px red;
|
|
|
|
|
transform: scale(0) rotate(-20deg);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
z-index: 52;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bang-text.show {
|
|
|
|
|
animation: bangPop 0.5s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes bangPop {
|
|
|
|
|
0% { transform: scale(0) rotate(-20deg); opacity: 1; }
|
|
|
|
|
50% { transform: scale(1.5) rotate(0deg); opacity: 1; }
|
|
|
|
|
100% { transform: scale(1) rotate(0deg); opacity: 0; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 名字倒地动画 */
|
|
|
|
|
.victim-name.dead {
|
|
|
|
|
animation: fallDead 1.5s forwards cubic-bezier(0.6, -0.28, 0.735, 0.045);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.victim-name.dead .face-emoji {
|
|
|
|
|
content: "😵"; /* 可以在 CSS 中无法直接改内容,通过 JS 改 */
|
|
|
|
|
animation: none; /* 停止跳动 */
|
|
|
|
|
transform: rotate(180deg); /* 表情倒过来 */
|
|
|
|
|
transition: all 0.5s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes fallDead {
|
|
|
|
|
0% { transform: rotate(0); color: #fff; }
|
|
|
|
|
10% { transform: rotate(-5deg) translateX(0); color: #ff0000; } /* 中弹瞬间变红 */
|
|
|
|
|
30% { transform: rotate(10deg) translateX(10px); }
|
|
|
|
|
100% {
|
|
|
|
|
transform: rotate(100deg) translateY(300px);
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
color: #57606f; /* 变成死灰色 */
|
|
|
|
|
text-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 13:40:04 +08:00
|
|
|
.close-btn {
|
|
|
|
|
margin-top: 50px;
|
|
|
|
|
padding: 10px 30px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: 2px solid #fff;
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
border-radius: 30px;
|
|
|
|
|
transition: 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.close-btn:hover {
|
|
|
|
|
background: #fff;
|
|
|
|
|
color: #000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
<h1>🔮 命运之轮 🔮</h1>
|
|
|
|
|
|
|
|
|
|
<div class="wheel-container">
|
|
|
|
|
<div class="pointer"></div>
|
|
|
|
|
<canvas id="wheelCanvas" width="500" height="500"></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button id="spinBtn">开始点名</button>
|
|
|
|
|
|
|
|
|
|
<!-- 结果模态框 -->
|
|
|
|
|
<div id="resultModal">
|
|
|
|
|
<canvas id="fireCanvas"></canvas>
|
|
|
|
|
<div class="lucky-title">恭喜这位“幸运”同学...</div>
|
|
|
|
|
<div class="victim-name" id="victimName">
|
|
|
|
|
???
|
|
|
|
|
<div class="face-emoji">😱</div>
|
|
|
|
|
</div>
|
2026-01-20 13:48:35 +08:00
|
|
|
|
|
|
|
|
<!-- 枪的容器 -->
|
|
|
|
|
<div id="gunWrapper" class="gun-wrapper">
|
|
|
|
|
<div class="bang-text">BANG!</div>
|
|
|
|
|
<div class="muzzle-flash"></div>
|
|
|
|
|
<!-- SVG 手枪 -->
|
|
|
|
|
<svg class="gun-svg" viewBox="0 0 100 60" fill="#333">
|
|
|
|
|
<path d="M10,10 L60,10 L60,20 L80,20 L80,30 L60,30 L60,40 L30,40 L30,50 L10,50 Z" stroke="#000" stroke-width="2"/>
|
|
|
|
|
<rect x="0" y="5" width="10" height="5" fill="#555"/>
|
|
|
|
|
<rect x="60" y="15" width="5" height="5" fill="#555"/>
|
|
|
|
|
<!-- 简单的手枪造型优化 -->
|
|
|
|
|
<path d="M5,10 h50 a5,5 0 0 1 5,5 v5 h25 v10 h-25 v15 a5,5 0 0 1 -5,5 h-20 a5,5 0 0 1 -5,-5 v-15 h-20 a5,5 0 0 1 -5,-5 v-10 a5,5 0 0 1 5,-5 z" fill="#2d3436"/>
|
|
|
|
|
<rect x="60" y="12" width="20" height="4" fill="#000"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-20 13:40:04 +08:00
|
|
|
<button class="close-btn" onclick="closeModal()">放过他/她吧</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// ================= 配置数据 =================
|
2026-01-20 19:16:55 +08:00
|
|
|
const students = ["刘美希", "林子琪", "唐纯瑞", "林子皓", "刘若曦","冯筱壹", "王艺诺", "邹泓凯", "王梓博", "肖靖泽", "彭馨瑶", "刘丰源", "黄琬乔", "赵敏智"];
|
2026-01-20 13:40:04 +08:00
|
|
|
const colors = [
|
|
|
|
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
|
|
|
|
|
'#F7DC6F', '#BB8FCE', '#F1948A', '#82E0AA', '#D7BDE2',
|
|
|
|
|
'#F0B27A', '#85C1E9', '#F5B7B1'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 吐槽弹幕库
|
|
|
|
|
const comments = [
|
|
|
|
|
"怎么又是我?!", "老师别看我...", "由于太帅被系统选中",
|
|
|
|
|
"如果能重来...", "此时一位靓仔失去了梦想", "不要啊!!!",
|
|
|
|
|
"这也太准了吧", "这就是命", "正在假装断网...", "瑟瑟发抖中"
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// ================= 变量与初始化 =================
|
|
|
|
|
const canvas = document.getElementById('wheelCanvas');
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
const spinBtn = document.getElementById('spinBtn');
|
|
|
|
|
const modal = document.getElementById('resultModal');
|
|
|
|
|
const victimNameEl = document.getElementById('victimName');
|
|
|
|
|
const fireCanvas = document.getElementById('fireCanvas');
|
|
|
|
|
const fireCtx = fireCanvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
let startAngle = 0;
|
|
|
|
|
const arc = Math.PI * 2 / students.length;
|
|
|
|
|
let spinTimeout = null;
|
|
|
|
|
let spinAngleStart = 10;
|
|
|
|
|
let spinTime = 0;
|
|
|
|
|
let spinTimeTotal = 0;
|
|
|
|
|
let isSpinning = false;
|
|
|
|
|
let fireAnimationId;
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
resizeFireCanvas();
|
|
|
|
|
drawRouletteWheel();
|
|
|
|
|
window.addEventListener('resize', resizeFireCanvas);
|
|
|
|
|
|
|
|
|
|
// ================= 转盘逻辑 =================
|
|
|
|
|
function drawRouletteWheel() {
|
|
|
|
|
const centerX = canvas.width / 2;
|
|
|
|
|
const centerY = canvas.height / 2;
|
|
|
|
|
const outsideRadius = 240;
|
|
|
|
|
const textRadius = 170;
|
|
|
|
|
const insideRadius = 50;
|
|
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
|
ctx.font = 'bold 18px Helvetica, Arial';
|
|
|
|
|
|
|
|
|
|
for(let i = 0; i < students.length; i++) {
|
|
|
|
|
const angle = startAngle + i * arc;
|
|
|
|
|
|
|
|
|
|
// 绘制扇形
|
|
|
|
|
ctx.fillStyle = colors[i % colors.length];
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(centerX, centerY, outsideRadius, angle, angle + arc, false);
|
|
|
|
|
ctx.arc(centerX, centerY, insideRadius, angle + arc, angle, true);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
|
|
|
|
// 绘制文字
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.shadowColor = "rgba(0,0,0,0.5)";
|
|
|
|
|
ctx.shadowBlur = 4;
|
|
|
|
|
ctx.fillStyle = "white";
|
|
|
|
|
ctx.translate(centerX + Math.cos(angle + arc / 2) * textRadius,
|
|
|
|
|
centerY + Math.sin(angle + arc / 2) * textRadius);
|
|
|
|
|
ctx.rotate(angle + arc / 2 + Math.PI / 2);
|
|
|
|
|
const text = students[i];
|
|
|
|
|
ctx.fillText(text, -ctx.measureText(text).width / 2, 0);
|
|
|
|
|
ctx.restore();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rotateWheel() {
|
|
|
|
|
spinTime += 30;
|
|
|
|
|
if(spinTime >= spinTimeTotal) {
|
|
|
|
|
stopRotateWheel();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 缓动算法
|
|
|
|
|
const spinAngle = spinAngleStart - easeOut(spinTime, 0, spinAngleStart, spinTimeTotal);
|
|
|
|
|
startAngle += (spinAngle * Math.PI / 180);
|
|
|
|
|
drawRouletteWheel();
|
|
|
|
|
|
|
|
|
|
// 播放转动音效(滴答声)
|
|
|
|
|
playTickSound();
|
|
|
|
|
|
|
|
|
|
spinTimeout = setTimeout(rotateWheel, 30);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopRotateWheel() {
|
|
|
|
|
clearTimeout(spinTimeout);
|
|
|
|
|
isSpinning = false;
|
|
|
|
|
spinBtn.disabled = false;
|
|
|
|
|
spinBtn.innerText = "再来一次";
|
|
|
|
|
|
|
|
|
|
const degrees = startAngle * 180 / Math.PI + 90;
|
|
|
|
|
const arcd = arc * 180 / Math.PI;
|
|
|
|
|
const index = Math.floor((360 - degrees % 360) / arcd);
|
|
|
|
|
|
|
|
|
|
const selectedStudent = students[index];
|
|
|
|
|
showResult(selectedStudent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function easeOut(t, b, c, d) {
|
|
|
|
|
const ts = (t/=d)*t;
|
|
|
|
|
const tc = ts*t;
|
|
|
|
|
return b+c*(tc + -3*ts + 3*t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spinBtn.addEventListener('click', () => {
|
|
|
|
|
if(isSpinning) return;
|
|
|
|
|
isSpinning = true;
|
|
|
|
|
spinBtn.disabled = true;
|
|
|
|
|
spinBtn.innerText = "命运抉择中...";
|
|
|
|
|
|
|
|
|
|
spinAngleStart = Math.random() * 10 + 10;
|
|
|
|
|
spinTime = 0;
|
|
|
|
|
spinTimeTotal = Math.random() * 3000 + 4000; // 4-7秒
|
|
|
|
|
rotateWheel();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ================= 结果与特效 =================
|
|
|
|
|
function showResult(name) {
|
|
|
|
|
victimNameEl.innerHTML = `${name} <div class="face-emoji">😱</div>`;
|
2026-01-20 13:48:35 +08:00
|
|
|
victimNameEl.classList.remove('dead'); // 重置状态
|
2026-01-20 13:40:04 +08:00
|
|
|
modal.classList.add('active');
|
|
|
|
|
|
|
|
|
|
// 启动火焰粒子
|
|
|
|
|
initFire();
|
|
|
|
|
|
2026-01-20 13:48:35 +08:00
|
|
|
// 枪击流程
|
|
|
|
|
startShootingSequence(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startShootingSequence(name) {
|
|
|
|
|
const gunWrapper = document.getElementById('gunWrapper');
|
|
|
|
|
const muzzleFlash = document.querySelector('.muzzle-flash');
|
|
|
|
|
const bangText = document.querySelector('.bang-text');
|
|
|
|
|
const faceEmoji = victimNameEl.querySelector('.face-emoji');
|
|
|
|
|
|
|
|
|
|
// 1. 枪伸出来 (0.5s后)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
gunWrapper.classList.add('active');
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
// 2. 开枪 (1.5s后)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
// 视觉特效
|
|
|
|
|
gunWrapper.classList.add('shoot');
|
|
|
|
|
muzzleFlash.classList.add('bang');
|
|
|
|
|
bangText.classList.add('show');
|
|
|
|
|
|
|
|
|
|
// 名字倒地
|
|
|
|
|
victimNameEl.classList.add('dead');
|
|
|
|
|
faceEmoji.innerText = "😵"; // 表情变成晕倒
|
|
|
|
|
|
|
|
|
|
// 音效
|
|
|
|
|
playGunShot();
|
|
|
|
|
playScreamSound(); // 惨叫配合枪声
|
|
|
|
|
|
|
|
|
|
// 朗读名字
|
|
|
|
|
setTimeout(() => speakName(name), 500);
|
|
|
|
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
|
|
|
|
// 3. 弹幕稍后出现 (2.5s后)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
launchDanmaku();
|
|
|
|
|
}, 2500);
|
2026-01-20 13:40:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeModal() {
|
|
|
|
|
modal.classList.remove('active');
|
|
|
|
|
cancelAnimationFrame(fireAnimationId);
|
2026-01-20 13:48:35 +08:00
|
|
|
|
|
|
|
|
// 重置枪的状态
|
|
|
|
|
const gunWrapper = document.getElementById('gunWrapper');
|
|
|
|
|
const muzzleFlash = document.querySelector('.muzzle-flash');
|
|
|
|
|
const bangText = document.querySelector('.bang-text');
|
|
|
|
|
|
|
|
|
|
gunWrapper.classList.remove('active', 'shoot');
|
|
|
|
|
muzzleFlash.classList.remove('bang');
|
|
|
|
|
bangText.classList.remove('show');
|
|
|
|
|
|
2026-01-20 13:40:04 +08:00
|
|
|
// 清除弹幕
|
|
|
|
|
document.querySelectorAll('.danmaku').forEach(el => el.remove());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function launchDanmaku() {
|
|
|
|
|
const container = document.getElementById('resultModal');
|
|
|
|
|
for(let i=0; i<15; i++) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
el.className = 'danmaku';
|
|
|
|
|
el.innerText = comments[Math.floor(Math.random() * comments.length)];
|
|
|
|
|
el.style.top = Math.random() * 80 + '%';
|
|
|
|
|
el.style.animationDuration = (Math.random() * 3 + 2) + 's';
|
|
|
|
|
container.appendChild(el);
|
|
|
|
|
|
|
|
|
|
// 动画结束后移除
|
|
|
|
|
el.addEventListener('animationend', () => el.remove());
|
|
|
|
|
}, Math.random() * 2000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ================= 音效系统 (Web Audio API) =================
|
|
|
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
|
|
|
const audioCtx = new AudioContext();
|
|
|
|
|
|
|
|
|
|
function playTickSound() {
|
|
|
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
|
|
const osc = audioCtx.createOscillator();
|
|
|
|
|
const gain = audioCtx.createGain();
|
|
|
|
|
|
|
|
|
|
osc.type = 'triangle';
|
|
|
|
|
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
|
|
|
|
|
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.1);
|
|
|
|
|
|
|
|
|
|
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
|
|
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
|
|
|
|
|
|
|
|
|
osc.connect(gain);
|
|
|
|
|
gain.connect(audioCtx.destination);
|
|
|
|
|
|
|
|
|
|
osc.start();
|
|
|
|
|
osc.stop(audioCtx.currentTime + 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 13:48:35 +08:00
|
|
|
function playGunShot() {
|
|
|
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
|
|
|
|
|
|
|
// 噪声源模拟枪声
|
|
|
|
|
const bufferSize = audioCtx.sampleRate * 0.5; // 0.5秒
|
|
|
|
|
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
|
|
|
const data = buffer.getChannelData(0);
|
|
|
|
|
for (let i = 0; i < bufferSize; i++) {
|
|
|
|
|
data[i] = Math.random() * 2 - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const noise = audioCtx.createBufferSource();
|
|
|
|
|
noise.buffer = buffer;
|
|
|
|
|
|
|
|
|
|
// 滤波器,模仿枪声的闷响
|
|
|
|
|
const filter = audioCtx.createBiquadFilter();
|
|
|
|
|
filter.type = 'lowpass';
|
|
|
|
|
filter.frequency.value = 1000;
|
|
|
|
|
|
|
|
|
|
// 包络
|
|
|
|
|
const gain = audioCtx.createGain();
|
|
|
|
|
gain.gain.setValueAtTime(1, audioCtx.currentTime);
|
|
|
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
|
|
|
|
|
|
|
|
|
|
noise.connect(filter);
|
|
|
|
|
filter.connect(gain);
|
|
|
|
|
gain.connect(audioCtx.destination);
|
|
|
|
|
|
|
|
|
|
noise.start();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 13:40:04 +08:00
|
|
|
function playScreamSound() {
|
|
|
|
|
// 模拟一个低沉的“咚”声,表示厄运降临
|
|
|
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
|
|
|
|
|
|
|
const osc = audioCtx.createOscillator();
|
|
|
|
|
const gain = audioCtx.createGain();
|
|
|
|
|
|
|
|
|
|
osc.type = 'sawtooth';
|
|
|
|
|
osc.frequency.setValueAtTime(100, audioCtx.currentTime);
|
|
|
|
|
osc.frequency.exponentialRampToValueAtTime(10, audioCtx.currentTime + 1);
|
|
|
|
|
|
|
|
|
|
gain.gain.setValueAtTime(0.5, audioCtx.currentTime);
|
|
|
|
|
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 1);
|
|
|
|
|
|
|
|
|
|
osc.connect(gain);
|
|
|
|
|
gain.connect(audioCtx.destination);
|
|
|
|
|
|
|
|
|
|
osc.start();
|
|
|
|
|
osc.stop(audioCtx.currentTime + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function speakName(name) {
|
|
|
|
|
// 使用浏览器语音合成
|
|
|
|
|
if ('speechSynthesis' in window) {
|
|
|
|
|
const utterance = new SpeechSynthesisUtterance("恭喜" + name + "同学");
|
|
|
|
|
utterance.pitch = 0.5; // 低沉的声音,比较吓人
|
|
|
|
|
utterance.rate = 0.8; // 慢一点
|
|
|
|
|
utterance.volume = 1;
|
|
|
|
|
window.speechSynthesis.speak(utterance);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ================= 火焰粒子特效 (Canvas) =================
|
|
|
|
|
let particles = [];
|
|
|
|
|
|
|
|
|
|
function resizeFireCanvas() {
|
|
|
|
|
fireCanvas.width = window.innerWidth;
|
|
|
|
|
fireCanvas.height = window.innerHeight * 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Particle {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
this.x = Math.random() * fireCanvas.width;
|
|
|
|
|
this.y = fireCanvas.height;
|
|
|
|
|
this.size = Math.random() * 20 + 10;
|
|
|
|
|
this.speedY = Math.random() * 3 + 2;
|
|
|
|
|
this.speedX = (Math.random() - 0.5) * 2;
|
|
|
|
|
this.life = 100; // 生命值
|
|
|
|
|
this.color = { r: 255, g: Math.floor(Math.random() * 150), b: 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update() {
|
|
|
|
|
this.y -= this.speedY;
|
|
|
|
|
this.x += this.speedX;
|
|
|
|
|
this.size *= 0.95; // 逐渐变小
|
|
|
|
|
this.life -= 2;
|
|
|
|
|
|
|
|
|
|
// 颜色从黄变红变黑
|
|
|
|
|
if (this.life < 80) this.color.g -= 5;
|
|
|
|
|
if (this.color.g < 0) this.color.g = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw() {
|
|
|
|
|
fireCtx.beginPath();
|
|
|
|
|
fireCtx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
|
|
|
fireCtx.fillStyle = `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.life / 100})`;
|
|
|
|
|
fireCtx.fill();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initFire() {
|
|
|
|
|
particles = [];
|
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
|
|
|
particles.push(new Particle());
|
|
|
|
|
}
|
|
|
|
|
animateFire();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function animateFire() {
|
|
|
|
|
fireCtx.clearRect(0, 0, fireCanvas.width, fireCanvas.height);
|
|
|
|
|
// 产生新粒子
|
|
|
|
|
if (particles.length < 300) {
|
|
|
|
|
particles.push(new Particle());
|
|
|
|
|
particles.push(new Particle());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
particles.forEach((p, index) => {
|
|
|
|
|
p.update();
|
|
|
|
|
p.draw();
|
|
|
|
|
if (p.life <= 0 || p.size <= 0.5) {
|
|
|
|
|
particles.splice(index, 1);
|
|
|
|
|
particles.push(new Particle()); // 循环利用
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fireAnimationId = requestAnimationFrame(animateFire);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|