阅读 3738

花1个小时动手撸一个"罗盘时钟"

前言

效果来自一款屏保软件:world-clock,觉得挺有意思的,于是深夜决定自己动手撸一个。

效果预览

在线预览:罗盘时钟

预览截图:

run.gif

动手实现

首先是要将多个文字元素呈圆形排布,之后将圆形以一定的角度定时旋转来达到效果。

接下来首先实现如何将文字呈圆形排布,搞定了这个,后面的也就不难了。

将文字呈圆形排布

接下来我们先来一步一步实现最外围的“秒”,秒有0-59一共60个元素,首先将他们定位在一个正方形div里。

<template>
  <div class="home">
    <!-- 秒 -->
    <div class="box-wrapper">
      <div class="circle-box" :style="boxStyle('seconds')">
        <span
          v-for="(item, index) in secondTexts"
          :key="item"
        >{{ item }}</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Home',

  data() {
    return {
      secondTexts: [
        '零零', '一秒', '二秒', '三秒', '四秒', '五秒', '六秒', '七秒', '八秒', '九秒', '十秒',
        '十一秒', '十二秒', '十三秒', '十四秒', '十五秒', '十六秒', '十七秒', '十八秒', '十九秒', '二十秒',
        '二十一秒', '二十二秒', '二十三秒', '二十四秒', '二十五秒', '二十六秒', '二十七秒', '二十八秒', '二十九秒', '三十秒',
        '三十一秒', '三十二秒', '三十三秒', '三十四秒', '三十五秒', '三十六秒', '三十七秒', '三十八秒', '三十九秒', '四十秒',
        '四十一秒', '四十二秒', '四十三秒', '四十四秒', '四十五秒', '四十六秒', '四十七秒', '四十八秒', '四十九秒', '五十秒',
        '五十一秒', '五十二秒', '五十三秒', '五十四秒', '五十五秒', '五十六秒', '五十七秒', '五十八秒', '五十九秒'
      ],
      // 盒子大小
      boxSize: {
        seconds: 580
      }
    }
  },

  methods: {
    // 设置文字外围盒子宽高
    boxStyle(key) {
      return {
        width: this.boxSize[key] + 'px',
        height: this.boxSize[key] + 'px'
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.home {
  height: 100%;
  width: 100%;
  background-color: #000000;
  color: #71767D;
  position: relative;
  min-width: 800px;
  min-height: 660px;
  padding: 20px 0;
  overflow: hidden;
}

.box-wrapper {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.circle-box {
  position: relative;
  border: 1px solid red;
}

.circle-box span {
  white-space: nowrap;
  font-size: 14px;
  position: absolute;
}
</style>
复制代码

Snipaste_2021-06-13_22-27-40.png

现在文字设置absolute定位后都堆叠在一起了,接下来通过位置计算让其呈圆形排布。

Snipaste_2021-06-13_22-16-07.png

从图中我们知道,要让文字排列在圆上,我们需要知道文字元素到O点(圆心)的横坐标和纵坐标,也就是a的高度和b的长度,对应我们要设置定位的top和left值。

半径我们是知道的,即盒子宽度的一半 r = 580 / 2 = 290,每个元素与圆心的夹角c = (360 / 60) * i,据此根据数学公式我们可以求出a和b的值。新增代码如下:

<template>
  <div class="home">
    <!-- 秒 -->
    <div class="box-wrapper">
      <div class="circle-box" :style="boxStyle('seconds')">
        <span
          v-for="(item, index) in secondTexts"
          :key="item"
          :style="spanStyle(boxSize.seconds, secondTexts, index)"
        >{{ item }}</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Home',
  methods: {
    spanStyle(size, texts, i) {
      const r = size / 2 // 半径
      const deg = this.getPerDeg(texts) // 元素平均间隔度数
      const angle = i * deg // 夹角
      const { a, b } = this.getHypotenuse(r, angle)
      const rotateDeg = deg * i // 文字旋转角度
      return {
        top: a + r + 'px',
        left: b + r + 'px',
        transform: `rotate(${rotateDeg}deg)`,
        transformOrigin: '0 0'
      }
    },

    // 元素平均间隔度数
    getPerDeg(texts) {
      return 360 / texts.length
    },

    // 已知角度和斜边,获取直角边
    getHypotenuse(long, angle) {
      // 获得弧度
      let radian = 2 * Math.PI / 360 * angle
      return {
        a: Math.sin(radian) * long, // 邻边
        b: Math.cos(radian) * long // 对边
      }
    }
  }
}
</script>
复制代码

Snipaste_2021-06-13_22-53-41.png

效果不错,接下来发挥cv大法,把分钟纬度给弄上去。新增代码如下:

<template>
  <div class="home">
    <!-- 分 -->
    <div class="box-wrapper">
      <div class="circle-box" :style="boxStyle('minutes', minutesDeg)">
        <span
          v-for="(item, index) in minuteTexts"
          :key="item"
          :style="spanStyle(boxSize.minutes, minuteTexts, index)"
        >{{ item }}</span>
      </div>
    </div>
    <!-- 秒 -->
    <div class="box-wrapper">
      <div class="circle-box" :style="boxStyle('seconds', secondsDeg)">
        <span
          v-for="(item, index) in secondTexts"
          :key="item"
          :style="spanStyle(boxSize.seconds, secondTexts, index)"
        >{{ item }}</span>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  name: 'Home',

  data() {
    return {
      secondTexts: [
        '零零', '一秒', '二秒', '三秒', '四秒', '五秒', '六秒', '七秒', '八秒', '九秒', '十秒',
        '十一秒', '十二秒', '十三秒', '十四秒', '十五秒', '十六秒', '十七秒', '十八秒', '十九秒', '二十秒',
        '二十一秒', '二十二秒', '二十三秒', '二十四秒', '二十五秒', '二十六秒', '二十七秒', '二十八秒', '二十九秒', '三十秒',
        '三十一秒', '三十二秒', '三十三秒', '三十四秒', '三十五秒', '三十六秒', '三十七秒', '三十八秒', '三十九秒', '四十秒',
        '四十一秒', '四十二秒', '四十三秒', '四十四秒', '四十五秒', '四十六秒', '四十七秒', '四十八秒', '四十九秒', '五十秒',
        '五十一秒', '五十二秒', '五十三秒', '五十四秒', '五十五秒', '五十六秒', '五十七秒', '五十八秒', '五十九秒'
      ],
      minuteTexts: [
        '零零', '一分', '二分', '三分', '四分', '五分', '六分', '七分', '八分', '九分', '十分',
        '十一分', '十二分', '十三分', '十四分', '十五分', '十六分', '十七分', '十八分', '十九分', '二十分',
        '二十一分', '二十二分', '二十三分', '二十四分', '二十五分', '二十六分', '二十七分', '二十八分', '二十九分', '三十分',
        '三十一分', '三十二分', '三十三分', '三十四分', '三十五分', '三十六分', '三十七分', '三十八分', '三十九分', '四十分',
        '四十一分', '四十二分', '四十三分', '四十四分', '四十五分', '四十六分', '四十七分', '四十八分', '四十九分', '五十分',
        '五十一分', '五十二分', '五十三分', '五十四分', '五十五分', '五十六分', '五十七分', '五十八分', '五十九分'
      ],
      // 盒子大小
      boxSize: {
        seconds: 580,
        minutes: 440
      }
    }
  }
}
</script>

复制代码

Snipaste_2021-06-13_23-08-39.png

实现罗盘转动

新增代码如下:

<template>
  <div class="home">
    <!-- 分 -->
    <div class="box-wrapper">
      <div class="circle-box" :style="boxStyle('minutes', minutesDeg)">
        <span
          v-for="(item, index) in minuteTexts"
          :key="item"
          :style="spanStyle(boxSize.minutes, minuteTexts, index)"
          :class="{'active': index === currentMinutes}"
        >{{ item }}</span>
      </div>
    </div>
    <!-- 秒 -->
    <div class="box-wrapper">
      <div class="circle-box" :style="boxStyle('seconds', secondsDeg)">
        <span
          v-for="(item, index) in secondTexts"
          :key="item"
          :style="spanStyle(boxSize.seconds, secondTexts, index)"
          :class="{'active': index === currentSeconds}"
        >{{ item }}</span>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  name: 'Home',

  data() {
    return {
      currentMinutes: 0, // 当前-分钟
      currentSeconds: 0, // 当前-秒
      minutesDeg: 0, // 当前-分钟-转动角度
      secondsDeg: 0, // 当前-面-转动角度
      timer: null // 定时器
    }
  },

  mounted() {
    this.init()
  },

  methods: {
    init() {
      const d = new Date()
      const minutes = d.getMinutes() // 分
      const seconds = d.getSeconds() // 秒
      // 当前时间
      this.currentMinutes = minutes
      this.currentSeconds = seconds
      // 角度
      this.minutesDeg = this.currentMinutes * this.getPerDeg(this.minuteTexts)
      this.secondsDeg = this.currentSeconds * this.getPerDeg(this.secondTexts)
      // 设置定时器
      this.timer = setInterval(() => {
        this.runClock()
      }, 1000)
      // 记得清除定时器
      this.$once('hook:beforeDestroy', () => {
        clearInterval(this.timer)
      })
    },

    boxStyle(key, deg) {
      return {
        // 设置文字外围盒子宽度、高度
        width: this.boxSize[key] + 'px',
        height: this.boxSize[key] + 'px',
        // 添加转动
        transform: `rotate(-${deg}deg)`
      }
    },

    // 元素平均间隔度数
    getPerDeg(texts) {
      return 360 / texts.length
    },

    runClock() {
      const d = new Date()
      const minutes = d.getMinutes() // 分
      const seconds = d.getSeconds() // 秒
      if (this.currentMinutes !== minutes) {
        this.currentMinutes = minutes
        this.minutesDeg += this.getPerDeg(this.minuteTexts)
      }
      this.currentSeconds = seconds
      this.secondsDeg += this.getPerDeg(this.secondTexts)
    }
  }
}
</script>

<style lang="scss" scoped>
.circle-box {
  position: relative;
  // 添加动画效果
  transition: transform 0.4s ease-in-out;
}

// 激活时文字颜色为白色
.circle-box span.active {
  color: #fff;
}
</style>

复制代码

Kapture 2021-06-13 at 23.32.14.gif

至此,整个工作就完成得差不多了,剩余的月、日、时等纬度也就是依样画葫芦的事,这里就不一一贴出来了,感兴趣的同学可以移步GitHub查看完整代码。

完整代码地址:GitHub

结尾

本文到这里就结束了,感谢阅读,码字不易,欢迎你的点赞👍!!!

本文如果有什么错误的地方,也欢迎评论区指正、交流!

文章分类
前端
文章标签