前言
大家好,我是林三心,中秋即将来临,预祝大家中秋高兴!!!我在想,对于中秋,我能写点什么分享给大家呢?这一天,我在看《西游记》,忽然想到了我儿时的女神,是谁呢?就是天蓬元帅苦苦谋求的嫦娥仙子,那可是我儿时的女神啊。嫦娥奔月的故事我置信大家都听过
前段时间,我看了荣顶大佬的这篇我用 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') // 获取上传主图按钮的DOMconst composeInput = document.getElementById('composeInput') // 获取多传组合图片按钮的DOMconst finishBtn = document.getElementById('finishBtn') // 获取生成最终后果按钮的DOMconst exportBtn = document.getElementById('exportBtn') // 获取倒出图片按钮的DOMconst canvas = new fabric.Canvas('canvas') // 实例一个fabric的canvas对象,传入的是canvas的idconst 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 = finishComposefunction 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);}
最终成果
彩蛋
如果你感觉此文对你有一丁点帮忙,点个赞,激励一下林三心哈哈。或者能够退出我的摸鱼群
想进学习群,摸鱼群,请点击这里[摸鱼](
https://juejin.cn/pin/6969565...)
哈哈我用王者光荣猪八戒的图片,组成了我本人
残缺代码
<!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') // 获取上传主图按钮的DOMconst composeInput = document.getElementById('composeInput') // 获取多传组合图片按钮的DOMconst finishBtn = document.getElementById('finishBtn') // 获取生成最终后果按钮的DOMconst exportBtn = document.getElementById('exportBtn') // 获取倒出图片按钮的DOMconst canvas = new fabric.Canvas('canvas') // 实例一个flare的canvas对象,传入的是canvas的idconst 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 = exportCanvasfunction 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);}
参考文章
- 荣顶大佬的这篇我用 10000 张图片合成咱们美妙的霎时
结语
我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜爱前端,想学习前端,那咱们能够交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】