乐趣区

关于前端:LOOK-直播活动地图生成器方案

本文作者:李一笑

对于前端而言,与视觉稿打交道是必不可少的,因为咱们须要对照着视觉稿来确定元素的地位、大小等信息。如果是比较简单的页面,手动调整每个元素所带来的工作量尚且能够承受;然而当视觉稿中素材数量较大时,手动调整每个元素便不再是个能够承受的策略了。

在最近的流动开发中,笔者就刚好碰到了这个问题。这次流动开发须要实现一款大富翁游戏,而作为一款大富翁游戏,地图天然是必不可少的。在整个地图中,有很多的不同品种的方格,如果一个个手动去调整地位,工作量是很大的。那么有没有一种计划可能帮忙咱们疾速确定方格的地位和品种呢?上面便是笔者所采纳的办法。

计划简述

位点图

首先,咱们须要视觉同学提供一张非凡的图片,称之为位点图。

这张图片要满足以下几个要求:

  1. 在每个方格左上角的地位,搁置一个 1px 的像素点,不同类型的方格用不同色彩示意。
  2. 底色为纯色:便于辨别背景和方格。
  3. 大小和地图背景图大小统一:便于从图中读出的坐标能够间接应用。

上图为一个示例,在每个门路方格左上角的地位都有一个 1px 的像素点。为了看起来显著一点,这里用红色的圆点来示意。在理论状况中,不同的点因为方格品种不同,色彩也是不同的。

上图中用彩色边框标出了素材图的轮廓。能够看到,红色圆点和每个门路方格是一一对应的关系。

读取位点图

在下面的位点图中,所有方格的地位和品种信息都被标注了进去。咱们接下来要做的,便是将这些信息读取进去,并生成一份 json 文件来供咱们后续应用。

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');

在这里咱们应用了 jimp 用于图像处理,通过它咱们可能去扫描这张图片中每个像素点的色彩和地位。

至此咱们失去了蕴含所有方格地位和品种信息的 json 文件:

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

其中,x y 为方格左上角的坐标;type 为方格品种,值为色彩值,代表不同品种的地图方格。

通路连通算法

对于咱们的我的项目而言,只确定门路点是不够的,还须要将这些点连接成一个残缺的通路。为此,咱们须要找到一条由这些点形成的最短连贯门路。

代码如下:

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);
}

到这里,咱们曾经实现了所有的筹备工作,能够开始绘制地图了。在绘制地图时,咱们只须要先读取 json 文件,再依据 json 文件内的坐标信息和品种信息来搁置对应素材即可。

计划优化

上述计划可能解决咱们的问题,但仍有一些不太不便的中央:

  1. 只有 1px 的像素点太小了,肉眼无奈分别。不论是视觉同学还是开发同学,如果点错了地位就很难排查。
  2. 位点图中蕴含的信息还是太少了,色彩仅仅对应品种,咱们心愿可能蕴含更多的信息,比方点之间的排列程序、方格的大小等。

像素点合并

对于第一个问题,咱们能够让视觉同学在画图的时候,将 1px 的像素点扩充成一个肉眼足够辨识的区域。须要留神两个区域之间不要有重叠。

这时候就要求咱们对代码做一些调整。在之前的代码中,当咱们扫描到某个色彩与背景色不同的点时,会间接记录其坐标和色彩信息;当初当咱们扫描到某个色彩与背景色不同的点时,还须要进行一次区域合并,将所有相邻且雷同色彩的点都纳入进来。

区域合并的思路借鉴了下图像处理的区域成长算法。区域成长算法的思路是以一个像素点为终点,将该点四周符合条件的点纳入进来,之后再以新纳入的点为终点,向新起点相邻的点扩张,直到所有符合条件条件的点都被纳入进来。这样就实现了一次区域合并。一直反复该过程,直到整个图像中所有的点都被扫描结束。

咱们的思路和区域成长算法十分相似:

  1. 顺次扫描图像中的像素点,当扫描到色彩与背景色不同的点时,记录下该点的坐标和色彩。

  2. 之后扫描与该点相邻的 8 个点,将这些点打上”已扫描“的标记。筛选出其中色彩与背景色不同且尚未被扫描过的点,放入待扫描的队列中。

  3. 从待扫描队列中取出下一个须要扫描的点,反复步骤 1 和步骤 2。
  4. 直到待扫描的队列为空时,咱们就扫描完了一整个有色彩的区域。区域合并结束。

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}));
                }
            }
        }
    });
};

色彩蕴含额定信息

在之前的计划中,咱们都是应用色彩值来示意品种,但实际上色彩值所能蕴含的信息还有很多。

一个色彩值能够用 rgba 来示意,因而咱们能够让 r、g、b、a 别离代表不同的信息,如 r 代表品种、g 代表宽度、b 代表高度、a 代表程序。尽管 rgba 每个的数量都无限(r、g、b 的范畴为 0-255,a 的范畴为 0-99),但根本足够咱们应用了。

当然,你甚至能够再进一步,让每个数字都示意一种信息,不过这样每种信息的范畴就比拟小,只有 0-9。

总结

对于素材量较少的场景,前端能够间接从视觉稿中确认素材信息;当素材量很多时,间接从视觉稿中确认素材信息的工作量就变得十分大,因而咱们应用了位点图来辅助咱们获取素材信息。

地图就是这样一种典型的场景,在下面的例子中,咱们曾经通过从位点图中读出的信息胜利绘制了地图。咱们的步骤如下:

  1. 视觉同学提供位点图,作为承载信息的载体,它须要满足以下三个要求:

    1. 大小和地图背景图大小统一:便于咱们从图中读出的坐标能够间接应用。
    2. 底色为纯色:便于辨别背景和方格。
    3. 在每个方格左上角的地位,搁置一个方格,不同色彩的方格示意不同类型。
  2. 通过 jimp 扫描图片上每个像素点的色彩,从而生成一份蕴含各个方格地位和品种的 json。
  3. 绘制地图时,先读取 json 文件,再依据 json 文件内的坐标信息和品种信息来搁置素材。

上述计划并非白璧无瑕的,在这里咱们次要对于位点图进行了改良,改良计划分为两方面:

  1. 因为 1px 的像素点对肉眼来说过小,视觉同学画图以及咱们调试的时候,都非常不不便。因而咱们将像素点扩充为一个区域,在扫描时,对相邻的雷同色彩的像素点进行合并。
  2. 让色彩的 rgba 别离对应一种信息,裁减位点图中的色彩值可能给咱们提供的信息。

咱们在这里只着重解说了获取地图信息的局部,至于如何绘制地图则不在本篇的叙述范畴之内。在我的我的项目中应用了 pixi.js 作为引擎来渲染,残缺我的项目能够参考这里,在此不做赘述。

FAQ

  • 在位点图上,间接应用色彩块的大小作为门路方格的宽高能够不?

    当然能够。但这种状况是有局限性的,当咱们的素材很多且彼此重叠的时候,如果仍然用方块大小作为宽高,那么在位点图上的方块就会彼此重叠,影响咱们读取地位信息。

  • 如何解决有损图的状况?

    有损图中,图形边缘处的色彩和核心的色彩会稍微有所差别。因而须要减少一个判断函数,只有扫描到的点的色彩与背景色的差值大于某个数字后,才认为是不同色彩的点,并开始区域合并。同时要留神在位点图中方块的色彩尽量选取与背景色色值相差较大的色彩。

    这个判断函数,就是咱们下面代码中的 isDifferentColor 函数。

    const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;
  • 判断两个色彩不相等的 0xf000ff 是怎么来的?

    轻易定的。这个和图片里蕴含色彩有关系,如果你的背景色和图片上点的色彩十分相近的话,这个值就须要小一点;如果背景色和图上点的色彩相差比拟大,这个值就能够大一点。

参考资料

  • https://zhuanlan.zhihu.com/p/89488964
  • https://codeantenna.com/a/B5fEty3uiP
退出移动版