Files
website/html-webproject/MahjongWeb/mahjong-overlay.html
2026-01-18 20:49:14 +08:00

347 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>麻将直播叠加 UI</title>
<style>
body { margin: 0; padding: 0; background: transparent; font-family: Arial, sans-serif; overflow: hidden; }
#video { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
.player-panel {
position: absolute; width: 200px; height: 80px; color: white; text-align: center; border-radius: 10px; padding: 10px;
display: flex; flex-direction: column; justify-content: center; opacity: 0.8; transition: all 0.3s;
}
.player-panel:hover { opacity: 1; }
#east { bottom: 5%; left: 5%; background: rgba(255, 0, 0, 0.7); }
#south { bottom: 5%; left: 28%; background: rgba(0, 255, 0, 0.7); }
#west { bottom: 5%; left: 51%; background: rgba(0, 0, 255, 0.7); }
#north { bottom: 5%; left: 74%; background: rgba(255, 255, 0, 0.7); }
.name { font-size: 18px; font-weight: bold; }
.score { font-size: 24px; }
.hu-detail { font-size: 12px; line-height: 1.2; display: none; white-space: pre-line; }
#field-left {
position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.5);
padding: 10px; border-radius: 5px; font-size: 14px;
}
#round {
position: absolute; top: 10px; right: 10px; color: white; background: rgba(0,0,0,0.5);
padding: 10px; border-radius: 5px; font-size: 31px; font-weight: bold;
}
#date {
position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
color: white; background: rgba(0,0,0,0.5); padding: 5px 10px; border-radius: 5px;
font-size: 12px;
}
#hu-popup {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(255,255,255,0.9); color: black; padding: 20px; border-radius: 10px;
font-size: 18px; text-align: center; display: none; animation: fadeInOut 3s;
box-shadow: 0 4px 15px rgba(0,0,0,0.3); max-width: 400px;
}
@keyframes fadeInOut { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } 20% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } }
.tenpai-highlight { box-shadow: 0 0 20px yellow; animation: pulse 1s infinite; }
@keyframes pulse { 0% { box-shadow: 0 0 20px yellow; } 50% { box-shadow: 0 0 40px yellow; } 100% { box-shadow: 0 0 20px yellow; } }
.fade-in { animation: fadeIn 0.5s; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
</head>
<body>
<video id="video" autoplay muted loop>
<source src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4" type="video/mp4">
</video>
<div id="east" class="player-panel">
<div class="place"></div>
<div class="name">玩家1</div>
<div class="hu-detail" data-player="east"></div>
<div class="score" data-player="east">25000</div>
</div>
<div id="south" class="player-panel">
<div class="place"></div>
<div class="name">玩家2</div>
<div class="hu-detail" data-player="south"></div>
<div class="score" data-player="south">25000</div>
</div>
<div id="west" class="player-panel">
<div class="place">西</div>
<div class="name">玩家3</div>
<div class="hu-detail" data-player="west"></div>
<div class="score" data-player="west">25000</div>
</div>
<div id="north" class="player-panel">
<div class="place"></div>
<div class="name">玩家4</div>
<div class="hu-detail" data-player="north"></div>
<div class="score" data-player="north">25000</div>
</div>
<div id="field-left">
本场: <span id="honba">0</span> | 立直棒: <span id="riichi_sticks">0</span> | 宝牌: <span id="dora">?</span>
</div>
<div id="round">
<span class="title">东1局</span>
</div>
<div id="clock"></div>
<style>
#clock {
position: fixed;
bottom: 10px;
right: 15px;
font-family: "Consolas", "Roboto Mono", "Microsoft YaHei", monospace;
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.35);
padding: 4px 10px;
border-radius: 8px;
letter-spacing: 1px;
z-index: 9999;
user-select: none;
transition: all 0.2s ease;
}
#clock:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
}
</style>
<script>
function updateClock() {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, "0");
const d = String(now.getDate()).padStart(2, "0");
const h = String(now.getHours()).padStart(2, "0");
const min = String(now.getMinutes()).padStart(2, "0");
const s = String(now.getSeconds()).padStart(2, "0");
document.getElementById("clock").textContent = `${y}.${m}.${d} ${h}:${min}:${s}`;
}
updateClock(); // 初始化
setInterval(updateClock, 1000); // 每秒更新
</script>
<div id="hu-popup"></div>
<script>
const channel = 'mahjong-channel';
let bc = null;
const scores = { east: 25000, south: 25000, west: 25000, north: 25000 };
const names = { east: '玩家1', south: '玩家2', west: '玩家3', north: '玩家4' };
const tenpai = {};
let currentRound = '东1局';
let currentDora = '?';
try {
bc = new BroadcastChannel(channel);
bc.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
handleEvent(msg);
} catch (e) {
console.error('消息解析失败:', e);
}
};
console.log('BroadcastChannel 就绪');
} catch (e) {
console.log('使用 localStorage');
window.addEventListener('storage', (e) => {
if (e.key === channel) {
try {
const msg = JSON.parse(e.newValue);
handleEvent(msg);
} catch (ee) {
console.error('Storage 消息解析失败:', ee);
}
}
});
}
function handleEvent(msg) {
switch (msg.type) {
case 'reset_scores':
const init = msg.data.initial || 25000;
['east','south','west','north'].forEach(p => updateScore(p, init));
break;
case 'update_name':
updateName(msg.data.player, msg.data.name);
break;
case 'update_score':
updateScore(msg.data.player, msg.data.score);
break;
case 'hu_pai':
huPai(msg.data);
break;
case 'update_field':
document.getElementById('honba').textContent = msg.data.honba;
document.getElementById('riichi_sticks').textContent = msg.data.riichi_sticks;
break;
case 'update_round':
currentRound = msg.data.round;
document.getElementById('round').textContent = currentRound;
break;
case 'update_dora':
currentDora = msg.data.dora;
document.getElementById('dora').textContent = currentDora;
break;
case 'tenpai':
toggleTenpai(msg.data.player, msg.data.status);
break;
case 'update_style':
updateStyle(msg.data.player, msg.data.color, msg.data.opacity);
break;
case 'update_video_src':
const video = document.getElementById('video');
const newSrc = msg.data.src;
if (video && newSrc) {
video.src = newSrc;
video.load(); // 重新加载视频以应用新源
console.log(`视频源更新为: ${newSrc}`);
}
break;
}
}
function updateName(player, name) {
const nameEl = document.querySelector(`#${player} .name`);
if (nameEl) nameEl.textContent = name;
names[player] = name;
}
function updateScore(player, score) {
const scoreEl = document.querySelector(`[data-player="${player}"].score`);
if (scoreEl) scoreEl.textContent = Math.round(score); // 确保无小数
scores[player] = Math.round(score);
}
function calculateBasic(han, fu) {
let bp;
if (han >= 13) {
bp = 8000;
} else if (han >= 11) {
bp = 6000;
} else if (han >= 8) {
bp = 4000;
} else if (han >= 6) {
bp = 3000;
} else if (han >= 5) {
bp = 2000;
} else {
bp = fu * Math.pow(2, han + 2);
if (bp > 2000) bp = 2000;
}
return bp;
}
function calculatePayment(bp, mult) {
let pmt = bp * mult;
return Math.ceil(pmt / 100) * 100;
}
function huPai(data) {
const bp = calculateBasic(data.fan, data.fu);
const honbaRon = data.honba * 300;
const honbaPer = data.honba * 100;
const riichiExtra = data.riichi_sticks * 1000;
const isDealer = data.winner === 'east';
let totalGain = riichiExtra;
let payments = {};
if (data.type === 'ron') {
const mult = isDealer ? 6 : 4;
const payment = calculatePayment(bp, mult);
const totalPay = payment + honbaRon;
payments[data.loser] = -totalPay;
totalGain += totalPay;
} else { // tsumo
if (isDealer) {
['south','west','north'].forEach(p => {
const pmt = calculatePayment(bp, 2);
const totalPmt = pmt + honbaPer;
payments[p] = -totalPmt;
totalGain += totalPmt;
});
} else {
const dealerPmt = calculatePayment(bp, 2);
const totalDealerPmt = dealerPmt + honbaPer;
payments.east = -totalDealerPmt;
totalGain += totalDealerPmt;
const others = ['south','west','north'].filter(p => p !== data.winner);
others.forEach(p => {
const pmt = calculatePayment(bp, 1);
const totalPmt = pmt + honbaPer;
payments[p] = -totalPmt;
totalGain += totalPmt;
});
}
}
payments[data.winner] = `+${totalGain}`;
// 显示玩家面板详情
['east','south','west','north'].forEach(p => {
const panel = document.getElementById(p);
const nameDiv = panel.querySelector('.name');
const detailDiv = panel.querySelector('.hu-detail');
nameDiv.style.display = 'none';
detailDiv.style.display = 'block';
detailDiv.classList.add('fade-in');
if (p === data.winner) {
detailDiv.innerHTML = `${data.yaku.join(', ')}\n${data.fan}${data.fu}\n+${totalGain}`;
} else if (payments[p]) {
detailDiv.innerHTML = `${payments[p]}`;
} else {
detailDiv.innerHTML = '';
}
});
/*
// 弹出窗口显示
const popup = document.getElementById('hu-popup');
popup.innerHTML = `胡牌!\n${names[data.winner]} ${data.type === 'ron' ? '荣和' : '自摸'}\n${data.yaku.join(', ')}\n${data.fan}番 ${data.fu}符\n+${totalGain}点`;
popup.style.display = 'block';
setTimeout(() => { popup.style.display = 'none'; }, 3000);
*/
// 3秒后恢复名字
setTimeout(() => {
['east','south','west','north'].forEach(p => {
const panel = document.getElementById(p);
const nameDiv = panel.querySelector('.name');
const detailDiv = panel.querySelector('.hu-detail');
nameDiv.style.display = 'block';
detailDiv.style.display = 'none';
detailDiv.classList.remove('fade-in');
});
}, 6000);
}
function toggleTenpai(player, status) {
const panel = document.getElementById(player);
if (status) {
panel.classList.add('tenpai-highlight');
} else {
panel.classList.remove('tenpai-highlight');
}
}
function updateStyle(player, color, opacity) {
const panel = document.getElementById(player);
const rgb = hexToRgb(color);
if (rgb) {
panel.style.background = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity || 0.7})`;
}
panel.style.opacity = opacity || 0.8;
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null;
}
// 初始化分数显示
Object.keys(scores).forEach(p => updateScore(p, scores[p]));
</script>
</body>
</html>