我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛
前言
大家好,我是林三心,中秋即将来临,预祝大家中秋快乐!!!我在想,关于中秋,我能写点什么分享给大家呢?这一天,我在看《西游记》,突然想到了我儿时的女神,是谁呢?就是天蓬元帅苦苦追求的嫦娥仙子,那可是我儿时的女神啊。嫦娥奔月的故事我相信大家都听过
前段时间,我看了荣顶大佬的这篇我用 10000 张图片合成我们美好的瞬间,发现原来图片的主色调是那样计算的,学到了很多。于是我站在荣顶巨人的肩膀上,用王者荣耀里嫦娥这一角色的不同图片,加上王者荣耀里后羿这一角色的不同图片(后羿是嫦娥老公),组成了我儿时女神——西游记嫦娥的图像。
开搞!!!
前置准备
由于需要用到canvas
,以及一些图片上传按钮,所以咱们先把HTML的代码写好,fabric
是一个非常实用的canvas库
,他提供了很多api
,方便我们更方便地在canvas
上画出可操作性的图像。fabric的代码在这里fabric库代码,创建一个文件,复制过来就行
<!-- 引入fabric这个库 -->
<script src="./fabric.js"></script>
<!-- 用来选主图 -->
<input type="file" id="mainInput" />
<!-- 用来选组成图片 多选 -->
<input type="file" id="composeInput" multiple />
<!-- 生成效果 -->
<button id="finishBtn">生成组合图</button>
<!-- 一块800 * 800 的canvas画布 -->
<canvas id="canvas" width="800" height="800"></div>
const mainInput = document.getElementById('mainInput') // 获取上传主图按钮的DOM
const composeInput = document.getElementById('composeInput') // 获取多传组合图片按钮的DOM
const finishBtn = document.getElementById('finishBtn') // 获取生成最终结果按钮的DOM
const exportBtn = document.getElementById('exportBtn') // 获取倒出图片按钮的DOM
const canvas = new fabric.Canvas('canvas') // 实例一个fabric的canvas对象,传入的是canvas的id
const ctx = canvas.getContext('2d') // 绘制2d图像
画出嫦娥姐姐
咱们需要先在页面上画出嫦娥姐姐的原始图像,图像如下
那咱们要怎么把一张图像画到HTML页面中呢?答案是canvas
,那咱们就先把这个图像绘制到页面上去吧!
咱们都知道,图片直接上传到浏览器,是不可能直接就给你绘制出来的,比如原生的canvas
需要把你这张图片转为base64
格式才能绘制到页面,而fabric
提供了一个fabric.Image.fromURL(url, img => {})
,需要传入一个图片的blob地址
,才能生成一张可绘制到页面的图片。那咱们怎么把咱们上传的图片转成blob地址
呢?其实JavaScript已经给我们提供了这么一个方法window.URL.createObjectURL
,用它就能实现啦。
// 监听上传主图按钮的上传变化
mainInput.onchange = function (e) {
// 只有一个图片,所以是e.target.files[0]
const url = window.URL.createObjectURL(e.target.files[0])
// 将生成的blob地址传入
drawMainImage(url)
}
function drawMainImage(url) {
// 接收传进来的url
fabric.Image.fromURL(url, img => {
console.log(img)
// 转换成功后的回调
// fabric.Image.fromURL会将此url转换成一张图片
// 需要缩放图片,height > width 就按照 width的缩放比例,反之用height的缩放比例
// 反过来是为了能充满整张图
const scale = img.height > img.width ? canvas.width / img.width : canvas.height / img.height
// 设置这张图像绘制的参数
img.set({
left: canvas.width / 2, // 距离canvas画板左边一半宽度
originX: 'center', // 水平方向居中
top: 0, // 距离顶部距离为0
scaleX: scale, // 图像水平缩放比例
scaleY: scale, // 图像竖直缩放比例
selectable: false // 不可操作,默认是true
})
// 把此图像绘制到canvas画板中
canvas.add(img)
// 图片绘制完成的回调函数
img.on('added', e => {
console.log('图片加载完成了啊')
setTimeout(() => {
// 绘制完成后,获取此图像中10000个格子的色彩信息,后面会实现
getMainRGBA()
}, 200) // 这里用延时器,是因为图像绘制有延迟
// 而这里需要保证图像真的完全绘制完,再去获取色彩信息
})
})
}
10000个格子
咱们都知道,咱们的canvas画布是800 * 800
的,咱们想要分成10000
个格子,那么每个格子就是8 * 8
。实现之前,咱们现在认识一个canvas获取色彩信息的api——ctx.getImageData(x, y, width, height)
,他接收4个参数
- x:获取范围的x坐标
- y:获取范围的y坐标
- width:获取范围的宽度
- height:获取范围的高度
他会返回一个对象,对象里有一个属性
data
,这个data
就是此范围的色彩信息,比如
const { data } = ctx.getImageData(40, 40, 8, 8)
那么data就是x为40,y为40,宽度高度都是8,这一个范围内的色彩信息,这个色彩信息是一个数组,比如这个范围是8 * 8
,那么这个数组就有8 * 8 * 4 = 256
个元素,因为8 * 8
就有64个像素,而每一个像素的rgba(r, g, b, a)
是4个值,所以这个数组就有8 * 8 * 4 = 256
个元素,所以下面咱们要4个4个收集,因为每4个元素就是一个像素的rgba
,而一个8 * 8
的格子,就会有64个像素,也就是64个rgba数组
let mainColors = [] // 用来收集1000个格子的主色调rgba,后面会实现
function getMainRGBA() {
const rgbas = [] // 用来收集10000个格子的色彩信息
for (let y = 0; y < canvas.height; y += 8) {
for (let x = 0; x < canvas.width; x += 8) {
// 获取每一块格子的色彩data
const { data } = ctx.getImageData(x, y, 8, 8)
rgbas[y / 8 * 100 + x / 8] = []
for (let i = 0; i < data.length; i += 4) {
// 4个4个收集,因为每4个就组成一个像素的rgba
rgbas[y / 8 * 100 + x / 8].push([
data[i],
data[i + 1],
data[i + 2],
data[i + 3]
])
}
}
}
// 算出10000个格子,每个格子的主色调,后面实现
mainColors = getMainColorStyle(rgbas)
}
每个格子主色调
上面咱们已经获取到了10000个格子,他们每个格子都拥有64个像素,也就是64个rgba数组,那每个格子拥有64个rgba,咱们怎么才能得到这个格子的主色调呢?很简单嘛,rgba(r, g, b, a)有4个值,咱们算出这4个值各自的平均值,然后组成一个新的rgba,这个rgba就是每个格子的主色调啦!!!
function getMainColorStyle(rgbas) {
const mainColors = []
for (let colors of rgbas) {
let r = 0, g = 0, b = 0, a = 0
for (let color of colors) {
// 累加
r += color[0]
g += color[1]
b += color[2]
a += color[3]
}
mainColors.push([
Math.round(r / colors.length), // 取平均值
Math.round(g / colors.length), // 取平均值
Math.round(b / colors.length), // 取平均值
Math.round(a / colors.length) // 取平均值
])
}
return mainColors
}
上传组合图片
主图片的功能都实现了,现在就剩组合图片了,咱们可以多传组合图片。但是我们要算出每一张组合图片的主色调,因为后面咱们要主色调来对比那10000个格子的主色调,决定哪个格子放哪张组合图片
这里有一个问题要强调一下,想要获取图片的颜色信息,就得把图片画到canvas画板上才能获取,但是咱们又不想在这里把图片画到页面上的canvas里,咋办呢?咱们可以创建临时的canvas画板,画完,获取完颜色信息,咱们就把它销毁
let composeColors = [] // 收集组合图片主色调
// 监听多选按钮的上传
composeInput.onchange = async function (e) {
const promises = [] // promises数组
for (file of e.target.files) {
// 将每张图片生成blob地址
const url = window.URL.createObjectURL(file)
// 传入blob地址
promises.push(getComposeColorStyle(url, file.name))
}
const res = await Promise.all(promises) // 顺序执行所有promise
composeColors = res // 将结果赋值给composeColors
}
function getComposeColorStyle(url, name) {
return new Promise(resolve => {
// 创建一个 20 * 20的canvas画板
// 理论上这里宽高可以自己定,但是越大,色彩会越精准
const composeCanvas = document.createElement('canvas')
const composeCtx = composeCanvas.getContext('2d')
composeCanvas.width = 20
composeCanvas.height = 20
// 创建img对象
const img = new Image()
img.src = url
img.onload = function () {
const scale = composeCanvas.height / composeCanvas.height
img.height *= scale
img.width *= scale
// 将img画到临时canvas画板
composeCtx.drawImage(img, 0, 0, composeCanvas.width, composeCanvas.height)
// 获取颜色信息data
const { data } = composeCtx.getImageData(0, 0, composeCanvas.width, composeCanvas.height)
// 累加 r,g,b,a
let r = 0, g = 0, b = 0, a = 0
for (let i = 0; i < data.length; i += 4) {
r += data[i]
g += data[i + 1]
b += data[i + 2]
a += data[i + 3]
}
resolve({
// 主色调
rgba: [
Math.round(r / (data.length / 4)), // 取平均值
Math.round(g / (data.length / 4)), // 取平均值
Math.round(b / (data.length / 4)), // 取平均值
Math.round(a / (data.length / 4)) // 取平均值
],
url,
name
})
}
})
}
对比主色调并绘制
- canvas画板中的嫦娥姐姐有10000个格子,每个格子都有自己的主色调
- 上传的每张组合图片也有自己的主色调
那咱们要怎么实现最终效果呢?很简单嘛!!!遍历10000个格子,拿着每个格子的主色调,去跟每张组合图片的主色调一一对比,最接近色调的图片,就拿来绘制到这个8 * 8
的格子里。
// 监听完成按钮
finishBtn.onclick = finishCompose
function finishCompose() {
const urls = [] // 收集最终绘制的10000张图片
for (let main of mainColors) { // 遍历10000个格子主色调
let closestIndex = 0 // 最接近主色调的图片的index
let minimumDiff = Infinity // 相差值
for (let i = 0; i < composeColors.length; i++) {
const { rgba } = composeColors[i]
// 格子主色调rgba四个值,减去图片主色调rgba四个值,的平方
const diff = (rgba[0] - main[0]) ** 2 + (rgba[1] - main[1]) ** 2
+ (rgba[2] - main[2]) ** 2 + (rgba[3] - main[3]) ** 2
// 然后开跟比较
if (Math.sqrt(diff) < minimumDiff) {
minimumDiff = Math.sqrt(diff)
closestIndex = i
}
}
// 把最小色差的图片url添加进数组urls
urls.push(composeColors[closestIndex].url)
}
// 将urls中10000张图片,分别绘制在对应的10000个格子中
for (let i = 0; i < urls.length; i++) {
fabric.Image.fromURL(urls[i], img => {
const scale = img.height > img.width ? 8 / img.width : 8 / img.height;
img.set({
left: i % 100 * 8,
top: Math.floor(i / 100) * 8,
originX: "center",
scaleX: scale,
scaleY: scale,
});
canvas.add(img)
})
}
}
导出图片
// 监听导出按钮
exportBtn.onclick = exportCanvas
//导出图片
function exportCanvas() {
const dataURL = canvas.toDataURL({
width: canvas.width,
height: canvas.height,
left: 0,
top: 0,
format: "png",
});
const link = document.createElement("a");
link.download = "嫦娥姐姐.png";
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
最终效果
彩蛋
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼
哈哈我用王者荣耀猪八戒的图片,组成了我自己
完整代码
<!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>
<!-- 引入flare这个库 -->
<script src="./flare.js"></script>
</head>
<body>
<!-- 用来选主图 -->
<input type="file" id="mainInput" />
<!-- 用来选组成图片 多选 -->
<input type="file" id="composeInput" multiple />
<!-- 生成效果 -->
<button id="finishBtn">生成组合图</button>
<!-- 导出图片 -->
<button id="exportBtn">导出图片</button>
<!-- 一块800 * 800 的canvas画布 -->
<canvas id="canvas" width="800" height="800"></div>
</body>
<script src="./index2.js"></script>
</html>
const mainInput = document.getElementById('mainInput') // 获取上传主图按钮的DOM
const composeInput = document.getElementById('composeInput') // 获取多传组合图片按钮的DOM
const finishBtn = document.getElementById('finishBtn') // 获取生成最终结果按钮的DOM
const exportBtn = document.getElementById('exportBtn') // 获取倒出图片按钮的DOM
const canvas = new fabric.Canvas('canvas') // 实例一个flare的canvas对象,传入的是canvas的id
const ctx = canvas.getContext('2d') // 绘制2d图像
let mainColors = []
let composeColors = []
// 监听上传主图按钮的上传变化
mainInput.onchange = function (e) {
// 只有一个图片,所以是e.target.files[0]
const url = window.URL.createObjectURL(e.target.files[0])
// 将生成的blob地址传入
drawMainImage(url)
}
composeInput.onchange = async function (e) {
const promises = [] // promises数组
for (file of e.target.files) {
// 将每张图片生成blob地址
const url = window.URL.createObjectURL(file)
// 传入blob地址
promises.push(getComposeColorStyle(url, file.name))
}
const res = await Promise.all(promises) // 顺序执行所有promise
composeColors = res // 将结果赋值给composeColors
}
// 监听完成按钮
finishBtn.onclick = finishCompose
// 监听导出按钮
exportBtn.onclick = exportCanvas
function drawMainImage(url) {
// 接收传进来的url
fabric.Image.fromURL(url, img => {
console.log(img)
// 转换成功后的回调
// fabric.Image.fromURL会将此url转换成一张图片
// 需要缩放图片,height > width 就按照 width的缩放比例,反之用height的缩放比例
// 反过来是为了能充满整张图
const scale = img.height > img.width ? canvas.width / img.width : canvas.height / img.height
// 设置这张图像绘制的参数
img.set({
left: canvas.width / 2, // 距离canvas画板左边一半宽度
originX: 'center', // 水平方向居中
top: 0, // 距离顶部距离为0
scaleX: scale, // 图像水平缩放比例
scaleY: scale, // 图像竖直缩放比例
selectable: false // 不可操作,默认是true
})
// 图片绘制完成的回调函数
img.on('added', e => {
console.log('图片加载完成了啊')
setTimeout(() => {
// 绘制完成后,获取此图像中10000个格子的色彩信息
getMainRGBA()
}, 200) // 这里用延时器,是因为图像绘制有延迟
// 而这里需要保证图像真的完全绘制完,再去获取色彩信息
})
// 把此图像绘制到canvas画板中
canvas.add(img)
})
}
function getMainRGBA() {
const rgbas = [] // 用来收集10000个格子的色彩信息
for (let y = 0; y < canvas.height; y += 8) {
for (let x = 0; x < canvas.width; x += 8) {
// 获取每一块格子的色彩data
const { data } = ctx.getImageData(x, y, 8, 8)
rgbas[y / 8 * 100 + x / 8] = []
for (let i = 0; i < data.length; i += 4) {
// 4个4个收集,因为每4个就组成一个像素的rgba
rgbas[y / 8 * 100 + x / 8].push([
data[i],
data[i + 1],
data[i + 2],
data[i + 3]
])
}
}
}
// 算出10000个格子,每个格子的主色调
mainColors = getMainColorStyle(rgbas)
}
function getMainColorStyle(rgbas) {
const mainColors = [] // 用来收集1000个格子的主色调rgba
for (let colors of rgbas) {
let r = 0, g = 0, b = 0, a = 0
for (let color of colors) {
// 累加
r += color[0]
g += color[1]
b += color[2]
a += color[3]
}
mainColors.push([
Math.round(r / colors.length), // 取平均值
Math.round(g / colors.length), // 取平均值
Math.round(b / colors.length), // 取平均值
Math.round(a / colors.length) // 取平均值
])
}
return mainColors
}
function getComposeColorStyle(url, name) {
return new Promise(resolve => {
// 创建一个 20 * 20的canvas画板
// 理论上这里宽高可以自己定,但是越大,色彩会越精准
const composeCanvas = document.createElement('canvas')
const composeCtx = composeCanvas.getContext('2d')
composeCanvas.width = 20
composeCanvas.height = 20
// 创建img对象
const img = new Image()
img.src = url
img.onload = function () {
const scale = composeCanvas.height / composeCanvas.height
img.height *= scale
img.width *= scale
// 将img画到临时canvas画板
composeCtx.drawImage(img, 0, 0, composeCanvas.width, composeCanvas.height)
// 获取颜色信息data
const { data } = composeCtx.getImageData(0, 0, composeCanvas.width, composeCanvas.height)
// 累加 r,g,b,a
let r = 0, g = 0, b = 0, a = 0
for (let i = 0; i < data.length; i += 4) {
r += data[i]
g += data[i + 1]
b += data[i + 2]
a += data[i + 3]
}
resolve({
// 主色调
rgba: [
Math.round(r / (data.length / 4)), // 取平均值
Math.round(g / (data.length / 4)), // 取平均值
Math.round(b / (data.length / 4)), // 取平均值
Math.round(a / (data.length / 4)) // 取平均值
],
url,
name
})
}
})
}
function finishCompose() {
const urls = [] // 收集最终绘制的10000张图片
for (let main of mainColors) { // 遍历10000个格子主色调
let closestIndex = 0 // 最接近主色调的图片的index
let minimumDiff = Infinity // 相差值
for (let i = 0; i < composeColors.length; i++) {
const { rgba } = composeColors[i]
// 格子主色调rgba四个值,减去图片主色调rgba四个值,的平方
const diff = (rgba[0] - main[0]) ** 2 + (rgba[1] - main[1]) ** 2
+ (rgba[2] - main[2]) ** 2 + (rgba[3] - main[3]) ** 2
// 然后开跟比较
if (Math.sqrt(diff) < minimumDiff) {
minimumDiff = Math.sqrt(diff)
closestIndex = i
}
}
// 把最小色差的图片url添加进数组urls
urls.push(composeColors[closestIndex].url)
}
// 将urls中10000张图片,分别绘制在对应的10000个格子中
for (let i = 0; i < urls.length; i++) {
fabric.Image.fromURL(urls[i], img => {
const scale = img.height > img.width ? 8 / img.width : 8 / img.height;
img.set({
left: i % 100 * 8,
top: Math.floor(i / 100) * 8,
originX: "center",
scaleX: scale,
scaleY: scale,
});
canvas.add(img)
})
}
}
//导出图片
function exportCanvas() {
const dataURL = canvas.toDataURL({
width: canvas.width,
height: canvas.height,
left: 0,
top: 0,
format: "png",
});
const link = document.createElement("a");
link.download = "嫦娥姐姐.png";
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
参考文章
结语
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者加入我的群哈哈,咱们一起摸鱼一起学习 : meron857287645