This is the third day of my participation in the August More text Challenge. For details, see: August More Text Challenge

The moon shone back into the middle of the lake and the wild crane ran to the idle clouds

preface

Yesterday was Valentine’s Day, we all think very happy to spend the holiday ~ I also 😚

Ok, no more nonsense, today to bring you a very interesting project, by cutting the target image, get 10,000 squares, with the image we selected, the corresponding filling square to achieve a thousand image imaging effect. You can use it to spell any meaningful you want to spell a larger version. (such as me, you want to use it and I love to get married all of the photos used to do a super super super super big wedding photos, covered with the grass in the hometown poyang lake, using unmanned aerial vehicle (uav) bird ‘s-eye view, yeah, interesting ~ first buried here, hope in a few years to achieve 😊)

First of all, this article is a practical case study based on my last introduction to Fabric, which I also used for my own practice and summary. I would like to share it with you and grow together!

Get into the business

First we start with an 800 by 800 canvas

(The style of the interface, HERE I will not describe more, we mainly talk about the realization of logical function)

// Initialize the canvas
initCanvas() {
    this.canvas = new fabric.Canvas("canvas", {
        selectable: false.selection: false.hoverCursor: "pointer"});this.ctx = canvas.getContext("2d");
    this.addCanvasEvent();// Add events to the canvas
},
Copy the code

According to the configuration of your computer from the definition of the size of the canvas, so far there is no direct in the Web side to do similar thousand image imaging, in the Web side to achieve this function is really very performance consumption, because the amount of data to deal with a large amount of computing also need to pay attention to: An 800*800 canvas has 640,000 pixels, and each pixel obtained through ctx.getimageData has 4 values, which is 2,560,000 values, and we need to deal with 2,560,000 values later, so I’m not going to make it big here

Draws the target image with Fabric

Note that we draw to the canvas from the local image, and we need to get the file throughwindow.URL.createObjectURL(file)Convert the file to a URL of bloB type

If you like to use elementUI upload component, you write this

// Select the target image callback
slectFile(file, fileList) {
    let tempUrl = window.URL.createObjectURL(file.raw);
    this.drawImage(tempUrl);
},
Copy the code

I don’t like the component here, because when I select the resource images, I will have a file list for thousands of images, and I don’t want to hide it (mostly to share the custom file selection), so I wrote it this way

export function inputFile() {
    return new Promise(function (resolve, reject) {
        if (document.getElementById("myInput")) {
            let inputFile = document.getElementById("myInput");
            inputFile.onchange = (e) = > {
                let urlArr = [];
                for (let i = 0; i < e.target.files.length; i++) {
                    urlArr.push(URL.createObjectURL(e.target.files[i]));
                }
                resolve(urlArr);
            };
            inputFile.click();
        } else {
            let inputFile = document.createElement("input");
            inputFile.setAttribute("id"."myInput");
            inputFile.setAttribute("type"."file");
            inputFile.setAttribute("accept"."image/*");
            inputFile.setAttribute("name"."file");
            inputFile.setAttribute("multiple"."multiple");
            inputFile.setAttribute("style"."display: none");
            inputFile.onchange = (e) = > {
                // console.log(e.target.files[0]);
                // console.log(e.target.files);
                // let tempUrl = URL.createObjectURL(e.target.files[0]);
                // console.log(tempUrl);
                let urlArr = [];
                for (let i = 0; i < e.target.files.length; i++) {
                    urlArr.push(URL.createObjectURL(e.target.files[i]));
                }
                resolve(urlArr);
            };
            document.body.appendChild(inputFile); inputFile.click(); }}); }Copy the code

After obtaining the file through the above method, I have converted the image file into the URL of bloB for our use (it should be noted that the file selection is asynchronous, so we need to use promise here).

// Draw the target image
drawImage(url) {
    fabric.Image.fromURL(url, (img) = > {
        // Set the scale to this.canvas.width/img.width for the long image and this.canvas.height/img.height for the wide image
        let scale =
            img.height > img.width
                ? this.canvas.width / img.width
                : this.canvas.height / img.height;
        img.set({
            left: this.canvas.height / 2.// The distance to the left
            originX: "center".// Align the image at the origin
            top: 0.scaleX: scale, // Scale horizontally
            scaleY: scale, // Scale vertically
            selectable: false./ / interactive
        });
        // Image added to the canvas callback function
        img.on("added".(e) = > {
            // There is a problem with added. The added event is the previous canvas pixel data. Other manually triggered events do not have this problem
            // So use an asynchronous solution
            setTimeout(() = > {
                this.getCanvasData();
            }, 500);
        });
        this.canvas.add(img); // Add the image to the canvas
        this.drawLine(); // Draw the grid lines
    });
},
Copy the code

Draw a 100*100 grid on the canvas as you finish drawing the image

/ / grid line
drawLine() {
    const blockPixel = 8;
    for (let i = 0; i <= this.canvas.width / blockPixel; i++) {
        this.canvas.add(
            new fabric.Line([i * blockPixel, 0, i * blockPixel, this.canvas.height], {
                left: i * blockPixel,
                stroke: "gray".selectable: false.// Whether it can be selected}));this.canvas.add(
            new fabric.Line([0, i * blockPixel, this.canvas.height, i * blockPixel], {
                top: i * blockPixel,
                stroke: "gray".selectable: false.// Whether it can be selected})); }},Copy the code

After drawing, you can see the effect of the picture plus grid lines, or very nice ~😘

Save the image color blocks in an array

It crashed the browser in the first place


I cry 😥, so write loop nesting too much (and the base is 800*800*4==2560000–> have to write well, otherwise sorry pixelList was crazy operation 2560000 times) need to optimize the writing method, since the browser burst, stupid method can not work, that can only change ~

First of all, here we give the length and width of each piece 8 pixels (the smaller the size, the more precise the composite image will be).

// Get the canvas pixel data
getCanvasData() {
    for (let Y = 0; Y < this.canvas.height / 8; Y++) {
        for (let X = 0; X < this.canvas.width / 8; X++) {
            // Each 8*8 pixel area group
            let tempColorData = this.ctx.getImageData(X * 8, Y * 8.8.8).data;
            // Sets the obtained data in groups of 4, each group being one pixel
            this.blockList[Y * 100 + X] = { position: [X, Y], color: []};for (let i = 0; i < tempColorData.length; i += 4) {
                this.blockList[Y * 100 + X].color.push([
                    tempColorData[i],
                    tempColorData[i + 1],
                    tempColorData[i + 2],
                    tempColorData[i + 3]]); }}}console.log(mostBlockColor(this.blockList));
    this.mostBlockColor(this.blockList);// Get the main color of each block
    this.loading = false;
},
Copy the code

😅 is written in a different way. Here, we divide each 8*8 pixel block into a group to get 10,000 elements, and each element has 4 values, respectively representing the value of RGBA. Later, we will fill the corresponding pixel block with the corresponding 10000 pictures

After get all the pixels on the canvas, we need to request the mass-tone attune of the out each small squares Behind the mass-tone attune of the we need through the small square by it and resource pictures off color, to decide what a picture is the square concrete filled 😊 here is very excited, feeling is quick finished half, actually otherwise, the more catch 😭 scalp

 // Get the main color of each grid
mostBlockColor(blockList) {
    for (let i = 0; i < blockList.length; i++) {
        let colorList = [];
        let rgbaStr = "";
        for (let k = 0; k < blockList[k].color.length; k++) {
            rgbaStr = blockList[i].color[k];
            if (rgbaStr in colorList) {
                ++colorList[rgbaStr];
            } else {
                colorList[rgbaStr] = 1; }}let arr = [];
        for (let prop in colorList) {
            arr.push({
                // If only RGB is obtained, then 'RGB (${prop})'
                color: prop.split(","),
                // color: `rgba(${prop})`,
                count: colorList[prop],
            });
        }
        // Array sort
        arr.sort((a, b) = > {
            return b.count - a.count;
        });
        arr[0].position = blockList[i].position;
        this.blockMainColors.push(arr[0]);
    }
    console.log(this.blockMainColors);
},
Copy the code

Brain seeds are not good, the use of draft paper

Gets the dominant color of each resource map

export function getMostColor(imgUrl) {
    return new Promise((resolve, reject) = > {
        try {
            const canvas = document.createElement("canvas");
            // Set canvas width and height to 20. Smaller is faster, but smaller is less accurate
            canvas.width = 20;
            canvas.height = 20;
            const img = new Image(); // Create the img element
            img.src = imgUrl; // Set the image source address
            img.onload = () = > {
                const ctx = canvas.getContext("2d");
                const scaleH = canvas.height / img.height;
                img.height = canvas.height;
                img.width = img.width * scaleH;
                ctx.drawImage(img, 0.0, canvas.width, canvas.height);
                console.log(img.width, img.height);
                // Get the pixel data
                let pixelData = ctx.getImageData(0.0, canvas.width, canvas.height).data;
                let colorList = [];
                let color = [];
                let colorKey = "";
                let colorArr = [];
                // Grouping loops
                for (let i = 0; i < pixelData.length; i += 4) {
                    color[0] = pixelData[i];
                    color[1] = pixelData[i + 1];
                    color[2] = pixelData[i + 2];
                    color[3] = pixelData[i + 3];
                    colorKey = color.join(",");
                    if (colorKey in colorList) {
                        ++colorList[colorKey];
                    } else {
                        colorList[colorKey] = 1; }}for (let prop in colorList) {
                    colorArr.push({
                        color: prop.split(","),
                        count: colorList[prop],
                    });
                }
                // Sort the array of colors, taking the first color as the dominant color
                colorArr.sort((a, b) = > {
                    return b.count - a.count;
                });
                colorArr[0].url = imgUrl;
                console.log(
                    `%c rgba(${colorArr[0].color.join(",")}) `.`background: rgba(${colorArr[0].color.join(",")}) `
                );
                resolve(colorArr[0]);
            };
        } catch(e) { reject(e); }}); }Copy the code

We randomly selected some files and printed them out in their main colors to see how they looked

Color space

Requires color color difference, we first need to understand the definition of color, color has many kinds of ways, their standards are not the same, CMYK,RGB,HSB,LAB and so on… In this case, we’re using RGBA, which is the RGB color model with the extra Alpha information attached

RGBA is the color space representing Red, Green, Blue, and Alpha. Although it is sometimes described as a color space, it is really just an RGB model with additional information attached. The colors used were RGB and could belong to any RGB color space, but Catmull and Smith came up with this indispensable alpha value in 1971-1972 that made alpha rendering and alpha synthesis possible. The guy who came up with the name alpha comes from the classical linear interpolation equation αA + (1-α)B which is the Greek letter.

Other colors related introduction can look here: zhuanlan.zhihu.com/p/24281841 or baike.baidu.com/item/%E9%A2 here…

A method for finding color differences

Since the distribution of colors in space is shown in the above introduction, here we use the Euclidean distance method learned in middle school to find the absolute distance between two colors. Through their distance, we can know the degree of similarity between two colors

First let’s look at the basic concept of Euclidean distance

A Euclidean metric (also known as Euclidean distance) is a commonly used definition of distance. It refers to the true distance between two points in an M-dimensional space, or the natural length of a vector (that is, the distance between that point and the origin). The Euclidean distance in two and three dimensions is the actual distance between two points.

To convert the formula into code:

// Calculate the color difference
colorDiff(color1, color2) {
    let distance = 0;// Initialize the distance
    for (let i = 0; i < color1.length; i++) {
        distance += (color1[i] - color2[i]) ** 2;// Sum the squares of the differences between two groups of colors R,g,b[a]
    }
    return Math.sqrt(distance);// The square root of the two colors gives the absolute distance in the color space
},
Copy the code

There are many methods of computing color differences see wikiwand:www.wikiwand.com/en/Color_di… Or you can use a color processing library such as colorrna.js, which we won’t go into too much detail here

Calculate the difference and render the image

Here we need to compare the main color of each pixel block to the main color of all resource images, and take the one with the smallest difference and render it to the corresponding square

// Generate the image
generateImg() {
    this.loading = true;
    let diffColorList = [];
    // Go through all the squares
    for (let i = 0; i < this.blockMainColors.length; i++) {
        diffColorList[i] = { diffs: []};// Iterate over all images
        for (let j = 0; j < this.imgList.length; j++) {
            diffColorList[i].diffs.push({
                url: this.imgList[j].url,
                diff: this.colorDiff(this.blockMainColors[i].color, this.imgList[j].color),
                color: this.imgList[j].color,
            });
        }
        // Sort the images that have been compared, putting the ones with the least difference first
        diffColorList[i].diffs.sort((a, b) = > {
            return a.diff - b.diff;
        });
        // Select the 0th image
        diffColorList[i].url = diffColorList[i].diffs[0].url;
        diffColorList[i].position = this.blockMainColors[i].position;
        diffColorList[i].Acolor = this.blockMainColors[i].color;
        diffColorList[i].Bcolor = diffColorList[i].diffs[0].color;
    }
    this.loading = false;
    console.log(diffColorList);
    // Facilitates each block to render it
    diffColorList.forEach((item) = > {
        fabric.Image.fromURL(item.url, (img) = > {
            let scale = img.height > img.width ? 8 / img.width : 8 / img.height;
            // img.scale(8 / img.height);
            img.set({
                left: item.position[0] * 8.top: item.position[1] * 8.originX: "center".scaleX: scale,
                scaleY: scale,
            });
            this.canvas.add(img);
        });
    });
},
Copy the code

What a guy!!!!!! What the hell is this?? We’ve been here all night. What are we doing here?

I cried, now all more than five o ‘clock, I still haven’t slept ~

Don’t abandon don’t give up, stick to it is victory

Each step was carefully analyzed to find the problem step by step From the very beginning of the target image pixel data to see the correctness of the pixel data, but didn’t find the problem, the data do not have what problem, preliminary judgment is to calculate the mass-tone attune of the pixel block out of the question, so that the mass-tone attune will not take a picture or in most times one pixel color gives priority to tone, but take them as the average of all colors What about the main color? I’m excited to think about it! Almost woke up the sleeping melon baby, I began to comb again

In this case, I changed it to take the dominant color by average for each of the 8 by 8 cubes

// Get the main color of each grid
mostBlockColor(blockList) {
    for (let i = 0; i < blockList.length; i++) {
        let r = 0,
            g = 0,
            b = 0,
            a = 0;
        for (let j = 0; j < blockList[i].color[j].length; j++) {
            r += blockList[i].color[j][0];
            g += blockList[i].color[j][1];
            b += blockList[i].color[j][2];
            a += blockList[i].color[j][3];
        }
        // Take the average value
        r /= blockList[i].color[0].length;
        g /= blockList[i].color[0].length;
        b /= blockList[i].color[0].length;
        a /= blockList[i].color[0].length;
        // Round the final value
        r = Math.round(r);
        g = Math.round(g);
        b = Math.round(b);
        a = Math.round(a);
        this.blockMainColors.push({
            position: blockList[i].position,
            color: [r, g, b, a],
        });
    }
    console.log(this.blockMainColors);
}
Copy the code

Then, for each image, we changed it to take the main color by average

export function getAverageColor(imgUrl) {
    return new Promise((resolve, reject) = > {
        try {
            const canvas = document.createElement("canvas");
            // Set canvas width and height to 20. Smaller is faster, but smaller is less accurate
            canvas.width = 20;
            canvas.height = 20;
            const img = new Image(); // Create the img element
            img.src = imgUrl; // Set the image source address
            img.onload = () = > {
                console.log(img.width, img.height);
                let ctx = canvas.getContext("2d");
                const scaleH = canvas.height / img.height;
                img.height = canvas.height;
                img.width = img.width * scaleH;
                ctx.drawImage(img, 0.0, canvas.width, canvas.height);
                // Get the pixel data
                let data = ctx.getImageData(0.0, canvas.width, canvas.height).data;
                let r = 0,
                    g = 0,
                    b = 0,
                    a = 0;
                // Take the average of all pixels
                for (let row = 0; row < canvas.height; row++) {
                    for (let col = 0; col < canvas.width; col++) {
                        r += data[(canvas.width * row + col) * 4];
                        g += data[(canvas.width * row + col) * 4 + 1];
                        b += data[(canvas.width * row + col) * 4 + 2];
                        a += data[(canvas.width * row + col) * 4 + 3]; }}// Take the average value
                r /= canvas.width * canvas.height;
                g /= canvas.width * canvas.height;
                b /= canvas.width * canvas.height;
                a /= canvas.width * canvas.height;

                // Round the final value
                r = Math.round(r);
                g = Math.round(g);
                b = Math.round(b);
                a = Math.round(a);
                console.log(
                    `%c The ${"rgba(" + r + "," + g + "," + b + "," + a + ")"}
                                                                        `.`background: The ${"rgba(" + r + "," + g + "," + b + "," + a + ")"}; `
                );
                resolve({ color: [r, g, b, a], url: imgUrl });
            };
        } catch(e) { reject(e); }}); }Copy the code

The exciting time has come to!!!!!!!!!!!!! Ahhhhh!! I am very excited, victory is at hand, rimmen!

A operation, select the target picture, select the resource picture, click the generate picture button, I began to wait for the call of victory!

Oh, my god. It’s even uglier. I don’t know

Then I directly blood up, meet this kind of challenging things I am very energetic, I want to do it, it is not in line with my temperament, so I began to analyze the processed small pieces of the main color, I found that they seem to be regular

I think what is the impact of it, the picture drawing up can not be the same color ah, the same color is what??

Wo kao~ can’t be the 100*100 line I drew

So I went back,drawLineFunction, I commented it out

nice!

Each square can be stretched, rotated, and moved interactively, and at this point the basic function of the canvas is finishedAnd the flower🌹 🏵 🌸 💐 🌺 🌻 🌼 🌷

We can also export the generated pictures. Good friends of the machine can define a large canvas, or number the pictures and print them out, which can be used to make huge composite pictures (such as the wedding photos I mentioned before, etc., which is very interesting).

 // Export the image
exportCanvas() {
    const dataURL = this.canvas.toDataURL({
        width: this.canvas.width,
        height: this.canvas.height,
        left: 0.top: 0.format: "png"});const link = document.createElement("a");
    link.download = "canvas.png";
    link.href = dataURL;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
},
Copy the code

This Valentine’s Day, true is a little full, now is 6:30 in the morning ~ I liver a wave, sleep to sleep, protect life important, during the day but also go out to play 😅😅

The last

To sublimate:

Romantic Tanabata, even in the air are wafting a taste of love. Happy to the lover invited, after dusk, willow tip head, whispering, beautiful scenery, the full moon flowers good! Bless the world lovers, happiness!

This project I have on my Github (github.com/wangrongdin… ~

I am very happy to be stronger with you! You can follow my public account, qianpozhai. I have set up a front-end technology communication group. If you want to communicate and learn with like-minded friends, you can also add my personal wechat (ChicSparrow). I will pull you into the group and cheer together! I am Rong Ding, hopping with me over key caps and characters, through code and programs. 🦄