前言

这么可爱的萌妹子不想拥有一个吗。学起来,自定义一个抱回家

一、前期准备

初始化HTML,利用通配符 * 去除默认样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="divport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*,*::before,*::after{
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="app"></div>
</body>
<script ></script>
</html>

二、正文开始

先讲一下实现思路:

  1. 通过图片上传获取到图片文件,并生成图片地址
  2. 通过监听图片加载完成事件,将图片画在 canvas1
  3. 获取 canvas1 上的像素点数据,通过压缩,计算灰度,定位等操作,在 canvas2 上画出对应的点

2.1 图片上传

  1. 我们使用 input 来上传图片,通过 onchange 获取到图片的 file 文件。
  2. 使用 createObjectURL 生成图片地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="divport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*,*::before,*::after{
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="app">
<div><input type="file" accept="image/*" onChange="uploadImage(this)" /></div>
<img src="" id="image" style="display: none;">
</div>
</body>
<script >
const image = document.getElementById('image')


// 图片上传事件
function uploadImage(obj){
// 转换格式
const newSrc = getObjectURL(obj.files[0]);
testImage.src = newSrc;
}

// 生成file文件的临时路径
function getObjectURL(file) {
let url = null ;
// 下面函数执行的效果是一样的,只是需要针对不同的浏览器执行不同的 js 函数而已
if (window.createObjectURL!=undefined) { // basic
url = window.createObjectURL(file) ;
} else if (window.URL!=undefined) { // mozilla(firefox)
url = window.URL.createObjectURL(file) ;
} else if (window.webkitURL!=undefined) { // webkit or chrome
url = window.webkitURL.createObjectURL(file) ;
}
return url ;
}

</script>
</html>

2.2 画 canvas1

  1. 监听图片的 onload 事件
  2. 使用 canvasdrawImage方法将图片画在 canvas1
  3. 通过 canvasgetImageData方法获取 canvas1 的像素点数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="divport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*,*::before,*::after{
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="app">
<div><input type="file" accept="image/*" onChange="uploadImage(this)" /></div>

<img src="" id="image" style="display: none;">
<canvas id="canvas1"></canvas>
</div>
</body>
<script >
const image = document.getElementById('image')
const canvas1 = document.getElementById('canvas1')

// ... 上一步操作的代码与当前步骤无关,先隐藏

// 监听图片加载完成事件
image.onload = ()=>{

const ctx1 = canvas1.getContext('2d')

const { width, height } = image

canvas1.width = width
canvas1.height = height

// 1. 将图片画在画布 1 中
ctx1.drawImage(image,0, 0, width,height )

// 2. 获取canvas1的图片像素点数据
const imageData = ctx1.getImageData( 0, 0, width, height).data

}

</script>
</html>

2.3 画 canvas2

在获取到 canvas1 的像素点数据后,通过压缩,计算灰度,定位,再画在 canvas2 上。先上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
 // html
<div id="app">
<div><input type="file" accept="image/*" onChange="uploadImage(this)" /></div>

<img src="" id="image" style="display: none;">
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
</div>

// js
const image = document.getElementById('image')
const canvas1 = document.getElementById('canvas1')
const canvas2 = document.getElementById('canvas2')


// 监听图片加载完成事件
image.onload = ()=>{

const ctx1 = canvas1.getContext('2d')
const ctx2 = canvas2.getContext('2d')

// 计算尺寸
const { width, height } = image

canvas1.width = width
canvas1.height = height

canvas2.width = width
canvas2.height = height

// 1. 将图片画在画布 1 中
ctx1.drawImage(image, 0, 0, width, height)

// 2. 获取canvas1的图片像素点数据
const imageData = ctx1.getImageData( 0, 0, width, height).data

// 3. 根据 canvas1 的像素数据 在 canvas2 上画点
for(let i = 0; i < imageData.length; i += 4){

// 计算当前像素点的 x,y(第 x 列 ,第 y 行 )
const x = parseInt(i % (width * 4) / 4)
const y = parseInt(i / (width * 4))

// 像素点压缩倍数
const bl = 6

// 压缩像素点 => 判断当前 x, y 与 压缩倍数的余数是否都为 0
if(x % bl === 0 && y % bl === 0 ){

// 计算灰度
const [r, g, b, a] = [imageData[i], imageData[i + 1], imageData[i + 2], imageData[i + 3]]
const gray = (r + g + b) / 3

// 填充文字
ctx2.font = '12px'
ctx2.fillStyle = `rgba(${gray},${gray},${gray},${a})`
ctx2.fillText('a', x, y)
}
}
}

2.3.1 压缩

  1. 为啥要压缩? 如图所示,右边是我们要现实的最终效果,是可以清晰看出图片是由深浅不一的字母 a 组成的(如果觉得不够直观,图片右击,在新标签页中打开图片)。不进行压缩的话,即每个像素点都使用 a 来展示的话,字母 a 会因为太小而无法直观看出,如图左边部分效果。
  2. 如何压缩? 如图所示,假设红色框为一整张图片,每一个小灰格子为一个像素点,蓝色格子为四个像素点组成的。我们将蓝色格子的左上角的像素点的颜色作为这个蓝色格子颜色,这样我们就把四个像素点压缩成一个。

2.3.2 计算灰度

  1. 计算灰度前,我们先认识一下通过 canvasgetImageData 获取到的像素点数据。
`imageData` 为像素点颜色的RGBA色值数组构成,即[R,G,B,A,R,G,B,A,R,G,B,A]。数组每四个值代表一个像素点的颜色。

\2. 计算灰度
一个像素点的灰度值即为 gray=(R+G+B)/3。该像素点的灰度色值即为rgba(gray,gray,gray,A)

2.3.3 定位

在认识像素点数据的构成后,我们得知所有像素点的色值都放在同一个数据里,这样无法判断像素点在图片上的位置,因此需要通过计算来定位。

  1. 计算每个像素点的位置 (x , y)
    width 为一整行的像素点的个数。因为一个像素点由四个色值组成,所以一整行所占的数组长度为width*4
1
2
3
4
5
6
7
8
// 其中 width, height 为 canvas1 的宽高
const imageData = ctx1.getImageData( 0, 0, width, height).data

// 计算当前像素点的 x,y(第 x 列 ,第 y 行 )
for(let i = 0; i < imageData.length; i += 4){
const x = parseInt(i % (width * 4) / 4)
const y = parseInt(i / (width * 4))
}
  1. 根据前面压缩的算法,以左上角第一个像素点的颜色计算压缩后的颜色和位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 其中 width, height 为 canvas1 的宽高
const imageData = ctx1.getImageData( 0, 0, width, height).data

// 像素点压缩倍数
const bl = 6

// 计算当前像素点的 x,y(第 x 列 ,第 y 行 )
for(let i = 0; i < imageData.length; i += 4){
const x = parseInt(i % (width * 4) / 4)
const y = parseInt(i / (width * 4))

if(x % bl === 0 && y % bl === 0 ){
// 此时,当前像素点即为压缩后左上角的像素点
}
}

2.3.4 在canvas2上画点

1
2
3
4
5
 // 填充文字
ctx2.font = '12px'
ctx2.fillStyle = `rgba(${gray},${gray},${gray},${a})` // gray为计算出来的灰度值,a为像素点的透明度
ctx2.fillText('a', x, y) // x,y为计算出来的定位。‘a’为填充的字母,可为任意值
复制代码

三、完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="divport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*,*::before,*::after{
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="app">
<div><input type="file" accept="image/*" onChange="uploadImage(this)" /></div>

<img src="" id="image" style="display: none;">
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
</div>
</body>
<script >
const image = document.getElementById('image')
const canvas1 = document.getElementById('canvas1')
const canvas2 = document.getElementById('canvas2')

// 点击上传图片
function uploadImage(obj){
// 转换格式
const newSrc = getObjectURL(obj.files[0]);
image.src = newSrc;
}

// 生成file文件的临时路径
function getObjectURL(file) {
let url = null ;
// 下面函数执行的效果是一样的,只是需要针对不同的浏览器执行不同的 js 函数而已
if (window.createObjectURL!=undefined) { // basic
url = window.createObjectURL(file) ;
} else if (window.URL!=undefined) { // mozilla(firefox)
url = window.URL.createObjectURL(file) ;
} else if (window.webkitURL!=undefined) { // webkit or chrome
url = window.webkitURL.createObjectURL(file) ;
}
return url ;
}

// 计算尺寸
function computedSize(size){
const maxWidth = 600

let { width, height } = size

width > maxWidth && (height = height * maxWidth / width)
width > maxWidth && (width = maxWidth)

return { width, height }
}

// 监听图片加载完成事件
image.onload = ()=>{

const ctx1 = canvas1.getContext('2d')
const ctx2 = canvas2.getContext('2d')

// 计算尺寸
const { width, height } = computedSize(image)

canvas1.width = width
canvas1.height = height

canvas2.width = width
canvas2.height = height

// 1. 将图片画在画布 1 中
ctx1.drawImage(image, 0, 0, width, height)

// 2. 获取canvas1的图片像素点数据
const imageData = ctx1.getImageData( 0, 0, width, height).data

// 3. 根据 canvas1 的像素数据 在 canvas2 上画点
for(let i = 0; i < imageData.length; i += 4){

// 计算当前像素点的 x,y(第 x 列 ,第 y 行 )
const x = parseInt(i % (width * 4) / 4)
const y = parseInt(i / (width * 4))

// 像素点压缩倍数
const bl = 6

// 压缩像素点 => 判断当前 x, y 与 压缩倍数的余数是否都为 0
if(x % bl === 0 && y % bl === 0 ){

// 计算灰度
const [r, g, b, a] = [imageData[i], imageData[i + 1], imageData[i + 2], imageData[i + 3]]
const gray = (r + g + b) / 3

// 填充文字
ctx2.font = '12px'
ctx2.fillStyle = `rgba(${gray},${gray},${gray},${a})`
ctx2.fillText('a', x, y)
}
}
}

</script>
</html>

四、升级版

​ 通过文字 ,图片,视频 生成图像

  1. [文字效果]: http://wuxingxi.top/text-image/demo1.html “ “

视频效果大致思路:将img标签换成video标签,播放视频时,利用 requestAnimationFrame 循环将视频的每一帧通过 ctx1.drawImage(video,0, 0, width,height)画在 canvas