refact game.vue

This commit is contained in:
feie9456 2025-08-02 17:13:58 +08:00
parent 3a524f02a0
commit 7e9847f287
13 changed files with 1053 additions and 782 deletions

View File

@ -5,7 +5,7 @@ import Page1 from './pages/Page1.vue';
import assets from './assets'; import assets from './assets';
import Loader from './pages/Loader.vue'; import Loader from './pages/Loader.vue';
const stage = ref(-1); const stage = ref(1);
const userData = ref({ const userData = ref({
region: '奥莱', region: '奥莱',

Binary file not shown.

View File

@ -30,6 +30,7 @@ export default {
结尾主标: new URL('./ani/结尾主标.zip', import.meta.url).href, 结尾主标: new URL('./ani/结尾主标.zip', import.meta.url).href,
海报p1: new URL('./ani/海报p1.zip', import.meta.url).href, 海报p1: new URL('./ani/海报p1.zip', import.meta.url).href,
海报p2: new URL('./ani/海报p2.zip', import.meta.url).href, 海报p2: new URL('./ani/海报p2.zip', import.meta.url).href,
答题标题: new URL('./ani/答题标题.zip', import.meta.url).href,
}, },
icons: [ icons: [
[new URL('./icons/icon1.webp', import.meta.url).href, new URL('./icons/bigicon1.webp', import.meta.url).href], [new URL('./icons/icon1.webp', import.meta.url).href, new URL('./icons/bigicon1.webp', import.meta.url).href],

View File

@ -6,6 +6,8 @@ import 弹簧蓄力下拉音效 from "./弹簧蓄力下拉音效.mp3"
import from "./风声.mp3" import from "./风声.mp3"
import from "./首页标题、尾标出现音效.mp3" import from "./首页标题、尾标出现音效.mp3"
import from "./最终结算音效.mp3" import from "./最终结算音效.mp3"
import from "./正确.mp3"
import from "./错误.mp3"
const ea = new EasyAudio([ const ea = new EasyAudio([
{name: "按钮音效", audioUrl: 按钮音效, volume: 0.5}, {name: "按钮音效", audioUrl: 按钮音效, volume: 0.5},
@ -14,6 +16,8 @@ const ea = new EasyAudio([
{name: "风声", audioUrl: 风声, volume: 0.5}, {name: "风声", audioUrl: 风声, volume: 0.5},
{name: "标题出现", audioUrl: 标题出现, volume: 0.5}, {name: "标题出现", audioUrl: 标题出现, volume: 0.5},
{name: "结算", audioUrl: 结算, volume: 0.5}, {name: "结算", audioUrl: 结算, volume: 0.5},
{name: "正确", audioUrl: 正确, volume: 0.5},
{name: "错误", audioUrl: 错误, volume: 0.5},
]); ]);
export default ea; export default ea;

Binary file not shown.

Binary file not shown.

View File

@ -461,3 +461,172 @@ export const regions: RegionData = {
} }
] ]
} }
// 定义题目对象的接口,以确保数据类型的一致性
export interface QuizQuestion {
id: number; // 题目唯一ID
type: 'single' | 'multiple'; // 题目类型:'single' 为单选, 'multiple' 为多选
question: string; // 题目问题
options: string[]; // 所有选项
answers: string[]; // 正确答案(单选也使用数组,方便统一处理)
}
// ARC Retail 弹性福利平台答题竞赛题库
export const arcRetailQuiz: QuizQuestion[] = [
// --- 一、单选题 ---
{
id: 1,
type: 'single',
question: '弹性福利平台的核心功能是?',
options: [
'发放基本工资',
'实现福利积分兑换',
'进行绩效评估',
'规划职业发展',
],
answers: ['实现福利积分兑换'],
},
{
id: 2,
type: 'single',
question: '弹性福利平台上线后,健康福利的变化是?',
options: [
'只能选体检',
'可在体检和牙齿清洁中自主选择',
'同时免费领体检 + 洁牙',
'取消健康福利',
],
answers: ['可在体检和牙齿清洁中自主选择'],
},
{
id: 3,
type: 'single',
question: '员工想知道自己有多少福利积分,最便捷的方式是?',
options: [
'每月找 HR 查',
'登录弹性福利平台实时查看',
'等年底邮件通知',
'问同事打听',
],
answers: ['登录弹性福利平台实时查看'],
},
{
id: 4,
type: 'single',
question: '以下哪项是 2025 年 新增 的福利兑换项目?',
options: ['节日礼品', '员工洁牙', '法定社保', '商业保险'],
answers: ['员工洁牙'],
},
{
id: 5,
type: 'single',
question: '福利积分 主要通过什么方式获取?',
options: ['日常加班', '参与景仰计划', '绩效满分', '达成销售业绩'],
answers: ['参与景仰计划'],
},
{
id: 6,
type: 'single',
question: '员工每年最多可以参加多少次公司组织的景仰计划?',
options: ['1次', '2次', '3次', '无限制'],
answers: ['2次'],
},
{
id: 7,
type: 'single',
question: '如果觉得近期压力大,心情低落,哪一项解决路径是公司平台提供的?',
options: [
'可以求助公司专业的EAP服务',
'和朋友吐苦水',
'大吃大喝',
'睡一觉',
],
answers: ['可以求助公司专业的EAP服务'],
},
{
id: 8,
type: 'single',
question: '员工商保属于哪类福利?',
options: ['法定福利', '企业补充福利', '带薪休假', '绩效奖金'],
answers: ['企业补充福利'],
},
// --- 二、多选题 ---
{
id: 9,
type: 'multiple',
question: '弹性福利平台可兑换的福利包含哪些?',
options: ['员工体检', '员工洁牙', '节日礼包', '基本工资'],
answers: ['员工体检', '员工洁牙', '节日礼包'],
},
{
id: 10,
type: 'multiple',
question: '关于弹性福利平台,这些 “新体验” 是真的!',
options: ['福利能自己选', '积分随时看', '福利都搬到线上', '只有经理能使用'],
answers: ['福利能自己选', '积分随时看', '福利都搬到线上'],
},
{
id: 11,
type: 'multiple',
question: '家属也能通过弹性福利平台享受哪些福利?',
options: ['家属商保自选', '家属体检自选', '家属洁牙自选', '员工生日积分'],
answers: ['家属商保自选', '家属体检自选', '家属洁牙自选'],
},
{
id: 12,
type: 'multiple',
question: '2025 年,福利积分新增了哪些类型?',
options: ['生日积分', '周年积分', '长期服务积分', '景仰积分'],
answers: ['生日积分', '周年积分', '长期服务积分', '景仰积分'],
},
{
id: 13,
type: 'multiple',
question: '弹性福利平台上线后,福利的 “获取和使用” 发生了哪些改变?',
options: [
'获取:从 “等公司发”→“参与景仰计划赚积分”',
'使用:从 “被动接受固定福利”→“自主选想要的福利”',
'管理:从 “找 HR 咨询”→“平台随时查、限时兑”',
'福利变少了',
],
answers: [
'获取:从 “等公司发”→“参与景仰计划赚积分”',
'使用:从 “被动接受固定福利”→“自主选想要的福利”',
'管理:从 “找 HR 咨询”→“平台随时查、限时兑”',
],
},
{
id: 14,
type: 'multiple',
question: '景仰计划包含哪些层级的活动?',
options: [
'T1最美鸟人面向优秀员工',
'T2探界鸟人硬核挑战自主报名',
'T3领航鸟人领导力发展面向店铺管理',
'T4起翼新鸟新手入门自主报名',
],
answers: [
'T1最美鸟人面向优秀员工',
'T2探界鸟人硬核挑战自主报名',
'T3领航鸟人领导力发展面向店铺管理',
'T4起翼新鸟新手入门自主报名',
],
},
{
id: 15,
type: 'multiple',
question: '员工360关爱计划EAP的联系途径有哪些',
options: [
'400电话400 920 3300',
'微信公众号(职选)',
'网站https://global.helpwhereyouare.com',
'小红书',
],
answers: [
'400电话400 920 3300',
'微信公众号(职选)',
'网站https://global.helpwhereyouare.com',
],
},
];

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { computed, ref, useTemplateRef, type Ref } from 'vue';
const gPos: Ref<'left' | 'center' | 'right'> = ref('center');
const gDeg = computed(() => ({
left: 294 - 360,
center: 326 - 360,
right: 8
})[gPos.value]);
const emit = defineEmits<{
(e: 'gToggle', pos: 'left' | 'center' | 'right'): void;
}>();
const pMachine = useTemplateRef('pMachine');
function gToggle(pos: 'left' | 'center' | 'right') {
gPos.value = pos;
pMachine.value?.animate([
{ scale: 1, },
{ scale: 1.2, },
{ scale: 1, },
], {
duration: 200,
easing: 'ease-in-out',
fill: 'forwards',
});
emit('gToggle', pos);
}
defineExpose({
hide: () => {
(document.querySelector('.p-machine') as HTMLDivElement).style.animationDirection = 'reverse';
(document.querySelectorAll('.action img') as NodeListOf<HTMLImageElement>).forEach(img => {
img.style.animationDirection = 'reverse';
});
}
});
</script>
<template>
<img src="../../assets/game/鼓风机.webp" class="abs p-machine" ref="pMachine"
style="bottom: -40%; width: 66vw; left: 17%; transform-origin: 50% 63%; transition: all 0.2s;"
:style="{ transform: `rotate(${gDeg}deg)` }" />
<div class="action">
<img draggable="false" :style="{ animationDelay: '0.1s' }" src="../../assets/game/左.webp" alt=""
@click="gToggle('left')">
<img draggable="false" :style="{ animationDelay: '0.2s' }" src="../../assets/game/中.webp" alt=""
@click="gToggle('center')">
<img draggable="false" :style="{ animationDelay: '0.0s' }" src="../../assets/game/右.webp" alt=""
@click="gToggle('right')">
</div>
</template>
<style lang="scss" scoped>
.p-machine {
animation: warp-in 0.5s ease-out forwards;
}
.action {
display: flex;
position: absolute;
bottom: 19vw;
left: 50%;
transform: translateX(-50%);
gap: 2vw;
img {
flex: 1;
width: 28vw;
scale: 0;
animation: scale-in 0.4s ease-in-out forwards;
}
}
</style>

View File

@ -0,0 +1,260 @@
<style scoped>
.wecare-title {
scale: 0;
}
.icon-cloud img {
position: absolute;
width: 16vw;
transition: all 4s ease;
animation: scale-in 0.4s ease-in-out forwards;
transform: translate(-50%, -50%);
transform-origin: left top;
scale: 0;
}
</style>
<template>
<div class="icon-cloud" style="position: absolute; left: 50%; top: 20%;" ref="icon-cloud">
<img v-for="icon in floatIcons" :src="icon.src" alt="" :style="{
top: `${icon.top}vw`,
left: `${icon.left}vw`,
animationDelay: `${icon.delay}ms`,
}">
</div>
<img src="../../assets/game/wecare标题.webp" alt="" class="abs wecare-title" style="width: 72%;left: 14%;top: 12%;"
ref="title-img">
</template>
<script setup lang="ts">
import { nextTick, ref, useTemplateRef } from 'vue';
const iconCloud = useTemplateRef('icon-cloud');
const titleImg = useTemplateRef('title-img');
import assets from '../../assets';
const giftList = assets.icons;
defineExpose({
init: () => {
//
floatIcons.value = generateIconPositions();
},
gather: async () => {
const finalPositions = simulateGatherIcons();
await new Promise(resolve => setTimeout(resolve, 0)); //
floatIcons.value = finalPositions;
},
titleIn: async () => {
await iconCloud.value?.animate([
{ scale: 1 },
{ scale: 0 },
], {
duration: 400,
easing: 'ease-in-out',
fill: 'forwards',
}).finished;
await titleImg.value?.animate([
{ scale: 0 },
{ scale: 1 },
], {
duration: 400,
easing: 'ease-in-out',
fill: 'forwards',
}).finished;
},
titleOut: async () => {
await titleImg.value?.animate([
{ scale: 1 },
{ scale: 0 },
], {
duration: 400,
easing: 'ease-in-out',
fill: 'forwards',
}).finished;
}
});
// 16
const generateIconPositions = () => {
const icons = [...giftList, ...assets.metal];
const positions: Array<{ src: string; left: number; top: number; delay: number }> = [];
const minDistance = 16; //
const maxAttempts = 100; //
//
const getDistance = (pos1: { left: number; top: number }, pos2: { left: number; top: number }) => {
return Math.sqrt(Math.pow(pos1.left - pos2.left, 2) + Math.pow(pos1.top - pos2.top, 2));
};
//
const isPositionValid = (newPos: { left: number; top: number }) => {
return positions.every(existingPos => getDistance(newPos, existingPos) >= minDistance);
};
icons.forEach((icon, index) => {
let attempts = 0;
let validPosition = false;
let newPosition: { left: number; top: number };
//
while (!validPosition && attempts < maxAttempts) {
newPosition = {
left: Math.random() * 100 - 50, //
top: Math.random() * 100 - 50
};
if (positions.length === 0 || isPositionValid(newPosition)) {
validPosition = true;
positions.push({
src: icon[0],
left: newPosition.left,
top: newPosition.top,
delay: index * 50
});
}
attempts++;
}
// 使
if (!validPosition) {
console.warn(`Could not find non-conflicting position for icon ${index}, using fallback position`);
positions.push({
src: icon[0],
left: Math.random() * 100 - 50,
top: Math.random() * 100 - 50,
delay: Math.random() * 5 + index * 0.5
});
}
});
return positions;
};
const floatIcons = ref(generateIconPositions());
// -
const simulateGatherIcons = () => {
const moveSpeed = 0.5; //
const minDistance = 16; //
const centerTarget = { left: 0, top: 0 }; //
const threshold = 0.1; //
const maxIterations = 1000; //
//
const simulatedPositions = floatIcons.value.map(icon => ({
src: icon.src,
left: icon.left,
top: icon.top,
delay: icon.delay
}));
//
const getDistance = (pos1: { left: number; top: number }, pos2: { left: number; top: number }) => {
return Math.sqrt(Math.pow(pos1.left - pos2.left, 2) + Math.pow(pos1.top - pos2.top, 2));
};
//
const isPositionValid = (newPos: { left: number; top: number }, currentIndex: number, positions: typeof simulatedPositions) => {
return positions.every((icon, index) => {
if (index === currentIndex) return true;
return getDistance(newPos, icon) >= minDistance;
});
};
//
const moveTowardsTarget = (currentPos: { left: number; top: number }, targetPos: { left: number; top: number }, iconIndex: number, positions: typeof simulatedPositions) => {
const dx = targetPos.left - currentPos.left;
const dy = targetPos.top - currentPos.top;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < threshold) {
return currentPos; //
}
//
const unitX = dx / distance;
const unitY = dy / distance;
//
const newPos = {
left: currentPos.left + unitX * moveSpeed,
top: currentPos.top + unitY * moveSpeed
};
//
if (isPositionValid(newPos, iconIndex, positions)) {
return newPos;
} else {
//
const angles = [Math.PI / 4, -Math.PI / 4, Math.PI / 2, -Math.PI / 2, 3 * Math.PI / 4, -3 * Math.PI / 4];
for (const angle of angles) {
const rotatedX = unitX * Math.cos(angle) - unitY * Math.sin(angle);
const rotatedY = unitX * Math.sin(angle) + unitY * Math.cos(angle);
const alternativePos = {
left: currentPos.left + rotatedX * moveSpeed * 0.5,
top: currentPos.top + rotatedY * moveSpeed * 0.5
};
if (isPositionValid(alternativePos, iconIndex, positions)) {
return alternativePos;
}
}
//
return currentPos;
}
};
//
let iterations = 0;
while (iterations < maxIterations) {
let hasMovement = false;
//
const newPositions = simulatedPositions.map((icon, index) => {
const oldPos = { left: icon.left, top: icon.top };
const newPos = moveTowardsTarget(oldPos, centerTarget, index, simulatedPositions);
//
if (Math.abs(newPos.left - oldPos.left) > threshold || Math.abs(newPos.top - oldPos.top) > threshold) {
hasMovement = true;
}
return {
...icon,
left: newPos.left,
top: newPos.top
};
});
//
simulatedPositions.splice(0, simulatedPositions.length, ...newPositions);
//
if (!hasMovement) {
console.log(`图标聚集模拟完成!迭代次数: ${iterations}`);
break;
}
iterations++;
}
if (iterations >= maxIterations) {
console.warn('图标聚集模拟达到最大迭代次数限制');
}
//
return simulatedPositions;
};
</script>

157
src/pages/Game/LastPage.vue Normal file
View File

@ -0,0 +1,157 @@
<script setup lang="ts">
import { ref, onMounted, watch, useTemplateRef } from 'vue';
import AniEle from '../../components/AniEle.vue';
import assets from '../../assets';
import AudioEffects from '../../assets/sounds'
defineProps<{
userdata: {
region: string;
store: string;
username: string;
},
timeSpent: number;
fmtTime: (time: number) => string;
}>();
import QRCode from 'qrcode';
const qrcodeUrl = ref('');
QRCode.toDataURL(`https://arcteryx-game-demo.xn--876a.net`, {
width: 256,
}).then(url => {
qrcodeUrl.value = url;
}).catch(err => {
console.error('生成二维码失败:', err);
});
const posterP2Ele = useTemplateRef('gui-ani-end-post-p2');
const emit = defineEmits(['resetGame', 'showShareMask']);
const hasP2AnimationPlayed = ref(false);
onMounted(() => {
// Intersection Observer p2
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasP2AnimationPlayed.value) {
// p2
hasP2AnimationPlayed.value = true;
posterP2Ele.value?.jumpTo('p2');
console.log('Poster P2 animation triggered');
//
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.5, // 50%
rootMargin: '0px'
});
// p2
const p2Element = posterP2Ele.value?.$el;
if (p2Element) {
observer.observe(p2Element);
console.log('Started observing P2 element');
} else {
console.warn('P2 element not found');
}
// 8p2p2
setTimeout(() => {
if (!hasP2AnimationPlayed.value) {
console.log('Auto scrolling to P2 after 8 seconds');
const posterContainer = document.querySelector('.poster-container');
if (posterContainer) {
// p250%
posterContainer.scrollTo({
left: posterContainer.scrollWidth * 0.5,
behavior: 'smooth'
});
}
}
}, 8000); // 8 = 8000
})
</script>
<template>
<div class="last-page"
style="z-index: 0;position: absolute; inset: 0;height: 100%; width: 100%; pointer-events: none;">
<AniEle :url="assets.ani.结尾主标" :width="925" :height="340"
:rules="[{ name: 'main', frame: 41, loop: 1, pauseAfter: true, duration: 33, reverse: false }]"
style="position: absolute;top: 13%;width: 80%;left: 11%; pointer-events:none;" ref="main-logo" />
<div class="poster-container"
style="position: absolute;top: 29.3%;left: 37.2%;width: 57.3%; height: 38%; overflow-x: auto; overflow-y: hidden; pointer-events: all; scroll-snap-type: x mandatory;">
<div class="poster-wrapper" style="display: flex; width: 200%; height: 100%; flex-direction: row;"
dir="ltr">
<AniEle :url="assets.ani.海报p1" ref="gui-ani-end-post-p1 "
style="width: 50%; flex-shrink: 0; scroll-snap-align: start;" class="gui-ani-end-post" :height="940"
:width="671" :rules="[
{ name: 'p1', frame: 32, loop: 1, pauseAfter: true, duration: 33 },
]" />
<AniEle :url="assets.ani.海报p2" ref="gui-ani-end-post-p2"
style="width: 50%; flex-shrink: 0; scroll-snap-align: start;" class="gui-ani-end-post" :height="940"
:width="671" :rules="[
{ name: 'wait', frame: 1, loop: 1, pauseAfter: true, duration: 33 },
{ name: 'p2', frame: 31, loop: 1, pauseAfter: true, duration: 33 },
]" />
</div>
</div>
<AniEle :url="assets.ani.线" ref="gui-ani-end" class="gui-ani-end" :height="2462" :width="1179" :rules="[
{ name: 'all', frame: 54, loop: 1, pauseAfter: false, duration: 16 },
]" style="width: 100%; height: auto;position: absolute;inset: 0;pointer-events: none;" />
<img src="../../assets/game/床.webp" alt="" class="abs last-bed" style="width: 66vw;bottom: -19%;left: 17%;">
<img src="../../assets/game/分享海报.webp" alt="" class="abs" @click="AudioEffects.play('按钮音效'); emit('showShareMask')"
style="width: 34vw;bottom: -19%;left: 15%;animation: last-btn-in 0.3s ease-out forwards; cursor: pointer; pointer-events: all;">
<img src="../../assets/game/再玩一次.webp" alt="" class="abs" @click="AudioEffects.play('按钮音效'); emit('resetGame')"
style="width: 34vw;bottom: -19%;left: 51%;animation: last-btn-in 0.3s ease-out forwards; cursor: pointer; animation-delay: 200ms; pointer-events: all;">
<div class="time-spent abs" style="left: 10%; top:44%;width: 24%;height: 23%; display: flex;
align-items: center; flex-direction: column;">
<div class="line-1"
style="display: flex; align-items: center; gap: 1vw; animation: line-in 0.5s ease-out 0.6s forwards;transform: translateX(-210%);">
<div class="username" style="font-size: 2.9vw;">{{
userdata.username }}</div>
<div class="cost-info"
style="font-size: 2.9vw; color: white; display: flex;align-items: center;justify-content: center;background-color: black;padding: 0 1.2vw; height: 4.5vw; border-radius: 4vw;">
用时</div>
</div>
<div class="time"
style="font-size: 10vw;animation: line-in 0.5s ease-out 0.8s forwards;transform: translateX(-210%);">
{{ fmtTime(timeSpent / 1000) }}</div>
<img :src="qrcodeUrl" alt=""
style="width: 100%;bottom:7%;position: absolute;animation: line-in 0.5s ease-out 1s forwards;transform: translateX(-210%);">
</div>
<div class="region-area abs" style="left: 10%; top:30%;width: 24%;height: 10%;gap:2vw; display: flex;animation: line-in 0.5s ease-out 0.4s forwards;transform: translateX(-210%);
align-items: center; flex-direction: column;justify-content: center;">
<div class="region"
style="width: 20vw; background-color: white;font-size: 3vw;text-align: center;height: 5.2vw;line-height: 5.2vw; border-radius: 4.5vw;">
{{ userdata.region }}</div>
<div class="store" style="font-size: 2.9vw; color: gray;">{{
userdata.store }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.poster-container {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.poster-wrapper {
scroll-behavior: smooth;
}
.gui-ani-end-post {
object-fit: contain;
}
</style>

11
src/pages/Game/Quiz.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import assets from '../../assets';
import AniEle from '../../components/AniEle.vue';
</script>
<template>
<AniEle :url="assets.ani.答题标题" :width="1000" :height="500" :rules="[
{ name: 'main', frame: 60, duration: 33, loop: 1, pauseAfter: true, reverse: false }
]" />
</template>

View File

@ -0,0 +1,161 @@
<script setup lang="ts">
defineProps<{
leaderBoard: Array<{
name: string;
time: number;
region: string;
store?: string;
}>;
fmtTime: (time: number) => string;
}>();
</script>
<template>
<div class="scoreboard">
<div class="bar-container">
<div class="bar" v-for="(item, index) in [leaderBoard[1], leaderBoard[0], leaderBoard[2]]" :key="index"
:class="{
'first': index === 1, 'second': index === 0, 'third': index === 2
}">
<div class="name">{{ item.name }}</div>
<div class="time">{{ fmtTime(item.time) }}</div>
<div class="region">{{ item.region }}</div>
<div class="store">{{ item.store }}</div>
</div>
</div>
<div class="lines">
<div class="line" v-for="(item, index) in leaderBoard.slice(3, 7)" :key="index"
:style="{ animationDelay: index * 100 + 400 + 'ms' }">
<div class="rank">{{ index + 4 }}</div>
<div class="name">{{ item.name }}</div>
<div class="time">{{ fmtTime(item.time) }}</div>
<div class="region">{{ item.region }}</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.scoreboard {
position: absolute;
top: 44%;
width: 100%;
.bar-container {
width: 100%;
justify-content: center;
align-items: center;
display: flex;
gap: 2vw;
}
.bar {
position: relative;
height: 60vw;
width: 26vw;
background-size: contain;
background-repeat: no-repeat;
mask-image: linear-gradient(to bottom, black, black, transparent);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5vw;
animation: bar-in 0.7s ease-out forwards;
transform: translateY(150%);
.name {
font-size: 4.5vw;
}
.time {
font-size: 3vw;
}
.region {
font-size: 3vw;
color: white;
background-color: #7EA0CA;
padding: 0 2vw;
border-radius: 2vw;
}
.store {
max-width: 13vw;
text-align: center;
font-size: 2.4vw;
color: gray;
}
&.first {
top: 3vw;
background-image: url('../../assets/game/first.webp');
animation-delay: 0ms;
}
&.second {
background-image: url('../../assets/game/second.webp');
animation-delay: 250ms;
top: 9vw;
}
&.third {
background-image: url('../../assets/game/third.webp');
animation-delay: 500ms;
top: 9vw;
}
}
.lines {
display: flex;
position: relative;
top: -5vw;
flex-direction: column;
align-items: center;
gap: 1vw;
.line {
display: flex;
align-items: center;
justify-content: space-between;
width: 80%;
font-size: 3.2vw;
background-color: #FFFFFF;
padding: 0 2vw;
height: 8vw;
border-radius: 8vw;
animation: line-in 0.5s ease-out forwards;
transform: translateX(-150%);
.rank {
// italic
font-style: italic;
flex: 1;
font-size: 4vw;
text-align: center;
color: #7EA0CA;
}
.name {
flex: 2;
text-align: center;
border-right: .35vw solid black;
}
.time {
flex: 2;
text-align: center;
border-right: .35vw solid black;
}
.region {
flex: 2;
color: gray;
text-align: center;
}
}
}
}
</style>