Author of this article: Li Yixiao
For the front-end, dealing with the visual draft is essential, because we need to compare the visual draft to determine the position, size and other information of the element. If it is a relatively simple page, the workload of manually adjusting each element is acceptable; however, when the amount of material in the visual draft is large, manually adjusting each element is no longer an acceptable strategy.
In the recent activity development, the author just ran into this problem. The development of this event requires the completion of a Monopoly game, and as a Monopoly game, maps are naturally indispensable. In the entire map, there are many different types of grids. If you manually adjust the position one by one, the workload is very large. So is there a solution that can help us quickly determine the location and type of squares? The following is the method used by the author.
Brief description of the scheme
Site map
First of all, we need visual students to provide a special picture, called a site map.
This picture must meet the following requirements:
- Place a 1px pixel at the upper left corner of each grid. Different types of grids are represented by different colors.
- The background color is a solid color: it is easy to distinguish the background and the grid.
- The size is the same as the size of the map background: the coordinates that are easy to read from the map can be used directly.
The above picture is an example, and there is a 1px pixel in the upper left corner of each path grid. In order to make it more obvious, it is represented by a red dot here. In actual situations, different points have different colors due to different types of squares.
In the above image, the outline of the material image is marked with a black border. It can be seen that there is a one-to-one correspondence between the red dots and each path square.
Read site map
In the above site map, the location and type information of all the squares are marked. What we need to do next is to read out this information and generate a json file for our subsequent use.
const JImp = require('jimp');
const nodepath = require('path');
function parseImg(filename) {
JImp.read(filename, (err, image) => {
const { width, height } = image.bitmap;
const result = [];
// 图片左上角像素点的颜色, 也就是背景图的颜色
const mask = image.getPixelColor(0, 0);
// 筛选出非 mask 位置点
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 y 坐标
x,
y,
// 方格种类
type: color.toString(16).slice(0, -2),
});
}
}
}
// 输出
console.log(JSON.stringify({
// 路径
path: result,
}));
});
}
parseImg('bitmap.png');
Here we use jimp
for image processing, through which we can scan the color and position of each pixel in this picture.
So far we have got the json file that contains the position and type information of all the grids:
{
"path": [
{
"type": "",
"x": 0,
"y": 0,
},
// ...
],
}
Among them, xy is 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 grids.
Path connectivity algorithm
For our project, it is not enough to only determine the waypoints, but also need to connect these points into a complete path. For this, we need to find the shortest connection path formed by these points.
code show as below:
function takePath(point, points) {
const candidate = (() => {
// 按照距离从小到大排序
const pp = [...points].filter((i) => i !== point);
const [one, two] = pp.sort((a, b) => measureLen(point, a) - measureLen(point, b));
if (!one) {
return [];
}
// 如果两个距离 比较小,则穷举两个路线,选择最短连通图路径。
if (two && measureLen(one, two) < 20000) {
return [one, two];
}
return [one];
})();
let min = Infinity;
let minPath = [];
for (let i = 0; i < candidate.length; ++i) {
// 递归找出最小路径
const subpath = takePath(candidate[i], removeItem(points, candidate[i]));
const path = [].concat(point, subpath);
// 测量路径总长度
const distance = measurePathDistance(path);
if (distance < min) {
min = distance;
minPath = subpath;
}
}
return [].concat(point, minPath);
}
At this point, we have completed all the preparatory work and can start drawing the map. When drawing a 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.
Solution optimization
The above solution can solve our problem, but there are still some inconvenient places:
- Only 1px pixels are too small to be distinguished by the naked eye. Whether it is a visual classmate or a development classmate, if you click the wrong position, it is difficult to troubleshoot.
- The bitmap contains too little information. The color only corresponds to the type. We hope to include more information, such as the order of the dots and the size of the grid.
Pixel merge
For the first question, we can ask visual students to expand the 1px pixel point into an area that is enough to be recognized by the naked eye when drawing a picture. needs to be careful not to overlap between the two areas.
At this time, we are required to make some adjustments to the code. In the previous code, when we scan a point with a color different from the background color, we will directly record its coordinates and color information; now when we scan a point with a color different from the background color, we need to do it again Areas are merged, all adjacent and same color points are included.
The idea of region merging is based on the region growing algorithm of image processing. The idea of the region growing algorithm is to take a pixel as the starting point and include the qualified points around the point, and then use the newly included points as the starting point to expand to the points adjacent to the new starting point until all the points meet the conditions. Are all included. This completes a regional merger. This process is repeated continuously until all points in the entire image have been scanned.
Our idea is very similar to the area growth algorithm:
Scan the pixel points in the image sequentially, and record the coordinates and color of the point when a point with a color different from the background color is scanned.
Then scan the 8 points adjacent to this point and mark these points with the "scanned" mark. Filter out the points whose color is different from the background color and have not been scanned, and put them in the queue to be scanned.
- Take the next point to be scanned from the queue to be scanned, and repeat steps 1 and 2.
Until the queue to be scanned is empty, we have scanned an entire colored area. The regions are merged.
const JImp = require('jimp');
let image = null;
let maskColor = null;
// 判断两个颜色是否为相同颜色 -> 为了处理图像颜色有误差的情况, 不采用相等来判断
const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;
// 判断是(x,y)是否超出边界
const isWithinImage = ({ x, y }) => x >= 0 && x < image.width && y >= 0 && y < image.height;
// 选择数量最多的颜色
const selectMostColor = (dotColors) => { /* ... */ };
// 选取左上角的坐标
const selectTopLeftDot = (reginDots) => { /* ... */ };
// 区域合并
const reginMerge = ({ x, y }) => {
const color = image.getPixelColor(x, y);
// 扫描过的点
const reginDots = [{ x, y, color }];
// 所有扫描过的点的颜色 -> 扫描完成后, 选择最多的色值作为这一区域的颜色
const dotColors = {};
dotColors[color] = 1;
for (let i = 0; i < reginDots.length; i++) {
const { x, y, color } = reginDots[i];
// 朝临近的八个个方向生长
const seeds = (() => {
const candinates = [/* 左、右、上、下、左上、左下、右上、右下 */];
return candinates
// 去除超出边界的点
.filter(isWithinImage)
// 获取每个点的颜色
.map(({ x, y }) => ({ x, y, color: image.getPixelColor(x, y) }))
// 去除和背景色颜色相近的点
.filter((item) => isDifferentColor(item.color, maskColor));
})();
for (const seed of seeds) {
const { x: seedX, y: seedY, color: seedColor } = seed;
// 将这些点添加到 reginDots, 作为下次扫描的边界
reginDots.push(seed);
// 将该点设置为背景色, 避免重复扫描
image.setPixelColor(maskColor, seedX, seedY);
// 该点颜色为没有扫描到的新颜色, 将颜色增加到 dotColors 中
if (dotColors[seedColor]) {
dotColors[seedColor] += 1;
} else {
// 颜色为旧颜色, 增加颜色的 count 值
dotColors[seedColor] = 1;
}
}
}
// 扫描完成后, 选择数量最多的色值作为区域的颜色
const targetColor = selectMostColor(dotColors);
// 选择最左上角的坐标作为当前区域的坐标
const topLeftDot = selectTopLeftDot(reginDots);
return {
...topLeftDot,
color: targetColor,
};
};
const parseBitmap = (filename) => {
JImp.read(filename, (err, img) => {
const result = [];
const { width, height } = image.bitmap;
// 背景颜色
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);
// 颜色不相近
if (isDifferentColor(color, maskColor)) {
// 开启种子生长程序, 依次扫描所有临近的色块
result.push(reginMerge({ x, y }));
}
}
}
});
};
Color contains extra information
In the previous schemes, we all used color values to represent the types, but in fact, the color values can contain a lot of information.
A color value can be represented by rgba, so we can let r, g, b, and a represent different information, such as r for type, g for width, b for height, and a for order. Although the number of rgba is limited (the range of r, g, b is 0-255, and the range of a is 0-99), it is basically enough for us to use.
Of course, you can even go one step further and let each number represent a type of information, but in this way, the range of each type of information is relatively small, only 0-9.
Summarize
For scenes with a small amount of material, the front-end can directly confirm the material information from the visual draft; when the amount of material is large, the workload of confirming the material information directly from the visual draft becomes very large, so we use a bitmap to Assist us in obtaining material information.
A map is such a typical scenario. In the above example, we have successfully drawn the map based on the information read from the site map. Our steps are as follows:
Vision students provide a site map, as a carrier of information, it needs to meet the following three requirements:
- The size is the same as the size of the map background: it is convenient for us to read the coordinates from the map and use them directly.
- The background color is a solid color: it is easy to distinguish the background and the grid.
- Place a square at the upper left corner of each square, and squares with different colors indicate different types.
- Scan the color of each pixel on the picture by
jimp
to generate a json containing the position and type of each grid. - When drawing a map, first read the json file, and then place the materials according to the coordinate information and type information in the json file.
The above scheme is not perfect. Here we mainly improve the site map. The improvement scheme is divided into two aspects:
- Since 1px pixels are too small for the naked eye, it is very inconvenient for visual students to draw pictures and when we debug. Therefore, we expand the pixels into a region, and merge adjacent pixels of the same color when scanning.
- Let the color rgba correspond to a kind of information, and expand the information that the color value in the bitmap can provide us.
We only focus on the part of obtaining map information here, and how to draw a map is beyond the scope of this article. I used pixi.js as the engine for rendering in my project. For the complete project, please refer to here , and I won’t repeat it here.
FAQ
On the location map, can you directly use the size of the color block as the width and height of the path grid?
sure. But this situation has limitations. When we have a lot of materials and overlap with each other, if we still use the square size as the width and height, then the squares on the bitmap will overlap with each other, which affects us to read the position information.
How to deal with the case of lossy graphs?
In a lossy graph, the color at the edge and the center of the graph will be slightly different. 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, it is considered as a point of a different color, and the region merges. At the same time, it should be noted that the color of the square in the site map should be selected as far as possible from the color value of the background color.
This judgment function is the isDifferentColor function in our code above.
const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;
0xf000ff
judge the two colors are not equal, come from?Set it arbitrarily. This is related to the color contained in the picture. If your background color is very similar to the color of the dots on the picture, this value needs to be smaller; if the background color is quite different from the color of the dots on the picture, this value can be larger .
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。