我正在参加「掘金·启航计划」
前言
这么可爱的萌妹子不想拥有一个吗。学起来,自定义一个抱回家
一、前期准备
初始化HTML,利用通配符 *
去除默认样式
<!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>
二、正文开始
先讲一下实现思路:
- 通过图片上传获取到图片文件,并生成图片地址
- 通过监听图片加载完成事件,将图片画在
canvas1
上 - 获取 canvas1 上的像素点数据,通过压缩,计算灰度,定位等操作,在
canvas2
上画出对应的点
2.1 图片上传
- 我们使用
input
来上传图片,通过onchange
获取到图片的 file 文件。 - 使用
createObjectURL
生成图片地址
<!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
- 监听图片的
onload
事件 - 使用
canvas
的drawImage
方法将图片画在canvas1
上 - 通过
canvas
的getImageData
方法获取canvas1
的像素点数据
<!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
上。先上代码
// 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 压缩
- 为啥要压缩? 如图所示,右边是我们要现实的最终效果,是可以清晰看出图片是由深浅不一的字母 a 组成的(如果觉得不够直观,图片右击,在新标签页中打开图片)。不进行压缩的话,即每个像素点都使用 a 来展示的话,字母 a 会因为太小而无法直观看出,如图左边部分效果。
- 如何压缩?
如图所示,假设红色框为一整张图片,每一个小灰格子为一个像素点,蓝色格子为四个像素点组成的。我们将蓝色格子的左上角的像素点的颜色作为这个蓝色格子颜色,这样我们就把四个像素点压缩成一个。
2.3.2 计算灰度
- 计算灰度前,我们先认识一下通过
canvas
的getImageData
获取到的像素点数据。
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 定位
在认识像素点数据的构成后,我们得知所有像素点的色值都放在同一个数据里,这样无法判断像素点在图片上的位置,因此需要通过计算来定位。
- 计算每个像素点的位置 (x , y)
width
为一整行的像素点的个数。因为一个像素点由四个色值组成,所以一整行所占的数组长度为width*4
。
// 其中 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))
}
- 根据前面压缩的算法,以左上角第一个像素点的颜色计算压缩后的颜色和位置
// 其中 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上画点
// 填充文字
ctx2.font = '12px'
ctx2.fillStyle = `rgba(${gray},${gray},${gray},${a})` // gray为计算出来的灰度值,a为像素点的透明度
ctx2.fillText('a', x, y) // x,y为计算出来的定位。‘a’为填充的字母,可为任意值
三、码上掘金
四、结束语
此篇文章介绍的是通过图片生成,用视频的话会更有意思。这里讲一下大概思路:将img
标签换成video
标签,播放视频时,利用 requestAnimationFrame
循环将视频的每一帧通过 ctx1.drawImage(video,0, 0, width,height)
画在 canvas1
上,其他步骤差不多。有兴趣的同学可以自己动手实现下。