Do you remember Chang'e in journey to the west? I used 10000 pictures to spell the childhood goddess!

This article was written on the eve of the Mid Autumn Festival in 2021

preface

Hello, I'm Lin Sanxin. The Mid Autumn Festival is coming. I wish you 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 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 used 10000 pictures to synthesize our beautiful moments , 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's impossible to draw a picture directly when it's 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 url of the image with the smallest color difference to the array urls
        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

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]

Keywords: Javascript Front-end canvas

Added by coool on Fri, 21 Jan 2022 11:27:30 +0200