Files
frost-navigation/src/views/ToolboxView/Other/GenshinImpactClock/ClockElement.vue

737 lines
17 KiB
Vue
Raw Normal View History

<template>
<div
ref="clockElement"
:class="{
'clock-element': true,
'clock-rotation': clockState.isRotation,
}"
:style="elStyle"
@touchmove.prevent
>
<!-- 外部 -->
<div class="clock-outer bg-contain"></div>
<!-- 内部 -->
<div class="clock-inner">
<!-- 背景 -->
<div class="inner-bg bg-cover"></div>
<div class="inner-star bg-cover"></div>
<!-- 齿轮 -->
<div class="clock-gear">
<div class="gear-6"></div>
<div class="gear-5"></div>
<div class="gear-4"></div>
<div class="gear-3"></div>
<div class="gear-2"></div>
<div class="gear-1"></div>
</div>
<!-- 色环 -->
<clock-color
:curr-angle="upperRealAngle"
:start-angle="lowerPointer.viewAngle"
/>
<!-- 下层指针 -->
<div class="pointer-wrapper pointer-lower bg-contain">
<div class="pointer-content"></div>
</div>
<!-- 上层指针 -->
<div class="pointer-wrapper pointer-upper bg-contain">
<div
class="pointer-content"
@pointerdown="handleDragPointer('upper')"
></div>
</div>
</div>
<!-- 表盘 -->
<div class="clock-dial bg-contain">
<div class="time-icons">
<div
class="time-morning bg-contain"
style="--self-angle-1: 270; --self-angle-2: 270;"
></div>
<div
class="time-noon bg-contain"
style="--self-angle-1: 0; --self-angle-2: 360;"
></div>
<div
class="time-dusk bg-contain"
style="--self-angle-1: 90; --self-angle-2: 90;"
></div>
<div
class="time-night bg-contain"
style="--self-angle-1: 180; --self-angle-2: 180;"
></div>
</div>
</div>
<!-- 遮罩层用于阻止操作 -->
<div v-show="isAutoRotating" class="clock-mask"></div>
</div>
</template>
<script setup>
import {
computed,
onBeforeUnmount, onMounted,
reactive, ref,
watch,
} from 'vue';
import {
IMAGE_CLOCK_BG_INNER, IMAGE_CLOCK_BG_OUTER, IMAGE_CLOCK_DIAL,
IMAGE_CLOCK_GEAR_1, IMAGE_CLOCK_GEAR_4,
IMAGE_CLOCK_GEAR_5, IMAGE_CLOCK_GEAR_6,
IMAGE_CLOCK_PARTICLES, IMAGE_POINTER_LOWER, IMAGE_POINTER_UPPER,
IMAGE_TIME_ICON_DUSK, IMAGE_TIME_ICON_MORNING,
IMAGE_TIME_ICON_NIGHT, IMAGE_TIME_ICON_NOON,
isAutoRotating, isTimeExceeded, isTimeTooEarly,
timeCurrHour, timeCurrMinute, timeCurrValue,
timeDiffLabel, timeDiffLabelStill,
timeNewHour, timeNewMinute, timeNewValue,
} from './common-data';
import ClockColor from './ClockColor.vue';
const clockElement = ref(null);
/** 时钟状态 */
const clockState = reactive({
/** 指针是否正在旋转 */
isRotation: false,
/** 最新一次获取到的指针角度 */
lastAngle: 0,
/** 定时器 ID */
rotationWatcher: null,
});
/** 下层指针状态 */
const lowerPointer = reactive({
/** 视图角度,用于显示 */
viewAngle: 0,
});
/** 上层指针状态 */
const upperPointer = reactive({
/** 是否为第二圈 */
isSecond: false,
/** 上层指针与下层指针相差的角度 */
dataAngle: 0,
/** 视图角度,用于显示和交互 */
viewAngle: 0,
});
window.upperPointer = upperPointer;
/** 元素 CSS */
const elStyle = computed(() => {
return {
'--image-clock-bg-inner': IMAGE_CLOCK_BG_INNER,
'--image-clock-bg-outer': IMAGE_CLOCK_BG_OUTER,
'--image-clock-dial': IMAGE_CLOCK_DIAL,
'--image-clock-gear-1': IMAGE_CLOCK_GEAR_1,
'--image-clock-gear-4': IMAGE_CLOCK_GEAR_4,
'--image-clock-gear-5': IMAGE_CLOCK_GEAR_5,
'--image-clock-gear-6': IMAGE_CLOCK_GEAR_6,
'--image-clock-particles': IMAGE_CLOCK_PARTICLES,
'--image-pointer-lower': IMAGE_POINTER_LOWER,
'--image-pointer-upper': IMAGE_POINTER_UPPER,
'--image-time-icon-dusk': IMAGE_TIME_ICON_DUSK,
'--image-time-icon-morning': IMAGE_TIME_ICON_MORNING,
'--image-time-icon-night': IMAGE_TIME_ICON_NIGHT,
'--image-time-icon-noon': IMAGE_TIME_ICON_NOON,
'--pointer-lower-angle': `${lowerPointer.viewAngle}deg`,
'--pointer-lower-angle-value': lowerPointer.viewAngle,
'--pointer-upper-angle': `${upperPointer.viewAngle}deg`,
'--pointer-upper-angle-value': upperPointer.viewAngle,
};
});
/** 上层指针实际已旋转角度 */
const upperRealAngle = computed(() => {
const { dataAngle, isSecond } = upperPointer;
return (isSecond ? dataAngle + 360 : dataAngle);
});
/** 初始化定时器 */
function timerInit() {
// 指针旋转检测
clockState.rotationWatcher = setInterval(function () {
const currAngle = upperRealAngle.value;
const lastAngle = clockState.lastAngle;
clockState.isRotation = (Math.abs(currAngle - lastAngle) >= 5);
clockState.lastAngle = currAngle;
}, 250);
}
/** 重置定时器 */
function timerReset() {
clearInterval(clockState.rotationWatcher);
}
/**
* @description 获取元素中心坐标
* @param {HTMLElement} el
*/
function getCenterPoint(el) {
if (el) {
const rect = el.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
} else {
return null;
}
}
/** 处理拖拽上层指针 */
function handleDragPointer() {
const center = getCenterPoint(clockElement.value);
if (!center) {
return;
}
const centerX = center.x;
const centerY = center.y;
// 节流
let last = 0;
/**
* @description 处理光标移动
* @param {PointerEvent} ev
*/
let handleMove = function (ev) {
let curr = Date.now();
if (curr - last >= 20) {
last = curr;
} else {
return;
}
let { pageX, pageY } = ev;
let numX = pageX - centerX;
let numY = pageY - centerY;
// 计算两点弧度 & 转换为角度(-180 ~ 180
let calcAngle = Math.round(Math.atan2(numY, numX) * (180 / Math.PI));
// 转换为视图角度0 ~ 359
let viewAngle = calcAngle + (calcAngle >= -90 ? 90 : 450);
// 用于数据处理的角度
let dataAngle = 0;
// 起始角度偏移值
let offsetAngle = lowerPointer.viewAngle;
// 处理偏移,获取数据角度
if (viewAngle >= offsetAngle) {
dataAngle = viewAngle - offsetAngle;
} else if (viewAngle < offsetAngle) {
dataAngle = viewAngle + (360 - offsetAngle);
}
// 新角度与原角度的差值
let diff = dataAngle - upperPointer.dataAngle;
// 顺时针越过起始点
if (diff <= -180) {
if (upperPointer.isSecond) {
// 当前为第二圈,阻止移动
dataAngle = 360;
viewAngle = 360 + offsetAngle;
} else {
// 当前为第一圈,进入第二圈
upperPointer.isSecond = true;
}
}
// 逆时针越过起始点
if (diff >= 180) {
if (upperPointer.isSecond) {
// 当前为第二圈,返回第一圈
upperPointer.isSecond = false;
} else {
// 当前为第一圈,阻止移动
dataAngle = 0;
viewAngle = offsetAngle;
}
}
upperPointer.dataAngle = dataAngle;
upperPointer.viewAngle = viewAngle;
};
window.addEventListener('pointermove', handleMove);
window.addEventListener('pointerup', function () {
window.removeEventListener('pointermove', handleMove);
}, { once: true });
}
/** 确定当前选择的时间,下层指针旋转至上层指针的位置 */
function handleSubmitTime() {
let lowerAngleStart = lowerPointer.viewAngle;
let upperAngleStart = upperRealAngle.value;
let upperAngleCurr = upperAngleStart;
let timer = setInterval(() => {
// 每次 -2最小值为 0
upperAngleCurr = Math.max(0, upperAngleCurr - 2);
// 结束
if (upperAngleCurr === 0) {
clearInterval(timer);
isAutoRotating.value = false;
timeDiffLabelStill.value = '';
}
let upperAngleDiff = upperAngleStart - upperAngleCurr;
let lowerAngleCurr = lowerAngleStart + upperAngleDiff;
lowerPointer.viewAngle = lowerAngleCurr % 360;
upperPointer.dataAngle = upperAngleCurr % 360;
upperPointer.isSecond = (upperAngleCurr >= 360);
}, 50);
/** 更新状态 */
isAutoRotating.value = true;
// 固定时间差文本
timeDiffLabelStill.value = timeDiffLabel.value;
}
defineExpose({
handleSubmitTime,
});
// 检测角度变化,计算时间信息(自动旋转时)
watch(() => {
return lowerPointer.viewAngle;
}, (viewAngle) => {
// 转换为对应 24 小时的角度
let timeAngle = viewAngle + (viewAngle < 180 ? 180 : -180);
let timeValue = timeAngle / 15;
let currHour = Math.floor(timeValue);
let currMinute = Math.round((timeValue - currHour) * 60);
// 计算时间值
timeCurrHour.value = String(currHour).padStart(2, '0');
timeCurrMinute.value = String(currMinute).padStart(2, '0');
timeCurrValue.value = timeValue;
}, { immediate: true });
// 检测角度变化,计算时间信息(用户操作时)
watch(() => {
return upperPointer.dataAngle;
}, (dataAngle) => {
// 注15° / 小时
let isSecond = upperPointer.isSecond;
let currAngle = lowerPointer.viewAngle;
let viewAngle = upperPointer.viewAngle;
let diffAngle = dataAngle + (isSecond ? 360 : 0);
let diffAngle1 = 0; // +1 日角度差
let diffAngle2 = 0; // +2 日角度差
let diffLabel = '';
// 转换为对应 24 小时的角度
let timeAngle = viewAngle + (viewAngle < 180 ? 180 : -180);
let timeValue = timeAngle / 15;
let newHour = Math.floor(timeValue);
let newMinute = Math.round((timeValue - newHour) * 60);
if (currAngle < 180) {
diffAngle1 = 180 - currAngle;
diffAngle2 = diffAngle1 + 360;
} else {
diffAngle1 = 540 - currAngle; // 360 + 180
diffAngle2 = diffAngle1 + 360;
}
// 处理时间差信息
if (diffAngle < diffAngle1) {
diffLabel = '今日';
} else if (diffAngle < diffAngle2) {
diffLabel = '次日';
} else {
diffLabel = '+2日';
}
// 注:
// 若指针起始位置位于表盘左半边,
// 且拖拽指针旋转满 2 圈,
// 此时计算出的小时值会大于或等于 24。
if (newHour >= 24) {
newHour -= 24;
}
// 处理提示信息显示
isTimeTooEarly.value = diffAngle < 7.5;
isTimeExceeded.value = diffAngle === 720;
// 更新时间差信息
timeDiffLabel.value = diffLabel;
// 计算时间值
timeNewHour.value = String(newHour).padStart(2, '0');
timeNewMinute.value = String(newMinute).padStart(2, '0');
timeNewValue.value = timeValue;
}, { immediate: true });
onMounted(() => {
timerInit();
});
onBeforeUnmount(() => {
timerReset();
});
</script>
<style lang="less" scoped>
.bg-contain {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.bg-cover {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.clock-element {
--pointer-lower-angle: 0deg;
--pointer-upper-angle: 0deg;
position: relative;
width: 32em;
height: 32em;
border-radius: 50%;
font-size: 12px;
overflow: hidden;
filter: brightness(1.1) saturate(0.9);
}
.clock-outer {
position: absolute;
top: -1%;
left: -1%;
width: 102%;
height: 102%;
background-image: var(--image-clock-bg-outer);
}
.clock-inner {
--size: 46%;
position: absolute;
top: calc(50% - var(--size) / 2);
left: calc(50% - var(--size) / 2);
width: var(--size);
height: var(--size);
border-radius: 50%;
background-color: #000;
overflow: hidden;
.inner-bg, .inner-star {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.inner-bg {
background-image: var(--image-clock-bg-inner);
animation: rotation-backward 60s linear infinite;
}
.inner-star {
background-image: var(--image-clock-particles);
}
}
.clock-gear {
--size: 120%;
--ratio: 1.55;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.6;
> div {
--ani-duration: 2s;
position: absolute;
animation-duration: var(--ani-duration);
animation-iteration-count: infinite;
animation-timing-function: linear;
transform: rotate(0);
will-change: transform;
&::after {
display: block;
content: "";
width: 100%;
height: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
animation-duration: calc(var(--ani-duration) / 12);
animation-iteration-count: infinite;
animation-name: inherit;
animation-play-state: paused;
animation-timing-function: linear;
}
}
.gear-1 {
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
--gear-teeth: 32;
top: calc(50% - var(--size) / 2);
left: calc(50% - var(--size) / 2);
width: var(--size);
height: var(--size);
animation-name: rotation-backward;
&::after {
background-image: var(--image-clock-gear-1);
}
}
.gear-2 {
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
--gear-teeth: 32;
top: calc(0% - var(--size) / 2);
left: calc(72% - var(--size) / 2);
width: var(--size);
height: var(--size);
animation-name: rotation-backward;
&::after {
background-image: var(--image-clock-gear-1);
}
}
.gear-3 {
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
--gear-teeth: 32;
top: calc(77% - var(--size) / 2);
left: calc(100% - var(--size) / 2);
width: var(--size);
height: var(--size);
animation-name: rotation-backward;
opacity: 0.5;
&::after {
background-image: var(--image-clock-gear-1);
}
}
.gear-4 {
--angle-offset: 4deg;
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
--gear-teeth: 32;
top: calc(77% - var(--size) / 2);
left: calc(100% - var(--size) / 2);
width: var(--size);
height: var(--size);
animation-name: rotation-forward;
&::after {
background-image: var(--image-clock-gear-4);
}
}
// 正常:约 74s / 圈
// 加速:约 7s / 圈
.gear-5 {
--ani-duration: calc(1s * (var(--gear-teeth) - 1) * var(--ratio));
--gear-teeth: 49;
top: calc(60% - var(--size) / 2);
left: calc(33% - var(--size) / 2);
width: var(--size);
height: var(--size);
animation-name: rotation-backward;
&::after {
background-image: var(--image-clock-gear-5);
}
}
.gear-6 {
--ani-duration: calc(1s * var(--gear-teeth) * var(--ratio));
--gear-teeth: 49;
top: calc(42% - var(--size) / 2);
left: calc(62% - var(--size) / 2);
width: var(--size);
height: var(--size);
animation-name: rotation-backward;
&::after {
background-image: var(--image-clock-gear-6);
}
}
}
// 齿轮加速转动
.clock-rotation .clock-gear > div::after {
animation-play-state: running;
}
.pointer-wrapper {
--size: 180%;
position: absolute;
top: 50%;
left: 50%;
width: var(--size);
height: var(--size);
&.pointer-lower {
background-image: var(--image-pointer-lower);
transform: translate(-50%, -50%) rotate(var(--pointer-lower-angle));
}
&.pointer-upper {
background-image: var(--image-pointer-upper);
transform: translate(-50%, -50%) rotate(var(--pointer-upper-angle));
}
.pointer-content {
--width: 5%;
position: absolute;
left: calc(50% - var(--width) / 2);
width: var(--width);
height: 50%;
cursor: grab;
}
}
.clock-dial {
--size: 88%;
position: absolute;
top: calc(50% - var(--size) / 2);
left: calc(50% - var(--size) / 2);
width: var(--size);
height: var(--size);
background-image: var(--image-clock-dial);
pointer-events: none;
.time-icons {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
> div {
// [角度 1 相关计算]
// 计算指针角度与图标对应角度的差值(可能为负数)
--angle-1-diff: calc(var(--self-angle-1) - var(--pointer-lower-angle-value));
// 计算角度差值的绝对值
--angle-1-abs: ~"max(var(--angle-1-diff), var(--angle-1-diff) * -1)";
// 限制角度最大差值为 90
--angle-1-use: ~"min(90, var(--angle-1-abs))";
// [角度 2 相关计算,主要用于 360°]
// 计算指针角度与图标所在角度的差值(可能为负数)
--angle-2-diff: calc(var(--self-angle-2) - var(--pointer-lower-angle-value));
// 计算角度差值的绝对值
--angle-2-abs: ~"max(var(--angle-2-diff), var(--angle-2-diff) * -1)";
// 限制角度最大差值为 90
--angle-2-use: ~"min(90, var(--angle-2-abs))";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
// 根据角度计算透明度
// 注:计算两个角度的透明度,取最大值
opacity: ~"calc((90 - min(var(--angle-1-use), var(--angle-2-use))) / 90)";
}
.time-morning {
background-image: var(--image-time-icon-morning);
}
.time-noon {
background-image: var(--image-time-icon-noon);
}
.time-dusk {
background-image: var(--image-time-icon-dusk);
}
.time-night {
background-image: var(--image-time-icon-night);
}
}
}
.clock-mask {
position: absolute;
left: 0;
top: 0;
z-index: 100;
width: 100%;
height: 100%;
cursor: wait;
}
// 顺时针旋转
@keyframes rotation-forward {
0% {
transform: rotate(calc(0deg + var(--angle-offset, 0deg)));
}
100% {
transform: rotate(calc(360deg + var(--angle-offset, 0deg)));
}
}
// 逆时针旋转
@keyframes rotation-backward {
0% {
transform: rotate(calc(360deg + var(--angle-offset, 0deg)));
}
100% {
transform: rotate(calc(0deg + var(--angle-offset, 0deg)));
}
}
</style>