玩原神学编程-原神时钟

前言

最近喜欢玩原神这种开放世界探索的游戏(还有黑神话、古墓丽影等),只能说纳塔版本的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旋转速度

添加鼠标事件

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

<label
  ref="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 = 0
  if (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` }" />
        <label
          ref="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 - 180
  return { hoursAngle: endHoursAngle, minutesAngle };
}

/**
 * 获取默认扇区
 * @param angle
 */
function getInitSection(angle: number){
  let section = 0
  if (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>

加上场景试试
配合场景

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Spring】深入解析 Spring 原理:Bean 的多方面剖析(源码阅读)

&#x1f525;个人主页&#xff1a; 中草药 &#x1f525;专栏&#xff1a;【Java】登神长阶 史诗般的Java成神之路 一、Bean的作用域 在 Java Spring 框架中&#xff0c;Bean 的作用域是一个关键概念&#xff0c;它决定了 Bean 的生命周期和实例化方式&#xff0c;对应用的性…

基于高德地图js api实现掩膜效果 中间矢量 周围卫星图

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>管网服务</title><style>html,body,#ma…

Vue.js组件(6):echarts组件

1 前言 本章主要对常用的echars图表展示进行基本的组件封装。使用该组件前需要在项目中引入echarts。官网&#xff1a;Apache ECharts npm install echarts --save 2 图表组件 2.1 折线图组件 组件属性&#xff1a;chartId&#xff0c;指定图表挂载div的id&#xff0c;注意不…

RCE常见姿势

文章目录 常见漏洞执行函数&#xff1a;1.系统命令执行函数2.代码执行函数 命令拼接符读取文件命令绕过&#xff1a;空格过滤绕过关键字绕过长度过滤绕过无参数命令执行绕过无字母数字绕过利用%0A截断利用回溯绕过利用create_function()代码注入无回显RCE1.反弹shell2.dnslog外…

selenium执行js

JS知识 获取元素 document.getElement 移除属性&#xff1a;removeAttribute("xx") 窗口移动&#xff1a;window.scrollTo(0, document.body.scrollHeight)方法 drivier.execute_script(js)场景&#xff1a; 日期选择框&#xff0c;不能输入&#xff0c;只能设置…

三维场景重建与3D高斯点渲染技术探讨

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;编程探索专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年12月25日10点11分 神秘男子影, 秘而不宣藏。 泣意深不见, 男子自持重, 子夜独自沉。 文章源地址(有视频)&#xff1a;链接h…

springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失

这个包丢失了 启动不了 起因是pom中加入了 <tomcat.version></tomcat.version>版本指定&#xff0c;然后idea自动编译后&#xff0c;包丢了&#xff0c;删除这个配置后再也找不回来&#xff0c; 这个包正常在 <dependency><groupId>org.springframe…

Java日志框架:log4j、log4j2、logback

文章目录 配置文件相关1. properties测试 2. XMl使用Dom4j解析XML Log4j与Log4j2日志门面 一、Log4j1.1 Logges1.2 Appenders1.3 Layouts1.4 使用1.5 配置文件详解1.5.1 配置根目录1.5.2 配置日志信息输出目的地Appender1.5.3 输出格式设置 二、Log4j22.1 XML配置文件解析2.2 使…

基于LSTM长短期记忆神经网络的多分类预测【MATLAB】

在深度学习中&#xff0c;长短期记忆网络&#xff08;LSTM, Long Short-Term Memory&#xff09;是一种强大的循环神经网络&#xff08;RNN&#xff09;变体&#xff0c;专门为解决序列数据中的长距离依赖问题而设计。LSTM因其强大的记忆能力&#xff0c;广泛应用于自然语言处理…

机器学习基础 衡量模型性能指标

目录 1 前言 ​编辑1.1 错误率(Error rate)&精度(Accuracy)&误差(Error)&#xff1a; 1.2 过拟合(overfitting): 训练误差小&#xff0c;测试误差大 1.3 欠拟合(underfitting)&#xff1a;训练误差大&#xff0c;测试误差大 1.4 MSE: 1.5 RMSE: 1.6 MAE: 1.7 R-S…

TCP的流量控制的实现

滑动窗口的介绍 滑动窗口是tcp协议中的一个重要概念&#xff0c;滑动窗口是字节为单位&#xff0c;而tcp头部的序列化和确认号也是以字节为单位的&#xff0c;滑动窗口里是含有可以传输的字节的数量&#xff08;可以传输不是已经传输&#xff09;&#xff0c;而滑动窗口的大小是…

【0x001D】HCI_Read_Remote_Version_Information命令详解

目录 一、命令概述 二、命令格式及参数说明 2.12. HCI_Read_Remote_Version_Information 命令格式 2.2. Connection_Handle 三、生成事件 3.1. HCI_Command_Status 事件 3.2. HCI_Read_Remote_Version_Information_Complete 事件 四、命令执行流程 4.1. 命令发起阶段(…

一篇文章学会HTML

目录 页面结构 网页基本标签 图像标签 超链接标签 文本链接 图像链接 锚链接 功能链接 列表 有序列表 无序列表 自定义列表 表格 跨列/跨行 表头 媒体元素 视频 音频 网站的嵌套 表单 表单元素 文本框 单选框 多选框 按钮 下拉框 文本域和文件域 表…

C语言项目 天天酷跑(上篇)

前言 这里讲述这个天天酷跑是怎么实现的&#xff0c;我会在天天酷跑的下篇添加源代码&#xff0c;这里会讲述天天酷跑这个项目是如何实现的每一个思路&#xff0c;都是作者自己学习于别人的代码而创作的项目和思路&#xff0c;这个代码和网上有些许不一样&#xff0c;因为掺杂了…

如何完全剔除对Eureka的依赖,报错Cannot execute request on any known server

【现象】 程序运行报错如下&#xff1a; com.netflix.discovery.shared.transport.TransportException报错Cannot execute request on any known server 【解决方案】 &#xff08;1&#xff09;在Maven工程中的pom去掉Eureka相关的引用&#xff08;注释以下部分&#xff0…

华为云国内版与国际版的主要区别解析

华为云作为全球领先的云计算服务提供商&#xff0c;提供了国内版和国际版两种服务。虽然它们都旨在为用户提供高效、可靠的云计算解决方案&#xff0c;但在功能、服务、合规性等方面存在一些显著的区别。我们九河云通过本文将详细分析华为云国内版与国际版的主要区别&#xff0…

基于北斗短报文+4G的森林草原火险因子综合监测方案

近年来&#xff0c;全球气候变暖的趋势日益严重&#xff0c;气温升高导致森林火灾的发生频率和严重程度逐年增加&#xff0c;对人类社会和自然生态环境造成了严重的危害。森林火灾的发生受到植被类型、气象条件、扑救方式和监管方式等多种因素的影响。 因此&#xff0c;林业建…

小程序app封装公用顶部筛选区uv-drop-down

参考ui:DropDown 下拉筛选 | 我的资料管理-uv-ui 是全面兼容vue32、nvue、app、h5、小程序等多端的uni-app生态框架 样式示例&#xff1a; 封装公用文件代码 dropDownTemplete <template><!-- 顶部下拉筛选区封装公用组件 --><view><uv-drop-down ref&…

3 JDK 常见的包和BIO,NIO,AIO

JDK常见的包 java.lang:系统基础类 java.io:文件操作相关类&#xff0c;比如文件操作 java.nio:为了完善io包中的功能&#xff0c;提高io性能而写的一个新包 java.net:网络相关的包 java.util:java辅助类&#xff0c;特别是集合类 java.sql:数据库操作类 IO流 按照流的流向分…

Uniapp 手机基座调试App 打包成Apk文件,并上传到应用商店

1.Uniapp手机基座调试App。 1.1 以下是我另一篇文章 讲解 uniapp连接手机基座调试App、 Hbuildx使用SUB运行到手机基座测试_hbuilder基座-CSDN博客 2.打包本地的uniapp项目为apk文件。 打包的方式有很多种&#xff0c;我们可以选择本地打包和远程云端打包两种方式。 我们在打包…