347 lines
12 KiB
HTML
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> |