Author: Li Yixiao

For the front end, dealing with visual art is essential because we need to determine the location, size, and other information of elements against visual art. For simple pages, the amount of work required to manually adjust each element is acceptable. However, when there is a large amount of material in the visual artwork, manually adjusting each element is no longer an acceptable strategy.

In recent activity development, the author just encountered this problem. The event development needs to complete a Monopoly game, and as a Monopoly game, the map is naturally essential. In the whole map, there are many different kinds of squares, if one by one to adjust the position, the workload is very large. So is there a way to quickly determine the location and type of squares? Here’s how I did it.

Plan outlining the

Site map

First, we need visual students to provide a special picture, called a site map.

This image should meet the following requirements:

  1. Place a 1px pixel in the top left corner of each grid, with different types of grids in different colors.
  2. Solid background color: easy to distinguish between background and grid.
  3. Size is the same as the map background: coordinates that are easy to read from the map can be used directly.

As an example, each path square has a 1px pixel in the upper left corner. And just to make it obvious, I’m going to draw it as a red dot. In practice, different points have different colors due to different types of squares.

The outline of the stock image is shown with a black border. As you can see, the red dots have a one-to-one relationship with each path square.

Read point map

In the loci diagram above, the location and type information of all squares is marked. All we need to do is read this information out and generate a JSON file for our future use.

const JImp = require('jimp');
const nodepath = require('path');

function parseImg(filename) {
    JImp.read(filename, (err, image) = > {
        const { width, height } = image.bitmap;

        const result = [];

        // The color of the pixels in the upper left corner of the image is the color of the background image
        const mask = image.getPixelColor(0.0);

        // Filter out non-mask locations
        for (let y = 0; y < height; ++y) {
            for (let x = 0; x < width; ++x) {
                const color = image.getPixelColor(x, y);
                if(mask ! == color) { result.push({// x and y coordinates
                        x,
                        y,
                        // Type of grid
                        type: color.toString(16).slice(0, -2)}); }}}/ / output
        console.log(JSON.stringify({
            / / path
            path: result,
        }));
    });
}

parseImg('bitmap.png');
Copy the code

Here we use JIMP for image processing, through which we can scan the color and position of each pixel in this picture.

At this point we have a JSON file that contains information about the position and type of all squares:

{
    "path": [{"type": ""."x": 0."y": 0,},// ...],}Copy the code

Where, x and y are the coordinates of the upper-left corner of the grid; Type is the type of grid, and the value is the color value, representing different types of map grid.

Path connectivity algorithm

For our project, it is not enough to identify waypoints, but to connect them into a complete path. To do this, we need to find a shortest connection path consisting of these points.

The code is as follows:

function takePath(point, points) {
    const candidate = (() = > {
        // Sort by distance from smallest to largest
        const pp = [...points].filter((i) = >i ! == point);const [one, two] = pp.sort((a, b) = > measureLen(point, a) - measureLen(point, b));

        if(! one) {return [];
        }

        // If the two distances are small, choose the shortest connected graph path.
        if (two && measureLen(one, two) < 20000) {
            return [one, two];
        }
        return[one]; }) ();let min = Infinity;
    let minPath = [];
    for (let i = 0; i < candidate.length; ++i) {
        // Find the minimum path recursively
        const subpath = takePath(candidate[i], removeItem(points, candidate[i]));

        const path = [].concat(point, subpath);
        // Measure the total path length
        const distance = measurePathDistance(path);

        if(distance < min) { min = distance; minPath = subpath; }}return [].concat(point, minPath);
}
Copy the code

At this point, we have done all the preparatory work and are ready to start mapping. When drawing the map, we only need to read the JSON file first, and then place the corresponding materials according to the coordinate information and type information in the JSON file.

Scheme optimization

The above solution solves our problem, but there are still some inconveniences:

  1. Pixels at just 1px are too small to be discernible by the naked eye. Whether visual students or development students, if the wrong location is difficult to check.
  2. The information contained in the site map is still too little, the color only corresponds to the type, we hope to include more information, such as the order between the points, the size of the grid, etc.

Pixel merge

For the first question, we can ask the visual students to enlarge the 1px pixels into an area that can be identified by the naked eye when drawing. Care needs to be taken that there is no overlap between the two regions.

At this point, we are required to make some adjustments to the code. In the previous code, when we scan a point whose color is different from the background color, we directly record its coordinates and color information; Now when we scan a point whose color is different from the background color, we also need to do a sub-region merge to include all adjacent points with the same color.

The idea of region merging is based on the region growth algorithm of image processing. The idea of region growth algorithm is to take a pixel point as the starting point, including the points around the point that meet the conditions, and then take the newly included point as the starting point, and expand to the points adjacent to the new starting point, until all the points that meet the conditions are included. Thus a regional integration was completed. This process is repeated until all points in the entire image have been scanned.

The idea is very similar to the region growth algorithm:

  1. Scan the pixels in the image successively, and record the coordinates and colors of the point when the color is different from the background color.

  2. Then scan the 8 adjacent points and mark them as “scanned”. The points whose color is different from the background color and have not been scanned are screened out and put into the queue to be scanned.

  3. Remove the next point to be scanned from the queue, and repeat Step 1 and Step 2.

  4. Until the queue to be scanned is empty, we have scanned an entire colored area. Area consolidation completed.

const JImp = require('jimp');

let image = null;
let maskColor = null;

// Determine whether two colors are the same color -> In order to deal with the situation of image color error, do not use equality to judge
const isDifferentColor = (color1, color2) = > Math.abs(color1 - color2) > 0xf000ff;

// Determine if (x,y) exceeds the bounds
const isWithinImage = ({ x, y }) = > x >= 0 && x < image.width && y >= 0 && y < image.height;

// Select the largest number of colors
const selectMostColor = (dotColors) = > { / *... * / };

// Select the coordinates in the upper left corner
const selectTopLeftDot = (reginDots) = > { / *... * / };

// Region merge
const reginMerge = ({ x, y }) = > {
    const color = image.getPixelColor(x, y);
    // The scanned points
    const reginDots = [{ x, y, color }];
    // Color of all scanned points -> After the scan is complete, select the most color values as the color of this area
    const dotColors = {};
    dotColors[color] = 1;

    for (let i = 0; i < reginDots.length; i++) {
        const { x, y, color } = reginDots[i];

        // Grow in the adjacent eight directions
        const seeds = (() = > {
            const candinates = [/* Left, right, up, down, left, left, left, right, up, right */];

            return candinates
                // Remove points beyond the boundary
                .filter(isWithinImage)
                // Get the color of each point
                .map(({ x, y }) = > ({ x, y, color: image.getPixelColor(x, y) }))
                // Remove the points that are close to the background color
                .filter((item) = >isDifferentColor(item.color, maskColor)); }) ();for (const seed of seeds) {
            const { x: seedX, y: seedY, color: seedColor } = seed;

            // Add these points to reginDots as the boundary for the next scan
            reginDots.push(seed);

            // Set this point to the background color to avoid repeated scanning
            image.setPixelColor(maskColor, seedX, seedY);

            // This dot color is a new color that has not been scanned. Add the color to dotColors
            if (dotColors[seedColor]) {
                dotColors[seedColor] += 1;
            } else {
                // The color is the old color, increment the color count value
                dotColors[seedColor] = 1; }}}// After the scan is complete, select the largest number of color values as the color of the region
    const targetColor = selectMostColor(dotColors);

    // Select the upper-left coordinate for the current region
    const topLeftDot = selectTopLeftDot(reginDots);

    return {
        ...topLeftDot,
        color: targetColor,
    };
};

const parseBitmap = (filename) = > {
    JImp.read(filename, (err, img) = > {
        const result = [];
        const { width, height } = image.bitmap;
        // Background color
        maskColor = image.getPixelColor(0.0);
        image = img;

        for (let y = 0; y < height; ++y) {
            for (let x = 0; x < width; ++x) {
                const color = image.getPixelColor(x, y);

                // The color is not similar
                if (isDifferentColor(color, maskColor)) {
                    // Start the seed growth program and scan all adjacent color blocks in turnresult.push(reginMerge({ x, y })); }}}}); };Copy the code

Colors contain additional information

In the previous scheme, we used color values to represent categories, but actually color values can contain a lot more information.

A color value can be represented by RGBA, so we can have r, G, B, and A represent different information, such as R for type, G for width, B for height, and A for order. Although rGBA has a limited number of each (r, G, b range 0-255, a range 0-99), it’s basically enough for us to use.

Of course, you could even go one step further and have each number represent one piece of information, but then the range of each piece of information would be small, from 0 to 9.

conclusion

For scenes with less material, the front end can confirm material information directly from the visual draft. When there is a large amount of material, the work of confirming material information directly from the visual draft becomes very large, so we use the site map to assist us in obtaining material information.

A map is a typical example of such a scenario, and in the example above we have successfully drawn the map from the information read from the site map. Our steps are as follows:

  1. Visual students provide a site map as a carrier of information, which needs to meet the following three requirements:
    1. Size is the same as the map background: coordinates that are easy to read from the map can be used directly.
    2. Solid background color: easy to distinguish between background and grid.
    3. In the upper left corner of each square, place a square with different colors indicating different types.
  2. throughjimpThe color of each pixel on the image is scanned to generate a JSON that contains the location and type of each square.
  3. When drawing the map, the json file is read first, and then the material is placed according to the coordinate information and type information in the JSON file.

The above scheme is not perfect. We mainly improve the site map here, and the improvement scheme can be divided into two aspects:

  1. Since 1px pixels are too small for the naked eye, it is very inconvenient for visual students to draw and debug. Therefore, we expand the pixels into a region, and merge the adjacent pixels with the same color during scanning.
  2. Let the color rGBA correspond to one information, and expand the color value in the loci diagram to give us the information.

We have focused here only on obtaining map information; how to make a map is beyond the scope of this article. In my project, I used pixi.js as the engine to render. The complete project can be referred to here, and I will not repeat it here.

FAQ

  • Is it ok to use the size of the color block directly as the width and height of the path square on the locus map?

    B: Sure. However, this situation has limitations. When we have a lot of overlapping materials, if we still use the square size as the width and height, the squares on the site map will overlap each other, affecting our reading of location information.

  • How to handle lossy graph cases?

    In lossy graphics, the color at the edges of the figure is slightly different from the color at the center. Therefore, it is necessary to add a judgment function. Only when the difference between the color of the scanned point and the background color is greater than a certain number, can it be considered as a point of different color and start region merging. At the same time, it should be noted that the color of the Chinese block in the site map should be larger than the color value of the background.

    This function is the isDifferentColor function in our code above.

    const isDifferentColor = (color1, color2) = > Math.abs(color1 - color2) > 0xf000ff;
    Copy the code
  • How to identify two colors are not equal 0xf000FF?

    It’s arbitrary. This depends on the color in the image. If your background color is very similar to the color of the dots in the image, this value needs to be smaller; This value can be larger if the background color differs greatly from the color of the points on the graph.

The resources

  • zhuanlan.zhihu.com/p/89488964
  • Codeantenna.com/a/B5fEty3ui…