Commit 01d53c7d authored by 董政锦's avatar 董政锦

feat: PC 端 game5 马的动画;

parent 3b34f232
...@@ -182,9 +182,10 @@ onUnmounted(() => { ...@@ -182,9 +182,10 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<Loading v-if="screen === '!loading'" :players="loadingPlayers" :player-count="playerCount" <Loading v-if="screen !== 'loading'" :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" /> @start="startGameWithMusic" />
<!-- <Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" /> --> <!-- <Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" -->
<Rank1 v-else ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" /> <Rank1 v-else ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic"
@next="nextRoundWithMusic" />
<!-- <Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" /> --> <!-- <Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" /> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch, onMounted, onUnmounted } from "vue";
import DesignStage from '../../../components/DesignStage.vue' import DesignStage from "../../../components/DesignStage.vue";
import type Player from '../../../commons/player.ts' import type Player from "@/commons/player.ts";
import { $is_run_local } from '@/commons/utils.ts'; import { $is_run_local } from "@/commons/utils.ts";
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts'; import { cssAssetUrl, assetUrl } from "@/commons/assets.ts";
const props = defineProps<{ const props = defineProps<{
rankList: Player[]; rankList: Player[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
start: []; start: [];
next: [];
}>(); }>();
const RANK_LIMIT = 10; const RANK_LIMIT = 10;
const START_COUNTDOWN_SECONDS = 3;
const GU_FRAME_DURATION = 300;
const GU_LOOP_DELAY = 1000;
const designStage = ref<InstanceType<typeof DesignStage> | null>(null); const designStage = ref<InstanceType<typeof DesignStage> | null>(null);
const rankEffects = ref<Record<string, "up" | "down" | "new">>({}); const rankEffects = ref<Record<string, "up" | "down" | "new">>({});
const guFrameIndex = ref(0); const gameStarted = ref(false);
const guBurstVisible = ref(false); const showResult = ref(false);
const guBurstKey = ref(0);
let guFramesReady: Promise<void> | undefined;
let guTimer: ReturnType<typeof setTimeout> | undefined;
let guBurstTimer: ReturnType<typeof setTimeout> | undefined;
const loadGuFrames = (dir: string) => Array.from(
{ length: 10 },
(_, index) => assetUrl(`${dir}/${String(index + 1).padStart(2, '0')}.png`),
);
const guFrames = [
loadGuFrames('game1/gu'),
loadGuFrames('game1/gu2'),
];
const imageUrls = { const imageUrls = {
bg: cssAssetUrl('game1/bg.png'), bg: cssAssetUrl("game5/bg.svg"),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl('game1/title3.png'), title: cssAssetUrl("game5/title3.webp"),
fireworks: cssAssetUrl('game1/fireworks.png'), titleRank: cssAssetUrl("game1/title-rank.png"),
titleRank: cssAssetUrl('game1/title-rank.png'), rankNo4: cssAssetUrl("game1/no4.png"),
rankNo4: cssAssetUrl('game1/no4.png'), rankNo1: cssAssetUrl("game1/no1.png"),
rankNo1: cssAssetUrl('game1/no1.png'), rankNo2: cssAssetUrl("game1/no2.png"),
rankNo2: cssAssetUrl('game1/no2.png'), rankNo3: cssAssetUrl("game1/no3.png"),
rankNo3: cssAssetUrl('game1/no3.png'), rankIcon: cssAssetUrl("game1/no-icon.png"),
rankIcon: cssAssetUrl('game1/no-icon.png'), rankNo1Icon: cssAssetUrl("game1/no1-icon.png"),
rankNo1Icon: cssAssetUrl('game1/no1-icon.png'), rankNo2Icon: cssAssetUrl("game1/no2-icon.png"),
rankNo2Icon: cssAssetUrl('game1/no2-icon.png'), rankNo3Icon: cssAssetUrl("game1/no3-icon.png"),
rankNo3Icon: cssAssetUrl('game1/no3-icon.png'), avatar: assetUrl("avatar.png"),
avatar: assetUrl('avatar.png'),
}; };
const emptyPlayer = (index: number): Player => ({ // ========== 马匹相关 ==========
wechat_id: `empty-${index}`, const HORSE_TYPES = [1, 2, 3, 4] as const;
nickname: '-', const HORSE_FRAME_COUNT = 21; // 每匹马 21 帧(00-20)
avatar: '',
score: 0, interface Horse {
}); id: string;
horseType: number; // 1-4 颜色
lane: "top" | "bottom";
xPercent: number; // 0=屏幕左侧外, 100=屏幕右侧外
frameIndex: number; // 当前帧 0-20
frameTimer: number; // 帧切换计时器
}
const horses = ref<Horse[]>([]);
let horseIdCounter = 0;
const list = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyPlayer(index))); /** 获取指定马颜色和帧的图片 URL */
const playerKey = (player: Player, index = 0) => player.userid || player.wechat_id || `empty-${index}`; const getHorseFrameUrl = (type: number, frame: number) => {
const mockRankStep = ref(0); const padded = String(frame).padStart(2, "0");
return assetUrl(`game5/horse${type}/no-circle/qcnf-ma${type}_${padded}.png`);
};
/** 马的水平像素位移:xPercent→屏幕px */
const horseTranslateX = (xPercent: number) => {
// 从左侧-330px 跑到右侧 1920px,总行程 2250px
return -330 + xPercent * 22.5;
};
const makeMockPlayer = (id: number, score: number): Player => ({ const isGameRunning = ref(false);
wechat_id: `mock-${id}`, const emptyPlayer = (index: number): Player => ({
nickname: `Player ${id}`, wechat_id: `empty-${index}`,
avatar: '', nickname: "-",
score, avatar: "",
score: 0,
}); });
const mockRankRounds: Player[][] = [ const list = ref<Player[]>(
[ Array.from({ length: RANK_LIMIT }, (_, index) => emptyPlayer(index)),
makeMockPlayer(1, 980), );
makeMockPlayer(2, 900), const playerKey = (player: Player, index = 0) =>
makeMockPlayer(3, 860), player.userid || player.wechat_id || `empty-${index}`;
makeMockPlayer(4, 820),
makeMockPlayer(5, 780),
makeMockPlayer(6, 720),
makeMockPlayer(7, 680),
makeMockPlayer(8, 640),
makeMockPlayer(9, 600),
makeMockPlayer(10, 560),
],
[
makeMockPlayer(4, 1040),
makeMockPlayer(1, 1010),
makeMockPlayer(7, 960),
makeMockPlayer(2, 940),
makeMockPlayer(11, 880),
makeMockPlayer(3, 850),
makeMockPlayer(5, 830),
makeMockPlayer(8, 790),
makeMockPlayer(6, 760),
makeMockPlayer(10, 700),
],
[
makeMockPlayer(7, 1180),
makeMockPlayer(11, 1090),
makeMockPlayer(4, 1060),
makeMockPlayer(12, 1000),
makeMockPlayer(1, 980),
makeMockPlayer(8, 930),
makeMockPlayer(2, 890),
makeMockPlayer(5, 850),
makeMockPlayer(13, 810),
makeMockPlayer(3, 780),
],
];
// ========== 排名更新 ==========
const updateRankList = (rankList: Player[]) => { const updateRankList = (rankList: Player[]) => {
const oldRanks = new Map(list.value.map((player, index) => [playerKey(player, index), index])); const oldRanks = new Map(
const next = Array.from({ length: RANK_LIMIT }, (_, index) => rankList[index] ?? emptyPlayer(index)); list.value.map((player, index) => [playerKey(player, index), index]),
const effects: Record<string, "up" | "down" | "new"> = {}; );
const next = Array.from(
next.forEach((player, index) => { { length: RANK_LIMIT },
const key = playerKey(player, index); (_, index) => rankList[index] ?? emptyPlayer(index),
if (!key || key.startsWith("empty-")) return; );
const effects: Record<string, "up" | "down" | "new"> = {};
const oldIndex = oldRanks.get(key);
if (oldIndex === undefined) effects[key] = "new"; next.forEach((player, index) => {
else if (oldIndex > index) effects[key] = "up"; const key = playerKey(player, index);
else if (oldIndex < index) effects[key] = "down"; if (!key || key.startsWith("empty-")) return;
});
const oldIndex = oldRanks.get(key);
list.value = next; if (oldIndex === undefined) effects[key] = "new";
rankEffects.value = {}; else if (oldIndex > index) effects[key] = "up";
requestAnimationFrame(() => { else if (oldIndex < index) effects[key] = "down";
rankEffects.value = effects; });
});
list.value = next;
rankEffects.value = {};
requestAnimationFrame(() => {
rankEffects.value = effects;
});
}; };
watch( watch(
() => props.rankList, () => props.rankList,
(rankList) => updateRankList(rankList), (rankList) => {
{ immediate: true, deep: true }, if (rankList && rankList.length > 0) {
updateRankList(rankList);
}
},
{ immediate: true, deep: true },
); );
const playMockRankAnimation = () => { // ========== startIntro:倒计时 + 游戏流程 ==========
if ($is_run_local()) { const startIntro = async () => {
updateRankList(mockRankRounds[mockRankStep.value] ?? mockRankRounds[0] ?? []); showResult.value = false;
mockRankStep.value = (mockRankStep.value + 1) % mockRankRounds.length; gameStarted.value = false;
} isGameRunning.value = false;
horses.value = [];
horseIdCounter = 0;
// 启动完整倒计时:3s 开场 + 60s 游戏
const countdownPromise = designStage.value?.startCountdown(3, 60);
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
await new Promise((r) => setTimeout(r, 3500));
// Phase 2: 倒计时结束 → 游戏正式开始,背景动画+马匹启动
gameStarted.value = true;
isGameRunning.value = true;
startHorseLoop();
startSpawning();
// 等待 60s 游戏倒计时完全结束
await countdownPromise;
stopGame();
}; };
const startGame = () => { const stopGame = () => {
if ($is_run_local()) { isGameRunning.value = false;
emit('start'); showResult.value = true;
} stopHorseLoop();
stopSpawning();
}; };
const stopPlayerGu = () => { // ========== 马匹动画循环 (~60fps) ==========
if (guTimer) { let horseLoopTimer: ReturnType<typeof setInterval> | undefined;
clearTimeout(guTimer); const FRAME_TICK_MS = 16;
guTimer = undefined; const HORSE_FRAME_INTERVAL = 3; // 每 3 tick 切一帧 → ~20fps 奔马动画
const HORSE_SPEED_PERCENT_PER_SEC = 100 / 60; // 60s 跑完全程
const startHorseLoop = () => {
stopHorseLoop();
horseLoopTimer = setInterval(() => {
if (!isGameRunning.value) return;
for (let i = horses.value.length - 1; i >= 0; i--) {
const horse = horses.value[i]!;
horse.xPercent += HORSE_SPEED_PERCENT_PER_SEC * (FRAME_TICK_MS / 1000);
horse.frameTimer++;
if (horse.frameTimer >= HORSE_FRAME_INTERVAL) {
horse.frameTimer = 0;
horse.frameIndex = (horse.frameIndex + 1) % HORSE_FRAME_COUNT;
}
// 跑出右边界后移除
if (horse.xPercent >= 120) {
horses.value.splice(i, 1);
}
} }
if (guBurstTimer) { }, FRAME_TICK_MS);
clearTimeout(guBurstTimer); };
guBurstTimer = undefined;
}
guBurstVisible.value = false;
guFrameIndex.value = 0;
}
const preloadGuFrames = () => {
if (!guFramesReady) {
guFramesReady = Promise.all(guFrames.flat().map((src) => {
const img = new Image();
img.src = src;
return img.decode
? img.decode().catch(() => undefined)
: new Promise<void>((resolve) => {
img.onload = () => resolve();
img.onerror = () => resolve();
});
})).then(() => undefined);
}
return guFramesReady;
}
const playGuLoopDelayAnimation = () => { const stopHorseLoop = () => {
if (guBurstTimer) { if (horseLoopTimer) {
clearTimeout(guBurstTimer); clearInterval(horseLoopTimer);
guBurstTimer = undefined; horseLoopTimer = undefined;
} }
};
guBurstVisible.value = false; // ========== 随机生成马匹 ==========
requestAnimationFrame(() => { let spawnTimer: ReturnType<typeof setTimeout> | undefined;
guBurstKey.value += 1;
guBurstVisible.value = true; const spawnHorse = () => {
guBurstTimer = setTimeout(() => { const type = HORSE_TYPES[Math.floor(Math.random() * HORSE_TYPES.length)]!;
guBurstVisible.value = false; const lane = Math.random() > 0.5 ? "top" : "bottom";
guBurstTimer = undefined; horses.value.push({
}, GU_LOOP_DELAY); id: `horse-${++horseIdCounter}`,
}); horseType: type,
} lane,
xPercent: -15, // 从左侧屏幕外进场
const playerGu = async () => { frameIndex: Math.floor(Math.random() * HORSE_FRAME_COUNT),
stopPlayerGu(); frameTimer: 0,
const frameCount = Math.min(...guFrames.map((frames) => frames.length)); });
if (frameCount <= 0) return; };
await preloadGuFrames();
let frameIndex = 0;
const tick = () => {
guFrameIndex.value = frameIndex;
if (frameIndex >= frameCount - 1) {
frameIndex = 0;
playGuLoopDelayAnimation();
guTimer = setTimeout(tick, GU_LOOP_DELAY);
return;
}
frameIndex += 1;
guTimer = setTimeout(tick, GU_FRAME_DURATION);
}
tick(); const startSpawning = () => {
} stopSpawning();
// 开局立刻出 1~2 匹
spawnHorse();
if (Math.random() > 0.3) spawnHorse();
const scheduleNext = () => {
if (!isGameRunning.value) return;
const delay = 2000 + Math.random() * 6000; // 2~8s 随机间隔
spawnTimer = setTimeout(() => {
if (!isGameRunning.value) return;
// 场上最多 8 匹,每次出 1~2 匹
if (horses.value.length < 8) {
spawnHorse();
if (Math.random() > 0.5 && horses.value.length < 8) spawnHorse();
}
scheduleNext();
}, delay);
};
scheduleNext();
};
const startIntro = async () => { const stopSpawning = () => {
playerGu(); if (spawnTimer) {
await designStage.value?.startCountdown(START_COUNTDOWN_SECONDS, 60); clearTimeout(spawnTimer);
stopPlayerGu(); spawnTimer = undefined;
}
}; };
defineExpose({ onMounted(() => {
startIntro, if ($is_run_local()) {
gameStarted.value = true;
isGameRunning.value = true;
startHorseLoop();
startSpawning();
}
});
onUnmounted(() => {
stopGame();
stopHorseLoop();
stopSpawning();
}); });
defineExpose({
startIntro,
});
// ========== 头像渲染 ==========
const avatarStyle = (item: Player) => { const avatarStyle = (item: Player) => {
const avatar = item.avatar || imageUrls.avatar const avatar = item.avatar || imageUrls.avatar;
return {
backgroundImage: `url("${avatar}")`,
};
};
return { // ========== rank item 样式 class ==========
backgroundImage: `url("${avatar}")`, const rankItemClass = (index: number) => {
} if (index === 0) return "rank-top-1";
} if (index === 1) return "rank-top-2";
if (index === 2) return "rank-top-3";
return "";
};
</script> </script>
<template> <template>
<DesignStage ref="designStage"> <DesignStage ref="designStage">
<div class="bg"> <div class="screen-container" :class="{ paused: !gameStarted || showResult }">
<div class="img-logo"></div> <!-- 背景层 -->
<div class="img-title"></div> <div class="bg">
<div class="img-gu-group"> <div class="img-logo"></div>
<div class="img-gu"> <div class="img-title"></div>
<div v-for="(frame, index) in guFrames[1]" :key="`left-frame-${frame}`" class="gu-frame"
:class="{ active: index === guFrameIndex }" :style="{ backgroundImage: `url(${frame})` }"> <!-- 排名列表 -->
</div> <!-- <div class="rank-container">
<div :key="`left-${guBurstKey}`" class="gu-burst" :class="{ active: guBurstVisible }"></div> <div class="rank-txt"></div>
</div> <div class="rank-list">
<div class="img-gu"> <TransitionGroup name="rank">
<div v-for="(frame, index) in guFrames[0]" :key="`right-frame-${frame}`" class="gu-frame" <div
:class="{ active: index === guFrameIndex }" :style="{ backgroundImage: `url(${frame})` }"> v-for="(item, index) in list"
</div> :key="playerKey(item, index)"
<div :key="`right-${guBurstKey}`" class="gu-burst" :class="{ active: guBurstVisible }"></div> :class="[
</div> 'rank-item',
</div> rankItemClass(index),
<div class="rank-container"> rankEffects[playerKey(item, index)] || '',
<div class="rank-txt" @click="startGame()" @contextmenu.prevent="playMockRankAnimation()"></div> ]"
<TransitionGroup name="rank" tag="div" class="rank-list"> >
<div v-for="(item, index) in list" :key="playerKey(item, index)" class="rank-item" <div class="rank-icon">{{ index + 1 }}</div>
:class="[rankEffects[playerKey(item, index)], index < 3 ? `rank-top-${index + 1}` : '']"> <div class="rank-avatar" :style="avatarStyle(item)"></div>
<div class="rank-icon" :key="`rank-icon-${index}`">{{ index > 2 ? index + 1 : '' }}</div> <div class="rank-nickname">{{ item.nickname }}</div>
<div class="rank-avatar" :style="avatarStyle(item)"></div> <div class="rank-score">{{ item.score }}</div>
<div class="rank-nickname">{{ item.nickname }}</div> </div>
<div class="rank-score">{{ item.score ?? 0 }}</div> </TransitionGroup>
</div> </div>
</TransitionGroup> </div> -->
</div>
<!-- 底部跑道背景(倒计时结束后滚动) -->
<div class="runway-track">
<div class="runway-inner"></div>
<div class="runway-inner"></div>
</div> </div>
</DesignStage>
<!-- 马匹(倒计时结束后出现) -->
<template v-if="gameStarted">
<div
v-for="horse in horses"
:key="horse.id"
class="horse-wrapper"
:class="`horse-lane-${horse.lane}`"
:style="{ transform: `translateX(${horseTranslateX(horse.xPercent)}px)` }"
>
<img
class="horse-img"
:src="getHorseFrameUrl(horse.horseType, horse.frameIndex)"
alt="horse"
/>
</div>
</template>
</div>
</div>
</DesignStage>
</template> </template>
<style scoped> <style scoped>
.screen-container {
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
}
.screen-container.paused .runway-track {
animation-play-state: paused;
}
.bg { .bg {
width: 1920px; width: 1920px;
height: 1080px; height: 1080px;
background: v-bind('imageUrls.bg') center / cover no-repeat; background: v-bind("imageUrls.bg") center / cover no-repeat;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
.img-logo { .img-logo {
background: v-bind('imageUrls.logo'); background: v-bind("imageUrls.logo");
background-size: cover; background-size: cover;
width: 428px; width: 428px;
height: 77px; height: 77px;
position: absolute; position: absolute;
left: 37px; left: 37px;
top: 82px; top: 82px;
z-index: 20;
} }
.img-title { .img-title {
background: v-bind('imageUrls.title'); background: v-bind("imageUrls.title") no-repeat;
width: 967px; width: 675px;
height: 122px; height: 209px;
margin: 0 auto; margin: 0 auto;
margin-top: 37px; margin-top: 37px;
} position: relative;
z-index: 20;
.img-gu-group { }
.img-gu {
width: 400px;
height: 400px;
transform: scale(1.6);
position: absolute;
top: 400px;
}
.img-gu:nth-child(1) { /* ========== 排名容器 ========== */
left: 200px; .rank-container {
} width: 503px;
height: 810px;
background: #fff;
opacity: 0.9;
border-radius: 30px;
position: absolute;
left: 50%;
top: 210px;
transform: translateX(-50%);
z-index: 10;
overflow: hidden;
}
.img-gu:nth-child(2) { .rank-txt {
right: 200px; background: v-bind("imageUrls.titleRank");
} width: 304px;
height: 40px;
margin: 10px auto auto auto;
}
.img-gu { .rank-list {
.gu-frame { position: relative;
position: absolute;
inset: 0;
opacity: 0;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
pointer-events: none;
will-change: opacity;
}
.gu-frame.active {
opacity: 1;
}
.gu-burst {
width: 200px;
height: 200px;
position: absolute;
right: 80px;
top: 30px;
opacity: 0;
transform: scale(0.6);
transform-origin: center center;
background: v-bind('imageUrls.fireworks') center / cover no-repeat;
pointer-events: none;
&.active {
animation: gu-burst-expand 1s ease-out both;
}
}
}
} }
.rank-container { .rank-item {
width: 503px; background: v-bind("imageUrls.rankNo4");
height: 810px; width: 468px;
background: #fff; height: 68px;
opacity: 0.9; margin: 6px auto 0 auto;
border-radius: 30px; display: grid;
position: absolute; grid-template-columns: 95px 68px 1fr 100px;
left: 50%; align-items: center;
top: 210px; color: #e00000;
transform: translateX(-50%); font-weight: bold;
z-index: 1; }
overflow: hidden;
.rank-txt {
background: v-bind('imageUrls.titleRank');
width: 304px;
height: 40px;
margin: 10px auto auto auto;
}
.rank-list { .rank-item.rank-top-1 {
position: relative; background: v-bind("imageUrls.rankNo1");
} }
.rank-item { .rank-item.rank-top-2 {
background: v-bind('imageUrls.rankNo4'); background: v-bind("imageUrls.rankNo2");
width: 468px; }
height: 68px;
margin: 6px auto 0 auto; .rank-item.rank-top-3 {
display: grid; background: v-bind("imageUrls.rankNo3");
grid-template-columns: 95px 68px 1fr 100px; }
align-items: center;
color: #E00000; .rank-icon {
font-weight: bold; background: v-bind("imageUrls.rankIcon") center / cover no-repeat;
width: 40px;
&.rank-top-1 { height: 40px;
background: v-bind('imageUrls.rankNo1'); margin: auto auto auto 20px;
} text-align: center;
color: white;
&.rank-top-2 { font-weight: bold;
background: v-bind('imageUrls.rankNo2'); font-size: 22px;
} line-height: 40px;
}
&.rank-top-3 {
background: v-bind('imageUrls.rankNo3'); .rank-item.rank-top-1 .rank-icon {
} background: v-bind("imageUrls.rankNo1Icon") center / cover no-repeat;
width: 60px;
.rank-icon { height: 38px;
background: v-bind('imageUrls.rankIcon') center / cover no-repeat; margin: auto auto auto 10px;
width: 40px; }
height: 40px;
margin: auto auto auto 20px; .rank-item.rank-top-2 .rank-icon {
text-align: center; background: v-bind("imageUrls.rankNo2Icon") center / cover no-repeat;
color: white; width: 60px;
font-weight: bold; height: 38px;
font-size: 22px; margin: auto auto auto 10px;
line-height: 40px; }
}
.rank-item.rank-top-3 .rank-icon {
&.rank-top-1 .rank-icon { background: v-bind("imageUrls.rankNo3Icon") center / cover no-repeat;
background: v-bind('imageUrls.rankNo1Icon') center / cover no-repeat; width: 60px;
width: 60px; height: 38px;
height: 38px; margin: auto auto auto 10px;
margin: auto auto auto 10px; }
}
.rank-avatar {
&.rank-top-2 .rank-icon { background-position: center;
background: v-bind('imageUrls.rankNo2Icon') center / cover no-repeat; background-repeat: no-repeat;
width: 60px; background-size: cover;
height: 38px; width: 48px;
margin: auto auto auto 10px; height: 48px;
} border-radius: 24px;
margin-left: 10px;
&.rank-top-3 .rank-icon { }
background: v-bind('imageUrls.rankNo3Icon') center / cover no-repeat;
width: 60px; .rank-nickname {
height: 38px; text-indent: 10px;
margin: auto auto auto 10px;
}
.rank-avatar {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
width: 48px;
height: 48px;
border-radius: 24px;
margin-left: 10px;
}
.rank-nickname {
text-indent: 10px;
}
.rank-score {
text-align: right;
margin-right: 30px;
}
}
} }
.rank-score {
text-align: right;
margin-right: 30px;
}
/* ========== 过渡动画 ========== */
.rank-move { .rank-move {
transition: transform 0.45s ease; transition: transform 0.45s ease;
} }
.rank-enter-active, .rank-enter-active,
.rank-leave-active { .rank-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease; transition: opacity 0.25s ease, transform 0.25s ease;
} }
.rank-leave-active { .rank-leave-active {
position: absolute; position: absolute;
left: 17px; left: 17px;
} }
.rank-enter-from, .rank-enter-from,
.rank-leave-to { .rank-leave-to {
opacity: 0; opacity: 0;
transform: translateY(90px); transform: translateY(90px);
} }
.rank-item.up, .rank-item.up,
.rank-item.new { .rank-item.new {
animation: rank-up-flash 0.8s ease; animation: rank-up-flash 0.8s ease;
} }
.rank-item.down { .rank-item.down {
animation: rank-down-flash 0.8s ease; animation: rank-down-flash 0.8s ease;
}
/* ========== 底部跑道背景 ========== */
.runway-track {
width: 7680px;
height: 543px;
position: absolute;
bottom: 0;
left: 0;
display: flex;
animation: bg-scroll-left 20s linear infinite;
z-index: 2;
}
.runway-inner {
width: 3840px;
height: 543px;
background: url("@/assets/images/game5/paodao.webp") 0 0 / 3840px 543px
no-repeat;
flex-shrink: 0;
}
@keyframes bg-scroll-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-3840px);
}
}
/* ========== 马匹 ========== */
.horse-wrapper {
position: absolute;
left: 0;
width: 330px;
height: 300px;
will-change: transform;
z-index: 5;
}
.horse-wrapper.horse-lane-top {
bottom: 320px;
}
.horse-wrapper.horse-lane-bottom {
bottom: 40px;
}
/* 马脚下阴影 */
.horse-wrapper::after {
content: "";
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
width: 180px;
height: 24px;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.45) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
}
.horse-img {
width: 330px;
height: 300px;
object-fit: contain;
position: relative;
z-index: 1;
}
@keyframes rank-up-flash {
0% {
background-color: #ffffcc;
}
50% {
background-color: #ffff99;
}
100% {
background-color: transparent;
}
}
@keyframes rank-down-flash {
0% {
background-color: #ffe0e0;
}
50% {
background-color: #ffcccc;
}
100% {
background-color: transparent;
}
} }
</style> </style>
...@@ -28,7 +28,7 @@ const imageUrls = { ...@@ -28,7 +28,7 @@ const imageUrls = {
pai2: cssAssetUrl('game1/pai2.png'), pai2: cssAssetUrl('game1/pai2.png'),
guan3: cssAssetUrl('game1/guan2.png'), guan3: cssAssetUrl('game1/guan2.png'),
pai3: cssAssetUrl('game1/pai3.png'), pai3: cssAssetUrl('game1/pai3.png'),
rankIcon: cssAssetUrl('game1/no-icon.png'), rankIcon: cssAssetUrl('game5/no-icon.webp'),
avatarAnimation: cssAssetUrl('avatar_animation.png'), avatarAnimation: cssAssetUrl('avatar_animation.png'),
next: cssAssetUrl('game5/next-1.webp'), next: cssAssetUrl('game5/next-1.webp'),
}; };
...@@ -361,8 +361,8 @@ const renderAvatar = (item: any | null) => { ...@@ -361,8 +361,8 @@ const renderAvatar = (item: any | null) => {
.no-icon { .no-icon {
background: v-bind('imageUrls.rankIcon') no-repeat center; background: v-bind('imageUrls.rankIcon') no-repeat center;
width: 40px; width: 42px;
height: 40px; height: 47px;
position: absolute; position: absolute;
text-align: center; text-align: center;
line-height: 40px; line-height: 40px;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment