I am participating in the Mid-Autumn Festival Creative Submission contest, please see: Mid-Autumn Festival Creative Submission Contest for details

preface

Hello everyone, I am Lin Sanxin, Mid-Autumn Festival is coming, I wish everyone a happy Mid-Autumn Festival!! I was wondering, what can I write to share with you about Mid-Autumn Festival? This day, I was reading journey to the West, suddenly thought of my childhood goddess, who is it? Is the chang ‘e fairy tian Peng marshal pursued hard, but it is my childhood goddess ah. I believe we have all heard the story of Chang ‘e flying to the moon

Some time ago, I read this article by Rongding Big guy. I used 10000 pictures to synthesize our beautiful moments. I found that the main tone of the original picture was calculated in that way, and I learned a lot. So I stood on the shoulders of the giant rongding, and made up the image of my childhood goddess, Chang ‘e, using different pictures of chang ‘e in the king of Glory, and different pictures of Houyi, chang ‘e’s husband, in the king of Glory.

Do it!!

Lead to

Since we need to use canvas and some image upload buttons, let’s write the HTML code well first. Flbraic is a very practical Canvas library, which provides many apis for us to draw operable images on canvas more easily. The fabric code is in here the fabric library code, create a file and copy it over

<! Import the fabric library --><script src="./fabric.js"></script><! Select main image --><input type="file" id="mainInput" /><! -- Used to select composition picture multiple selection --><input type="file" id="composeInput" multiple /><! Generate effect --><button id="finishBtn">Generate composite diagram</button><! - a800 * 800Canvas canvas --><canvas id="canvas" width="800" height="800"></div>
Copy the code
const mainInput = document.getElementById('mainInput') // Get the DOM of the thumbnail button
const composeInput = document.getElementById('composeInput') // Get the DOM of the multipass composite image button
const finishBtn = document.getElementById('finishBtn') // Get the DOM of the button that generates the final result
const exportBtn = document.getElementById('exportBtn') // Get the DOM of the pour image button
const canvas = new fabric.Canvas('canvas') // Instantiate a Canvas object of fabric, passing in the canvas ID
const ctx = canvas.getContext('2d') // Draw a 2D image
Copy the code

Draw sister Chang ‘e

We need to draw the original image of Sister Chang ‘e on the page first, as shown below

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, images uploaded directly to the browser cannot be drawn directly to you. For example, the native Canvas needs to convert your Image to base64 format to draw the page, while Fabric provides a fabric.image. fromURL(URL, img => {}), You need to pass in the BLOB address of an image to generate an image that can be drawn to a page. How do we convert our uploaded images into blob addresses? JavaScript has actually window provides us with such a method. The URL. CreateObjectURL, can be done with it.

// Listen for upload changes on the main image button
mainInput.onchange = function (e) {
    // Only one image, so 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 the incoming URL
    fabric.Image.fromURL(url, img= > {
        console.log(img)
        // Callback after successful conversion
        // fabric.image. fromURL will convert this URL to an Image

        // To scale the image, use height > width to scale the image, and vice versa
        // The reverse is to fill the entire graph
        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 the Canvas palette
            originX: 'center'.// The horizontal direction is centered
            top: 0.// The distance from the top is 0
            scaleX: scale, // Image horizontal scaling
            scaleY: scale, // Image vertical scale
            selectable: false // Not operable. Default is true
        })

        // Draw this image to the Canvas palette
        canvas.add(img)
        
        // The callback function to draw the picture
        img.on('added'.e= > {
            console.log('I've loaded the image.')
            setTimeout(() = > {
                // After drawing, get the color information of 10000 squares in this image
                getMainRGBA()
            }, 200) // We use the delayer here because there is a delay in drawing the image
                    // We need to make sure that the image is completely drawn before we get the color information})})}Copy the code

The 10000 grid

As we all know, our canvas canvas is800 * 800Yes, we want a cut10000Two cells, so each of these cells is8 * 8. Before implementation, we now know a Canvas to get color information API —ctx.getImageData(x, y, width, height), he 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’s going to return an object with a property called data, which is the color information for that range, for example

const { data } = ctx.getImageData(40.40.8.8)
Copy the code

So data is the color information in the range x is 40, y is 40, width and height is 8, the color information is an array, for example, the range is 8 * 8, the array has 8 * 8 * 4 = 256 elements, because 8 * 8 has 64 pixels, Each pixel rGBA (r, G, B, a) is 4 values, so this array has 8 * 8 * 4 = 256 elements, so we will collect 4 4 elements, because every 4 elements is a pixel RGBA, and an 8 * 8 grid, will have 64 pixels. That’s 64 RGBA arrays

let mainColors = [] // The main color rGBA used to collect 1000 cells, will be implemented later

function getMainRGBA() {
    const rgbas = [] // Collect color information for 10000 squares
    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) {
                // 4 4 collect, because every 4 makes up a pixel rGBA
                rgbas[y / 8 * 100 + x / 8].push([
                    data[i],
                    data[i + 1],
                    data[i + 2],
                    data[i + 3]])}}}// Calculate 10000 squares, the main color of each grid, the later implementation
    mainColors = getMainColorStyle(rgbas)
}
Copy the code

The main tone of each grid

Above we have 10000 cells, each cell has 64 pixels, that is 64 RGBA array, so each cell has 64 RGBA, how can we get the main color of this cell? 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 box!!

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
            Math.round(g / colors.length), // take the average
            Math.round(b / colors.length), // take the average
            Math.round(a / colors.length) // take the average])}return mainColors
}
Copy the code

Upload composite images

The function of the main picture has been realized, now it is left to the combination picture, we can upload more combination picture. But we need to figure out the dominant color of each composite image, because later we need the dominant color to compare the dominant color of the 10,000 cells and decide which cells to put in which composite image

There is a problem to be emphasized here. If you want to get the color information of the picture, you have to draw the picture on the canvas board to get it, but we don’t want to draw the picture on the canvas of the page. What can we do? We can create a temporary Canvas, and when we’re done drawing and getting the color information, we’ll destroy it

let composeColors = [] // Collect and combine the main colors of the picture

// Listen for multiple select button uploads
composeInput.onchange = async function (e) {
    const promises = [] / / an array of promises
    for (file of e.target.files) {
        // Generate a BLOB address for each image
        const url = window.URL.createObjectURL(file)
        // Pass in the blob address
        promises.push(getComposeColorStyle(url, file.name))
    }
    const res = await Promise.all(promises) // Execute all promises sequentially
    composeColors = res // Assign the result to composeColors
}

function getComposeColorStyle(url, name) {
    return new Promise(resolve= > {
        // Create a canvas canvas of 20 by 20
        // In theory, the width and height can be determined by yourself, but the bigger the color, the more accurate it will be
        const composeCanvas = document.createElement('canvas')
        const composeCtx = composeCanvas.getContext('2d')
        composeCanvas.width = 20
        composeCanvas.height = 20

        // Create an 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 onto the temporary Canvas palette
            composeCtx.drawImage(img, 0.0, composeCanvas.width, composeCanvas.height)
            // Get the 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 colors
                rgba: [
                    Math.round(r / (data.length / 4)), // take the average
                    Math.round(g / (data.length / 4)), // take the average
                    Math.round(b / (data.length / 4)), // take the average
                    Math.round(a / (data.length / 4)) // take the average
                ],
                url,
                name
            })
        }
    })
}
Copy the code

Contrast the main tone and draw

  • Chang ‘e in the canvas board has 10,000 squares, each with its own main color
  • Each composite image uploaded also has its own main hue

So how do we get to the end result? Very simple!! Go through 10,000 grids, take the main color of each grid and compare it with the main color of each combined picture one by one. The picture closest to the color is drawn into this 8 * 8 grid.

// Listen for the finish button
finishBtn.onclick = finishCompose

function finishCompose() {
    const urls = [] // Collect 10000 final drawings

    for (let main of mainColors) { // Walk through 10000 main colors of the grid

        let closestIndex = 0 // Index of the image closest to the main tone
        let minimumDiff = Infinity / / value

        for (let i = 0; i < composeColors.length; i++) {
            const { rgba } = composeColors[i]
            // The square of the four values of the grid main color rGBA minus the four values of the image main color Rgba
            const diff = (rgba[0] - main[0]) * *2 + (rgba[1] - main[1]) * *2
                + (rgba[2] - main[2]) * *2 + (rgba[3] - main[3]) * *2

            // Then start the comparison
            if (Math.sqrt(diff) < minimumDiff) {
                minimumDiff = Math.sqrt(diff)
                closestIndex = i
            }
        }

        // Add the image URL with the smallest chromatic aberration to the array urls
        urls.push(composeColors[closestIndex].url)
    }


    // Draw 10000 images of urls in 10000 squares
    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)
        })
    }
}
Copy the code

Export images

// Listen for the export button
exportBtn.onclick = exportCanvas

// Export the image
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);
}
Copy the code

The final result

eggs

If you think this article is a little bit helpful to you, click a like and encourage Lin Sanxin haha. Or you can join my shoal of fish to get into the learning group, shoal of fish, click here to fish

Ha ha, I use the king of Glory pig eight quit pictures, formed my own

The complete code

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta Width =device-width, initial-scale=1.0"> <title>Document</title> <! - the introduction of flare the library - > < script SRC = ". / flare. Js "> < / script > < / head > < body > <! To choose the main figure - > < input type = "file" id = "mainInput" / > <! <input type="file" id="composeInput" multiple /> <! <button id="finishBtn"> </button> <! <button id="exportBtn"> </button> <! Canvas id="canvas" width="800" height="800"></div> </body> <script src="./index2.js"></script> </html>Copy the code
const mainInput = document.getElementById('mainInput') // Get the DOM of the thumbnail button
const composeInput = document.getElementById('composeInput') // Get the DOM of the multipass composite image button
const finishBtn = document.getElementById('finishBtn') // Get the DOM of the button that generates the final result
const exportBtn = document.getElementById('exportBtn') // Get the DOM of the pour image button
const canvas = new fabric.Canvas('canvas') // Instantiate a flare canvas object, passing in the canvas ID
const ctx = canvas.getContext('2d') // Draw a 2D image

let mainColors = []
let composeColors = []

// Listen for upload changes on the main image button
mainInput.onchange = function (e) {
    // Only one image, so 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 = [] / / an array of promises
    for (file of e.target.files) {
        // Generate a BLOB address for each image
        const url = window.URL.createObjectURL(file)
        // Pass in the blob address
        promises.push(getComposeColorStyle(url, file.name))
    }
    const res = await Promise.all(promises) // Execute all promises sequentially
    composeColors = res // Assign the result to composeColors
}

// Listen for the finish button
finishBtn.onclick = finishCompose

// Listen for the export button
exportBtn.onclick = exportCanvas

function drawMainImage(url) {
    // Receive the incoming URL
    fabric.Image.fromURL(url, img= > {
        console.log(img)
        // Callback after successful conversion
        // fabric.image. fromURL will convert this URL to an Image

        // To scale the image, use height > width to scale the image, and vice versa
        // The reverse is to fill the entire graph
        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 the Canvas palette
            originX: 'center'.// The horizontal direction is centered
            top: 0.// The distance from the top is 0
            scaleX: scale, // Image horizontal scaling
            scaleY: scale, // Image vertical scale
            selectable: false // Not operable. Default is true
        })

        // The callback function to draw the picture
        img.on('added'.e= > {
            console.log('I've loaded the image.')
            setTimeout(() = > {
                // After drawing, get the color information of 10000 grids in this image
                getMainRGBA()
            }, 200) // We use the delayer here because there is a delay in drawing the image
            // We need to make sure that the image is completely drawn before we get the color information
        })

        // Draw this image to the Canvas palette
        canvas.add(img)
    })
}


function getMainRGBA() {
    const rgbas = [] // Collect color information for 10000 squares
    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) {
                // 4 4 collect, because every 4 makes up a pixel rGBA
                rgbas[y / 8 * 100 + x / 8].push([
                    data[i],
                    data[i + 1],
                    data[i + 2],
                    data[i + 3]])}}}// Calculate 10000 squares, the main color of each square
    mainColors = getMainColorStyle(rgbas)
}

function getMainColorStyle(rgbas) {
    const mainColors = [] // The main color rGBA used to collect 1000 squares
    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
            Math.round(g / colors.length), // take the average
            Math.round(b / colors.length), // take the average
            Math.round(a / colors.length) // take the average])}return mainColors
}

function getComposeColorStyle(url, name) {
    return new Promise(resolve= > {
        // Create a canvas canvas of 20 by 20
        // In theory, the width and height can be determined by yourself, but the bigger the color, the more accurate it will be
        const composeCanvas = document.createElement('canvas')
        const composeCtx = composeCanvas.getContext('2d')
        composeCanvas.width = 20
        composeCanvas.height = 20

        // Create an 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 onto the temporary Canvas palette
            composeCtx.drawImage(img, 0.0, composeCanvas.width, composeCanvas.height)
            // Get the 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 colors
                rgba: [
                    Math.round(r / (data.length / 4)), // take the average
                    Math.round(g / (data.length / 4)), // take the average
                    Math.round(b / (data.length / 4)), // take the average
                    Math.round(a / (data.length / 4)) // take the average
                ],
                url,
                name
            })
        }
    })
}

function finishCompose() {
    const urls = [] // Collect 10000 final drawings

    for (let main of mainColors) { // Walk through 10000 main colors of the grid

        let closestIndex = 0 // Index of the image closest to the main tone
        let minimumDiff = Infinity / / value

        for (let i = 0; i < composeColors.length; i++) {
            const { rgba } = composeColors[i]
            // The square of the four values of the grid main color rGBA minus the four values of the image main color Rgba
            const diff = (rgba[0] - main[0]) * *2 + (rgba[1] - main[1]) * *2
                + (rgba[2] - main[2]) * *2 + (rgba[3] - main[3]) * *2

            // Then start the comparison
            if (Math.sqrt(diff) < minimumDiff) {
                minimumDiff = Math.sqrt(diff)
                closestIndex = i
            }
        }

        // Add the image URL with the smallest chromatic aberration to the array urls
        urls.push(composeColors[closestIndex].url)
    }


    // Draw 10000 images of urls in 10000 squares
    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 the image
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);
}
Copy the code

Refer to the article

  • I took 10,000 pictures of our beautiful moments in this piece by Rongding