This article was written on the eve of the 2021 Mid-Autumn Festival
foreword
Hello everyone, I'm Lin Sanxin, the Mid-Autumn Festival is coming soon, I wish everyone a happy Mid-Autumn Festival! ! ! I was thinking, about the Mid-Autumn Festival, what can I write to share with you? On this day, I was watching "Journey to the West" and suddenly thought of my childhood goddess, who is she? It is the Fairy Chang'e that Marshal pursued so hard. the goddess of my childhood. I believe everyone has heard the story of Chang'e
Some time ago, I read wing top chiefs of this I use synthetic 10000 pictures of our beautiful moments , found that the main colors of the original image is so calculated, learned a lot. So I stood on the shoulders of the giant Rongding , and used different pictures of the role of Chang'e Houyi Glory of the King. Formed the image of my childhood goddess - Journey to the West Chang'e.
Let's do it! ! !
pre-preparation
Since we need to use canvas
and some image upload buttons, let's write the HTML code first. fabric
is a very practical canvas library. It provides a lot of
api
, which is convenient for us to draw and operate canvas
sexual images. The code of fabric is here fabric library code , create a file and copy it over
<!-- 引入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图像
Draw sister Chang'e
We need to draw the original image of Sister Chang'e on the page first, the image is as follows
So how do we draw an image into an HTML page? The answer is canvas
, so let's draw this image to the page first!
We all know that it is impossible to draw the picture directly to the browser. For example, the native canvas
needs to convert your picture to base64
format to draw it to the page, while fabric
provides a fabric.Image.fromURL(url, img => {})
, which needs to be uploaded. Enter the blob address of a picture to generate a picture that can be drawn to the page. So how do we convert the pictures we uploaded into
blob addresses? In fact, JavaScript has already provided us with such a method
window.URL.createObjectURL
, which can be achieved by using it.
// 监听上传主图按钮的上传变化
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 grids
We all know that our canvas is 800 * 800
, we want to divide it into 10000
grids, then each grid is 8 * 8
. Before implementation, we now know an api for canvas to obtain color information - ctx.getImageData(x, y, width, height)
, which receives 4 parameters
- x: get the x coordinate of the range
- y: get the y coordinate of the range
- width: get the width of the range
- height: Get the height of the range
He will return an object with an attributedata
, thisdata
is the color information of this range, such as
const { data } = ctx.getImageData(40, 40, 8, 8)
Then data is x is 40, y is 40, width and height are 8, the color information in this range, this color information is an array, for example, this range is 8 * 8
, then this array has 8 * 8 * 4 = 256
elements, because 8 * 8
is There are 64 pixels, and each pixel's rgba(r, g, b, a)
is 4 values, so this array has 8 * 8 * 4 = 256
elements, so we need 4 sets of 4, because each 4 elements is a pixel of rgba
, and a 8 * 8
grid, there will be 64 pixels, which is 64 rgba arrays
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)
}
Main color of each plaid
We have obtained 10,000 grids above, and each grid has 64 pixels, which is 64 rgba arrays, so each grid has 64 rgba, how can we get the main color of this grid? It's very simple, rgba(r, g, b, a) has 4 values, we calculate the average value of these 4 values, and then form a new rgba, this rgba is the main color of each grid! ! !
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
}
Upload combined image
The functions of the main picture have been realized, and now there are only combined pictures left. We can transmit more combined pictures. But we have to calculate the main color of each combined picture, because later we need the main color to compare the main color of the 10,000 grids, and decide which grid to put which combination picture
There is a problem to emphasize here. If you want to get the color information of the picture, you have to draw the picture on the canvas drawing board to get it, but we don't want to draw the picture to the canvas on the page here. What should we do? We can create a temporary canvas drawing board, after drawing, after obtaining the color information, we will destroy it
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
})
}
})
}
Contrast main colors and draw
- Chang'e sister in the canvas drawing board has 10000 grids, each grid has its own main color
- Each uploaded combined image also has its own dominant color
So how do we achieve the final result? It's very simple! ! ! Traverse 10,000 grids, hold the main color of each grid, and compare it with the main color of each combined picture one by one. The picture with the closest color tone is drawn into this 8 * 8
grid.
// 监听完成按钮
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)
})
}
}
export image
// 监听导出按钮
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);
}
final effect
Easter eggs
If you think this article helps you a little bit, give it a like and encourage Lin Sanxin haha. Or you can join my fishing group
If you want to join the learning group and touch the fish group, please click here [touch the fish](
https://juejin.cn/pin/6969565162885873701)
Haha, I used the pictures of Pig Bajie
full code
<!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);
}
Reference article
Epilogue
I'm Lin Sanxin, an enthusiastic front-end rookie programmer. If you are motivated, like the front-end, and want to learn the front-end, then we can make friends and fish together haha, touch the fish group, add me, please note [Si No]
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。