Javascript伍-酷狗歌词文字粒子效果JS实现

1

伍-酷狗歌词文字粒子效果JS实现

阅读:371 时间:2022年11月14日

前言废话 听歌时偶然发现了酷狗音乐APP的歌词增加了粒子效果。感觉很酷。然后就尝试着用JS在前端实现了一下。细节和酷狗还有差距。主要是尝试一下实现思路。 先看效果: 录屏效果太差了,直接看在线演示好一些...

前言废话

听歌时偶然发现了酷狗音乐APP的歌词增加了粒子效果。感觉很酷。然后就尝试着用JS在前端实现了一下。细节和酷狗还有差距。主要是尝试一下实现思路。

先看效果:

录屏效果太差了,直接看在线演示好一些

http://demo.ccued.com/fonts-lizi/

以下是分析和实现的记录,总的来说我写的比较烂。看看思路就好。

分析

首先肯定要上画布了。

将文字写在画布上,在逐个点的检查是否是文字内容。然后将文字内容的位置,颜色信息记录在一起。

在渲染时,将文字拆解的点向上移动,做出飞出的效果。当然不能全部一起移动,要从上到下一部分一部分移动,且速度方向要有变化,才会有散开的粒子效果。

大至想法就是这样了。准备实现。

实现过程

肯定是要先准备一个画布了。因为只为探究实现方法,没有使用离屏画布。全在一个画布上搞了。

html文档

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html, body{
      margin: 0;
      padding: 0;
      height: 100%;
      overflow: hidden;
    }
    body{
      display: flex;
      align-items: center;
      justify-content: center;
      background: #000 url(./bg.jpg) center center no-repeat;

      background-size: cover;
    }

  </style>
</head>
<body>
<canvas class="canvas"></canvas>
<script src="./Lizi.js"></script>  
<script>
const lrc = `我和你本应该
各自好各自坏
各自生活的自在
毫无关联的存在
直到你出现在
我眼中躲不开
我也占领你的心海
充实着你的空白
为何出现在彼此的生活又离开
只留下在心里深深浅浅的表白
谁也没有想过再更改
谁也没有想过再想回来
哦我不明白
我和你不应该
制造感觉表达爱
试探未知和未来
相信那胡言一派
当天空暗下来
当周围又安静起来
当我突然梦里醒来
就等着太阳出来
为何出现在彼此的生活又离开
只留下在心里深深浅浅的表白
谁也没有想过再更改
谁也没有想过再想回来
哦我不明白
我们紧紧相拥
头也不抬
因为不想告别
就悄然离开
不用认真的说
多舍不得你
每一个未来
都有人在
每一个未来
都有人在
那你无需感慨
我别徘徊
因为谁也没有想过再更改
谁也没有想过再想回来`
const data = lrc.split('\n')
let index = 0
const lizi = new Lizi(document.querySelector('.canvas'))
function render() {
  if (index < data.length) {
    const text = data[index]
    lizi.setText(text)
    setTimeout(() => {
      lizi.requestRender()
      index++
      setTimeout(() => {
        render()
      }, 1000)
    }, 2000)
  }
}
render()

</script>
</body>
</html>

这段html文档里主要是放置显示文字的canvas元素。

以及用setTimeout模拟歌词同步的效果来展示粒子。

脚本

class Point{
  constructor({x, y, ease, color, moved}) {
    this.x = x
    this.y = y
    this.ease = ease
    this.moved = moved
    this.color = {}
    this.color.r = color?.r
    this.color.g = color?.g
    this.color.b = color?.b
    this.color.a = color?.a
    this.easeSpace = Math.random() * 6
    this.life = Math.random() * this.easeSpace
    this.rgba = this.rgbaColor()
  }
// 将颜色拼接一个完整的rgba颜色值
  rgbaColor() {
    return `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.color.a})`
  }
// 更新点的颜色和位置
  update() {
    this.life += 0.1
    if (this.moved) {
      if (this.color.a > 0) {
        this.color.a -= Math.random() * 0.05 || 0
      }
      this.y -= this.ease * 0.1
      const xOffset = this.life % this.easeSpace - this.easeSpace * 0.5
      this.x += xOffset
      this.ease += 1.5
    } else {
      this.moved = true                    
      this.y -= this.ease * 0.1
    }
    this.rgba = this.rgbaColor()
  }

}

class Lizi{
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.width = 400
    this.height = 300
    this.text = 'ready'
    this.request = -1
    this.data = []
    this.init()
  }

  init() {
    const {width, height} = this.canvas.getBoundingClientRect()
    this.width = width
    this.height = height
    this.canvas.width = width
    this.canvas.height = height
  }
// 创建渐变色
  createGradient(topColor, bottomColor, topPosition, bottomPosition) {
    const linearGradient = this.ctx.createLinearGradient(0, topPosition, 0, bottomPosition)
    linearGradient.addColorStop(0, topColor || 'rgba(255, 255, 255, 0.5)')
    linearGradient.addColorStop(1, bottomColor || 'rgba(255, 255, 255, 1)')  
    return linearGradient 
  }
// 解析画布每个像素的信息,将有文字信息的像素找出来并创建为Point对象
  savePoints(imageData, width) {
    const data = []
    for (let i = 0; i < imageData.length; i += 4) {
      const x = i / 4 % width
      const y = Math.floor(i / 4 / width)
      if (imageData[i] === 0 && imageData[i+1] === 0 && imageData[i+2] === 0 && imageData[i+3] === 0) {

      } else {
        data.push(new Point({
          x, y,
          moved: false,
          ease: 1,
          color: {
            r: imageData[i],
            g: imageData[i+1],
            b: imageData[i+2],
            a: imageData[i+3] / 255,
          }
        }))
      }
    }    
    return data
  }
// 设置文字到画布
  setText(text, style = 'normal small-caps normal 32px arial') {
    if (this.request) {
      window.cancelAnimationFrame(this.request)
    }
    this.text = text
    this.ctx.font = style
    const measure = this.ctx.measureText(this.text)
    const height = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent
    const padding = 40
    this.width = padding * 2 + measure.width
    this.height = height * 3
    this.canvas.width = this.width
    this.canvas.height = this.height
    this.ctx.clearRect(0, 0, this.width, this.height)
    this.ctx.font = style
    const linearGradient = this.createGradient('rgba(255, 255, 255, 0.5)', 'rgba(255, 255, 255, 1)', height * 2, height * 3)
    this.ctx.fillStyle = linearGradient
    this.ctx.fillText(this.text, padding, height * 2.5)
    const imageData = this.ctx.getImageData(0, 0, this.width, this.height)
    this.data = this.savePoints(imageData.data, this.width)
  }
// 动画循环
  requestRender() {
    if (this.request) {
      window.cancelAnimationFrame(this.request)
    }
    this.render()
    this.request = window.requestAnimationFrame(() => {
      this.requestRender()
    })
  }
// 是否要更新像素位置和颜色信息,并渲染
  render() {
    this.ctx.clearRect(0, 0, this.width, this.height)
    let firstIndex = 0
    this.data.forEach(point => {
      if (point.y > -this.height) {
        if (point.moved) {
          point.update()
        } else {
          firstIndex++
          if (firstIndex < this.width * 0.5) {
            point.moved = true
            point.update()
          }
        }
      }
      this.ctx.fillStyle = point.rgba
      this.ctx.fillRect(point.x, point.y, 1, 1)
    })
  }

}

我准备了两个类,Point类是文字拆解的点, Lizi类是用来实现粒子效果

Point类

就是记录和更新像素位置与颜色信息的。

easeSpace, life都是用来控制移动范围的。

moved是用来记录这个像素是否是开始参与移动了。因为不是所有点同时移动的,那就成了整体文字平移了。

Lizi类

是效果实现

setText方法是将文字画到画布上,并计算文字占用空间大小,根据文字空间来重新设置画布尺寸。

然后将文字所有像素信息分析记录

savePoint方法就是用于分析像素信息的,如果像素rgba四个值全为0,说明这个像素是个空白区域,无需记录

render方法遍历记录的这些点,并进行位置颜色更新后,重新渲染在画布上。

大概就是这样吧,说的不太清楚,可以看代码和demo。

 

 

 

 

发表评论说说你的看法吧

精品模板蓝瞳原创精品网站模板

^