文本置乱动画的解析

  |   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

评论

发表评论