955 lines
22 KiB
Vue
955 lines
22 KiB
Vue
<script setup>
|
|
import {
|
|
computed,
|
|
getCurrentInstance,
|
|
nextTick,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
ref,
|
|
watch,
|
|
} from "vue";
|
|
import BowShotEffect from "@/components/BowShotEffect.vue";
|
|
import PointSwitcher from "@/components/PointSwitcher.vue";
|
|
import TargetCanvas from "@/components/TargetCanvas.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: () => [],
|
|
},
|
|
isSvip: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
shotEffectToken: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
mode: {
|
|
type: String,
|
|
default: "solo", // solo 单排,team 双排
|
|
},
|
|
stop: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
coordinateRadius: {
|
|
type: Number,
|
|
default: 20,
|
|
},
|
|
hitRadiusPx: {
|
|
type: Number,
|
|
default: 2,
|
|
},
|
|
zoomHitRadiusPx: {
|
|
type: Number,
|
|
default: 5,
|
|
},
|
|
showCrosshair: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
sectorCount: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
activeSector: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
activeRing: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
showSectorLabels: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
stableShotEffect: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(["shot-effect-complete"]);
|
|
|
|
const pMode = ref(true);
|
|
const latestOne = ref(null);
|
|
const bluelatestOne = ref(null);
|
|
const prevScores = ref([]);
|
|
const prevBlueScores = ref([]);
|
|
const timer = ref(null);
|
|
const dirTimer = ref(null);
|
|
const angle = ref(null);
|
|
const circleColor = ref("");
|
|
const shotEffect = ref(null);
|
|
const pendingShotEffect = ref(null);
|
|
const hiddenLatestKey = ref("");
|
|
const targetShaking = ref(false);
|
|
const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 });
|
|
const shakeTimer = ref(null);
|
|
const instance = getCurrentInstance();
|
|
let shotEffectRequestGeneration = 0;
|
|
const ROUND_TIP_OFFSET_Y = -32;
|
|
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
|
|
|
const getNumber = (value, fallback = 0) => {
|
|
const numberValue = Number(value);
|
|
return Number.isFinite(numberValue) ? numberValue : fallback;
|
|
};
|
|
|
|
const safeTargetRadius = computed(() => {
|
|
return Math.max(getNumber(props.coordinateRadius, 20), 1);
|
|
});
|
|
|
|
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 }
|
|
);
|
|
}
|
|
|
|
function clearTipTimer() {
|
|
if (!timer.value) return;
|
|
clearTimeout(timer.value);
|
|
timer.value = null;
|
|
}
|
|
|
|
function showShotTip(shot) {
|
|
clearTipTimer();
|
|
latestOne.value = shot;
|
|
timer.value = setTimeout(() => {
|
|
latestOne.value = null;
|
|
timer.value = null;
|
|
}, 1000);
|
|
}
|
|
|
|
function hasShotPoint(shot) {
|
|
return !!getShotPoint(shot);
|
|
}
|
|
|
|
function shouldPlayShotEffect(shot) {
|
|
return (
|
|
props.isSvip &&
|
|
!!shot &&
|
|
Number(shot.ring) > 0 &&
|
|
hasShotPoint(shot)
|
|
);
|
|
}
|
|
|
|
function buildShotEffectKey(shot, index) {
|
|
return [
|
|
props.shotEffectToken,
|
|
index,
|
|
shot?.playerId ?? "",
|
|
shot?.x ?? "",
|
|
shot?.y ?? "",
|
|
shot?.ring ?? "",
|
|
shot?.ringX ? 1 : 0,
|
|
].join("-");
|
|
}
|
|
|
|
function triggerShotEffect(shot, index, viewportMode = false) {
|
|
const key = buildShotEffectKey(shot, index);
|
|
clearTipTimer();
|
|
latestOne.value = null;
|
|
hiddenLatestKey.value = key;
|
|
shotEffect.value = {
|
|
key,
|
|
shot,
|
|
token: props.shotEffectToken,
|
|
viewportMode,
|
|
};
|
|
}
|
|
|
|
async function prepareShotEffect(shot, index) {
|
|
const requestGeneration = ++shotEffectRequestGeneration;
|
|
const key = buildShotEffectKey(shot, index);
|
|
pendingShotEffect.value = { generation: requestGeneration, key };
|
|
clearTipTimer();
|
|
latestOne.value = null;
|
|
|
|
const viewportMode = props.stableShotEffect
|
|
? await updateTargetRect()
|
|
: false;
|
|
|
|
if (requestGeneration !== shotEffectRequestGeneration) {
|
|
if (pendingShotEffect.value?.generation === requestGeneration) {
|
|
pendingShotEffect.value = null;
|
|
}
|
|
return;
|
|
}
|
|
pendingShotEffect.value = null;
|
|
triggerShotEffect(shot, index, viewportMode);
|
|
}
|
|
|
|
function completeShotEffect(key) {
|
|
if (!shotEffect.value || shotEffect.value.key !== key) return;
|
|
|
|
const completedEffect = shotEffect.value;
|
|
const shot = completedEffect.shot;
|
|
hiddenLatestKey.value = "";
|
|
shotEffect.value = null;
|
|
showShotTip(shot);
|
|
emit("shot-effect-complete", {
|
|
key,
|
|
token: completedEffect.token,
|
|
});
|
|
}
|
|
|
|
function shouldHideLatestHit(index) {
|
|
return (
|
|
(!!hiddenLatestKey.value || !!pendingShotEffect.value) &&
|
|
index === props.scores.length - 1
|
|
);
|
|
}
|
|
|
|
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 hasValidTargetRect(rect = targetRect.value) {
|
|
return (
|
|
Number.isFinite(Number(rect?.left)) &&
|
|
Number.isFinite(Number(rect?.top)) &&
|
|
Number(rect?.width) > 0 &&
|
|
Number(rect?.height) > 0
|
|
);
|
|
}
|
|
|
|
async function updateTargetRect() {
|
|
await nextTick();
|
|
|
|
return new Promise((resolve) => {
|
|
let settled = false;
|
|
const finish = (rect) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
|
|
const isValid = hasValidTargetRect(rect);
|
|
if (isValid) {
|
|
targetRect.value = {
|
|
left: Number(rect.left),
|
|
top: Number(rect.top),
|
|
width: Number(rect.width),
|
|
height: Number(rect.height),
|
|
};
|
|
}
|
|
resolve(isValid);
|
|
};
|
|
|
|
try {
|
|
const query = instance?.proxy
|
|
? uni.createSelectorQuery().in(instance.proxy)
|
|
: uni.createSelectorQuery();
|
|
|
|
query
|
|
.select(".target")
|
|
.boundingClientRect()
|
|
.exec((result) => finish(Array.isArray(result) ? result[0] : null));
|
|
} catch {
|
|
finish(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleWindowResize() {
|
|
void updateTargetRect();
|
|
}
|
|
|
|
watch(
|
|
() => props.scores,
|
|
(newVal) => {
|
|
if (newVal.length - prevScores.value.length === 1) {
|
|
showShotTip(newVal[newVal.length - 1]);
|
|
} else if (newVal.length < prevScores.value.length) {
|
|
shotEffectRequestGeneration += 1;
|
|
pendingShotEffect.value = null;
|
|
clearTipTimer();
|
|
latestOne.value = null;
|
|
hiddenLatestKey.value = "";
|
|
shotEffect.value = null;
|
|
}
|
|
prevScores.value = [...newVal];
|
|
},
|
|
{
|
|
deep: true,
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.shotEffectToken,
|
|
(token) => {
|
|
// token 只由实时 ShootResult 推进,同步快照不会重播飞箭。
|
|
if (!token || props.scores.length === 0) return;
|
|
const latestIndex = props.scores.length - 1;
|
|
const latestShot = props.scores[latestIndex];
|
|
if (shouldPlayShotEffect(latestShot)) {
|
|
void prepareShotEffect(latestShot, latestIndex);
|
|
} else {
|
|
shotEffectRequestGeneration += 1;
|
|
pendingShotEffect.value = null;
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.blueScores,
|
|
(newVal) => {
|
|
if (newVal.length - prevBlueScores.value.length === 1) {
|
|
bluelatestOne.value = newVal[newVal.length - 1];
|
|
if (timer.value) clearTimeout(timer.value);
|
|
timer.value = setTimeout(() => {
|
|
bluelatestOne.value = null;
|
|
}, 1000);
|
|
}
|
|
prevBlueScores.value = [...newVal];
|
|
},
|
|
{
|
|
deep: true,
|
|
}
|
|
);
|
|
|
|
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%)`,
|
|
};
|
|
});
|
|
|
|
const showSectorCanvas = computed(() => {
|
|
const count = Number(props.sectorCount);
|
|
return props.totalRound > 0 && Number.isInteger(count) && count > 0;
|
|
});
|
|
|
|
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);
|
|
void updateTargetRect();
|
|
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
shotEffectRequestGeneration += 1;
|
|
pendingShotEffect.value = null;
|
|
clearTipTimer();
|
|
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 }]">
|
|
<image
|
|
class="target-image"
|
|
src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png"
|
|
mode="aspectFit"
|
|
/>
|
|
<TargetCanvas
|
|
v-if="showSectorCanvas"
|
|
class="target-highlight-layer"
|
|
:coordinateRadius="coordinateRadius"
|
|
:showCrosshair="false"
|
|
:showRingLabels="false"
|
|
:highlightOnly="true"
|
|
:sectorCount="sectorCount"
|
|
:activeSector="activeSector"
|
|
:activeRing="activeRing"
|
|
:showSectorLabels="showSectorLabels"
|
|
/>
|
|
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
|
|
<view :style="{ background: circleColor }">
|
|
<image src="https://static.shelingxingqiu.com/shootmini/static/dot-circle.png" mode="widthFix" />
|
|
</view>
|
|
</view>
|
|
<view v-if="stop" class="stop-sign">中场休息</view>
|
|
<view
|
|
v-if="
|
|
!shotEffect &&
|
|
latestOne &&
|
|
latestOne.ring &&
|
|
user.id === latestOne.playerId
|
|
"
|
|
class="e-value fade-in-out"
|
|
:style="getExperienceTipStyle(latestOne)"
|
|
>
|
|
经验 +1
|
|
</view>
|
|
<view
|
|
v-if="!shotEffect && 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 && isSvip && bow.ring > 0 && !shouldHideLatestHit(index)"
|
|
class="svip-hit-bg"
|
|
src="../../../static/vip/svip-xuan.png"
|
|
:style="getSvipHitBgStyle(bow)"
|
|
mode="aspectFit"
|
|
/>
|
|
<view
|
|
v-if="bow.ring > 0 && !shouldHideLatestHit(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">
|
|
<view
|
|
v-if="bow.ring > 0"
|
|
: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
|
|
v-if="!shotEffect || !shotEffect.viewportMode"
|
|
: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"
|
|
/>
|
|
</view>
|
|
<BowShotEffect
|
|
v-if="shotEffect && shotEffect.viewportMode"
|
|
:shot="shotEffect.shot"
|
|
:playKey="shotEffect.key"
|
|
:targetRadius="safeTargetRadius"
|
|
:targetLeft="targetRect.left"
|
|
:targetTop="targetRect.top"
|
|
:targetWidth="targetRect.width"
|
|
:targetHeight="targetRect.height"
|
|
:hitOffsetPx="currentHitRadiusPx"
|
|
:viewportMode="true"
|
|
@impact="shakeTarget"
|
|
@complete="completeShotEffect"
|
|
/>
|
|
<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;
|
|
}
|
|
.target-image {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
.target-highlight-layer {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
}
|
|
.e-value {
|
|
position: absolute;
|
|
background-color: #0006;
|
|
color: #fff;
|
|
font-size: 12px;
|
|
padding: 4px 7px;
|
|
border-radius: 5px;
|
|
z-index: 4;
|
|
width: 50px;
|
|
text-align: center;
|
|
}
|
|
.round-tip {
|
|
position: absolute;
|
|
color: #fff;
|
|
font-size: 30px;
|
|
font-weight: bold;
|
|
z-index: 4;
|
|
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;
|
|
}
|
|
.svip-hit-bg {
|
|
position: absolute;
|
|
width: 48rpx;
|
|
height: 48rpx;
|
|
z-index: 2;
|
|
pointer-events: none;
|
|
transform-origin: center center;
|
|
animation: svip-hit-xuan 1.2s linear infinite;
|
|
}
|
|
.hit {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
z-index: 3;
|
|
color: #fff;
|
|
transition: all 0.3s ease;
|
|
box-sizing: border-box;
|
|
}
|
|
.s-point {
|
|
}
|
|
.b-point {
|
|
border: 1px solid #fff;
|
|
z-index: 3;
|
|
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;
|
|
}
|
|
.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: 5;
|
|
font-weight: bold;
|
|
}
|
|
.arrow-dir {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 52%;
|
|
left: 50%;
|
|
bottom: 50%;
|
|
z-index: 4;
|
|
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>
|