preface
Hello, I'm Lin Sanxin. The Mid Autumn Festival is coming. I wish you a Happy Mid Autumn Festival!!! I was thinking, what can I write to share with you about the Mid Autumn Festival? On this day, I was watching journey to the West and suddenly thought of my childhood goddess. Who is it? It is the Chang'e fairy pursued by Marshal Tianpeng. That's the goddess of my childhood. I believe everyone has heard the story of Chang'e running to the moon
Some time ago, I saw it Rongding Big guy's article I combined our beautiful moments with 10000 pictures , I found that the main tone of the original picture was calculated in that way and learned a lot. So I stood Rongding On the shoulders of the giant, different pictures of the role of Chang'e in the king's glory, plus different pictures of the role of Hou Yi in the king's glory (Hou Yi is Chang'e's husband), constitute the image of Chang'e in my childhood goddess journey to the West.
Do it!!!
Pre preparation
Because 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 many APIs to make it easier for us to draw operable images on canvas. The code of fabric is here fabric library code , create a file and copy it
<!-- introduce fabric This library --> <script src="./fabric.js"></script> <!-- Used to select the main graph --> <input type="file" id="mainInput" /> <!-- Used to select multiple pictures --> <input type="file" id="composeInput" multiple /> <!-- Generation effect --> <button id="finishBtn">Generate composite graph</button> <!-- $800 * 800 of canvas canvas --> <canvas id="canvas" width="800" height="800"></div>
const mainInput = document.getElementById('mainInput') // Get the DOM of the upload main image button const composeInput = document.getElementById('composeInput') // Get the DOM of multi pass combined picture button const finishBtn = document.getElementById('finishBtn') // Gets the DOM of the button that generates the final result const exportBtn = document.getElementById('exportBtn') // Gets the DOM of the inverted picture button const canvas = new fabric.Canvas('canvas') // Instance a canvas object of fabric. The id of canvas is passed in const ctx = canvas.getContext('2d') // Draw 2d image
Draw sister Chang'e
We need to draw the original image of sister Chang'e on the page first. The image is as follows
How do we draw an image into an HTML page? The answer is canvas. Let's draw this image on the page first!
As we all know, it is impossible to draw the picture directly to you when it is uploaded to the browser. For example, the native canvas needs to convert your picture to base64 format to draw it to the page, and fabric provides a fabric Image. From URL (URL, IMG = > {}), you need to pass in the blob address of a picture to generate a picture that can be drawn to the page. How can we turn the pictures we upload into blob addresses? In fact, JavaScript has provided us with such a method window URL. Createobjecturl, you can use it.
// Monitor the upload changes of the upload main map button mainInput.onchange = function (e) { // There is only one picture, so it is e.target files[0] const url = window.URL.createObjectURL(e.target.files[0]) // Pass in the generated blob address drawMainImage(url) } function drawMainImage(url) { // Receive incoming url fabric.Image.fromURL(url, img => { console.log(img) // Callback after successful conversion // fabric. Image. The fromurl converts this url into a picture // If the image needs to be scaled, height > width will be scaled according to the scale of width; otherwise, use the scale of height // The reverse is to fill the whole picture const scale = img.height > img.width ? canvas.width / img.width : canvas.height / img.height // Set the parameters for drawing this image img.set({ left: canvas.width / 2, // Half the width to the left of canvas drawing board originX: 'center', // Center horizontally top: 0, // 0 from top scaleX: scale, // Image horizontal scale scaleY: scale, // Image vertical scale selectable: false // Not operable. The default value is true }) // Draw this image into the canvas palette canvas.add(img) // Callback function for picture drawing completion img.on('added', e => { console.log('The picture loading is complete') setTimeout(() => { // After drawing, obtain the color information of 10000 grids in this image, which will be implemented later getMainRGBA() }, 200) // The delay device is used here because there is a delay in image rendering // Here, we need to ensure that the image is really completely drawn, and then get the color information }) }) }
10000 grids
We all know that our canvas canvas is 800 * 800. We want to divide it into 10000 grids, so 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 four parameters
- x: Gets the X coordinate of the range
- y: Gets the Y coordinate of the range
- Width: gets the width of the range
- Height: gets the height of the range
It will return an object with an attribute data, which is the color information of this range, such as
const { data } = ctx.getImageData(40, 40, 8, 8)
Then data is the color information within the range of x = 40, y = 40, width and height = 8. The color information is an array. For example, if the range is 8 * 8, the array has 8 * 8 * 4 = 256 elements. Because 8 * 8 has 64 pixels, and rgba(r, g, b, a) of each pixel is 4 values, the array has 8 * 8 * 4 = 256 elements, So let's collect four and four, because every four elements is an rgba of one pixel, and an 8 * 8 lattice will have 64 pixels, that is, 64 rgba arrays
let mainColors = [] // It is used to collect the main tone rgba of 1000 grids, which will be implemented later function getMainRGBA() { const rgbas = [] // Used to collect color information of 10000 grids for (let y = 0; y < canvas.height; y += 8) { for (let x = 0; x < canvas.width; x += 8) { // Get the color data of each grid const { data } = ctx.getImageData(x, y, 8, 8) rgbas[y / 8 * 100 + x / 8] = [] for (let i = 0; i < data.length; i += 4) { // Four four collections, because every four forms a pixel rgba rgbas[y / 8 * 100 + x / 8].push([ data[i], data[i + 1], data[i + 2], data[i + 3] ]) } } } // Calculate 10000 grids and the main tone of each grid, which will be realized later mainColors = getMainColorStyle(rgbas) }
Main tone of each grid
We have obtained 10000 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? Very simple, rgba(r, g, b, a) has four values. Let's calculate the average of these four values, and then form a new rgba. This rgba is the main tone of each lattice!!!
function getMainColorStyle(rgbas) { const mainColors = [] for (let colors of rgbas) { let r = 0, g = 0, b = 0, a = 0 for (let color of colors) { // accumulation r += color[0] g += color[1] b += color[2] a += color[3] } mainColors.push([ Math.round(r / colors.length), // Take the average value Math.round(g / colors.length), // Take the average value Math.round(b / colors.length), // Take the average value Math.round(a / colors.length) // Take the average value ]) } return mainColors }
Upload combined pictures
The functions of the main picture have been realized. Now there are only combined pictures left. We can transmit more combined pictures. But we have to calculate the main tone of each combined picture, because later we need to compare the main tone of the 10000 squares to decide which grid to put which combined picture
Here's a question to emphasize. If you want to get the color information of the picture, you have to draw the picture on the canvas drawing board, but we don't want to draw the picture on the canvas on the page. What should we do? We can create a temporary canvas drawing board. After drawing and obtaining the color information, we will destroy it
let composeColors = [] // Collect the main colors of combined pictures // Monitor the upload of multiple selection buttons composeInput.onchange = async function (e) { const promises = [] // promises array for (file of e.target.files) { // Generate blob address for each picture const url = window.URL.createObjectURL(file) // Incoming blob address promises.push(getComposeColorStyle(url, file.name)) } const res = await Promise.all(promises) // Execute all promise s sequentially composeColors = res // Assign the result to composeColors } function getComposeColorStyle(url, name) { return new Promise(resolve => { // Create a canvas drawing board of 20 * 20 // Theoretically, the width and height can be determined by yourself, but the larger the size, the more accurate the color will be const composeCanvas = document.createElement('canvas') const composeCtx = composeCanvas.getContext('2d') composeCanvas.width = 20 composeCanvas.height = 20 // Create img object const img = new Image() img.src = url img.onload = function () { const scale = composeCanvas.height / composeCanvas.height img.height *= scale img.width *= scale // Draw img to temporary canvas palette composeCtx.drawImage(img, 0, 0, composeCanvas.width, composeCanvas.height) // Get color information data const { data } = composeCtx.getImageData(0, 0, composeCanvas.width, composeCanvas.height) // Add 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({ // Main tone rgba: [ Math.round(r / (data.length / 4)), // Take the average value Math.round(g / (data.length / 4)), // Take the average value Math.round(b / (data.length / 4)), // Take the average value Math.round(a / (data.length / 4)) // Take the average value ], url, name }) } }) }
Contrast main colors and draw
- Sister Chang'e in canvas drawing board has 10000 grids, and each grid has its own main color
- Each combined picture uploaded also has its own main color
How can we achieve the final effect? Very simple!!! Traverse 10000 grids, take the main tone of each grid and compare it with the main tone of each combined picture one by one. The picture closest to the tone is drawn into this 8 * 8 grid.
// Listening completion button finishBtn.onclick = finishCompose function finishCompose() { const urls = [] // Collect the final 10000 pictures for (let main of mainColors) { // Traverse 10000 grid main colors let closestIndex = 0 // The index of the picture closest to the main tone let minimumDiff = Infinity // Phase difference value for (let i = 0; i < composeColors.length; i++) { const { rgba } = composeColors[i] // The square of the four values of rgba of the main color of the grid minus the four values of rgba of the main color of the picture const diff = (rgba[0] - main[0]) ** 2 + (rgba[1] - main[1]) ** 2 + (rgba[2] - main[2]) ** 2 + (rgba[3] - main[3]) ** 2 // Then drive and compare if (Math.sqrt(diff) < minimumDiff) { minimumDiff = Math.sqrt(diff) closestIndex = i } } // Add the image url with the smallest color difference to the urls array urls.push(composeColors[closestIndex].url) } // Draw 10000 pictures in urls in the corresponding 10000 grids 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
// Listen Export button exportBtn.onclick = exportCanvas //Export picture 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 = "Sister Chang'e.png"; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
Final effect
Easter egg
If you think this article is of little help to you, give a praise and encourage Lin Sanxin, ha ha. Or you can join my fishing group
If you want to join the learning group and fish, please click here [fish](
https://juejin.cn/pin/6969565...)
Ha ha, I use the picture of the king glorifying pig Bajie to form myself
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> <!-- introduce flare This library --> <script src="./flare.js"></script> </head> <body> <!-- Used to select the main graph --> <input type="file" id="mainInput" /> <!-- Used to select multiple pictures --> <input type="file" id="composeInput" multiple /> <!-- Generation effect --> <button id="finishBtn">Generate composite graph</button> <!-- Export picture --> <button id="exportBtn">Export picture</button> <!-- $800 * 800 of canvas canvas --> <canvas id="canvas" width="800" height="800"></div> </body> <script src="./index2.js"></script> </html>
const mainInput = document.getElementById('mainInput') // Get the DOM of the upload main image button const composeInput = document.getElementById('composeInput') // Get the DOM of multi pass combined picture button const finishBtn = document.getElementById('finishBtn') // Gets the DOM of the button that generates the final result const exportBtn = document.getElementById('exportBtn') // Gets the DOM of the inverted picture button const canvas = new fabric.Canvas('canvas') // Instance a canvas object of flare, and the id of canvas is passed in const ctx = canvas.getContext('2d') // Draw 2d image let mainColors = [] let composeColors = [] // Monitor the upload changes of the upload main map button mainInput.onchange = function (e) { // There is only one picture, so it is e.target files[0] const url = window.URL.createObjectURL(e.target.files[0]) // Pass in the generated blob address drawMainImage(url) } composeInput.onchange = async function (e) { const promises = [] // promises array for (file of e.target.files) { // Generate blob address for each picture const url = window.URL.createObjectURL(file) // Incoming blob address promises.push(getComposeColorStyle(url, file.name)) } const res = await Promise.all(promises) // Execute all promise s sequentially composeColors = res // Assign the result to composeColors } // Listening completion button finishBtn.onclick = finishCompose // Listen Export button exportBtn.onclick = exportCanvas function drawMainImage(url) { // Receive incoming url fabric.Image.fromURL(url, img => { console.log(img) // Callback after successful conversion // fabric. Image. The fromurl converts this url into a picture // If the image needs to be scaled, height > width will be scaled according to the scale of width; otherwise, use the scale of height // The reverse is to fill the whole picture const scale = img.height > img.width ? canvas.width / img.width : canvas.height / img.height // Set the parameters for drawing this image img.set({ left: canvas.width / 2, // Half the width to the left of canvas drawing board originX: 'center', // Center horizontally top: 0, // 0 from top scaleX: scale, // Image horizontal scale scaleY: scale, // Image vertical scale selectable: false // Not operable. The default value is true }) // Callback function for picture drawing completion img.on('added', e => { console.log('The picture loading is complete') setTimeout(() => { // After drawing, obtain the color information of 10000 grids in this image getMainRGBA() }, 200) // The delay device is used here because there is a delay in image rendering // Here, we need to ensure that the image is really completely drawn, and then get the color information }) // Draw this image into the canvas palette canvas.add(img) }) } function getMainRGBA() { const rgbas = [] // Used to collect color information of 10000 grids for (let y = 0; y < canvas.height; y += 8) { for (let x = 0; x < canvas.width; x += 8) { // Get the color data of each grid const { data } = ctx.getImageData(x, y, 8, 8) rgbas[y / 8 * 100 + x / 8] = [] for (let i = 0; i < data.length; i += 4) { // Four four collections, because every four forms a pixel rgba rgbas[y / 8 * 100 + x / 8].push([ data[i], data[i + 1], data[i + 2], data[i + 3] ]) } } } // Calculate 10000 grids and the main color of each grid mainColors = getMainColorStyle(rgbas) } function getMainColorStyle(rgbas) { const mainColors = [] // rgba is used to collect the main color of 1000 grids for (let colors of rgbas) { let r = 0, g = 0, b = 0, a = 0 for (let color of colors) { // accumulation r += color[0] g += color[1] b += color[2] a += color[3] } mainColors.push([ Math.round(r / colors.length), // Take the average value Math.round(g / colors.length), // Take the average value Math.round(b / colors.length), // Take the average value Math.round(a / colors.length) // Take the average value ]) } return mainColors } function getComposeColorStyle(url, name) { return new Promise(resolve => { // Create a canvas drawing board of 20 * 20 // Theoretically, the width and height can be determined by yourself, but the larger the size, the more accurate the color will be const composeCanvas = document.createElement('canvas') const composeCtx = composeCanvas.getContext('2d') composeCanvas.width = 20 composeCanvas.height = 20 // Create img object const img = new Image() img.src = url img.onload = function () { const scale = composeCanvas.height / composeCanvas.height img.height *= scale img.width *= scale // Draw img to temporary canvas palette composeCtx.drawImage(img, 0, 0, composeCanvas.width, composeCanvas.height) // Get color information data const { data } = composeCtx.getImageData(0, 0, composeCanvas.width, composeCanvas.height) // Add 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({ // Main tone rgba: [ Math.round(r / (data.length / 4)), // Take the average value Math.round(g / (data.length / 4)), // Take the average value Math.round(b / (data.length / 4)), // Take the average value Math.round(a / (data.length / 4)) // Take the average value ], url, name }) } }) } function finishCompose() { const urls = [] // Collect the final 10000 pictures for (let main of mainColors) { // Traverse 10000 grid main colors let closestIndex = 0 // The index of the picture closest to the main tone let minimumDiff = Infinity // Phase difference value for (let i = 0; i < composeColors.length; i++) { const { rgba } = composeColors[i] // The square of the four values of rgba of the main color of the grid minus the four values of rgba of the main color of the picture const diff = (rgba[0] - main[0]) ** 2 + (rgba[1] - main[1]) ** 2 + (rgba[2] - main[2]) ** 2 + (rgba[3] - main[3]) ** 2 // Then drive and compare if (Math.sqrt(diff) < minimumDiff) { minimumDiff = Math.sqrt(diff) closestIndex = i } } // Add the image url with the smallest color difference to the urls array urls.push(composeColors[closestIndex].url) } // Draw 10000 pictures in urls in the corresponding 10000 grids 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 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 = "Sister Chang'e.png"; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
Reference articles
- Rongding Big guy's article I combined our beautiful moments with 10000 pictures
epilogue
I'm Lin Sanxin, an enthusiastic front-end rookie programmer. If you are self-motivated, like the front end and want to learn from the front end, we can make friends and fish together. Ha ha, fish school, add me, please note [Si no]