likes
comments
collection
share

canvas - 搞点跟像素级别的小玩意

作者站长头像
站长
· 阅读数 12

前言

1> 像素文字 - 万物皆可像素化。

canvas - 搞点跟像素级别的小玩意

2> 图片溶解进入 - 解锁更多图片轮播特效。

因为性能问题,颗粒选择的比较大。

canvas - 搞点跟像素级别的小玩意

思路简介

前言中已经说明了,核心 API 只用到了 canvas.context.getImageData

这个 api 需要传入四个参数,我把它称为 canvas 的四大金刚:

x: 开始坐标的 x 值
y: 开始坐标的 y 值
w: 宽度
h: 高度

这个接口会返回一个对象,该对象包含指定的 ImageData 对象的图像数据。 对于 ImageData 对象中的每个像素的 RGBA 信息。

值可以描述为张这个样子 [r0,g0,b0,a0,r1,g1,b1,a1,r2,g2,b2,a2...] ,这样子的,从左上角开始向右挨个展示出来,不会换行,但是可以使用宽度去计算。

例如:坐标为 (0,0) 的像素的 rgba 数据为 [r0,b0,g0,a0],坐标为 (0,1) 的像素的 rgba 数据为 [r1,b1,g1,a1]。 宽度为 10,则坐标为 (1,0) 的像素的 rgba 数据为 [r10,b10,g10,a10]。

如上就是关于这个 API 的完整解释了,如果还想更深一步的了解,自己网上自学一下吧。

下面思路都是基于已经拿到这个imageData的前提下展开来的。

1. 像素文字

像素文字其实很简单,我们假设已经有了一个白色的画板,用黑色的颜料在白板上写下一个字,或者绘制别的字符。一定不能是白色的!

这样我们就通过遍历 ImageData 就可以找到和白色不同的像素点了。

例如我展示的那个龙字,我们只需要在canvas上铺满小球,然后看下圆心的位置是白色还是别的颜色就行啦,如果是白色,小球就变成透明的,如果是其他颜色就根据自己的喜欢画一个小球。当然也可以变成其他的形状。

这就是像素文字的思路,升级版本就是再用一个数组把这些小球搜集起来,使用数学只是让他去动态的显示。

进阶思路

  1. 结合计时器实现霓虹灯效果
  2. 图片像素化
  3. 图片裁切工具的实现(已经实现了,有机会分享)

2. 溶解图片

溶解图片其实就是按照固定的大小去切割原图片。依次存在一个数组里面。

例如原图片的大小为 500 * 400,我们将其切割成 10 * 10 的小格子,使用 getImageData 获取一下信息和坐标,并存进一个数组里面。然后再将其按照一定的顺序绘制出来就好了。

绘制的方法是:putImageData。

进阶思路

  1. 实现图片轮播,即两张图片来回渲染
  2. 更多的渲染效果,从左到右,从上到下,中心到四周,四周到中心,菱形,回字形,扇形...
  3. 撕裂图片,折叠图片. . .

核心代码

核心代码主要分为四个:

  1. canvas 引擎,简单封装了几个api用来初始化一个 canvas
  2. px-img.js,像素化图片的核心主逻辑,继承自 Canvas。
  3. px-word.js,像素化文字的核心主逻辑,继承自 Canvas。
  4. index.html

canvas 引擎

// canvas.class.js

export default class Canvas {
  constructor(id, config) {

    if (id !== 0 && !id) {
      throw new Error('id 不能为空!')
    }

    this.cv = document.getElementById(id)
    this.ctx = this.cv.getContext('2d')

    this.config = Object.assign({
      isScreen: true
    }, config)

    this.initSize()
  }

  initSize() {
    this.cv.style.display = "block"

    if (this.config.isScreen) {
      this.cv.width = window.innerWidth
      this.cv.height = window.innerHeight
      window.addEventListener("resize", this.resizeCanvas.bind(this), false)
    } else {
      this.cv.width = this.cv.parentNode.offsetWidth
      this.cv.height = this.cv.parentNode.offsetHeight
    }

  }

  clearCanvas() {
    this.ctx.clearRect(0, 0, this.cv.width, this.cv.height)
  }

  resizeCanvas() {
    this.cv.width = window.innerWidth
    this.cv.height = window.innerHeight
    this.draw()
  }
}

px-img.js

// px-img.js

import Canvas from '../libs/canvas.class.js'

export default class cGame extends Canvas {
  constructor(id, config) {
    super(id, config)
    this.config = {
      granularity: 4,
      imgW: 500,
      imgH: 400,
      aniType: 'left' // right
    }
    this.init()
  }

  init() {
    var image = new Image()
    image.src = './01.jpg'
    image.onload = () => {
      this.ctx.drawImage(image, 0, 0, 500, 400);
      let arr = []
      for (let i = 0; i < 500 / this.config.granularity; i++) {
        let tempArr = []
        for (let j = 0; j < 400 / this.config.granularity; j++) {
          tempArr.push(this.ctx.getImageData(i * this.config.granularity, j * this.config.granularity, this.config.granularity, this.config.granularity))
        }
        arr.push(tempArr)
      }
      this.imageList = arr
      this.draw()
    }
  }

  draw() {

    this.imageList.forEach((item, i) => {
      item.forEach((ele, j) => {
        // setTimeout 代替 setInterval
        setTimeout(() => {
          this.ctx.putImageData(ele, i * this.config.granularity, j * this.config.granularity + 400);
        }, parseInt(Math.random() * 300))
      })
    })
  }

  update() {
    this.draw()
  }
}

px-word.js

// px-word.js

import Bubble from '../libs/bubble.class.js'
import Canvas from '../libs/canvas.class.js'
import * as utils from '../libs/utils.js'

export default class cGame extends Canvas {
  constructor(id, config) {
    super(id, config)
    this.init()
    // this.play = setInterval(this.update.bind(this), 1000 / 60)
  }

  init() {
    let textWidth = 600

    this.ctx.beginPath()
    this.ctx.font = '200px 微软雅黑'
    this.ctx.fillStyle = '#000'
    this.ctx.textBaseline = "hanging"
    this.ctx.fillText('龍', 0, 0)
    this.ctx.closePath()

    let imageData = this.ctx.getImageData(0, 0, textWidth, 200)
    utils.clearCanvas(this.cv, this.ctx)

    let arr = []
    for (let i = 0; i < imageData.data.length; i += 40) {
      let obj = {
        x: i / 4 % textWidth,
        y: parseInt(i / 4 / textWidth),
        color: `rgba(${[imageData.data[i], imageData.data[i + 2], imageData.data[i + 2], imageData.data[i + 3]].join(',')})`,
        time: i / 4 * 1
      }
      arr.push(obj)
    }
    this.wordData = arr
    this.draw()
  }

  draw() {
    this.wordData.map(item => {
      if (item.y % 10 == 0) {
        new Bubble(this.ctx, { x: item.x, y: item.y + 300, radius: 3, color: item.color == 'rgba(0,0,0,0)' ? item.color : false, shadow: true })
      }
    })
  }

  update() {
    this.draw()
  }
}

index.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>像素</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    canvas {
      background: pink;
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
</body>
<script type="module">
  import cGame from './px-img.js'
  // import cGame from './px-word.js'
  const c = new cGame("canvas", {
    isScreen: true,
  })
</script>

</html>

补充 bubble.class.js 绘制小球的类

export default class Bubble {
  constructor(ctx, config) {

    let defaultColor = config.color || `rgba(${parseInt(Math.random() * 255)},${parseInt(Math.random() * 255)},${parseInt(Math.random() * 255)}, 1)`
    // console.log(defaultColor)
    this.ctx = ctx
    this.config = Object.assign({
      x: 0, // 圆心 x
      y: 0, // 圆心 y
      radius: 20, // 半径

      dirLine: false, // 是否画方向线
      direction: 0, // 指向角度

      lineWidth: 1, // 线宽
      strokeStyle: defaultColor, // 描边颜色
      ifLine: true, // 是否描边

      fill: true, // 是否填充
      fillStyle: defaultColor,

      shadow: true, // 是否有阴影
      shadowStyle: {  // 阴影样式
        offsetX: 0,
        offsetY: 0,
        blur: 10,
        color: defaultColor
      }
    }, config)

    this.draw()
  }

  draw() {
    this.ctx.beginPath()

    this.ctx.lineWidth = this.config.lineWidth

    if (this.config.shadow) {
      this.ctx.shadowOffsetX = this.config.shadowStyle.offsetX
      this.ctx.shadowOffsetY = this.config.shadowStyle.offsetY
      this.ctx.shadowBlur = this.config.shadowStyle.blur
      this.ctx.shadowColor = this.config.shadowStyle.color
    }

    if (this.config.fill) {
      this.ctx.fillStyle = this.config.fillStyle
      this.ctx.arc(this.config.x, this.config.y, this.config.radius, 0, 2 * Math.PI)
      this.ctx.fill()
    } else {
      this.ctx.strokeStyle = this.config.strokeStyle
      this.ctx.arc(this.config.x, this.config.y, this.config.radius, 0, 2 * Math.PI)
      this.ctx.stroke()
    }

    this.ctx.closePath()

    if (this.config.dirLine) {  // 用于测试小球运动是查看小球运动指向
      this.ctx.beginPath()

      this.ctx.strokeStyle = "black"
      this.ctx.moveTo(this.config.x, this.config.y)
      this.ctx.lineTo(this.config.x + Math.cos(this.config.direction * Math.PI / 180) * this.config.radius, this.config.y + Math.sin(this.config.direction * Math.PI / 180) * this.config.radius)
      this.ctx.stroke()

      this.ctx.closePath()
    }
  }
}

后记

在日复一日的CURD中,渐渐地已经忘记了这些花里花哨的东西了,接下来会慢慢的把这些东西都过一遍,看看能不能搞出一些小玩意出来。

有所思必有所得:

  1. canvas 的尽头是数学,如果想在这方面有所建树必须对数学有不低的造诣,我就没机会了,研究小球碰撞都把自己研究抑郁了. . .
  2. 想要实现漂亮的 canvas 动画效果,多刷 leetcode,每次刷到能优化数组效率的问题,都会对canvas动画有新的认识,研究烟花把我研究的快哭了 . . .
  3. 一个开箱即用的像素化思路,我们可以用它实现一个漂亮倒计时,也可以结合计时器模拟霓虹灯的效果,而且要比div更好用哟!
  4. 重新回忆了一下2年前学的一个 api。