feat(工具箱): 完善“原神时钟”,添加动画效果,支持旋转指针
This commit is contained in:
163
src/views/ToolboxView/Other/GenshinImpactClock/ClockColor.vue
Normal file
163
src/views/ToolboxView/Other/GenshinImpactClock/ClockColor.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="clock-color"
|
||||||
|
:viewBox="`0 0 ${elSize} ${elSize}`"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 定义 -->
|
||||||
|
<defs>
|
||||||
|
<!-- 背景图案 -->
|
||||||
|
<pattern
|
||||||
|
id="color-pattern"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
patternContentUnits="userSpaceOnUse"
|
||||||
|
:width="elSize"
|
||||||
|
:height="elSize"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
:width="elSize"
|
||||||
|
:height="elSize"
|
||||||
|
:href="IMAGE_TIME_ZONE_COLOR"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 外层圆环 -->
|
||||||
|
<path
|
||||||
|
class="color-circle"
|
||||||
|
:class="{ faded: currAngle > 360 }"
|
||||||
|
:d="state.dOuter"
|
||||||
|
fill="url(#color-pattern)"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
|
||||||
|
<!-- 内层圆环 -->
|
||||||
|
<path
|
||||||
|
ref="innerCircle"
|
||||||
|
class="color-circle"
|
||||||
|
:d="state.dInner"
|
||||||
|
fill="url(#color-pattern)"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
reactive, watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IMAGE_TIME_ZONE_COLOR,
|
||||||
|
} from './common-data';
|
||||||
|
|
||||||
|
import arc from '@/assets/js/svg-arc';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
|
||||||
|
elSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
|
||||||
|
currAngle: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
startAngle: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
type: Number,
|
||||||
|
default: 150,
|
||||||
|
},
|
||||||
|
|
||||||
|
thickness: {
|
||||||
|
type: Number,
|
||||||
|
default: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dInner: '',
|
||||||
|
dOuter: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const halfSize = computed(() => {
|
||||||
|
return (props.elSize / 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 角度变化时更新状态
|
||||||
|
watch(() => (props.currAngle), () => {
|
||||||
|
updateCircle();
|
||||||
|
});
|
||||||
|
watch(() => (props.startAngle), () => {
|
||||||
|
updateCircle();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 更新圆环状态 */
|
||||||
|
function updateCircle() {
|
||||||
|
|
||||||
|
const { currAngle, startAngle, thickness } = props;
|
||||||
|
|
||||||
|
const radius = halfSize.value;
|
||||||
|
const size = thickness;
|
||||||
|
const offset = size / 4;
|
||||||
|
|
||||||
|
const endAngleInner = Math.max(0, currAngle - 360) + startAngle;
|
||||||
|
const endAngleOuter = Math.min(360, currAngle) + startAngle;
|
||||||
|
|
||||||
|
// 内层圆环
|
||||||
|
state.dInner = arc({
|
||||||
|
x: radius,
|
||||||
|
y: radius,
|
||||||
|
R: radius - size * 2 - offset,
|
||||||
|
r: radius - size - offset,
|
||||||
|
start: startAngle,
|
||||||
|
end: endAngleInner,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 外层圆环
|
||||||
|
state.dOuter = arc({
|
||||||
|
x: radius,
|
||||||
|
y: radius,
|
||||||
|
R: radius - size,
|
||||||
|
r: radius,
|
||||||
|
start: startAngle,
|
||||||
|
end: endAngleOuter,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateCircle();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.clock-color {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.color-circle {
|
||||||
|
filter: brightness(1);
|
||||||
|
transition: filter 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faded {
|
||||||
|
filter: brightness(0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
603
src/views/ToolboxView/Other/GenshinImpactClock/ClockElement.vue
Normal file
603
src/views/ToolboxView/Other/GenshinImpactClock/ClockElement.vue
Normal file
@@ -0,0 +1,603 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="clockElement"
|
||||||
|
:class="{
|
||||||
|
'clock-element': true,
|
||||||
|
'clock-rotation': clockState.isRotation,
|
||||||
|
}"
|
||||||
|
:style="elStyle"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 外部 -->
|
||||||
|
<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"
|
||||||
|
@mousedown="handleDragPointer('upper')"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表盘 -->
|
||||||
|
<div class="clock-dial bg-contain">
|
||||||
|
<div class="time-icons">
|
||||||
|
<div class="time-morning bg-contain"></div>
|
||||||
|
<div class="time-noon bg-contain"></div>
|
||||||
|
<div class="time-dusk bg-contain"></div>
|
||||||
|
<div class="time-night bg-contain"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
onBeforeUnmount, onMounted,
|
||||||
|
reactive, ref,
|
||||||
|
} 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,
|
||||||
|
} 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: 60,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 上层指针状态 */
|
||||||
|
const upperPointer = reactive({
|
||||||
|
|
||||||
|
/** 是否为第二圈 */
|
||||||
|
isSecond: false,
|
||||||
|
|
||||||
|
/** 实际角度 */
|
||||||
|
dataAngle: 0,
|
||||||
|
|
||||||
|
/** 视图角度 */
|
||||||
|
viewAngle: 60,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 元素 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-upper-angle': `${upperPointer.viewAngle}deg`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 上层指针实际已旋转角度 */
|
||||||
|
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;
|
||||||
|
|
||||||
|
document.onmousemove = 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;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onmouseup = function () {
|
||||||
|
document.onmousemove = null;
|
||||||
|
document.onmouseup = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确定当前选择的时间,下层指针旋转至上层指针的位置 */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let upperAngleDiff = upperAngleStart - upperAngleCurr;
|
||||||
|
let lowerAngleCurr = lowerAngleStart + upperAngleDiff;
|
||||||
|
|
||||||
|
lowerPointer.viewAngle = lowerAngleCurr % 360;
|
||||||
|
upperPointer.dataAngle = upperAngleCurr % 360;
|
||||||
|
upperPointer.isSecond = (upperAngleCurr >= 360);
|
||||||
|
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
handleSubmitTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 1rem;
|
||||||
|
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 {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
// transition: opacity 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顺时针旋转
|
||||||
|
@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>
|
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div class="genshin-button">
|
||||||
|
<span
|
||||||
|
v-if="hasIcon"
|
||||||
|
class="btn-icon mdi"
|
||||||
|
:class="iconName"
|
||||||
|
:style="{ color: iconColor }"
|
||||||
|
></span>
|
||||||
|
<span class="btn-label">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
|
||||||
|
hasIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
iconColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#FFFFFF',
|
||||||
|
},
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
type: String,
|
||||||
|
default: 'mdi-help',
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.genshin-button {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
background-color: #ECE3D6;
|
||||||
|
color: #494246;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #2D2D2D;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 3.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,9 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tool-detail-page"></div>
|
<div class="tool-detail-page">
|
||||||
|
|
||||||
|
<clock-element ref="clockRef" />
|
||||||
|
|
||||||
|
<!-- <genshin-button
|
||||||
|
icon-color="#F44336"
|
||||||
|
icon-name="mdi-close"
|
||||||
|
>取消</genshin-button> -->
|
||||||
|
|
||||||
|
<genshin-button
|
||||||
|
icon-color="#FFC107"
|
||||||
|
icon-name="mdi-circle-outline"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>确认</genshin-button>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import ClockElement from './ClockElement.vue';
|
||||||
|
import GenshinButton from './GenshinButton.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 时钟元素 ref
|
||||||
|
* @type {VueRef<InstanceType<ClockElement>>}
|
||||||
|
*/
|
||||||
|
const clockRef = ref(null);
|
||||||
|
|
||||||
|
/** 处理点击确认按钮 */
|
||||||
|
function handleConfirm() {
|
||||||
|
|
||||||
|
let el = clockRef.value;
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
el.handleSubmitTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
.tool-detail-page {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
export const IMAGE_BASE = `https://c.frost-zx.top/data/static/image/genshin-impact-clock`;
|
||||||
|
export const IMAGE_CLOCK_BG_INNER = `url("${IMAGE_BASE}/clock_bg_inner.png")`;
|
||||||
|
export const IMAGE_CLOCK_BG_OUTER = `url("${IMAGE_BASE}/clock_bg_outer.png")`;
|
||||||
|
export const IMAGE_CLOCK_DIAL = `url("${IMAGE_BASE}/clock_dial.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_1 = `url("${IMAGE_BASE}/clock_gear_1.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_4 = `url("${IMAGE_BASE}/clock_gear_4.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_5 = `url("${IMAGE_BASE}/clock_gear_5.png")`;
|
||||||
|
export const IMAGE_CLOCK_GEAR_6 = `url("${IMAGE_BASE}/clock_gear_6.png")`;
|
||||||
|
export const IMAGE_CLOCK_PARTICLES = `url("${IMAGE_BASE}/clock_particles.gif")`;
|
||||||
|
export const IMAGE_POINTER_LOWER = `url("${IMAGE_BASE}/pointer_lower.png")`;
|
||||||
|
export const IMAGE_POINTER_UPPER = `url("${IMAGE_BASE}/pointer_upper.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_DUSK = `url("${IMAGE_BASE}/time_icon_dusk.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_MORNING = `url("${IMAGE_BASE}/time_icon_morning.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_NIGHT = `url("${IMAGE_BASE}/time_icon_night.png")`;
|
||||||
|
export const IMAGE_TIME_ICON_NOON = `url("${IMAGE_BASE}/time_icon_noon.png")`;
|
||||||
|
export const IMAGE_TIME_ZONE_COLOR = `${IMAGE_BASE}/time_zone_color.png`;
|
Reference in New Issue
Block a user