文本置乱动画的解析

  |   0 评论   |   0 浏览   |   Erioifpud

动效地址:https://codepen.io/soulwire/pen/mErPAK
动效作者:Justin Windle

效果预览

See the Pen Text Scramble Effect by Erioifpud (@erioifpud) on CodePen.

解析

他的代码可以分成两块,一块是处理文本置乱效果的类,另一块是使用实例,我们要特别关注的是这个类。并且,我依然会从最最简单的部分开始讲解,一步步解析代码。

class TextScramble {
  constructor(el) {
    this.el = el
    this.chars = '!<>-_\\\\/[]{}—=+*^?#________'
    this.update = this.update.bind(this)
  }
  setText(newText) {
    const oldText = this.el.innerText
    const length = Math.max(oldText.length, newText.length)
    const promise = new Promise((resolve) => this.resolve = resolve)
    this.queue = []
    for (let i = 0; i < length; i++) {
      const from = oldText[i] || ''
      const to = newText[i] || ''
      const start = Math.floor(Math.random() * 40)
      const end = start + Math.floor(Math.random() * 40)
      this.queue.push({ from, to, start, end })
    }
    cancelAnimationFrame(this.frameRequest)
    this.frame = 0
    this.update()
    return promise
  }
  update() {
    let output = ''
    let complete = 0
    for (let i = 0, n = this.queue.length; i < n; i++) {
      let { from, to, start, end, char } = this.queue[i]
      if (this.frame >= end) {
        complete++
        output += to
      } else if (this.frame >= start) {
        if (!char || Math.random() < 0.28) {
          char = this.randomChar()
          this.queue[i].char = char
        }
        output += `<span class="dud">${char}</span>`
      } else {
        output += from
      }
    }
    this.el.innerHTML = output
    if (complete === this.queue.length) {
      this.resolve()
    } else {
      this.frameRequest = requestAnimationFrame(this.update)
      this.frame++
    }
  }
  randomChar() {
    return this.chars[Math.floor(Math.random() * this.chars.length)]
  }
}

Math.random()

这个内置函数是用来获取随机浮点数的,得知道他的范围在 [0, 1),包括 0 但不包括 1。

randomChar

randomChar() {
  return this.chars[Math.floor(Math.random() * this.chars.length)]
}

这个函数是用来获取随机符号的,this.chars 中装了很多符号,可以扮演乱码的角色。

setText

setText(newText) {
  const oldText = this.el.innerText
  const length = Math.max(oldText.length, newText.length)
  const promise = new Promise((resolve) => this.resolve = resolve)
  this.queue = []
  for (let i = 0; i < length; i++) {
    const from = oldText[i] || ''
    const to = newText[i] || ''
    const start = Math.floor(Math.random() * 40)
    const end = start + Math.floor(Math.random() * 40)
    this.queue.push({ from, to, start, end })
  }
  cancelAnimationFrame(this.frameRequest)
  this.frame = 0
  this.update()
  return promise
}

这是 TextScramble 的关键函数,用于播放动画,首先需要新旧两个文本,因为我们的目的就是从旧文本转换成“乱码”再转换成新文本,接下来几行代码(跳过 promise)的功能如下:

  1. 确定两个文本的最大长度 length,因为我们要在新旧文本间做每一个字符的映射,短文本要用乱码补全到长文本的长度
  2. 确定当前位置的旧字符 from 和新字符 to,字符为空时就用 '' 代替。
  3. 确定每一个旧字符 from 应该从哪一帧开始变成乱码。
  4. 确定每一个乱码应该从哪一帧开始变成新字符 to。(3 和 4 的随机数范围在[0, 40),也就是动画最长持续时间小于 80 帧,最短大于等于 0 帧)
  5. 构建出对象 { from, to, start, end }(下文称其为字符变化描述对象),放到 queue 中。

r3IZeH.png

promise

接下来是被跳过的 promise,这个写法将 resolve 函数提取到了 this 上,并且将 promise 作为 setText 的返回值。

那么很好理解,resolve 就是在动画结束时调用的,但 setText 调用完成并不等于动画结束,所以 resolve 就相当于是一个用于标记动画完成的函数,并且能交给 class 范围中的其他函数使用。

update

现在我们有初始的帧数 frame描述单个字符变化的列表 queue

update() {
  let output = ''
  let complete = 0
  for (let i = 0, n = this.queue.length; i < n; i++) {
    let { from, to, start, end, char } = this.queue[i]
    if (this.frame >= end) {
      complete++
      output += to
    } else if (this.frame >= start) {
      if (!char || Math.random() < 0.28) {
        char = this.randomChar()
        this.queue[i].char = char
      }
      output += `<span class="dud">${char}</span>`
    } else {
      output += from
    }
  }
  this.el.innerHTML = output
  if (complete === this.queue.length) {
    this.resolve()
  } else {
    this.frameRequest = requestAnimationFrame(this.update)
    this.frame++
  }
}

这个函数遍历 queue,拿到每一个字符变化描述对象,用当前的帧 frame字符需要变化时的帧 startend 做对比,决定这个字符此刻(帧)要不要“改变”和要变成什么样,接着再将结果拼接出当前帧的文本内容 output,就能把他放进 el.innerHTML 中显示了,以下是 output 的样例:

1<span class="dud">#</span><span class="dud">@</span>
// dud 的样式为灰色文本,表示乱码

char

字符变化描述对象中的乱码符号 char 是从哪来的呢?可以看到在构造字符变化描述对象时并没有这个属性,那是因为 char**只在字符需要置乱时(到了 start 那一帧)**才会被 randomChar 选出来。

选出后立马被加回到字符变化描述对象里,为下一次的渲染做准备,下次 update 时发现 char 不为空,那么就不需要再次抽选乱码符号。

if (!char || Math.random() < 0.28) {
  char = this.randomChar()
  this.queue[i].char = char
}

complete

接下来看 complete,这个变量表示有多少个字符已经完成了动画(已经从 from 变成了 to,相当于动画的整体进度,为什么要用他来表示进度呢?因为之前说了 update每一帧都要被调用的,不能以 update 调用结束当作动画完成的标志,所以需要一个表示进度的变量。

当全部字符都转变为 to 了,那么就表示动画结束了,这时候应该调用之前保存的 this.resolve,告诉 promise 任务完成了。

那么动画没完成时,update 会通过 requestAnimationFrame 调用自己,构成递归,让 update 在下一帧进行新的绘制。

r3Iewd.png

frameRequest

接下来是 frameRequest,这个就和 setTimeout 返回的 id 类似了,requestAnimationFrame 是一个异步函数,在页面下一帧的时候调用参数函数,他也会返回一个 id,在他执行前,可以使用 cancelAnimationFrame 随时取消。

他这里是避免短期内重复调用 setText,导致多段动画互相影响的问题,可以看到 setText 中会有一句 cancelAnimationFrame(this.frameRequest),来取消上一次的动画。

总结

  1. 用 JavaScript 做动画应该使用 requestAnimationFrame,在每一帧上进行绘制,而不是使用 setInterval
  2. 做动画应该按帧去设计,分别计算每一帧的状态。
  3. Promiseresolve 可以拆分,放在另一个作用域中使用。
  4. 代码结构。
class Foo {
  constructor () {
    this.update = this.update.bind(this)
  }
  run () {
    cancelAnimationFrame(this.frameRequest)
    const promise = new Promise(resolve => this.resolve = resolve)
    this.update()
  }
  update () {

    // draw something

    if (this.complete) {
      this.resolve()
    } else {
      this.frameRequest = requestAnimationFrame(this.update)
      this.frame++
    }
  }
}

标题:文本置乱动画的解析
作者:Erioifpud
地址:https://blog.doiduoyi.com/articles/1608177404558.html

评论

发表评论