# 一种生成随机地图的方式
前言: 前两天收到一个产品需求,需要根据不同主播的贡献值 按照比例随机生成地图。很多同学第一时间想到的可能是使用 echart 的地图,但是使用过的同学都知道 echart 里面的地图是需要各种经纬度的。随后笔者 google 了一下,发现很少有类似的,于是自己造了一个轮子js-map-generator (opens new window).
# 先看看效果图
要求: 大地图随机生成,点击每一块需要使用小地图的方式填充大地图的内容,大地图相邻展示
# 实现方式
对于此类的需求相信大家脑袋里面蹦出来的第一个想法就是使用 canvas 进行绘制。那么我们需要解决如下几个问题:
- 如何随机生成地图?
- 如何判断点击的是哪块区域?
- 如何填充指定的区域块?
# 如何随机生成地图
在生成地图之前我们需要将地图 转换成程序里面的二维数组,以便于我们对地图的数据进行修改,删除,和查询。
const canvasWidth = ctx.canvas.width;
const canvasHeight = ctx.canvas.height;
var xLineTotals = Math.floor(canvasHeight / ItemGridSize);
var yLineTotals = Math.floor(canvasWidth / ItemGridSize);
this.numYs = xLineTotals;
this.numXs = yLineTotals;
// 构建数据二维地图
for (let i = 0; i < this.numXs; i++) {
const tmp: {
value: number,
name: string,
color?: string,
}[] = [];
for (let j = 0; j < this.numYs; j++) {
tmp.push({ color: "transparent", value: -1, name: "" });
}
this.dataMap.push(tmp);
}
二维数组构建好之后,我们开始着手考虑如何生成一个随机地图,其步骤大致如下:
- 选取起始点(默认以地图中心);
- 判断起始点是否已被绘制
- 已被绘制: 随机一个方向继续查询
- 未被绘制: 标记数据地图
- 找起始点的随机一个方向,看随机方向是否可以使用
- 可以使用: 标记数据地图
- 不可使用: 找随机方向的随机方向进行判断
其代码大致如下:
// 获取一个点的随机方向
_getContiguous(frontier: PointType) {
return [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
].map((dir) => ({
x: frontier.x + dir[0],
y: frontier.y + dir[1],
}));
}
// 数据填充
fill(dataMap: DataMapType, start: PointType) {
// 保证循环次数最多整个网格的数量的一半
if (this._loopCount >= (this.xCount * this.yCount * 2) / 3) {
console.log("超出查询最大的次数");
return;
}
this._loopCount++;
// 未被占用则标记
if (
start.x > 0 &&
start.y > 0 &&
dataMap[start.x][start.y] &&
dataMap[start.x][start.y].value === -1
) {
this.frontierCount++;
this.frontiers[`${start.x}:${start.y}`] = true;
this.changeMapItem(dataMap, start.x, start.y);
}
// 随机找前后左右
let newCoors = this._getContiguous({
x: start.x,
y: start.y,
});
// 找前后最后可以使用的
let canUseCoors = this.filterCanUse(dataMap, newCoors);
if (canUseCoors.length === 0) {
// 极端情况处理
// 找斜对角
const skewCoors = this._getSkewContiguous(start);
const skewCanUse = this.filterCanUse(dataMap, skewCoors);
if (skewCanUse.length === 0) {
// 相当于一个点的前后左右,斜对面全部被占满了,则尽量从前后左右再次突围
this.fill(dataMap, newCoors[random(3)]);
return;
} else {
newCoors = skewCoors;
canUseCoors = skewCanUse;
}
}
// 标记所有可用的
for (let j = 0; j < canUseCoors.length; j++) {
const ele = canUseCoors[j];
this.changeMapItem(dataMap, ele.x, ele.y);
this.frontiers[`${ele.x}:${ele.y}`] = true;
this.frontierCount++;
}
// ... 继续进行随机fill
}
如何保证可连续性?
如果每一次都从中心点进行判断肯定大量的不必要的判断; 我们可以找到已经生成的地图的边界,然后从随机边界上依次递归判断;
# 如何判断点击的是哪块区域?
经过上面随机地图的生成,我们已经将要渲染的部分标记到数据地图上,由此我们可以对 canvas 监听绑定事件,判断触发的位置是否存在某个区域即可。
function getEventPosition(ev: any) {
var x, y;
if (ev.layerX || ev.layerX === 0) {
x = ev.layerX;
y = ev.layerY;
} else if (ev.offsetX || ev.offsetX === 0) {
x = ev.offsetX;
y = ev.offsetY;
}
return { x: x, y: y };
}
getEventInWhereMap(position: PointType) {
for (let i = 0; i < this.labelQueue.length; i++) {
const ele = this.labelQueue[i];
if (ele.positions[`${position.x}:${position.y}`]) {
return { id: ele.id, index: i };
}
}
return {
index: -1,
id: "",
};
}
this.canvasRef.current?.addEventListener("click", (event) => {
// 获取事件的坐标
const p = getEventPosition(event);
// 转换为数据地图中的坐标
const mapPosition = {
x: Math.floor(p.x / ItemGridSize),
y: Math.floor(p.y / ItemGridSize),
};
// 查看点击位置在那个区域
const selectMap = this.getEventInWhereMap(mapPosition);
if (selectMap.index !== -1) {
const info = this.labelQueue[selectMap.index];
this.props.callback && this.props.callback(info);
}
});
# 如何填充指定的区域块?
笔者一共提供了四种方式:
- 只展示父地图
- 横向按比例展示子地图
- 纵向按比例展示子地图
- 随机占比展示子地图
父地图
最简单的方式是父节点和子节点网格数量一致,按照对应的数据地图进行填充即可直接得到父区域的内容;但是考虑到子地图展示的区域很小,无需较大画布展示,所以我们需要进行一层坐标数据转换。
if (props.center && props.positions) {
//找到子地图的中心
const mapCenterX = Math.floor(this.numXs / 2);
const mapCenterY = Math.floor(this.numYs / 2);
const { center, color } = props;
const xC = center.x - mapCenterX;
const yC = center.y - mapCenterY;
// 更新前先清除一波
this.clearMap();
// 将父区域部分 复制到子地图的中心
for (let i = 0; i < this.numXs; i++) {
for (let j = 0; j < this.numYs; j++) {
if (props.positions[`${i + xC}:${j + yC}`]) {
this.dataMap[i][j].value = 0;
this.dataMap[i][j].color = color;
}
}
}
}
横向按比例展示子地图
for (let y = 0; y < this.yCount; y++) {
for (let x = 0; x < this.xCount; x++) {
if (dataMap[x][y].value === 0 && count <= this.value) {
this.frontiers[`${x}:${y}`] = true;
this.changeMapItem(dataMap, x, y);
count++;
}
}
}
纵向按比例展示子地图
for (let x = 0; x < this.xCount; x++) {
for (let y = 0; y < this.yCount; y++) {
if (dataMap[x][y].value === 0 && count <= this.value) {
this.frontiers[`${x}:${y}`] = true;
this.changeMapItem(dataMap, x, y);
count++;
}
}
}
随机占比展示子地图
此处逻辑和父地图逻辑类似,有兴趣可以查看源码 (opens new window)
至此整个地图大致生成完毕,当然还有很多需要处理的边界情况。
# 封装使用
上面讲了这么多,笔者已经将其封装成一个 react 组件,有兴趣的同学可以按照如下的方式使用:
npm install js-map-generator
详情请点击js-map-generator (opens new window)查看如何使用。
- 本文链接: https://mrgaogang.github.io/javascript/proposal/%E4%B8%80%E7%A7%8D%E7%94%9F%E6%88%90%E9%9A%8F%E6%9C%BA%E5%9C%B0%E5%9B%BE%E7%9A%84%E6%96%B9%E5%BC%8F.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!