Canvas 是 HTML5 新增的一个元素,功能非常强大,通过它可以用于在网页上绘制图形、动画等。这里通过使用 Canvas 将用户上传的图片,根据选择需要抠掉的像素点的RGB数值范围,最后生成抠图后的图片,从而实现了一个简单的图片抠图工具。
上传图片
首先是上传图片,这个没什么好讲的,直接看代码:
<div class="block">
<div class="title">原图上传</div>
<div class="main">
<input id="upload-btn" type="file" accept="image/*" />
<div class="img-box" id="origin"></div>
</div>
</div>
<script lang="javascript">
let originImgDom
document.getElementById('upload-btn').addEventListener('change', e => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
if (!originImgDom) {
originImgDom = document.createElement('img')
originImgDom.src = reader.result
document.getElementById('origin').append(originImgDom)
} else {
originImgDom.src = reader.result
}
}
})
</script>
选择图片的 RGB 数值范围
在进行抠图之前,需要选择抠图区域的 RGB 数值范围。先看代码:
<div id="rgb-inputs">
<div>
R:
<select data-key="r">
<option value="ge">>=</option>
<option value="le"><=</option>
<option value="eq">===</option>
</select>
<input data-key="r" type="number" min="0" max="255" />
</div>
<div>
G:
<select data-key="g">
<option value="ge">>=</option>
<option value="le"><=</option>
<option value="eq">===</option>
</select>
<input data-key="g" type="number" min="0" max="255" />
</div>
<div>
B:
<select data-key="b">
<option value="ge">>=</option>
<option value="le"><=</option>
<option value="eq">===</option>
</select>
<input data-key="b" type="number" min="0" max="255" />
</div>
<div>
<button id="create-btn">生 成</button>
</div>
</div>
<script lang="javascript">
const rgbOpts = {
r: { val: undefined, act: 'ge' },
g: { val: undefined, act: 'ge' },
b: { val: undefined, act: 'ge' }
}
document.getElementById('rgb-inputs').addEventListener('change', e => {
const target = e.target
const key = target.dataset.key
if (key && Object.prototype.hasOwnProperty.call(rgbOpts, key)) {
const tag = target.tagName.toUpperCase()
if (tag === 'INPUT') {
rgbOpts[key].val = parseInt(target.value)
} else if (tag === 'SELECT') {
rgbOpts[key].act = target.value
}
}
})
</script>
这段代码的作用是监听页面上 RGB 数值输入框和比较符号下拉框的变化,然后更新 rgbOpts 对象中对应属性的 val 和 act 值。由于为一个个的 input 绑定事件太繁琐,就使用了事件代理,以及通过dataset 确定每个操作对应的 key 值,再通过 HTML 标签名判断需要赋值到哪个属性上。rgbOpts 对象用于存储用户选择的RGB数值和比较符号,它会被传递给 compare 函数,用于比较图片中的像素点是否符合要求。
实现抠图
function imageMatting(originImage) {
return new Promise((resolve, reject) => {
let cvs = document.createElement('canvas')
const ctx = cvs.getContext('2d')
const img = new Image()
img.src = originImage
img.onload = () => {
const { width, height } = img
cvs.width = width
cvs.height = height
ctx.drawImage(img, 0, 0, width, height)
const originImageData = ctx.getImageData(0, 0, width, height)
const data = originImageData.data
for (let i = 0; i < data.length; i += 4) {
const rgbObj = { r: data[i], g: data[i + 1], b: data[i + 2] }
if (compare(rgbObj)) data[i + 3] = 0
}
ctx.putImageData(originImageData, 0, 0)
resolve(cvs.toDataURL('image/png'))
cvs = null
}
img.onerror = err => {
reject(err)
}
})
}
这段代码首先将图片绘制在 canvas 中,通过 canvas 中的 getImageData 方法获取图片的 RGB 数值。然后遍历取得的 RGB 数值,通过 compare 函数进行比较。
通过比较的结果,对符合要求的图片像素点的 alpha 通道值设为0,从而实现图片抠图的效果。之后再使用 putImageData 方法将图片绘制回 canvas 中,再通过 canvas.toDataURL('image/png') 获得抠图后的图片。
其中的 compare 函数实现如下:
function compare(rgbObj) {
let isDiff = false
const compareKeys = Object.keys(rgbOpts).filter(key => rgbOpts[key].val === 0 || rgbOpts[key].val)
if (compareKeys.length) {
isDiff = compareKeys.every(key => {
let flag = false
const { act, val } = rgbOpts[key]
if (act === 'eq') {
flag = rgbObj[key] === val
} else if (act === 'ge') {
flag = rgbObj[key] >= val
} else if (act === 'le') {
flag = rgbObj[key] <= val
}
return flag
})
}
return isDiff
}
它首先会对用户输入的 RGB 数值进行过滤,仅比较用户已输入的 RGB 值。然后根据用户选择的比较操作和 RGB 值对图片像素进行比较,返回比较结果。
完整代码
<!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>
body {
background-color: rgba(0, 0, 0, 0.05);
}
.block {
max-width: 1000px;
min-height: 100px;
padding: 10px;
margin: 5px;
border: 1px solid #999;
}
.block + .block {
margin-top: 10px;
}
.block .title {
padding-bottom: 10px;
font-size: 16px;
font-weight: bold;
}
.block .main {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.block .img-box {
width: 300px;
}
.block .img-box img {
display: block;
width: 300px;
object-fit: contain;
}
#rgb-inputs {
flex: 1;
}
#rgb-inputs div {
display: flex;
align-items: center;
padding: 5px 0;
}
#rgb-inputs div select {
margin: 0 5px;
}
</style>
</head>
<body>
<div class="block">
<div class="title">原图上传</div>
<div class="main">
<input id="upload-btn" type="file" accept="image/*" />
<div class="img-box" id="origin"></div>
</div>
</div>
<div class="block">
<div class="title">图片生成</div>
<div class="desc">通过比较以下 RGB 数值与图片每个像素点上的 RGB 数值,生成对应的图片</div>
<div class="main">
<div id="rgb-inputs">
<div>
R:
<select data-key="r">
<option value="ge">>=</option>
<option value="le"><=</option>
<option value="eq">===</option>
</select>
<input data-key="r" type="number" min="0" max="255" />
</div>
<div>
G:
<select data-key="g">
<option value="ge">>=</option>
<option value="le"><=</option>
<option value="eq">===</option>
</select>
<input data-key="g" type="number" min="0" max="255" />
</div>
<div>
B:
<select data-key="b">
<option value="ge">>=</option>
<option value="le"><=</option>
<option value="eq">===</option>
</select>
<input data-key="b" type="number" min="0" max="255" />
</div>
<div>
<button id="create-btn">生 成</button>
</div>
</div>
<div class="img-box" id="modify"></div>
</div>
</div>
<script lang="javascript">
const rgbOpts = {
r: { val: undefined, act: 'ge' },
g: { val: undefined, act: 'ge' },
b: { val: undefined, act: 'ge' }
}
document.getElementById('rgb-inputs').addEventListener('change', e => {
const target = e.target
const key = target.dataset.key
if (key && Object.prototype.hasOwnProperty.call(rgbOpts, key)) {
const tag = target.tagName.toUpperCase()
if (tag === 'INPUT') {
rgbOpts[key].val = parseInt(target.value)
} else if (tag === 'SELECT') {
rgbOpts[key].act = target.value
}
}
})
let originImgDom
document.getElementById('upload-btn').addEventListener('change', e => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
if (!originImgDom) {
originImgDom = document.createElement('img')
originImgDom.src = reader.result
document.getElementById('origin').append(originImgDom)
} else {
originImgDom.src = reader.result
}
}
})
let modifyImgDom
document.getElementById('create-btn').addEventListener('click', async () => {
if (!originImgDom) return
const img = await imageMatting(originImgDom.src)
if (!modifyImgDom) {
modifyImgDom = document.createElement('img')
modifyImgDom.src = img
document.getElementById('modify').append(modifyImgDom)
} else {
modifyImgDom.src = img
}
})
function imageMatting(originImage) {
return new Promise((resolve, reject) => {
let cvs = document.createElement('canvas')
const ctx = cvs.getContext('2d')
const img = new Image()
img.src = originImage
img.onload = () => {
const { width, height } = img
cvs.width = width
cvs.height = height
ctx.drawImage(img, 0, 0, width, height)
const originImageData = ctx.getImageData(0, 0, width, height)
const data = originImageData.data
for (let i = 0; i < data.length; i += 4) {
const rgbObj = { r: data[i], g: data[i + 1], b: data[i + 2] }
if (compare(rgbObj)) data[i + 3] = 0
}
ctx.putImageData(originImageData, 0, 0)
resolve(cvs.toDataURL('image/png'))
cvs = null
}
img.onerror = err => {
reject(err)
}
})
}
function compare(rgbObj) {
let isDiff = false
const compareKeys = Object.keys(rgbOpts).filter(key => rgbOpts[key].val === 0 || rgbOpts[key].val)
if (compareKeys.length) {
isDiff = compareKeys.every(key => {
let flag = false
const { act, val } = rgbOpts[key]
if (act === 'eq') {
flag = rgbObj[key] === val
} else if (act === 'ge') {
flag = rgbObj[key] >= val
} else if (act === 'le') {
flag = rgbObj[key] <= val
}
return flag
})
}
return isDiff
}
</script>
</body>
</html>