前言
好久之前写的一个两个小demo了,记得原来看过大佬写的一篇文章 《# 产品经理:你能不能用div给我画条龙?》,学习到了 context.getImageData()
这个api,然后基于这个 api 自己简单研究了一下做了两个小效果:
1> 像素文字 - 万物皆可像素化。
2> 图片溶解进入 - 解锁更多图片轮播特效。
因为性能问题,颗粒选择的比较大。
思路简介
前言中已经说明了,核心 API 只用到了 canvas.context.getImageData
。
这个 api 需要传入四个参数,我把它称为 canvas 的四大金刚:
txt
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上铺满小球,然后看下圆心的位置是白色还是别的颜色就行啦,如果是白色,小球就变成透明的,如果是其他颜色就根据自己的喜欢画一个小球。当然也可以变成其他的形状。
这就是像素文字的思路,升级版本就是再用一个数组把这些小球搜集起来,使用数学只是让他去动态的显示。
进阶思路:
- 结合计时器实现霓虹灯效果
- 图片像素化
- 图片裁切工具的实现(已经实现了,有机会分享)
2. 溶解图片
溶解图片其实就是按照固定的大小去切割原图片。依次存在一个数组里面。
例如原图片的大小为 500 * 400,我们将其切割成 10 * 10 的小格子,使用 getImageData 获取一下信息和坐标,并存进一个数组里面。然后再将其按照一定的顺序绘制出来就好了。
绘制的方法是:putImageData。
进阶思路:
- 实现图片轮播,即两张图片来回渲染
- 更多的渲染效果,从左到右,从上到下,中心到四周,四周到中心,菱形,回字形,扇形...
- 撕裂图片,折叠图片. . .
核心代码
核心代码主要分为四个:
- canvas 引擎,简单封装了几个api用来初始化一个 canvas
- px-img.js,像素化图片的核心主逻辑,继承自 Canvas。
- px-word.js,像素化文字的核心主逻辑,继承自 Canvas。
- index.html
canvas 引擎
js
// 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
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
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
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 绘制小球的类
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中,渐渐地已经忘记了这些花里花哨的东西了,接下来会慢慢的把这些东西都过一遍,看看能不能搞出一些小玩意出来。
有所思必有所得:
- canvas 的尽头是数学,如果想在这方面有所建树必须对数学有不低的造诣,我就没机会了,研究小球碰撞都把自己研究抑郁了. . .
- 想要实现漂亮的 canvas 动画效果,多刷 leetcode,每次刷到能优化数组效率的问题,都会对canvas动画有新的认识,研究烟花把我研究的快哭了 . . .
- 一个开箱即用的像素化思路,我们可以用它实现一个漂亮倒计时,也可以结合计时器模拟霓虹灯的效果,而且要比div更好用哟!
- 重新回忆了一下2年前学的一个 api。