855 lines
19 KiB
Vue
855 lines
19 KiB
Vue
<script setup>
|
|
import {
|
|
ref,
|
|
watch,
|
|
onMounted,
|
|
onBeforeUnmount,
|
|
computed,
|
|
nextTick,
|
|
getCurrentInstance,
|
|
} from "vue";
|
|
import PointSwitcher from "./PointSwitcher.vue";
|
|
import BowShotEffect from "@/components/BowShotEffect.vue";
|
|
|
|
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
|
import { simulShootAPI, laserAimAPI, laserCloseAPI } from "@/apis";
|
|
import useStore from "@/store";
|
|
import { storeToRefs } from "pinia";
|
|
const store = useStore();
|
|
const { user, device } = storeToRefs(store);
|
|
|
|
const props = defineProps({
|
|
currentRound: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
totalRound: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
scores: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
blueScores: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
redTeam: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
blueTeam: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
latestShotFlash: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
mode: {
|
|
type: String,
|
|
default: "solo", // solo 单排,team 双排
|
|
},
|
|
stop: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
targetRadius: {
|
|
type: Number,
|
|
default: 20,
|
|
},
|
|
hitRadiusPx: {
|
|
type: Number,
|
|
default: 2,
|
|
},
|
|
zoomHitRadiusPx: {
|
|
type: Number,
|
|
default: 5,
|
|
},
|
|
});
|
|
|
|
const pMode = ref(true);
|
|
const latestOne = ref(null);
|
|
const bluelatestOne = ref(null);
|
|
const timer = ref(null);
|
|
const dirTimer = ref(null);
|
|
const angle = ref(null);
|
|
const circleColor = ref("");
|
|
const shotEffect = ref(null);
|
|
const hiddenRedLatestKey = ref("");
|
|
const hiddenBlueLatestKey = ref("");
|
|
const targetShaking = ref(false);
|
|
const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 });
|
|
const shakeTimer = ref(null);
|
|
const instance = getCurrentInstance();
|
|
const ROUND_TIP_OFFSET_Y = -32;
|
|
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
|
|
|
function buildShotEffectKey(team, shot, fallbackKey = "") {
|
|
return (
|
|
fallbackKey ||
|
|
[
|
|
team,
|
|
shot?.playerId ?? "",
|
|
shot?.x ?? "",
|
|
shot?.y ?? "",
|
|
shot?.ring ?? "",
|
|
shot?.ringX ? 1 : 0,
|
|
].join("-")
|
|
);
|
|
}
|
|
|
|
function findShotPlayer(shot, team) {
|
|
const players = team === "red" ? props.redTeam : props.blueTeam;
|
|
return players.find((player) => String(player?.id) === String(shot?.playerId));
|
|
}
|
|
|
|
function isSvipShot(shot, team) {
|
|
return findShotPlayer(shot, team)?.sVip === true;
|
|
}
|
|
|
|
function shouldPlayShotEffect(shot, team) {
|
|
return !!shot && Number(shot.ring) > 0 && isSvipShot(shot, team);
|
|
}
|
|
|
|
function clearTipTimer() {
|
|
if (timer.value) clearTimeout(timer.value);
|
|
timer.value = null;
|
|
}
|
|
|
|
function showShotTip(team, shootData) {
|
|
clearTipTimer();
|
|
|
|
if (team === "red") {
|
|
latestOne.value = shootData;
|
|
timer.value = setTimeout(() => {
|
|
latestOne.value = null;
|
|
timer.value = null;
|
|
}, 1000);
|
|
return;
|
|
}
|
|
|
|
bluelatestOne.value = shootData;
|
|
timer.value = setTimeout(() => {
|
|
bluelatestOne.value = null;
|
|
timer.value = null;
|
|
}, 1000);
|
|
}
|
|
|
|
function triggerShotEffect(team, shot, fallbackKey = "") {
|
|
const key = buildShotEffectKey(team, shot, fallbackKey);
|
|
|
|
if (shotEffect.value?.team === "red") hiddenRedLatestKey.value = "";
|
|
if (shotEffect.value?.team === "blue") hiddenBlueLatestKey.value = "";
|
|
|
|
if (team === "red") {
|
|
latestOne.value = null;
|
|
hiddenRedLatestKey.value = key;
|
|
} else {
|
|
bluelatestOne.value = null;
|
|
hiddenBlueLatestKey.value = key;
|
|
}
|
|
|
|
shotEffect.value = { key, team, shot };
|
|
}
|
|
|
|
function completeShotEffect(key) {
|
|
if (!shotEffect.value || shotEffect.value.key !== key) return;
|
|
|
|
const { team, shot } = shotEffect.value;
|
|
if (team === "red") hiddenRedLatestKey.value = "";
|
|
if (team === "blue") hiddenBlueLatestKey.value = "";
|
|
shotEffect.value = null;
|
|
showShotTip(team, shot);
|
|
}
|
|
|
|
function shakeTarget() {
|
|
targetShaking.value = false;
|
|
if (shakeTimer.value) {
|
|
clearTimeout(shakeTimer.value);
|
|
shakeTimer.value = null;
|
|
}
|
|
|
|
nextTick(() => {
|
|
targetShaking.value = true;
|
|
shakeTimer.value = setTimeout(() => {
|
|
targetShaking.value = false;
|
|
shakeTimer.value = null;
|
|
}, 260);
|
|
});
|
|
}
|
|
|
|
function updateTargetRect() {
|
|
nextTick(() => {
|
|
const query = instance?.proxy
|
|
? uni.createSelectorQuery().in(instance.proxy)
|
|
: uni.createSelectorQuery();
|
|
|
|
query
|
|
.select(".target")
|
|
.boundingClientRect((rect) => {
|
|
const left = Number(rect?.left);
|
|
const top = Number(rect?.top);
|
|
const width = Number(rect?.width);
|
|
const height = Number(rect?.height);
|
|
if (
|
|
!Number.isFinite(left) ||
|
|
!Number.isFinite(top) ||
|
|
!Number.isFinite(width) ||
|
|
!Number.isFinite(height)
|
|
) {
|
|
return;
|
|
}
|
|
if (width <= 0 || height <= 0) return;
|
|
targetRect.value = { left, top, width, height };
|
|
})
|
|
.exec();
|
|
});
|
|
}
|
|
|
|
function handleWindowResize() {
|
|
updateTargetRect();
|
|
}
|
|
|
|
function shouldHideRedHit(index) {
|
|
return !!hiddenRedLatestKey.value && index === props.scores.length - 1;
|
|
}
|
|
|
|
function shouldHideBlueHit(index) {
|
|
return !!hiddenBlueLatestKey.value && index === props.blueScores.length - 1;
|
|
}
|
|
|
|
function showShotFlash(flash) {
|
|
const shootData = flash?.shootData;
|
|
if (!shootData) {
|
|
hiddenRedLatestKey.value = "";
|
|
hiddenBlueLatestKey.value = "";
|
|
shotEffect.value = null;
|
|
return;
|
|
}
|
|
|
|
const team = flash.team === "red" ? "red" : "blue";
|
|
if (shouldPlayShotEffect(shootData, team)) {
|
|
triggerShotEffect(team, shootData, flash.key);
|
|
return;
|
|
}
|
|
|
|
showShotTip(team, shootData);
|
|
}
|
|
|
|
watch(
|
|
() => props.latestShotFlash,
|
|
(newVal) => {
|
|
showShotFlash(newVal);
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => props.scores.length,
|
|
(newLen, oldLen) => {
|
|
if (newLen > oldLen) return;
|
|
latestOne.value = null;
|
|
hiddenRedLatestKey.value = "";
|
|
if (shotEffect.value?.team === "red") shotEffect.value = null;
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.blueScores.length,
|
|
(newLen, oldLen) => {
|
|
if (newLen > oldLen) return;
|
|
bluelatestOne.value = null;
|
|
hiddenBlueLatestKey.value = "";
|
|
if (shotEffect.value?.team === "blue") shotEffect.value = null;
|
|
}
|
|
);
|
|
|
|
const safeTargetRadius = computed(() => {
|
|
const radius = Number(props.targetRadius);
|
|
return Number.isFinite(radius) && radius > 0 ? radius : 20;
|
|
});
|
|
|
|
const currentHitRadiusPx = computed(() => {
|
|
const radius = Number(
|
|
pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
|
|
);
|
|
return Number.isFinite(radius) && radius >= 0 ? radius : 0;
|
|
});
|
|
|
|
function getShotPoint(shot, fallbackCenter = false) {
|
|
const x = Number(shot?.x);
|
|
const y = Number(shot?.y);
|
|
if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
|
|
return fallbackCenter ? { x: 0, y: 0 } : null;
|
|
}
|
|
|
|
function getPointDirection(point) {
|
|
if (!point) return null;
|
|
const distance = Math.sqrt(point.x * point.x + point.y * point.y);
|
|
if (distance === 0) return null;
|
|
|
|
return {
|
|
x: point.x / distance,
|
|
y: point.y / distance,
|
|
};
|
|
}
|
|
|
|
function formatPxOffset(value) {
|
|
if (!value) return "";
|
|
const operator = value > 0 ? "+" : "-";
|
|
return ` ${operator} ${Math.abs(value)}px`;
|
|
}
|
|
|
|
function formatTargetPosition(percent, offset) {
|
|
const pxOffset = formatPxOffset(offset);
|
|
return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
|
|
}
|
|
|
|
function getTargetPositionStyle(point, offsetPx = 0, extraOffset = {}) {
|
|
if (!point) return { display: "none" };
|
|
|
|
const radius = safeTargetRadius.value;
|
|
const diameter = radius * 2;
|
|
const direction = getPointDirection(point);
|
|
const xOffset = (direction ? direction.x * offsetPx : 0) + (extraOffset.x || 0);
|
|
const yOffset = (direction ? -direction.y * offsetPx : 0) + (extraOffset.y || 0);
|
|
const leftPercent = ((point.x + radius) / diameter) * 100;
|
|
const topPercent = ((radius - point.y) / diameter) * 100;
|
|
|
|
return {
|
|
left: formatTargetPosition(leftPercent, xOffset),
|
|
top: formatTargetPosition(topPercent, yOffset),
|
|
transform: "translate(-50%, -50%)",
|
|
};
|
|
}
|
|
|
|
function getHitStyle(shot) {
|
|
const radius = currentHitRadiusPx.value;
|
|
const point = getShotPoint(shot);
|
|
|
|
return {
|
|
...getTargetPositionStyle(point, radius),
|
|
width: `${radius * 2}px`,
|
|
height: `${radius * 2}px`,
|
|
};
|
|
}
|
|
|
|
function getSvipHitBgStyle(shot) {
|
|
const radius = currentHitRadiusPx.value;
|
|
const point = getShotPoint(shot);
|
|
|
|
return {
|
|
...getTargetPositionStyle(point, radius),
|
|
};
|
|
}
|
|
|
|
function getRoundTipStyle(shot) {
|
|
const point = getShotPoint(shot, true);
|
|
return getTargetPositionStyle(
|
|
point,
|
|
shot?.ring ? currentHitRadiusPx.value : 0,
|
|
{ y: ROUND_TIP_OFFSET_Y }
|
|
);
|
|
}
|
|
|
|
function getExperienceTipStyle(shot) {
|
|
const point = getShotPoint(shot, true);
|
|
return getTargetPositionStyle(
|
|
point,
|
|
shot?.ring ? currentHitRadiusPx.value : 0,
|
|
{ y: EXPERIENCE_TIP_OFFSET_Y }
|
|
);
|
|
}
|
|
const simulShoot = async () => {
|
|
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
|
};
|
|
const simulShoot2 = async () => {
|
|
if (device.value.deviceId) {
|
|
const r1 = Math.random() > 0.5 ? 0.01 : 0.02;
|
|
await simulShootAPI(device.value.deviceId, r1, r1);
|
|
}
|
|
};
|
|
|
|
const openAim = async () => {
|
|
await laserAimAPI();
|
|
};
|
|
|
|
const closeAim = async () => {
|
|
await laserCloseAPI();
|
|
};
|
|
|
|
const env = computed(() => {
|
|
const accountInfo = uni.getAccountInfoSync();
|
|
return accountInfo.miniProgram.envVersion;
|
|
});
|
|
|
|
const arrowStyle = computed(() => {
|
|
return {
|
|
transform: `rotateX(180deg) translate(-50%, -50%) rotate(${
|
|
360 - angle.value
|
|
}deg) translateY(105%)`,
|
|
};
|
|
});
|
|
|
|
async function onReceiveMessage(message) {
|
|
if (Array.isArray(message)) return;
|
|
if (message.type === MESSAGETYPESV2.ShootResult && message.shootData) {
|
|
if (
|
|
message.shootData.playerId === user.value.id &&
|
|
!message.shootData.ring &&
|
|
message.shootData.angle >= 0
|
|
) {
|
|
angle.value = null;
|
|
setTimeout(() => {
|
|
if (props.scores[0]) {
|
|
circleColor.value =
|
|
message.shootData.playerId === props.scores[0].playerId
|
|
? "#ff4444"
|
|
: "#1840FF";
|
|
}
|
|
angle.value = message.shootData.angle;
|
|
}, 200);
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
uni.$on("socket-inbox", onReceiveMessage);
|
|
updateTargetRect();
|
|
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (timer.value) {
|
|
clearTimeout(timer.value);
|
|
timer.value = null;
|
|
}
|
|
if (dirTimer.value) {
|
|
clearTimeout(dirTimer.value);
|
|
dirTimer.value = null;
|
|
}
|
|
if (shakeTimer.value) {
|
|
clearTimeout(shakeTimer.value);
|
|
shakeTimer.value = null;
|
|
}
|
|
uni.$off("socket-inbox", onReceiveMessage);
|
|
if (uni.offWindowResize) uni.offWindowResize(handleWindowResize);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<view :class="['container', { 'container--effecting': shotEffect }]">
|
|
<view class="header" v-if="totalRound > 0">
|
|
<text v-if="totalRound > 0" class="round-count">{{
|
|
(currentRound > totalRound ? totalRound : currentRound) +
|
|
"/" +
|
|
totalRound
|
|
}}</text>
|
|
</view>
|
|
<view :class="['target', { 'target--shake': targetShaking }]">
|
|
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
|
|
<view :style="{ background: circleColor }">
|
|
<image src="../../../static/dot-circle.png" mode="widthFix" />
|
|
</view>
|
|
</view>
|
|
<view v-if="stop" class="stop-sign">中场休息</view>
|
|
<view
|
|
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
|
|
class="e-value fade-in-out"
|
|
:style="getExperienceTipStyle(latestOne)"
|
|
>
|
|
经验 +1
|
|
</view>
|
|
<view
|
|
v-if="latestOne"
|
|
class="round-tip fade-in-out"
|
|
:style="getRoundTipStyle(latestOne)"
|
|
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
|
|
}}<text v-if="latestOne.ring">环</text>
|
|
</view>
|
|
<view
|
|
v-if="
|
|
bluelatestOne &&
|
|
bluelatestOne.ring &&
|
|
user.id === bluelatestOne.playerId
|
|
"
|
|
class="e-value fade-in-out"
|
|
:style="getExperienceTipStyle(bluelatestOne)"
|
|
>
|
|
经验 +1
|
|
</view>
|
|
<view
|
|
v-if="bluelatestOne"
|
|
class="round-tip fade-in-out"
|
|
:style="getRoundTipStyle(bluelatestOne)"
|
|
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
|
|
}}<text v-if="bluelatestOne.ring">环</text></view
|
|
>
|
|
<block v-for="(bow, index) in scores" :key="index">
|
|
<image
|
|
v-if="
|
|
pMode &&
|
|
bow.ring > 0 &&
|
|
isSvipShot(bow, 'red') &&
|
|
!shouldHideRedHit(index)
|
|
"
|
|
class="svip-hit-bg"
|
|
src="../../../static/vip/svip-xuan.png"
|
|
:style="getSvipHitBgStyle(bow)"
|
|
mode="aspectFit"
|
|
/>
|
|
<view
|
|
v-if="bow.ring > 0 && !shouldHideRedHit(index)"
|
|
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
|
index === scores.length - 1 && latestOne ? 'pump-in' : ''
|
|
}`"
|
|
:style="{
|
|
...getHitStyle(bow),
|
|
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
|
|
}"
|
|
><text v-if="pMode">{{ index + 1 }}</text></view
|
|
>
|
|
</block>
|
|
<block v-for="(bow, index) in blueScores" :key="index">
|
|
<image
|
|
v-if="
|
|
pMode &&
|
|
bow.ring > 0 &&
|
|
isSvipShot(bow, 'blue') &&
|
|
!shouldHideBlueHit(index)
|
|
"
|
|
class="svip-hit-bg"
|
|
src="../../../static/vip/svip-xuan.png"
|
|
:style="getSvipHitBgStyle(bow)"
|
|
mode="aspectFit"
|
|
/>
|
|
<view
|
|
v-if="bow.ring > 0 && !shouldHideBlueHit(index)"
|
|
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
|
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
|
|
}`"
|
|
:style="{
|
|
...getHitStyle(bow),
|
|
backgroundColor: '#1840FF',
|
|
}"
|
|
>
|
|
<text v-if="pMode">{{ index + 1 }}</text>
|
|
</view>
|
|
</block>
|
|
<BowShotEffect
|
|
:shot="shotEffect && shotEffect.shot"
|
|
:playKey="shotEffect ? shotEffect.key : ''"
|
|
:targetRadius="safeTargetRadius"
|
|
:targetLeft="targetRect.left"
|
|
:targetTop="targetRect.top"
|
|
:targetWidth="targetRect.width"
|
|
:targetHeight="targetRect.height"
|
|
:hitOffsetPx="currentHitRadiusPx"
|
|
@impact="shakeTarget"
|
|
@complete="completeShotEffect"
|
|
/>
|
|
<image src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png" mode="widthFix" />
|
|
</view>
|
|
<view class="footer">
|
|
<PointSwitcher
|
|
:onChange="(val) => (pMode = val)"
|
|
:style="{ zIndex: 999 }"
|
|
/>
|
|
</view>
|
|
<view class="simul" v-if="env !== 'release'">
|
|
<button @click="simulShoot">模拟</button>
|
|
<button @click="simulShoot2">射箭</button>
|
|
<button @click="openAim">开瞄</button>
|
|
<button @click="closeAim">关瞄</button>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.container {
|
|
width: calc(100vw - 30px);
|
|
height: calc(100vw - 30px);
|
|
padding: 0px 15px;
|
|
position: relative;
|
|
z-index: 3;
|
|
}
|
|
.container--effecting {
|
|
z-index: 10000;
|
|
}
|
|
.target {
|
|
position: relative;
|
|
margin: 10px;
|
|
width: calc(100% - 20px);
|
|
height: calc(100% - 20px);
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
transform-origin: center center;
|
|
}
|
|
.target--shake {
|
|
animation: target-shake 0.26s ease-out;
|
|
}
|
|
.e-value {
|
|
position: absolute;
|
|
background-color: #0006;
|
|
color: #fff;
|
|
font-size: 12px;
|
|
padding: 4px 7px;
|
|
border-radius: 5px;
|
|
z-index: 2;
|
|
width: 50px;
|
|
text-align: center;
|
|
}
|
|
.round-tip {
|
|
position: absolute;
|
|
color: #fff;
|
|
font-size: 30px;
|
|
font-weight: bold;
|
|
z-index: 2;
|
|
width: 100px;
|
|
text-align: center;
|
|
}
|
|
.round-tip > text {
|
|
font-size: 24px;
|
|
margin-left: 5px;
|
|
}
|
|
@keyframes target-tip-fade-in-out {
|
|
0% {
|
|
transform: translate(-50%, -50%) translateY(20px);
|
|
opacity: 0;
|
|
}
|
|
|
|
30% {
|
|
transform: translate(-50%, -50%);
|
|
opacity: 1;
|
|
}
|
|
|
|
80% {
|
|
transform: translate(-50%, -50%);
|
|
opacity: 1;
|
|
}
|
|
|
|
100% {
|
|
transform: translate(-50%, -50%);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
.round-tip.fade-in-out,
|
|
.e-value.fade-in-out {
|
|
animation: target-tip-fade-in-out 1.2s ease forwards;
|
|
}
|
|
.target > image:last-child {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.svip-hit-bg {
|
|
position: absolute;
|
|
width: 48rpx;
|
|
height: 48rpx;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
transform-origin: center center;
|
|
animation: svip-hit-xuan 1.2s linear infinite;
|
|
}
|
|
.hit {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
z-index: 2;
|
|
color: #fff;
|
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
box-sizing: border-box;
|
|
}
|
|
.b-point {
|
|
border: 1px solid #fff;
|
|
z-index: 2;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.b-point > text {
|
|
font-size: 16rpx;
|
|
color: #fff;
|
|
font-family: "DINCondensed";
|
|
/* text-align: center;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);*/
|
|
margin-top: 2rpx;
|
|
}
|
|
@keyframes svip-hit-xuan {
|
|
0% {
|
|
opacity: 0.9;
|
|
transform: translate(-50%, -50%) rotate(0deg) scale(0.92);
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
transform: translate(-50%, -50%) rotate(180deg) scale(1.08);
|
|
}
|
|
100% {
|
|
opacity: 0.9;
|
|
transform: translate(-50%, -50%) rotate(360deg) scale(0.92);
|
|
}
|
|
}
|
|
@keyframes target-pump-in {
|
|
from {
|
|
transform: translate(-50%, -50%) scale(2);
|
|
}
|
|
|
|
to {
|
|
transform: translate(-50%, -50%) scale(1);
|
|
}
|
|
}
|
|
@keyframes target-shake {
|
|
0% {
|
|
transform: translate(0, 0);
|
|
}
|
|
14% {
|
|
transform: translate(-20rpx, 8rpx);
|
|
}
|
|
28% {
|
|
transform: translate(16rpx, -8rpx);
|
|
}
|
|
44% {
|
|
transform: translate(-12rpx, 6rpx);
|
|
}
|
|
64% {
|
|
transform: translate(8rpx, -4rpx);
|
|
}
|
|
82% {
|
|
transform: translate(-4rpx, 2rpx);
|
|
}
|
|
100% {
|
|
transform: translate(0, 0);
|
|
}
|
|
}
|
|
.hit.pump-in {
|
|
animation: target-pump-in 0.3s ease-out forwards;
|
|
transform-origin: center center;
|
|
}
|
|
.header {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: -40px;
|
|
}
|
|
.header > image:first-child {
|
|
width: 40px;
|
|
height: 40px;
|
|
}
|
|
.round-count {
|
|
font-size: 20px;
|
|
color: #fed847;
|
|
top: 75px;
|
|
font-weight: bold;
|
|
}
|
|
.footer {
|
|
width: calc(100% - 20px);
|
|
padding: 0 10px;
|
|
display: flex;
|
|
margin-top: -40px;
|
|
justify-content: flex-end;
|
|
position: relative;
|
|
z-index: 999;
|
|
}
|
|
.footer > image {
|
|
width: 40px;
|
|
min-height: 40px;
|
|
max-height: 40px;
|
|
border-radius: 50%;
|
|
border: 1px solid #fff;
|
|
}
|
|
.simul {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 20px;
|
|
margin-left: 20px;
|
|
z-index: 999;
|
|
}
|
|
.simul > button {
|
|
color: #fff;
|
|
}
|
|
.stop-sign {
|
|
position: absolute;
|
|
font-size: 44px;
|
|
color: #fff9;
|
|
text-align: center;
|
|
width: 200px;
|
|
height: 60px;
|
|
left: calc(50% - 100px);
|
|
top: calc(50% - 30px);
|
|
z-index: 99;
|
|
font-weight: bold;
|
|
}
|
|
.arrow-dir {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 52%;
|
|
left: 50%;
|
|
bottom: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.arrow-dir > view {
|
|
width: 40rpx;
|
|
height: 40rpx;
|
|
border-radius: 50%;
|
|
}
|
|
.arrow-dir > view > image {
|
|
width: 100rpx;
|
|
height: 100rpx;
|
|
transform: translate(-30%, -30%);
|
|
}
|
|
@keyframes spring-in {
|
|
0% {
|
|
transform: scale(2);
|
|
opacity: 0.4;
|
|
}
|
|
15% {
|
|
transform: scale(3);
|
|
opacity: 1;
|
|
}
|
|
30% {
|
|
transform: scale(2);
|
|
opacity: 0.4;
|
|
}
|
|
45% {
|
|
transform: scale(3);
|
|
opacity: 1;
|
|
}
|
|
60% {
|
|
transform: scale(2);
|
|
opacity: 0.4;
|
|
}
|
|
75% {
|
|
transform: scale(3);
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
@keyframes disappear {
|
|
0% {
|
|
opacity: 1;
|
|
}
|
|
75% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
.arrow-dir > view {
|
|
animation: disappear 3s ease forwards;
|
|
}
|
|
.arrow-dir > view > image {
|
|
animation: spring-in 3s ease forwards;
|
|
width: 100%;
|
|
}
|
|
</style>
|