Preface
Hello everyone, I’m Lin Sanxin, the Mid-Autumn Festival is coming soon, I wish you all a happy Mid-Autumn Festival! ! ! I was thinking, about the Mid-Autumn Festival, what can I write to share with you? On this day, when I was watching "Journey to the West", I suddenly thought of my childhood goddess, who is it? It is Marshal canopy hard in pursuit of Chang E fairy , that's my childhood goddess ah. The story of Chang'e the moon 161d3e26b2566c I believe everyone has heard
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 Chang'e the glory of the king, plus the different pictures of the role of 161d3e26b256c2 Houyi in the ). It forms the image of my childhood goddess—Journey to the West Chang'e.
Let's do it! ! !
Pre-preparation
Because we need canvas
and some picture 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 fabric code is here library code , create a file, just copy it
<!-- 引入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 on the page first!
As we all know, it is impossible to directly draw a picture for you by uploading it directly to the browser. For example, the original canvas
needs to convert your picture to base64
format to be drawn on the page, and 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 on the page. Then how do we convert the pictures we uploaded to
blob address? In fact, JavaScript has provided us with such a method
window.URL.createObjectURL
, which can be used to achieve 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, which has 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 both 8. The color information in this range is an array. For example, the range is 8 * 8
, then this array has 8 * 8 * 4 = 256
elements, because 8 * 8
is There are 64 pixels, and the rgba(r, g, b, a)
of each pixel is 4 values, so this array has 8 * 8 * 4 = 256
elements, so below we need 4 4 collections, because every 4 elements is a pixel of rgba
, and a 8 * 8
Grid, there will be 64 pixels, that 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 grid
We have obtained 10,000 grids above. Each grid has 64 pixels, that is, 64 rgba arrays. Then 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. Let's 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 a combined picture
The functions of the main picture have been implemented, and now there is only the combined picture, we can upload the combined picture more. 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 combined 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 and getting 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
})
}
})
}
Compare the main colors and draw
- Chang'e sister in the canvas drawing board has 10,000 grids, and each grid has its own main color
- Each combined picture uploaded also has its own main color
So how do we achieve the final effect? It's simple! ! ! Traverse 10,000 grids, take the main color of each grid, and compare it with the main color of each combined picture. The picture with the closest color is drawn into the grid of 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)
})
}
}
Export picture
// 监听导出按钮
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 is of little help to you, please give me a thumbs up and encourage Lin Sanxin haha. Or you can join my fish school
If you want to join the learning group and catch the fish, please click here [moyu](
https://juejin.cn/pin/6969565162885873701)
Haha I used the picture of Glory of the King Pig Bajie
Complete 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
Concluding remarks
I am 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, fish together haha, fish school, add me, please note [think]
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。