玩原神学编程-原神时钟

前言

最近喜欢玩原神这种开放世界探索的游戏(还有黑神话、古墓丽影等),只能说纳塔版本的boss盾真的厚,萌新的我去打boss,从白天打到黑夜,黑夜再打到白天(游戏里面的时间)。

闲话结束,进入正题…

说到游戏时间,原神里面有一个可以玩家自己调节时间的时钟,看着挺不错的,所以由“钟(感)”而发,利用JavaScript复刻一下。

原身神时钟

准备工作

前端框架:Vue3

动画库:GSAP(主要是为了方便统一处理时间线)

素材:https://github.com/Mashiro-Sorata/GenshinClock

复刻思路

搭建场景

这里先堆一下素材,他们的位置给相对固定住,然后确保旋转轴是素材的中心点位置,具体的位置自行调整

堆素材

对于样式,要解决的是径向渐变问题,先利用mask-image遮罩一层,然后给clock_TimeZone定义一个样式变量(直接用 :style绑定也是可以的,看个人喜欢),用来径向渐变的效果。

未使用mask-image遮罩
未使用遮罩

使用mask-image遮罩
使用遮罩

.clock_TimeZone{background: url("images/UI_Clock_TimeZoneColor.png") no-repeat; mask-image: url("images/UI_Clock_TimeZone.png");/*把图片background遮罩在UI_Clock_TimeZone内*/mask-size: cover;background-size: 100%;
}
.clock_TimeZone::after{position: absolute;content: '';/*定义一个样式变量,用来径向渐变的效果*/background: conic-gradient(from var(--start-value), #00bebe 0deg var(--mask-angle) ,#000000 0deg 360deg);top: 0;left: 0;right: 0;bottom: 0;
}

之所以要用–start-value和–mask-angle两个变量,是因为要确保–start-value增加的同时,–mask-angle要减少,才能保证结束位置的固定。

只有–start-value的时候

单限制

–start-value和–mask-angle都有的时候

双限制

齿轮旋转

接下来先处理齿轮的旋转,这里需要处理的是horoscope03、horoscope04、horoscope05、horoscope051、horoscope061这几个齿轮,给他们的style绑定上旋转属性。

  <label class="clock_unit_mask_wrapper clock_horoscope03" :style="{ rotate: `${horoscope03}deg` }" /><label class="center center_90 clock_horoscope04" :style="{ rotate: `${horoscope04}deg` }" /><div class="center center_35"><label class="center-clock clock_horoscope05_1" :style="{ rotate: `${horoscope051}deg` }" /></div><div class="center center_50"><label class="center-clock clock_horoscope05" :style="{ rotate: `${horoscope05}deg` }" /></div><div class="center"><label class="center-clock clock_horoscope06" /></div><div class="center"><label class="center-clock clock_horoscope06 clock_horoscope06_1" :style="{ rotate: `${horoscope061}deg` }" /></div><label class="timeZone_wrapper clock_TimeZone" /><label class="timeZone_wrapper clock_TimeZone clock_TimeZone_1" />
</div>

这里之所以要用GSAP,主要是因为,鼠标旋转指针时,所有的齿轮速度是要变化的,原本我也想直接用css的animated来处理就好,但会发现,每次旋转指针齿轮动画都会重新执行

const horoscope03 = ref(0);
const horoscope04 = ref(0);
const horoscope05 = ref(0);
const horoscope051 = ref(0);
const horoscope061 = ref(0);gsap.to(horoscope03,{ value: -360,duration: 40,repeat: -1,ease: 'none' });
gsap.to(horoscope04,{ value: -360,duration: 40,repeat: -1,ease: 'none' });
gsap.to(horoscope05,{ value: 360,duration: 20,repeat: -1,ease: 'none' });
gsap.to(horoscope051,{ value: 360,duration: 30,repeat: -1,ease: 'none' });
gsap.to(horoscope061,{ value: -360,duration: 30,repeat: -1,ease: 'none' });// ..........
gsap.globalTimeline.timeScale(toothedGearRotationSpeed.value); // toothedGearRotationSpeed旋转速度

添加鼠标事件

通过转换鼠标位置信息,实现时针的角度旋转

<labelref="rotatableElement"class="clock_unit clock_hourHand":style="{ rotate: `${rotation}deg` }"@mousedown.self="startRotate"@mousemove.self="rotate"@mouseleave.self="stopRotate"@mouseup.self="stopRotate"
/>

记录开始的位置信息

function startRotate(event: MouseEvent):void {event.stopPropagation();event.preventDefault();rotating.value = true;const target = event.target as HTMLDivElement;const elRect = target?.getBoundingClientRect();startX.value = elRect.left + elRect.width / 2;startY.value = elRect.top + elRect.height / 2;initRotation.value = rotation.value;
}

把位置差转换成角度把范围控制在[0, 360]之间。

function rotate(event: MouseEvent):void {if (!rotating.value) return;const deltaX = event.clientX - startX.value;const deltaY = event.clientY - startY.value;let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI) + 90; // 把范围控制在[0,360]之间if (angle < 0) {angle += 360;}// ......// }

放开鼠标后,记录位置信息,由于指针的位置,并不是每次都是从0位置开始的,就要记录当前角度。

function stopRotate():void {rotating.value = false;// 记录遮罩角度和结束角度initMaskAngle.value = rotation.value - initStartAngle.value > 0? rotation.value - initStartAngle.value: 360 - initStartAngle.value + rotation.value;
// 结束的位置,让初始的位置,不断逼近它,即执行动画endRotation.value = initMaskAngle.value + getCurrentAngle.hoursAngle;// ......
}

解决旋转问题

这里要解决几个问题

1、如何知道是正向旋转,还是逆向旋转?

2、如何知道是正向旋转多少圈,还是逆向旋转多少圈?

3、如何执行动画?

正向和逆向

如果用小于或大于当前位置判断方向

那么假设(前面通过把角度范围控制在[0, 360]之间):

指针初始位置是30°,当前位置角度>30°是顺时针,当前位置角度<30°是逆时针,那么如果当角度为0°时,下一个值将是360°>30°,就变成了顺时针,与实际相违背。

所有这里想到的做法是利用扇区来区分,即把一个圆分成四份

扇区

[0,90]->第一扇区、[90,180]->第二扇区、[180,270]->第三扇区、[270,360]->第四扇区,然后记录扇区的前后关系,即可知道是正向或逆向,例如30°,在第一扇区,前一个扇区是第二扇区,后一个扇区是第四扇区,逆向的情况就是大扇区向小扇区逼近,如果是在第一扇区,逆向是第四扇区,特殊处理一下就好。

/*** 保存当前位置信息* @param e*/
function mousePos(e:MouseEvent){if (e.pageX || e.pageY) {return { x: e.pageX, y: e.pageY };}return {x: e.clientX + document.body.scrollLeft - document.body.clientLeft,y: e.clientY + document.body.scrollTop - document.body.clientTop};
}
/*** 获取当前扇区和判断顺、逆时针* @param e* @param angle*/
function getAreaSection(e:MouseEvent,angle: number){let prePos = null;if (movePosArr.value.length > 0) {prePos = movePosArr.value[movePosArr.value.length - 1];}// 记录最新的位置curPos.value = mousePos(e);movePosArr.value[movePosArr.value.length] = curPos.value;if (prePos){if (angle >= 0 && angle < 90){// 右上扇区areaSection.value = 1; // 定义扇区值if (prePos.x < curPos.value.x && prePos.y < curPos.value.y){ // 顺时针isClock.value = true;}else if (prePos.x > curPos.value.x && prePos.y > curPos.value.y){  // 逆时针isClock.value = false;}}else if (angle >= 90 && angle < 180){// 右下扇区areaSection.value = 2;if (prePos.x > curPos.value.x && prePos.y < curPos.value.y) {isClock.value = true;}else if (prePos.x < curPos.value.x && prePos.y > curPos.value.y){isClock.value = false}}else if (angle >= 180 && angle < 270){// 左下扇区areaSection.value = 3;if (prePos.x > curPos.value.x && prePos.y > curPos.value.y){isClock.value = true;}else if(prePos.x < curPos.value.x && prePos.y < curPos.value.y){isClock.value = false;}}else if (angle >= 270 && angle < 360) {// 左上扇区areaSection.value = 4;if (prePos.x < curPos.value.x && prePos.y > curPos.value.y){isClock.value = true;}else if (prePos.x > curPos.value.x && prePos.y < curPos.value.y){isClock.value = false;}}}
}

记录默认位置的扇区

/*** 获取默认扇区* @param angle*/
function getInitSection(angle: number){let section = 0if (angle >= 0 && angle < 90){section = 1}else if (angle >= 90 && angle < 180){section = 2}else if (angle >= 180 && angle < 270){section = 3}else if (angle >= 270 && angle < 360){section = 4}let pre = 0; // 前一个扇区let next = 0; // 后一个扇区switch (section) {case 1: pre = 4; next = 2; break;case 2: pre = 1; next = 3; break;case 3: pre = 2; next = 4; break;case 4: pre = 3; next = 1; break;}return { section, pre, next }
}

圈数问题

上面已经解决正向和逆向和扇区问题,那么接下来要解决的是圈数问题,这里想到的是用步数来记录更细的数据,例如step = 1,即经过了一个扇区,step = 3(一圈),step = 6(两圈)

圈数限制

// 计算步数
watch(areaSection,(value,oldValue) => {if (value - oldValue > 0){step.value++;}else {step.value--;}if (value === 1 && oldValue === 4){step.value++; // 从第四过渡到第一扇区 ++}if (value === 4 && oldValue === 1){step.value--; // 从第一过渡到第四扇区 --}
});

执行动画

在requestAnimationFrame循环里执行动画,让initStartAngle.value不断逼近 endRotation.value

执行动画

function render(){isChange.value && animateAngle();requestAnimationFrame(() => {render();})
}function animateAngle(){update();gsap.globalTimeline.timeScale(toothedGearRotationSpeed.value);minutesAngle.value = initStartAngle.value // 指针位置if (isRotationAngle.value){ // 超出一圈时的处理if (initStartAngle.value < endRotation.value){document.documentElement.style.setProperty('--start-angle', `${initStartAngle.value += 1}deg`);document.documentElement.style.setProperty('--mask-angle1', `${initMaskAngle.value -= 1}deg`); // 遮罩角度}else {isRotationAngle.value = false;initMaskAngle.value = 360;initStartAngle.value = rotation.value;endRotation.value = 360 + rotation.value;document.documentElement.style.setProperty('--mask-angle1', `0deg`);}}else { // 一圈内的处理if (initStartAngle.value < endRotation.value) {document.documentElement.style.setProperty('--start-angle', `${initStartAngle.value += 1}deg`);document.documentElement.style.setProperty('--mask-angle', `${initMaskAngle.value -= 1}deg`); // 遮罩角度} else {document.documentElement.style.setProperty('--mask-angle', `0deg`);document.documentElement.style.setProperty('--mask-angle1', `0deg`);gsap.globalTimeline.timeScale(1); // 动画播放速度setTimeout(() => {isChange.value = false;handleClose();},500)}}
}

边界值处理

处理边界

// 在起点,既步数step=0时,不允许逆时针旋转的判断
if (initSection.section === 4){if (step.value <= 0){if (angle < time && angle > 180){return;}}
}
if (initSection.section === 1){if (step.value <= 0){if ((angle < time && angle > 0) || (angle > time && angle >= 180)){return;}}
}
if (initSection.section === 2 || initSection.section === 3){if (step.value <= 0){if (angle < time && angle > 0){return;}}
}if (step.value > 6){return;
}
// 在终点,既步数step=6时,不允许逆时针旋转的判断
if (step.value === 6){if (initSection.section === 4){ // 判断[0,135)的阈值if (angle >= 0 && angle < 135){return;}}if (initSection.section === 1){if (angle > 0 && angle < 180 && angle > time){return}}if (initSection.section !== 1){if (angle > time){return;}}
}

结语

完整代码

<!--时钟-->
<template><div class="clock-container"><div class="clock-header" @click="handleClose">返回</div><div class="clock-container_wrapper"><div class="clock-con_left" /><div class="clock-con_right"><label class="clock_unit clock_bg" /><div class="clock_unit_mask"><div class="clock_unit_mask_wrapper"><label class="clock_unit_mask_wrapper clock_hbg" /><label class="clock_unit_mask_wrapper clock_horoscope03" :style="{ rotate: `${horoscope03}deg` }" /><label class="center center_90 clock_horoscope04" :style="{ rotate: `${horoscope04}deg` }" /><div class="center center_35"><label class="center-clock clock_horoscope05_1" :style="{ rotate: `${horoscope051}deg` }" /></div><div class="center center_50"><label class="center-clock clock_horoscope05" :style="{ rotate: `${horoscope05}deg` }" /></div><div class="center"><label class="center-clock clock_horoscope06" /></div><div class="center"><label class="center-clock clock_horoscope06 clock_horoscope06_1" :style="{ rotate: `${horoscope061}deg` }" /></div><label class="timeZone_wrapper clock_TimeZone" /><label class="timeZone_wrapper clock_TimeZone clock_TimeZone_1" /></div></div><label class="clock_unit clock_dial" /><label class="clock_unit star_particles" /><label v-show="(270<=time && time <=360) || (time >=0 && time <= 90)" class="noon_state noon" /><label v-show="time>=0 && time <= 180" class="sun_state dusk" /><label v-show="time>=180 && time <= 360" class="sun_state morning" /><label v-show="time>=90 && time <= 270" class="noon_state night" /><label class="clock_unit clock_minuteHand" :style="{ rotate: `${minutesAngle}deg` }" /><labelref="rotatableElement"class="clock_unit clock_hourHand":style="{ rotate: `${rotation}deg` }"@mousedown.self="startRotate"@mousemove.self="rotate"@mouseleave.self="stopRotate"@mouseup.self="stopRotate"/><div class="clock-btn" @click="handleStartToEndAngle">确定</div></div></div></div>
</template><script setup lang="ts">
import { ref, watch, defineEmits } from 'vue'
import gsap from 'gsap'interface posType{x:number,y:number
}const emits = defineEmits(['update','close']); // 更新和关闭的emit
const getCurrentAngle = getCurrentTimeAngles(new Date());// 默认角度
const initSection = getInitSection(getCurrentAngle.hoursAngle); // 默认扇区
const time = getCurrentAngle.hoursAngle;
document.documentElement.style.setProperty('--start-angle', `${getCurrentAngle.hoursAngle}deg`); // 获取css变量const toothedGearRotationSpeed = ref(8); // 齿轮旋转速度
const rotation = ref(getCurrentAngle.hoursAngle); // 旋转角度
const minutesAngle = ref(getCurrentAngle.hoursAngle); // 分钟旋转角度
const startX = ref(0); // 开始X位置
const startY = ref(0); // 开始Y位置
const initRotation = ref(0); // 保存初始位置
const endRotation = ref(0); // 保存结束位置
const maskAngle = ref(-1); // 遮罩位置
const initStartAngle  = ref(Number(getCurrentAngle.hoursAngle)); // 保存初始开始角度
const initMaskAngle  = ref(0); // 保存mask角度
const rotating = ref(false); // 旋转状态
const isChange = ref(false); // 点击确定状态const movePosArr = ref < Array<any>>([]); // 保存经过的位置
const curPos = ref<posType | null>({ x: 0,y: 0 }); // 当前位置
// const clockwiseArrSection = ref([0,0,0,0]); // 顺时钟扇区
// const anticlockwiseArrSection = ref([0,0,0,0]); // 逆时钟扇区
// const cumulate = ref(0); // 圈数
const step = ref(0); // 经过步数(区分顺、逆时针)
const areaSection = ref(initSection.section); // 当前扇区
const isClock = ref(true); // 是否逆时针
const isRotationAngle = ref(false); // 是否完整一圈
// const isShowTitle = ref(false); // 是否显示提示const horoscope03 = ref(0);
const horoscope04 = ref(0);
const horoscope05 = ref(0);
const horoscope051 = ref(0);
const horoscope061 = ref(0);gsap.to(horoscope03,{ value: -360,duration: 40,repeat: -1,ease: 'none' });
gsap.to(horoscope04,{ value: -360,duration: 40,repeat: -1,ease: 'none' });
gsap.to(horoscope05,{ value: 360,duration: 20,repeat: -1,ease: 'none' });
gsap.to(horoscope051,{ value: 360,duration: 30,repeat: -1,ease: 'none' });
gsap.to(horoscope061,{ value: -360,duration: 30,repeat: -1,ease: 'none' });watch(maskAngle,(value,) => {// console.log(cumulate.value)// 如果在第四扇区,并且即将跨越到第一扇区,清除一下缓存if (areaSection.value === 4 && value > 340){clearState()}
});
// 计算步数
watch(areaSection,(value,oldValue) => {if (value - oldValue > 0){step.value++;}else {step.value--;}if (value === 1 && oldValue === 4){step.value++; // 从第四过渡到第一扇区 ++}if (value === 4 && oldValue === 1){step.value--; // 从第一过渡到第四扇区 --}
});function startRotate(event: MouseEvent):void {event.stopPropagation();event.preventDefault();rotating.value = true;// isClock.value = true;const target = event.target as HTMLDivElement;const elRect = target?.getBoundingClientRect();startX.value = elRect.left + elRect.width / 2;startY.value = elRect.top + elRect.height / 2;initRotation.value = rotation.value;
}function rotate(event: MouseEvent):void {if (!rotating.value) return;const deltaX = event.clientX - startX.value;const deltaY = event.clientY - startY.value;let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI) + 90;if (angle < 0) {angle += 360;}calculateSector(event,angle);if (step.value < 0){return;}// 在起点,既步数step=0时,不允许逆时针旋转的判断if (initSection.section === 4){if (step.value <= 0){if (angle < time && angle > 180){return;}}}if (initSection.section === 1){if (step.value <= 0){if ((angle < time && angle > 0) || (angle > time && angle >= 180)){return;}}}if (initSection.section === 2 || initSection.section === 3){if (step.value <= 0){if (angle < time && angle > 0){return;}}}if (step.value > 6){return;}// 在终点,既步数step=6时,不允许逆时针旋转的判断if (step.value === 6){if (initSection.section === 4){ // 判断[0,135)的阈值if (angle >= 0 && angle < 135){return;}}if (initSection.section === 1){if (angle > 0 && angle < 180 && angle > time){return}}if (initSection.section !== 1){if (angle > time){return;}}}rotation.value = angle;gsap.globalTimeline.timeScale(toothedGearRotationSpeed.value);const mask = angle - time > 0 ? angle - time : 360 - time + angle;  // 计算遮罩位置是从初始开始,而不是从0开始maskAngle.value = mask;isRotationAngle.value = getOneRotation(step.value, time, angle, initSection.section)if (isRotationAngle.value){ // 到达一圈后的计算document.documentElement.style.setProperty('--mask-angle1', `${mask}deg`);document.documentElement.style.setProperty('--mask-angle', `3600deg`);}else {document.documentElement.style.setProperty('--mask-angle1', `0deg`);document.documentElement.style.setProperty('--mask-angle', `${mask}deg`);}}function stopRotate():void {rotating.value = false;// 记录遮罩角度和结束角度initMaskAngle.value = rotation.value - initStartAngle.value > 0? rotation.value - initStartAngle.value: 360 - initStartAngle.value + rotation.value;// 结束的位置,让初始的位置,不断逼近它,即执行动画endRotation.value = initMaskAngle.value + getCurrentAngle.hoursAngle;step.value =  step.value < 0 ? 0 :  step.value;gsap.globalTimeline.timeScale(1);
}function handleStartToEndAngle():void{if(isChange.value) return;isChange.value = true;
}function handleClose(): void {if(isChange.value) return;clear();close();
}/*** 把值传递出去*/
function update(){emits('update',initStartAngle.value)
}function close(){emits('close',false)
}function animateAngle(){update();gsap.globalTimeline.timeScale(toothedGearRotationSpeed.value);minutesAngle.value = initStartAngle.value // 指针位置if (isRotationAngle.value){ // 超出一圈时的处理if (initStartAngle.value < endRotation.value){document.documentElement.style.setProperty('--start-angle', `${initStartAngle.value += 1}deg`);document.documentElement.style.setProperty('--mask-angle1', `${initMaskAngle.value -= 1}deg`); // 遮罩角度}else {isRotationAngle.value = false;initMaskAngle.value = 360;initStartAngle.value = rotation.value;endRotation.value = 360 + rotation.value;document.documentElement.style.setProperty('--mask-angle1', `0deg`);}}else { // 一圈内的处理if (initStartAngle.value < endRotation.value) {document.documentElement.style.setProperty('--start-angle', `${initStartAngle.value += 1}deg`);document.documentElement.style.setProperty('--mask-angle', `${initMaskAngle.value -= 1}deg`); // 遮罩角度} else {document.documentElement.style.setProperty('--mask-angle', `0deg`);document.documentElement.style.setProperty('--mask-angle1', `0deg`);gsap.globalTimeline.timeScale(1); // 动画播放速度setTimeout(() => {isChange.value = false;handleClose();},500)}}
}
render();/*** 更新步数*/
function render(){isChange.value && animateAngle();requestAnimationFrame(() => {render();})
}/*** 获取当前扇区和判断顺、逆时针* @param e* @param angle*/
function getAreaSection(e:MouseEvent,angle: number){let prePos = null;if (movePosArr.value.length > 0) {prePos = movePosArr.value[movePosArr.value.length - 1];}// 记录最新的位置curPos.value = mousePos(e);movePosArr.value[movePosArr.value.length] = curPos.value;if (prePos){if (angle >= 0 && angle < 90){// 右上扇区areaSection.value = 1;if (prePos.x < curPos.value.x && prePos.y < curPos.value.y){ // 顺时针isClock.value = true;// clockwiseArrSection.value[0] = 1;}else if (prePos.x > curPos.value.x && prePos.y > curPos.value.y){  // 逆时针// anticlockwiseArrSection.value[0] = 1;isClock.value = false;}}else if (angle >= 90 && angle < 180){// 右下扇区areaSection.value = 2;if (prePos.x > curPos.value.x && prePos.y < curPos.value.y) {isClock.value = true;// clockwiseArrSection.value[1] = 1;}else if (prePos.x < curPos.value.x && prePos.y > curPos.value.y){isClock.value = false// anticlockwiseArrSection.value[1] = 1;}}else if (angle >= 180 && angle < 270){// 左下扇区areaSection.value = 3;if (prePos.x > curPos.value.x && prePos.y > curPos.value.y){isClock.value = true;// clockwiseArrSection.value[2] = 1;}else if(prePos.x < curPos.value.x && prePos.y < curPos.value.y){isClock.value = false;// anticlockwiseArrSection.value[2] = 1;}}else if (angle >= 270 && angle < 360) {// 左上扇区areaSection.value = 4;if (prePos.x < curPos.value.x && prePos.y > curPos.value.y){isClock.value = true;// clockwiseArrSection.value[3] = 1;}else if (prePos.x > curPos.value.x && prePos.y < curPos.value.y){isClock.value = false;// anticlockwiseArrSection.value[3] = 1;}}}
}/*** 计算扇区位置* @param e* @param angle*/
function calculateSector(e:MouseEvent,angle: number){getAreaSection(e,angle)// 记录圈数// if (areaSection.value){//   let clockSections = 0;//   let antiClockSections = 0;//   for (let i = 0; i < clockwiseArrSection.value.length; i++) {//     if (clockwiseArrSection.value[i] === 1) {//       clockSections += 1;//     }//     if (anticlockwiseArrSection.value[i] === 1) {//       antiClockSections += 1;//     }//   }//   if (clockSections === 4) {//     // 计算顺时针是否为结束扇区的闭合点//     cumulate.value += 1;//     clearState();//   }////   if (antiClockSections === 4){ // 计算逆时针是否为结束扇区的闭合点//     if (cumulate.value === 0)return//     cumulate.value -= 1;//     clearState();//   }// }
}/*** 清除扇区缓存*/
function clearState(){movePosArr.value = [];curPos.value = null;clearArrSection()
}function clearArrSection(){// for (let i = 0; i < clockwiseArrSection.value.length; i++) {//   clockwiseArrSection.value[i] = 0;//   anticlockwiseArrSection.value[i] = 0;// }
}/*** 为了避免问题关闭窗口,重置参数*/
function clear(){gsap.killTweensOf(horoscope03);gsap.killTweensOf(horoscope04);gsap.killTweensOf(horoscope05);gsap.killTweensOf(horoscope051);gsap.killTweensOf(horoscope061);
}
/*** 保存当前位置信息* @param e*/
function mousePos(e:MouseEvent){if (e.pageX || e.pageY) {return { x: e.pageX, y: e.pageY };}return {x: e.clientX + document.body.scrollLeft - document.body.clientLeft,y: e.clientY + document.body.scrollTop - document.body.clientTop};
}/*** 获取是否到达一圈* @param step 顺时针经过的扇区数* @param initAngle 默认角度* @param angle 滑动角度* @param section 当前所处扇区*/
function getOneRotation(step:number,initAngle:number,angle:number,section:number){if (step > 3){return  true;}if (step === 3){if (section === 1){ // 处于第一扇区时,判断[0,90)区间return angle > initAngle && angle >= 0 && angle < 90;}if (section === 2 || section === 3){return angle > initAngle;}if (section === 4){ // 处于第四扇区时,当前角度大于或跨越到第一扇区,判断[0 135]区间的阈值return (angle > initAngle && angle >= 270 && angle < 360) || (angle >= 0 && angle < 135)}}return false;
}
/*** 获取当前时间的角度* @param time*/
function getCurrentTimeAngles(time: Date = new Date()) {const now = time;const hours = now.getHours();const minutes = now.getMinutes();const hoursAngle = hours * 15; // 0度为12点const minutesAngle = minutes * 6;let endHoursAngle = hoursAngle < 180 ? 180 + hoursAngle : hoursAngle - 180return { hoursAngle: endHoursAngle, minutesAngle };
}/*** 获取默认扇区* @param angle*/
function getInitSection(angle: number){let section = 0if (angle >= 0 && angle < 90){section = 1}else if (angle >= 90 && angle < 180){section = 2}else if (angle >= 180 && angle < 270){section = 3}else if (angle >= 270 && angle < 360){section = 4}let pre = 0; // 前一个扇区let next = 0; // 后一个扇区switch (section) {case 1: pre = 4; next = 2; break;case 2: pre = 1; next = 3; break;case 3: pre = 2; next = 4; break;case 4: pre = 3; next = 1; break;}return { section, pre, next }
}
</script><style scoped lang="less">
.clock-container{position: fixed;width: 100%;height: 100%;top: 0;left: 0;z-index: 9;background: linear-gradient( -90deg,#000000, 50%, transparent);
}.clock-header{text-align: right;width: 100%;padding: 20px;box-sizing: border-box;
}
.clock-container_wrapper{position: relative;transform: translate(-50%,-50%);top: 40%;left: 70%;width: 700px;height: 400px;display: flex;justify-items: center;justify-content: space-between;text-align: center;
}
.clock-con_left{flex: 1;
}
.clock-con_right{width: 400px;height: 100%;position: relative;
}.clock_unit{position: absolute;width: 100%;height: 100%;left: 0;top: 0;text-align: center;
}.star_particles{background: url("images/star_particles.gif") no-repeat center;background-size: 45%;
}.clock_bg{background-image: url("images/Clock_BG.png");background-size: 100%, 100%;
}.clock_dial{background: url("images/UI_Clock_Dial.png") no-repeat center;background-size: 86%;
}.clock_hbg{background: url("images/UI_Img_HoroscopeBg.png");background-size: 100%, 100%;
}.clock_unit_mask{position: relative;transform: translate(-50%,-50%);width: 45%;height: 45%;left: 50%;top: 50%;text-align: center;border-radius: 50%;overflow: hidden;
}
.clock_unit_mask_wrapper{position: absolute;width: 100%;height: 100%;left: 0;top: 0;
}.timeZone_wrapper{position: absolute;width: 100%;height: 100%;left: 50%;top: 50%;transform: translate(-50%,-50%);
}.center{position: absolute;transform: translate(-50%,-50%);top: 50%;left: 50%;width: 25%;height: 25%;
}.center-clock{position: absolute;left: 0;top: 0;width: 100%;height: 100%;
}
.center_35{width: 35%;height: 35%;opacity: 0.6;
}
.center_50{width: 50%;height: 50%;
}
.center_90{width: 90%;height: 90%;
}.clock_horoscope03{background: url("images/UI_Img_Horoscope03.png") no-repeat;background-size: 100%;left: 5%;top: -5%;//animation: rotationUp 40s linear infinite;
}.clock_horoscope04{background: url("images/UI_Img_Horoscope04.png") no-repeat;transform: rotate(40deg);background-size: 100%;left: -12%;top: 15%;//animation: rotationUp 40s linear infinite;
}.clock_horoscope05{background: url("images/UI_Img_Horoscope05.png") no-repeat;background-size: 100%;left: 94%;top: 60%;//animation: rotationDown 20s linear infinite;
}.clock_horoscope05_1{background: url("images/UI_Img_Horoscope05.png") no-repeat;background-size: 100%;left: 140%;top: 95%;//animation: rotationDown 30s linear infinite;
}.clock_horoscope06{background: url("images/UI_Img_Horoscope06.png") no-repeat;background-size: 100%;
}
.clock_horoscope06_1{top: -190%;left: 80%;//animation: rotationUp 30s linear infinite;
}.sun_state{position: absolute;transform: translate(-50%,-50%);top: 50%;left: 50%;width: 16%;height: 98%;
}
.noon_state{position: absolute;transform: translate(-50%,-50%);top: 50%;left: 50%;width: 85%;height: 16%;
}
.morning{background: url("images/UI_ClockIcon_Morning.png") no-repeat;background-size: 100%;left: 14%;
}
.dusk{background: url("images/UI_ClockIcon_Dusk.png") no-repeat;background-size: 100%;left: 86%;
}
.night{background: url("images/UI_ClockIcon_Night.png") no-repeat;background-size: 100%;top: 87%;
}
.noon{background: url("images/UI_ClockIcon_Noon.png") no-repeat;background-size: 100%;top: 15%;
}.clock_hourHand{background: url("images/UI_Clock_HourHand.png");background-size: 100%, 100%;
}.clock_minuteHand{background: url("images/UI_Clock_MinuteHand.png");background-size: 100%, 100%;transform: rotate(180deg);
}.clock_TimeZone{background: url("images/UI_Clock_TimeZoneColor.png") no-repeat;mask-image: url("images/UI_Clock_TimeZone.png");mask-size: cover;background-size: 100%;
}
.clock_TimeZone::after{position: absolute;content: '';background: conic-gradient(from var(--start-angle), transparent 0deg var(--mask-angle) ,rgba(0,0,0,0.8) 0deg 360deg);top: 0;left: 0;right: 0;bottom: 0;
}.clock_TimeZone_1{width: 95%;height: 95%;background-size: 100%;
}
.clock_TimeZone_1::after{background: conic-gradient(from var(--start-angle), transparent 0deg var(--mask-angle1) ,rgba(0,0,0,0.8) 0deg 360deg);
}.clock-btn{position: absolute;transform: translate(-50%,-50%);top: 110%;left: 50%;padding: 8px 50px;color: white;border-radius: 4px;background: #262626;border: 1px solid #3c3c3c;cursor: pointer;&:hover{background: #3c3c3c;}
}@keyframes rotationDown {0%{rotate: 0deg;}100%{rotate: calc(var(--rotate-step)* 360deg);}
}
@keyframes rotationUp {0%{rotate: 0deg;}100%{rotate: calc(var(--rotate-step) * -360deg);}
}</style>

加上场景试试
配合场景

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/65368.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习之PCA降维

主成分分析&#xff08;PCA&#xff0c;Principal Component Analysis&#xff09; 主成分分析&#xff08;PCA&#xff09;是一种常见的无监督学习技术&#xff0c;广泛应用于数据降维、数据可视化以及特征提取等任务。PCA的目标是通过线性变换将数据从高维空间映射到低维空间…

Mysql进阶SQL优化

SQL优化在开发场景中必不可少的技能之一&#xff0c;它能最大限度的提升SQL查询性能&#xff0c;如果随意使用也会出现不可预料的结局。 1、为什么要优化SQL 我们先说说不优化SQL造成什么现象。常见问题是响应时间长&#xff0c;用户体验感低。数据库频繁争抢锁&#xff0c;浪…

修改成清华镜像源解决Anaconda报The channel is not accessible源通道不可用问题

修改成清华镜像源解决Anaconda报The channel is not accessible源通道不可用问题 最近在通过pycharm开发python程序&#xff0c;引用anaconda环境建立虚拟环境时报错&#xff0c;报UnavailableInvalidChannel: The channel is not accessible or is invalid.应该是镜像源访问通…

Selenium+Java(21):Jenkins发送邮件报错Not sent to the following valid addresses解决方案

问题现象 小月妹妹近期在做RobotFrameWork自动化测试,并且使用Jenkins发送测试邮件的时候,发现报错Not sent to the following valid addresses,明明各个配置项看起来都没有问题,但是一到邮件发送环节,就是发送不出去,而且还不提示太多有用的信息,急的妹妹脸都红了,于…

Redis6为什么引入了多线程?

大家好&#xff0c;我是锋哥。今天分享关于【Redis6为什么引入了多线程&#xff1f;】面试题。希望对大家有帮助&#xff1b; Redis6为什么引入了多线程&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Redis 6 引入了多线程的主要目的是提高性能&#…

【模块一】kubernetes容器编排进阶实战之kubernetes 资源限制

kubernetes 资源限制 kubernetes中资源限制概括 1.如果运行的容器没有定义资源(memory、CPU)等限制&#xff0c;但是在namespace定义了LimitRange限制&#xff0c;那么该容器会继承LimitRange中的 默认限制。 2.如果namespace没有定义LimitRange限制&#xff0c;那么该容器可…

Rancher V2.9.0 Docker安装教程

1、创建Rancher挂载目录 mkdir -p /home/rancher/k3s/agent/images/ 2、复制容器中的镜像tar包&#xff0c;防止挂载之后镜像包丢失导致创建集群报错 避免&#xff1a;Internal error occurred: failed calling webhook "default.cluster.cluster.x-k8s.io" dock…

CPT203 Software Engineering 软件工程 Pt.1 概论和软件过程(中英双语)

文章目录 1.Introduction1.1 What software engineering is and why it is important&#xff08;什么是软件工程&#xff0c;为什么它很重要&#xff09;1.1 We can’t run the modern world without software&#xff08;我们的世界离不开软件&#xff09;1.1.1 What is Soft…

从 Coding (Jenkinsfile) 到 Docker:全流程自动化部署 Spring Boot 实战指南(简化篇)

前言 本文记录使用 Coding (以 Jenkinsfile 为核心) 和 Docker 部署 Springboot 项目的过程&#xff0c;分享设置细节和一些注意问题。 1. 配置服务器环境 在实施此过程前&#xff0c;确保服务器已配置好 Docker、MySQL 和 Redis&#xff0c;可参考下列链接进行操作&#xff1…

[WASAPI]音频API:从Qt MultipleMedia走到WASAPI,相似与不同

[WASAPI] 从Qt MultipleMedia 来看WASAPI 最近在学习有关Windows上的音频驱动相关的知识&#xff0c;在正式开始说WASAPI之前&#xff0c;我想先说一说Qt的Multiple Media&#xff0c;为什么呢&#xff1f;因为Qt的MultipleMedia实际上是WASAPI的一层封装&#xff0c;它在是线…

绝美的数据处理图-三坐标轴-散点图-堆叠图-数据可视化图

clc clear close all %% 读取数据 load(MyColor.mat) %读取颜色包for iloop 1:25 %提取工作表数据data0(iloop) {readtable(data.xlsx,sheet,iloop)}; end%% 解析数据 countzeros(23,14); for iloop 1:25index(iloop) { cell2mat(table2array(data0{1,iloop}(1,1)))};data(i…

第三百四十六节 JavaFX教程 - JavaFX绑定

JavaFX教程 - JavaFX绑定 JavaFX绑定同步两个值&#xff1a;当因变量更改时&#xff0c;其他变量更改。 要将属性绑定到另一个属性&#xff0c;请调用bind()方法&#xff0c;该方法在一个方向绑定值。例如&#xff0c;当属性A绑定到属性B时&#xff0c;属性B的更改将更新属性A…

详解VHDL如何编写Testbench

1.概述 仿真测试平台文件(Testbench)是可以用来验证所设计的硬件模型正确性的 VHDL模型&#xff0c;它为所测试的元件提供了激励信号&#xff0c;可以以波形的方式显示仿真结果或把测试结果存储到文件中。这里所说的激励信号可以直接集成在测试平台文件中&#xff0c;也可以从…

RNA-Seq 数据集、比对和标准化

RNA-Seq 数据集、比对和标准化|玉米中的元基因调控网络突出了功能上相关的调控相互作用。 RNA-Seq 表达分析代码和数据 该仓库是一个公开可用 RNA-Seq 数据集的集合&#xff08;主要是玉米数据&#xff09;&#xff0c;提供了系统分析这些数据的代码/流程&#xff0c;以及质量…

学技术学英文:Spring AOP和 AspectJ 的关系

AspectJ是AOP领域的江湖一哥&#xff0c; Spring AOP 只是一个小弟 Spring AOP is implemented in pure Java. There is no need for a special compilation process. Spring AOP does not need to control the class loader hierarchy and is thus suitable for use in a ser…

JVM学习-内存结构(二)

一、堆 1.定义 2.堆内存溢出问题 1.演示 -Xmx设置堆大小 3.堆内存的诊断 3.1介绍 1&#xff0c;2都是命令行工具&#xff08;可直接在ideal运行时&#xff0c;在底下打开终端&#xff0c;输入命令&#xff09; 1可以拿到Java进程的进程ID&#xff0c;2 jmap只能查询某一个时…

Browser Use:AI智能体自动化操作浏览器的开源工具

Browser Use:AI智能体自动化操作浏览器的开源工具 Browser Use 简介1. 安装所需依赖2. 生成openai密钥3. 编写代码4. 运行代码5. 部署与优化5.1 部署AI代理5.2 优化与扩展总结Browser Use 简介 browser-use是一个Python库,它能够帮助我们将AI代理与浏览器自动化操作结合起来;…

Spring Cloud——注册中心

介绍 什么是注册中心&#xff1f; 主要负责服务的注册与发现&#xff0c;确保服务之间的通信顺畅&#xff0c;具体来说&#xff0c;注册中心有以下主要功能&#xff1a;‌服务注册、服务发现、服务健康检查。 服务注册&#xff1a; 服务提供者在启动时会向注册中心注册自身服务…

CSS基础入门【2】

目录 一、知识复习 二、权重问题深入 2.1 同一个标签&#xff0c;携带了多个类名&#xff0c;有冲突&#xff1a; 2.2 !important标记 2.3 权重计算的总结 三、盒模型 3.1 盒子中的区域 3.2 认识width、height 3.3 认识padding 3.4 border 作业&#xff1a; 一、知识…

捋一捋相关性运算,以及DTD和NLP中的应用

捋一捋相关性运算&#xff0c;以及DTD和NLP中的应用 相关性和相干性,有木有傻傻分不清相关性数字信号的相关运算同维度信号的相关理解 相关--互相关--相干 回声消除过程如何套用这些知识相关性/相干性检测在DT中的应用时域的标量与向量结合的互相关方法适合block处理的频域相干…