【不可思议的前端】动如脱兔的小球

1,492 阅读5分钟

本篇分享给大家一个有意思的小案例,同时我也会手摸手地教大家这个小案例是如何实现的,最后做一个简短2019年总结和2020年的展望,毕竟人还活着呢。

看到这个动图不知道是否勾起了你的好奇心,如果你心想这是如何来实现的呢?那么请不要着急,接下来我会带你来实现它。再如果你只对动图里的字体感兴趣,同样也不要着急,文章底部已经贴好了源码链接,字体库就在其中。如果你想了解如何实现它的话,接下来就跟随我一探究竟。

分析

当让你根据动图所呈现的效果来实现它的话,我们的第一个想法会是什么?去 Google ...(sleep 3s)显然在这不现实。 那我们来换一种想法,先看它都有哪些元素。

从动图来看它有 文字小球动画 这三种重要元素。接下来我们只需要一一实现它就可以了。

每当我们实现一个需求或者完成一项任务的时候,首先最最需要的是抽象和结构化思维,在脑海中有一个大概的构思,实现思路大致如下:

  1. 插入文字并计算好文字距离左边的距离
  2. 根据距离计算出小球的每个落点
  3. 添加动画改变各自的状态

实现

第一步

首先我们什么都不用思考,上来先定义一个 class 名字叫做 BounceBall

class BounceBall {}

我们打算当用户创建一个实例时,传给我们一段文字和一个绑定在 dom 上的 id,我们就可以把文字插入在这个 dom 内。这样会在 html 中呈现类似下面的效果

<div>Goodbye 2019 Hello 2020</div>

问题出现了,当一段文字只放在一个元素内的话,我们无法计算出每个文字距离左边的距离,这时候我们就需要做一些改变。

根据空格切分字符串成为数组,将每一个文字插入单独的元素内,同样中间用空格分开。

class BounceBall {
    constructor (config) {
        const { id, text } = config
        this.id = id
        this.text = text
        this.$id = document.getElementById(this.id)
        this.init()
    }
    
    init () {
        const contentArray = this.text.split(' ')
        // this.append(this.$ball)
        for (let i = 0, len = contentArray.length; i < len; i++) {
            const text = contentArray[i]

            const $text = getSpan(text, 'text')
            this.append($text)

            const textLen = $text.offsetWidth

            if (i + 1 < contentArray.length) {
                this.append(getSpan(' '))
            }
        }
    }
    
    append (element) {
        this.$id.appendChild(element)
    }
}

这里有一个 getSpan 方法没有在上面代码中体现出来,它的目的是创建一个 span 标签元素,然后把字符串插入进去,添加 classname 等。 因为我没有借助任何库,所以很多 dom 操作需要单独封装成工具库。再看一下 html 中呈现的效果

<div id="main">
    <!-- <div class="ball"></div> -->
    <span class="text">Goodbye</span>
    <span> </span>
    <span class="text">2019</span>
    <span> </span>
    <span class="text">Hello</span>
    <span> </span>
    <span class="text">2020</span>
</div>

这样我们就可以根据 classname 获得每个文字的属性了。接下来小球的 dom 同样一并添加上,小球为上面注释部分。

给小球添加样式让它在文字的左上角位置。

.ball {
  position: absolute;
  top: 0;
  left: -20px;
  width: 10px;
  height: 10px;
  border-radius: 100%;
  background-color: green;
  margin-left: -5px;
}

第二步

计算出每段文字距离左边的距离宽度位置

init () {
    ...
    
    for (let i = 0, len = contentArray.length; i < len; i++) {
        const text = contentArray[i]

        const $text = getSpan(text, 'text')
        this.append($text)
        
        const textLen = $text.offsetWidth
        ...
        
        const ballLeft = $text.offsetLeft + textLen / 2
    
        const ballProps: BallProps = {
            left: ballLeft,
            textLen,
            textIndex: i
        }
        this.ballPropsArray.push(ballProps)
    }
}

我们定义了一个 ballPropsArray 数组,它存放着小球运动的轨迹。ballLeft 为每段文字中间距离左边的距离,也就是小球要到的位置。textLen 为每段文字的宽度。textIndex 为下标。

我们假设小球移动到每个 ballProps 的时间为 ${textLen} ms。当 ${textLen / 2} ms时,小球距离左边为 ${ballLeft} px,高度设置一个定值,再过 ${textLen / 2} ms 小球下落到底部。根据这个思路我们实现出以下代码:


let incrementingDelay = 0

for (let i = 0, len = this.ballPropsArray.length; i < len; i++) {
  const ballProps = this.ballPropsArray[i]
  setTimeout(() => {
    this.$ball.style.left = `${ballProps.left}px`
    this.$ball.style.top = '-1em'

    // 小球开始上升
    const halfwayReached = ballProps.textLen / 2
    setTimeout(() => {
      this.$ball.style.left = `${ballProps.left}px`
      this.$ball.style.top = '0px'
      
      // 小球开始下落
    }, halfwayReached)
  }, incrementingDelay)

  incrementingDelay += ballProps.textLen
}

第三步

为小球添加动画改变文字的颜色,首先给小球添加 transition 属性。

.ball {
  ...
  transition-property: left, top;
  transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1), cubic-bezier(0.25, 0.1, 0.25, 1);
}

在每次运动中设置 transition-duration 的值。

const leftDuration = `${ballProps.textLen}ms`
const topDuration = `${ballProps.textLen / 2}ms`

this.$ball.style.transitionDuration = `${leftDuration}, ${topDuration}`

在小球开始下落的 ${textLen / 2} ms 之后,修改文字的颜色。

let incrementingDelay = 0

for (let i = 0, len = this.ballPropsArray.length; i < len; i++) {
  const ballProps = this.ballPropsArray[i]
  setTimeout(() => {
    this.$ball.style.left = `${ballProps.left}px`
    this.$ball.style.top = '-1em'

    // 小球开始上升
    const halfwayReached = ballProps.textLen / 2
    setTimeout(() => {
      this.$ball.style.left = `${ballProps.left}px`
      this.$ball.style.top = '0px'
      
      // 小球开始下落
      setTimeout(() => {
        // 修改颜色
      }, halfwayReached)
    }, halfwayReached)
  }, incrementingDelay)

  incrementingDelay += ballProps.textLen
}

一个简版的跳动小球就做好了,之后我们可以继续为它添加更多的属性和进一步优化,比如 为它添加 speed 属性,或者让小球淡入淡出,或者改变它的形状等等。

结语

2019年马上要过去了,借着这篇文章做个年终总结。今年是我收获之年,有很多朋友、同事和亲戚,不管在工作中还是生活中都给我很大帮助。年初为自己制定的一些学习计划和工作计划也都完成的不错,比如每个月读一本书,每个月发一篇文章等。工作中要学一些新技术、做一些对团队有帮助意义的事情等。希望在2020年自己依旧能坚持下去,尝试更多新领域、探索更多未知。毕竟人活着总要给这个世界留下的什么吧。最后祝大家2020年一夜暴富。

附上小球源码 bounce-ball