创建程序生成的地图的边框

图片

斯科特·特纳(Scott Turner)继续从事他的程序化游戏的工作,现在决定解决设计地图边框的问题。 为此,他必须解决一些难题,甚至必须创建自己的语言来描述边界。

边界仍然是幻想卡的重要元素,幻想卡在我的名单上已经存在了一段时间。 功能地图通常具有简单的边界线 ,但是幻想地图和中世纪地图(其中前者经常借鉴思想)具有相当周到且具有艺术性的边界。 这些边界清楚地表明,地图是有意制作的,给观众一种奇观的感觉。

目前,在《 Dragons Abound》游戏中有几种简单的绘制边界的方法。 她可以在地图的周围绘制单线或双线,并在拐角处添加简单的元素,如下图所示:



游戏还可以在边框的底部添加一个字段,用于输入地图名称。 《 龙腾飞腾》中此字段有多种变体,包括诸如假螺丝头之类的复杂元素:


这些名称字段存在差异,但是它们都是手动创建的。

幻想卡片边界的一个有趣方面是,它们既具有创造力又是模板。 通常,它们由少量简单元素组成,这些元素以不同的方式组合在一起以产生独特的结果。 与往常一样,对我来说,处理新主题时的第一步是研究地图示例的集合,创建边界元素类型的目录,并研究其外观。

最简单的边界是一条沿地图边缘延伸并指示其边界的线。 正如我上面所说的,它也称为“框架线”:


地图中边框的位置也有所不同。 在此版本中,地图到达图像的边缘,但是边框在图像内部创建了虚拟边框:


可以使用任何类型的边框来完成此操作,但通常仅用于简单边框(如框架的边框)。

流行的幻想卡片设计概念是模拟仿佛它们是在旧的破烂的羊皮纸上绘制的。 有时这是通过将边框绘制为纸张的粗糙边缘来实现的:


这是一个更复杂的示例:


以我的经验,由于使用了数字工具,因此这种方法不太流行。 如果您希望卡片看起来像旧的破烂的羊皮纸,那么将羊皮纸纹理应用于卡片比手动绘制要容易得多。

创建地图边框最强大的工具是可重复性。 在最简单的情况下,重复一行以创建两行就足够了:


您可以通过更改重复元素的样式来增加对地图的兴趣,在这种情况下,可以通过将粗线和细线组合在一起来实现:


根据元素的不同,可能会有各种样式变化。 在此示例中,该行重复出现,但是颜色发生了变化:


要创建更复杂的模式,可以使用“可重复性”。 该边框由大约五条不同宽度和距离的单线组成:


此边框重复了线条,但将它们分开,使它们看起来像两个单独的细边框。 在本文的这一部分中,我将不讨论角度的处理,但是两条线的不同角度也有助于造成这种差异。


这是两行,四行还是六行? 我认为这完全取决于您如何绘制它们!

样式化的另一个元素是用颜色,图案或纹理填充元素之间的空间。 在此示例中,由于两行之间的强调色填充,边框变得更加有趣:


这是如何用图案填充边框的示例:


同样,可以对元素进行样式设置,使它们看起来是三维的。 这是一张地图,其中的边框被着色以使其看起来很丰富:


在此地图中,边框被着色为看起来是三维的,并且与边界在地图边缘内的位置结合在一起:


另一个常见的边框元素是彩色条纹形式的比例尺:


这些条纹形成一个栅格( 制图栅格 )。 在真实地图上,比例尺有助于确定距离,但是在幻想地图上,比例尺主要是装饰元素。

这些条纹通常以黑白绘制,但有时会添加红色或其他颜色:


该元素也可以与其他元素组合,例如在本示例中使用线条和比例尺:


这个例子有点不寻常。 通常,比例尺(如果有)是边框的最内部元素。

在此地图上,具有不同分辨率的不同比例尺(以及奇怪的符文注释!):


(在Reddit上,用户AbouBenAdhem告诉我,符文标记是用巴比伦楔形文字写的数字48和47。此外,“具有不同分辨率的标度”有6个格,分为10个较小的格,对应于巴比伦的十六进制数字系统。通常我指出了地图的来源,但是这篇文章中有太多小作品,所以我没有理会。但是, 这张地图是托马斯·雷Thomas Ray)为作者S.E.博林(S.E. Boleyn 创建的,所以也许他书中的动作发生在巴比伦的随行人员中。)

除了线条和比例尺外,最常见的元素是重复的几何图案。 它通常由圆形,菱形和矩形等部分组成:


可以对几何元素(如线)进行阴影处理以使其看起来是三维的:


可以通过以不同方式组合这些元素来创建复杂的边界。 这是结合了线条,几何图案和比例的边框:


上面显示的示例是数字卡,但是,当然,手写卡也可以完成相同的操作。 这是一个手工创建的简单几何图案的示例:


这些元素还可以通过多种方式灵活组合。 这是结合了“边缘参差不齐”的几何图案:


在上面显示的示例中,几何图案非常简单。 但是您可以通过以不同方式组合基本几何元素来创建非常复杂的图案:


图案的另一个流行元素是编织或凯尔特结:


这是一个更复杂的柳条边框,其中包含颜色,比例和其他元素:


在此地图上,编织与边缘参差不齐的元素结合在一起:


除了几何图案和编织以外,任何重复的图案都可以成为卡片边框的一部分。 这是使用类似于箭头的形状的示例:


这是一个重复波形的示例:


最后,符文或幻想字母的其他元素有时会添加到幻想卡的边缘:


以上示例摘自现代幻想地图,但以下是带有线条和手绘图案的历史(18世纪)地图示例:


当然,您可以找到边界带有许多其他元素的地图示例。 一些最漂亮的是完全手工绘制的,并具有精心制作的装饰,以至于它们可以超越卡片本身( 《阿尔玛世界》 ,弗朗西斯卡·巴格拉德):


关于对称性也值得一提。 像重复性一样,对称性是一种强大的工具,地图边界通常是对称的或具有对称的元素。

许多地图边框从内到外都是对称的,如以下示例所示:


在此,边框由多条填充和不填充的线组成,但理想情况是从外向内相对于边框的中心重复。

在这个更复杂的示例中,边框是对称的,除了比例尺的黑白条纹交替出现:


由于复制比例尺没有意义,因此即使边框的其余部分是对称的,也经常将其视为单独的元素。

除了内部-外部对称之外,边界通常沿其长度重新对称。 一些显示的边框可能具有简单的设计,跨越了地图边缘的整个长度,但在大多数情况下,图案很短且重复,从一个角到另一个角填充了边框:


请注意,在此示例中,模式包含一个不对称的元素(从左到右),但是常规模式是对称的并重复:


该规则的一个显着例外是边框充满了符文或字母字符。 通常,它们看起来是唯一的,就像在边框上写了一些长消息一样:


当然,这里还没有考虑其他许多地图边框元素的示例,但是我们已经有了一个很好的参考点。 在接下来的几部分中,我将在Dragons Abound中开发一些功能,以描述,显示并以程序方式生成类似于这些示例的地图边框。 在第二部分中,我们将从设置用于描述地图边界的语言开始。

第二部分


在这一部分中,我将创建“地图边框描述语言”(MBDL)的初始版本。

为什么要花时间创建地图边界描述语言? 首先,这将是我这一代人的目标。 稍后,我将编写一种用于创建新地图边界的算法,该算法的输出将是对MBDL上新边界的描述。 其次,MBDL将用作地图边界的文本表示。 特别是,我需要能够保存和重用我喜欢的边框。 为此,我需要一个可以书写并用于重新创建地图边框的文本符号。

我将通过定义最简单的元素(线)开始创建MBDL。 该线具有颜色和宽度。 因此,在MBDL中,我将以以下形式显示该行:

L(width, color)

以下是一些示例(对不起我的Photoshop技能):


元素的顺序是从外部到内部(*)的,因此我们假定这是地图顶部的边框:


看第二个示例-带边框的线表示为三个单独的线元素。

(*从外部到内部进行绘制是一个任意选择-在我看来,这比从内部向外部进行绘制更为自然。不幸的是,由于后来发现,有充分的理由朝相反的方向进行工作。很快我会告诉您的,但是一切都留在了帖子中-旧的,因为重做所有插图会花费很多时间)

方便地,空格可以表示为没有颜色的线:


但是具有特定的垂直空间元素会更直观:

VS(宽度)

以下简单元素是几何形状:条纹,菱形和椭圆形。 假定线条在边框的整个长度上伸展,因此它们没有明确指定的长度。 但是几何图形不能填满整条线,因此,除了宽度(*)外,每个几何图形还必须具有长度,轮廓颜色,轮廓宽度和填充颜色:

B(width, length, outline, outline width, fill)
D(width, length, outline, outline width, fill)
E(width, length, outline, outline width, fill)

(*我接受了,我将考虑从外到内的方向上的宽度,并沿着边框测量长度。)

以下是简单几何形状的示例:


为了使这些元素填充边框的整个长度,必须重复它们。 为了指示将重复以填充边框长度的元素组,我使用方括号:

[ element element element ... ]

这是矩形和菱形的重复图案的示例:


有时,在重复模式的元素之间需要一个(水平)空间。 尽管您可以使用没有颜色的元素来创建空间,但是拥有水平空间元素会更聪明,更方便:

HS(length)

MBDL的第一次迭代所需的最后一个功能是能够将元素堆叠在彼此之上。 这是边框示例:


描述它的最简单方法是在上方图案下的一条宽黄线。 您可以通过不同的方式(例如,负的垂直空间)实现此目的,但是我决定使用花括号指示元素向内的顺序:

{element element element ...}

实际上,该条目告诉您记住进入方括号时图案从外向内的位置,然后在离开方括号时返回到该点。 括号也可被视为对占据垂直空间的元素的描述。 因此,上面显示的边框可以描述如下:

L(1, black)
{L(20, yellow)}
VS(3)
[B(5, 10, black, 3, none)
D(5, 10, black,3,red)]
VS(3)
L(1, black)

我们绘制一条黑线,固定在哪里,绘制一条黄线,然后返回到先前的固定位置,放下一点,绘制矩形和菱形图案,放下一点,然后再绘制一条黑线。

MBDL还有很多事情要做,但这足以描述地图的许多边界。 下一步是将MBDL上的边界描述转换为边界本身。 这类似于将计算机程序(例如Javascript)的书面表示形式转换为该程序的执行形式。 第一步是语言的词法分析(解析) -将源文本转换为地图的真实边框或某种易于转换为边框的中间形式。

解析是计算机科学领域中经​​过充分研究的领域。 解析语言不是很简单,但是在我们的例子中,MBDL是一种无上下文语法,这很不错(*)。 上下文无关的语法很容易解析,并且有许多针对它们的Javascript解析工具 。 我选择了Nearley.js ,它似乎已经相当成熟,并且(更重要的是)一个有据可查的工具。

(*这不只是运气,我确保该语言是上下文无关的。)

我不会向您介绍无上下文语法,但是Nearley语法非常简单,您应该毫无问题地理解其含义。 语法Nearley由一组规则组成。 每个规则在规则的左侧,箭头和右侧都有一个字符,该字符可以是字符和非字符的序列,以及用“ |”分隔的各种选项 (或):

border -> element | element border
element ->
L"

每个规则都说左侧可以用右侧的任何选项代替。 也就是说,第一个规则说一个边界是一个元素或一个元素,后跟另一个边界。 本身可以是元素,也可以是后跟边框的元素,依此类推。 第二条规则说,一个元素只能是字符串“ L”。 也就是说,这些规则共同对应于以下边界:

L
LLL

并且不符合以下边界:

X
L3L

顺便说一句,如果您想在Nearley中尝试这种(或任何其他)语法,那么这里有一个在线沙箱。 您可以输入语法和测试用例以查看其匹配项和不匹配项。

这是线图元的更完整定义:

@builtin “number.ne"
@builtin “string.ne"
border -> element | element border
element -> “L(" decimal “," dqstring “)"

Nearley有几个常见的内置元素,数字是其中之一。 因此,我可以用它来识别线图元的数值宽度。 对于颜色识别,我使用了另一个内置元素,并允许在双引号中使用任何字符串。

在不同字符之间添加空格会很好,所以让我们开始吧。 Nearley支持字符类,并且RBNF使用“:*”表示“零个或多个”,因此我可以使用它来指定“零个或多个空格”,并将其粘贴到任何地方以允许在说明中使用空格:

@builtin "number.ne"
@builtin "string.ne"
border -> element | element border
WS -> [\s]:*
number -> WS decimal WS
color -> WS dqstring WS
element -> "L(" number "," color ")"

但是,由于遍及WS的使用使语法难以阅读,因此我将放弃它们,但可以想象它们是。

元素也可以是垂直空间:

@builtin "number.ne"
@builtin "string.ne"
border -> element | element " " border
number -> decimal
color -> dqstring
element -> "L(" number "," color ")"
element -> "VS(" number ")"

这对应于这样的边界

L(3.5,"black") VS(3.5)

接下来是带状,菱形和椭圆形的基元。

@builtin "number.ne"
@builtin "string.ne"
border -> element | element " " border
number -> decimal
color -> dqstring
element -> "L(" number "," color ")"
element -> "VS(" number ")"
geometric -> "B(" number "," number "," color "," number "," color ")"
geometric -> "E(" number "," number "," color "," number "," color ")"
geometric -> "D(" number "," number "," color "," number "," color ")"

它将匹配这样的元素

B(34, 17, "white", 3, "black")

(请注意,几何图元不是“元素”,因为它们不能在顶层单独存在。必须将其包含在模式中。)

我还需要一个水平空间图元:

@builtin "number.ne"
@builtin "string.ne"
border -> element | element " " border
number -> decimal
color -> dqstring
element -> "L(" number "," color ")"
element -> "VS(" number ")"
geometric -> "B(" number "," number "," color "," number "," color ")"
geometric -> "E(" number "," number "," color "," number "," color ")"
geometric -> "D(" number "," number "," color "," number "," color ")"
geometric -> "HS(" number ")"

现在,我将添加一个模式(重复)操作。 这是方括号内的一个或多个元素的序列。 我将使用RBNF运算符“:+”,在这里表示“一个或多个”。

@builtin "number.ne"
@builtin "string.ne"
border -> element | element " " border
number -> decimal
color -> dqstring
element -> "L(" number "," color ")"
element -> "VS(" number ")"
geometric -> "B(" number "," number "," color "," number "," color ")"
geometric -> "E(" number "," number "," color "," number "," color ")"
geometric -> "D(" number "," number "," color "," number "," color ")"
geometric -> "HS(" number ")"
element -> "[" (geometric):+ "]"

请注意,图案只能用几何图元填充。 例如,我们不能在图案内放置一条线。 现在pattern元素将匹配这样的内容。

[B(34,17,"white",3,"black")E(13,21,"white",3,"rgb(27,0,0)")]

语言的最后一部分是覆盖运算符。 这是括号内任意数量的元素。

@builtin "number.ne"
@builtin "string.ne"
border -> element | element " " border
number -> decimal
color -> dqstring
element -> "L(" number "," color ")"
element -> "VS(" number ")"
geometric -> "B(" number "," number "," color "," number "," color ")"
geometric -> "E(" number "," number "," color "," number "," color ")"
geometric -> "D(" number "," number "," color "," number "," color ")"
geometric -> "HS(" number ")"
element -> "[" (geometric ):+ "]"
element -> "{" (element ):+ "}"

这使我们可以执行以下操作:

{L(3.5,"rgb(98,76,15)")VS(3.5)}

(请注意,与重复运算符不同,可以在内部使用overlay运算符。)

清除描述并在必要的位置添加空格后,我们得到以下MBDL语法:

@builtin "number.ne"
@builtin "string.ne"
border -> (element WS):+
WS -> [\s]:*
number -> WS decimal WS
color -> WS dqstring WS
element -> "L(" number "," color ")"
element -> "VS(" number ")"
element -> "(" WS (element WS):+ ")"
element -> "[" WS (geometric WS):+ "]"
geometric -> "B(" number "," number "," color "," number "," color ")"
geometric -> "E(" number "," number "," color "," number "," color ")"
geometric -> "D(" number "," number "," color "," number "," color ")"
geometric -> "HS(" number ")"

因此,现在已经定义了MBDL,并且我们已经创建了该语言的语法。 它可以与Nearley一起使用以识别语言字符串。 在深入研究MBDL / Nearley之前,我想实现MBDL中使用的原语,以便可以显示MBDL上描述的边界。 我们将在下一部分中进行此操作。

第三部分


现在我们将开始自己实现渲染原语。 (目前,我还不需要将解析器绑定到呈现原语。为了进行测试,我将手动对其进行调用。)

让我们从原始行开始。 回忆一下它的样子:

L(width, color)

除了宽度和颜色,这里还有一个隐式参数-距地图外边缘的距离。 (我从地图的边缘向外绘制边界。请注意,我们从另一个边界开始!)它不应指向MBDL,因为运行MBDL绘制边界的解释器可以跟踪该边界。 但是,应将其输入所有渲染图元,以便它们知道在何处绘制它们。 我称这个参数为偏移量。

如果只需要在地图顶部绘制边框,则线图元将非常容易实现。 但是,实际上,我将需要从上方进行绘制。 底部,左侧和右侧。 (也许有一天我会认识到倾斜或弯曲的边界,但是现在我们将遵循标准的矩形边界。)此外,线元素的长度和位置取决于地图的大小(以及偏移量)。 因此,作为参数,我需要所有这些数据。

设置所有这些参数后,只需创建一个线图元并使用它在地图上画一条线就足够了:


(请注意,我使用了Dragons Abound的各种功能来绘制“手写”线。)让我们尝试创建一个更复杂的边框:

L(3, black) L(10, gold) L(3, black)

看起来像这样:


还不错 请注意,在某些情况下,黑线和金线由于波动而无法完全对齐。 如果我想摆脱这些斑点,那么您可以简单地减少振荡量。

实现垂直空间图元非常简单; 它只是执行偏移增量。 让我们添加一些空间:

L(3, black) L(10, gold) L(3, black)
VS(5)
L(3, black) L(10, red) L(3, black)


在绘制线时,可以通过在偏移量和沿地图的顺时针方向之间绘制来实现角度。 但总的来说,我需要在地图边框的每一侧实现截断以创建与bevel角度连接 。 这对于创建带有在拐角处正确连接的图案的边框是必要的,并且在通常情况下,将不需要绘制边缘具有一定角度的元素。 (*)

(注意:正如以下部分所述,随着时间的流逝,我在实现角度时拒绝使用截断区域。主要原因是要创建复杂的角度,例如正方形偏移:


需要越来越复杂的截断区域。 另外,随着时间的流逝,我发现了一种更好的方法来处理角落的图案。 我决定不返回并重写本文的这一部分,而是决定保留它来说明“创造力”的过程。)

主要思想是沿对角线截断每个边框并创建四个截断区域,在该区域中将绘制边框的每一侧:


截断时,在相应区域中绘制的所有内容都会以所需的角度被切除。


不幸的是,这会沿对角线产生小的间隙,这可能是因为浏览器未完全沿截断的边缘进行平滑处理。 测试表明,背景从两条边缘之间的缝隙中发出光芒。 可以通过稍微扩展其中一个遮罩来解决此问题(一半像素似乎足够了),但这有时不能解决问题。

接下来,您需要实现几何形状。 与线条不同,它们在图案中重复出现,填充了地图边框的一面:


一个人会从左到右绘制该图案,绘制一个矩形,一个菱形,然后重复该图案直到填充整个边框。 因此,这也可以在程序中通过沿边框绘制图案来实现。 但是,先绘制所有矩形,然后绘制所有菱形会更容易。 只需沿边界间隔绘制相同的几何图形就足够了。 每个元素具有相同的间隔非常方便。 当然,一个人不会这样做,因为将元素安排在正确的位置太困难了,但这对程序来说不是问题。

也就是说,绘制简单几何形状的过程需要使用参数,在该参数中,图形的所有尺寸和颜色(即宽度,长度,线宽,线颜色和填充)都将被转移,并且起始位置(由于其原因将很快变得清晰,我将考虑图形的中心),重复之间的过渡的水平空间间隔以及重复的次数。 以向量[dx,dy]的形式指示重复方向也很方便,这样我们就可以从左到右,从右到左,上或下执行重复,只需更改向量和起点即可。 将它们放在一起,得到一条重复的形状:


多次使用此代码并以相同的偏移量进行渲染,我可以结合使用黑白条纹来创建地图比例尺:


在我开始弄清楚如何将所有这些内容应用到地图的真实边界之前,让我们首先为椭圆和菱形实现相同的功能。

菱形只是具有旋转顶点的矩形,因此您只需要对代码进行一些更改。 事实证明,我仍然没有现成的代码来渲染椭圆,但是很容易获得椭圆参数视图并创建一个函数,该函数可以给我椭圆的要点:


这是一个使用上面实现的功能的示例(手动创建):


对于这么少量的代码,它看起来还不错!

现在,让我们解决带有重复元素(角)的边界的复杂情况。

如果边界带有重复元素,则有几种方法可以解决带有拐角的问题。 首先是调整重复,以使它们在角落处执行而不会引起明显的婚姻:


另一种选择是在两侧拐角附近的某个位置停止重复。 如果无法在角落轻松“旋转”图案,通常可以这样做:


最后一个选择是用一些角装饰关闭图案:


总有一天,我会去装饰角落,但现在我们将使用第一种选择。 如何在地图的角落中使条纹或圆形图案“旋转”而没有间隙?

主要思想是将pattern元素准确地放置在角落中,以使其一半位于地图的一侧,另一侧位于相邻的一侧。 在此示例中,圆正好在角上,并且可以从任何方向绘制:


在其他情况下,该元素在一个方向上绘制一半,在另一个方向上绘制一半,但边缘重合:


在这种情况下,在两侧都绘制了白色条纹,但在角落之间没有间隙地连接。

将元素放置在角落时,需要考虑两个方面。

首先,转角元素将相对于穿过元素中心的对角线进行拆分和镜像。 具有径向对称性的元素(例如正方形,圆形和星形)不会改变其形状。 没有径向对称的元素(例如矩形和菱形)在相对于对角线镜像时会改变形状。

其次,为了使两侧的角元素正确连接,地图的两侧必须有整数个元素(*)。 它们的编号不必相同,但是两侧的元素数必须为整数。 如果一侧上包含少量图案,则从一侧边缘开始,该图案将与相邻侧面不重合。

(*在某些情况下,例如,带有长条纹的情况下,部分重复可能会发生完全重复,并且元素无论如何都会对齐。但是,生成的角元素将是不对称的,并且其长度与地图两侧的相同元素不同。在此处可以看到以下示例:


出现白色比例尺,并带有不同的部分重复,因此获得了相对于中心偏移的元素。 对于地图比例尺,情况并非总是如此,因为它可以显示绝对距离并且不必对称。 但是对于装饰图案,通常看起来很糟糕。)

这是一个示例,显示了如何精确地修剪角落的整数重复:


如果从所有四个侧面都进行同样的操作,则边角将重合,并且图案将沿着边框的整个长度无缝定位:


经过仔细检查,您会发现该图案并非完全出现在角落。 每个角的半个圆是从每个侧面截取的,并且这两个半部是用手独立绘制的,因此它们不是完美的。 但是现在他们已经足够接近这个了。

因此,我们可以为角上的图案实现完美的连接,为每个边缘选择整数个重复。 但是,解决这个问题的方法并不简单。

首先,假设我们知道边长为866像素,并且我们想重复元素43次。 然后,该元素应每20.14像素重复一次。 我们如何设置元素的特定长度(通常情况下,元素的模式)? 在上面的示例中,我在圆圈之间添加了额外的空间。 但是,如果圆圈最初相互接触,则将改变模式。 也许值得扩大圈子,使他们继续相互接触?


现在元素正在接触,但是圆已变成椭圆,并且拐角处的形状很奇怪。 (请记住,我说过没有径向对称的元素在相对于角度反射时会改变形状吗?对于条纹来说,这不是什么大问题。)或者,也许值得压缩所有元素,使它们彼此接触并适合合适的长度:


但是为了实现这一点,我们需要使元素比原始元素小得多。 这些选项似乎都不完美。

当卡的侧面尺寸不同时,会发生第二个问题。 现在我们需要解决找到适合双方的整数重复的问题。 找到适合双方的解决方案将是理想的。 但是我不想这样做,但要以过多的模式更改为代价。 如果它们都足够接近原始图案,则在两侧都创建稍微不同的图案可能会更好。

最后,当我使用将多个元素相互叠加的功能时,会出现第三个问题:


我不想对模式进行任何更改,以免破坏元素之间的关系。我认为通过适当的缩放比例,整个比例将保持不变,但是我需要对此进行测试。

有趣的任务,对不对?到目前为止,我还没有针对她的高质量解决方案。也许它们会稍后出现!

第4部分


因此,我们已经实现了用于绘制线条和几何形状的图元。我开始使用重复形状填充边框,并谈到了在地图边框上放置任意图案以使其完美适合角落的困难。主要问题是,在一般情况下,您必须使图案更长(或更短),以使其适合侧面。更改模式长度的选项(添加或删除空格,更改模式元素的长度)会导致模式本身发生各种变化。从多个元素中选择模式似乎很困难!

当我遇到这些看似毫不妥协的任务时,我喜欢从实现一个简单的版本开始。不成功的任务通常可以通过反复解决“简单”问题来解决,直到结果足够好为止。有时,简单版本的实现可以提供一些理解,从而简化对更复杂问题的解决方案。如果情况没有好转,而问题仍然令人不舒服,那么至少我们会得到一个简化的版本,尽管它不尽如人意,但仍然可以派上用场。

最简单的方法是通过增加长度而不改变图案中的任何内容来改变图案的长度。本质上,这会在模式的末尾添加空白。(注意:最好在模式中的所有元素之间分配空白空间。)值得考虑的是,这种解决方案只能延长模式。我们总是可以在模式中添加空白空间,但是在必要时不能使用它-也许模式中将不再有空白空间!

使用这种方法,卡侧面的图案定位算法将非常简单:

  • 用卡片的一面的长度除以图案的长度,然后将其四舍五入,以确定适合该面的图案的重复次数。
  • 在这种情况下,元素之间的距离将等于边的长度除以重复次数。(鉴于我们只能增加空间,因此这是最接近原始位置的位置。)
  • 考虑到计算出的距离,沿着侧面画一个图案。

很难实施该系统。角落固执地不想重合。我花了太多时间才意识到,当地图不是正方形时,我无法从地图中心画出四边的截断区域,因为这会产生不等于45度的截断角。实际上,截断区域应类似于信封的背面:


当我弄清楚这一点时,该算法开始正常工作。

(但是请不要忘记前面的说明,随着时间的流逝,我会放弃截断区域!)

这是一个示例,比率约为2:1:

在这样的规模上,很难注意到,但是角落正确连接,并且两侧之间只有很小的视觉差异。在这种情况下,用于对齐图案的算法仅需要插入小数像素,因此肉眼看不到它,尤其是因为圆的轮廓被像素重叠。

这是另一个带有条纹的示例:


这是方形边框的顶部。这是矩形地图上的相同边框:


在这里,您可以看到在卡的侧面上,条带之间在视觉上有较大的间隙。该算法插入的空间不得超过一个完整元素的长度;因此,最糟糕的情况是当我们的元素较长且边长短而与合适的尺寸略有不同时。但是在大多数实际情况下,对齐并不是很有害。

这是带有多个元素的模式的示例:


这里的条纹与条纹重叠:


您可以看到,由于对每个元素执行相同的对齐,因此条纹保持相对于彼此居中。

我建议很难将图案放置在地图的侧面,但是采用非常简单的方法来均匀分布图案元素以填充所需的空间对于许多图案来说效果很好。这提醒我们所有人:无需假设决策必须很复杂;它可能比您想象的要容易!

但是,该解决方案不适用于带有触摸元素的图案,例如,地图比例尺。在这种情况下,增加空间会移动元素:


我上面提到的延长图案的另一种方法是拉伸图案的各个元素。它适用于诸如比例尺图案之类的东西,但是在具有对称元素的图案中看起来会很糟糕,因为拉伸会使它们不对称。

事实证明,通过拉伸实现该选项比我预期的要困难得多,这主要是因为我不得不以不同的尺寸拉伸地图不同边缘上的元素(因为地图可能不是正方形而是矩形),并且还基于新的拉伸对象动态更改了元素的排列方式大小。但是几个小时后,我设法实现了这一点:


现在,我具有绘制地图边框所需的所有功能(尽管border元素本身是手动创建的):


我将图像转换为灰度,因为我不想打扰颜色的选择,而且卡本身也很无聊,但作为概念证明,边框看起来很漂亮。

第5部分


在第2部分中,我开发了地图边界描述语言(MBDL)语法,在第3部分和第4部分中,我实现了执行所有语言原语的过程。现在,我将连接这些部分,以便我可以在MBDL上描述边框并将其绘制在地图上。

在第3部分中,我编写了MBDL语法,使其可以与Nearley Javascript 解析工具一起使用完成的语法如下所示:

@builtin " number.ne"
@builtin " string.ne"
border -> (element WS):+
WS -> [\s]:*
number -> WS decimal WS
color -> WS dqstring WS
element ->
" L(" number " ," color " )"
element -> " VS(" number " )"
element -> " (" WS (element WS):+ " )"
element -> " [" WS (geometric WS):+ " ]"
geometric -> " B(" number " ," number " ," color " ," number " ," color " )"
geometric -> " E(" number " ," number " ," color " ," number " ," color " )"
geometric -> " D(" number " ," number " ," color " ," number " ," color " )"
geometric -> " HS(" number " )"

默认情况下,当使用Nearley成功解析规则时,该规则将返回一个数组,其中包含与该规则右侧相对应的所有元素。例如,如果规则

test -> " A" | " B" | " C"

与字符串匹配

A

然后Nearley将返回

[ " A" ]

具有单个值的数组是对应于规则右侧的字符串“ A”。

使用此规则解析元素时,Nearley返回什么?

number -> WS decimal WS

规则的右侧分为三部分,因此它将返回一个包含三个值的数组。第一个值将是返回WS规则的值,第二个值将是返回十进制规则的值,第三个值将是返回WS规则的值。如果使用上述规则,我将解析为“ 57”,则结果将如下所示:

[
[ " " ],
[ "5", "7" ],
[ ]
]

Nearley解析的最终结果将是一个嵌套的数组数组,这是一个语法树。在某些情况下,语法树是非常有用的表示形式;在其他情况下,则不是。例如,Dragons Abound中,这样的树不是特别有用。

幸运的是,Nearley规则可以覆盖标准行为并返回所需的任何内容。实际上,十进制的(内置)规则不会返回数字列表,它会返回等效的Javascript数字,这在大多数情况下更为有用,也就是说,数字规则的返回值具有以下形式:

[
[ " " ],
57,
[ ]
]

Nearley规则通过向规则添加后处理器,采用标准数组并将其替换为所需的内容来重新定义标准行为。后处理器只是规则末尾特殊括号内的Javascript代码。例如,在数字规则中,我从不对数字两侧的空格感兴趣。因此,如果规则仅返回一个数字,而不是三个元素的数组,将很方便。这是执行此任务的后处理器:

number -> WS decimal WS {% default => default[1] %}

该后处理器采用标准结果(上面显示的三元素数组),并用该数组的第二个元素替换它,这是十进制规则中的Javascript数字因此,现在数字规则返回实数。

此功能可用于将输入语言处理为中间语言,从而更易于使用。例如,我可以使用Nearley语法将MBDL字符串转换为Javascript结构的数组,每个Javascript结构均表示由“ op”字段标识的原语。行基元的规则如下所示:

element -> " L(" number " ," color " )" {% data=> {op: " L", width: data[1], color: data[3]} %}

也就是说,解析“ L(13,黑色)”的结果将是Javascript结构:

{op: " L", width: 13, color: " black"}

添加适当的后处理后,从语法返回的结果可以是传入行的操作结构的序列(数组)。也就是说,解析字符串的结果

L( 415, “black")
VS(5)
[B(1, 2, “black", 3, “white") HS(5) E(1, 2, “black", 3, “white")]

将是

[
{op: "L", width: 415, color: "black"},
{op: "VS", width: 5},
{op: "P",
elements: [{op: "B", width: 1, length: 2,
olColor: "black", olWidth: 3,
fill: "white"},
{op: "HS", width: 5},
{op: "E", width: 1, length: 2,
olColor: "black", olWidth: 3,
fill: "white"}]}
]

创建地图边框要容易得多。

此时,您可能会提出一个问题-如果Nearley规则的后处理阶段可以包含任何Javascript,那么为什么不跳过中间视图并在后处理过程中直接绘制地图边框呢?对于许多任务,此方法将是理想的。我出于某些原因决定不使用它。

首先,在MBDL中,有两个(*)组件在解析过程中无法执行。例如,在解析过程中,我们无法绘制重复的几何元素(条形或菱形),因为我们需要从相同模式的其他元素中获取信息。特别是,我们需要知道模式的总长度,以便了解我们需要安排每个单独元素的重复的距离。也就是说,图案的元素仍应创建所有几何元素的中间表示。

(*我还没有谈论过其他具有类似限制的组件。)

其次,Nearley中的Javascript已嵌入到规则中,因此,除了全局变量之外,我们将无法将其他信息传递给Javascript。例如,要绘制边框,我需要知道地图的大小,所使用的四个截断区域等。尽管我可以添加使该信息可用于Nearley后处理器的代码,但是这会有些混乱,并且维护此类代码可能很困难。

由于这些原因,我正在解析为一个中间表示,然后执行该中间表示以创建地图本身的边框。

下一步是开发一个解释器,该解释器接收MBDL的中间表示并运行它以生成地图边界。这不是很难做到的。基本上,工作是设置初始条件(例如,为地图的四个边生成截断区域),并在每个中间表示的结构序列上进行迭代。

有一些湿滑的时刻。

首先,我需要从内部渲染到从内部到外部的绘制。原因是因为我希望大多数边界不与地图重叠,所以我需要绘制边界,以使内部边缘的线与地图的边缘重合。如果从外部向内绘制,则在开始绘制之前我需要知道边框的宽度,以便边框不会与地图重叠。如果从内向外绘制,则仅从地图边缘开始绘制。还可以选择在地图上加上边框。只需从负的垂直空间(VS)开始边界即可。

另一个困难点是重复模式。要绘制重复的图案,我需要查看图案的所有元素并确定最宽的元素,因为这将设置整个图案的宽度。我还需要查看并跟踪模式的长度,以便在每次重复之前知道要离开多少距离。

这是我用来测试解释器的一个相当复杂的边框示例:


我认为有可能(必要吗?)将其附加到解析器进行测试,但是对于这个边界,我只是手动创建了一个中间视图:

[
{op:'P', elements: [
{op:'B', width: 10, length: 37, lineWidth: 2, color: 'black', fill: 'white'},
{op:'B', width: 10, length: 37, lineWidth: 2, color: 'black', fill: 'black'},
]},
{op:'VS', width: 2},
{op:'L', width:3, color: 'black'},
{op:'PUSH'},
{op:'L', width:10, color: 'rgb(222,183,64)'},
{op:'POP'},
{op:'PUSH'},
{op:'P', elements: [
{op:'E', width: 5, length: 5, lineWidth: 1, color: 'black', fill: 'red'},
{op:'HS', length: 10},
]},
{op:'L', width:3, color: 'black'},
{op:'POP'},
{op:'VS', width: 2},
{op:'P', elements: [
{op:'E', width: 2, length: 2, lineWidth: 0, color: 'black', fill: 'white'},
{op:'HS', length: 13},
]},
]

我通过反复试验创建了此视图。不管怎么说,口译员都可以工作!

最后,让我使用解析器从MBDL版本创建中间视图。在这里没有太多要显示给我的东西:我必须修复一些字段名,但是否则代码可以正常工作。对于边框,我使用了稍有不同的MBDL版本:

[B(5,37,"black",2,"white") B(5,37,"black",2,"black")]
VS(3)
L(3,"black")
{L(10,"rgb(222,183,64)")}
[E(5,5,"black",1,"red") HS(-5) E(2,2,"none",0,"white") HS(10)]
L(3,"black")

她绘制相同的边框,但方式略有不同。我还更改了叠加层的语法,用花括号替换了括号,以使其与其他语法有更多不同。

为了说明为什么我要从内向外绘制,而不仅仅是自动将边框放置在地图的外部,我可以在此边框的开头添加一个负的垂直空间,以将地图比例尺移动到地图的边缘:


现在,我拥有了程序生成地图边界所需的大多数基础结构:边界描述语言,语言解析器和执行中间表示的过程。剩下的只是处理困难的部分-程序生成!

第6部分


现在,整个MBDL均已实现,我打算继续进行地图边界的程序化生成,但是我不确定该如何实现,因为我会花点时间并实现MBDL的其他一些功能。

在第一次讨论带模式的转角处理时,我谈到了几种不同的方法。最后,我意识到了斜角,但是还有第二种选择:将图案停在拐角附近,如以下示例所示:



当边框图案是某种非对称图形,符文或其他不能旋转90度而又保持对齐的东西时,通常使用这种解决方案。但这显然适用于几何形状。

这可能是在生成边框之前选择的选项,但是如果从边框的一部分启用它,而在另一部分使用斜角,则可以增加一些灵活性。为此,我必须向MBDL添加新命令。我怀疑边界的不同部分可能还会出现其他选项,因此我将添加一个常规选项命令:

element -> "O(MITER)"
element -> "O(STOPPED)"
element -> "O(STOPPED," number ")"

(为清楚起见,这里再次省略了空格和其他一些细节。)到目前为止,唯一的选择是“ MITER”(斜角)用于斜角,而“ STOPPED”(停止)用于在拐角附近停止。如果未停止发送任何值,则程序在距拐角的一段合理距离处停止模式。如果传送了该值,则图案在距拐角的那个距离处停止。

如果使用了STOPPED拐角,那么我将不再从拐角处绘制拐角图案。看起来是这样的:


在这里,我为黑白比例模式使用了MITER选项,因此它相对于角度是镜像的。对于金线内的红色圆圈和黑色正方形的图案(以及边界外的圆圈图案),我使用了STOPPED。您可以看到这两种模式在拐角处结束。

但是,有两个问题。首先,我们看到左边最靠近拐角的元素是一个黑色正方形,而上面是一个红色圆圈。发生这种情况是因为拐角的一侧在重复的起点附近,而在另一侧的重复的终点附近。但是看起来很奇怪。如果角是对称的,那会更好,即使为此我们必须在图案的末尾添加另一个元素。其次,您可以看到边框外的图案(半圆和黑点)也以一个重复出现在角上。但是,由于此重复的长度远小于红色圆圈/黑色正方形的长度,因此它们最终会出现在不同的位置。如果所有样式都停止在距拐角相同的距离处,那可能会更好。

要解决第一个问题,您需要在边框的每一边的末尾添加另一个重复图案的第一个元素。但这实际上有点复杂,因为我可以在图案内使用负的水平偏移量来重叠多个元素(如此处所示)。您还需要向具有与第一个元素相同起点的模式的任何元素添加另一个重复。


现在,图案相对于角度是对称的,并且看起来更好。

接下来,我需要跟踪最长的STOPPED模式并在此距离处停止每个STOPPED模式:


现在,更多地留出了白色圆圈的图案,但仍与红色圆圈的图案不对齐。 怎么了发生这种情况是因为白色圆圈图案距离地图的边缘更远,并且边框的长度比绘制红色圆圈图案的地方更长。要解决此问题,您还需要移动图案,并考虑其相对于地图边缘的偏移量。


现在,所有内容都已完美对齐。

角度的第二个选项是拐角处的正方形偏移,例如:


实施起来会更加困难!

但是,此选项的语法很简单,并使用Option操作码:

element -> "O(SQOFFSET)"
element -> "O(SQOFFSET," number ")"

数字表示地图边缘上元素的正方形位移的大小;具有不同偏移量的元素必须相应对齐。如果没有数字,程序将选择适当的偏移尺寸。将数字清零将禁用平方偏移。这使您可以创建边框,其中某些元素使用正方形偏移,而其他元素则不使用正方形偏移,例如此边框:


我意识到的第一件事是,我将需要其他截断区域,因为我使用截断来处理边界改变方向的位置。SQOFFSET将需要更复杂的截断区域;启用和禁用SQOFFSET时,您还将需要用于不同项目的单独区域。考虑到截断区域总会增加不需要的伪像,这似乎是太多的工作。

当我在上面的可停止模式上工作时,我实现了填充非对称模式以从模式的一端添加另一个重复。我还意识到,这将消除对斜角的需要。我将简单地沿边界顺时针绘制所有图案,在一个角开始该图案,在下一个角附近结束。这将使我摆脱截断区域。

在这种新的转角处理方式中,最重要的是,图案的第一个元素不再“分为”两个侧面。如果您在上面的地图上查看黑白比例尺图案,您会发现有一个白色矩形穿过角落。现在,白色矩形将邻接角部:


地图是通过两种方式绘制的,但这不是很大的问题。

首先,我实现了行的偏移量。为此,只需相对于相应角度旋转线即可:


如您所知,我可以将角度与偏移量和规则角度结合起来,如上图所示:


当然,拐弯处的图案更加困难。一般的想法是沿着边界从一个角到另一个角,依此类推,直到我们回到起点。从理论上讲,仅绘制水平和垂直图案就足够了,并且所有内容都应精美对齐;实际上跟踪所有这些都是沉闷的。实际上,我必须完全重写两次代码,并写一堆纸,但我不会详细讨论。只显示结果:


拐角处出现令人讨厌的视觉错觉-拐角元素似乎更不靠近拐角的外部居中。实际上,事实并非如此,但事实并非如此,因为在靠近拐角内侧的地方,在视觉上有更多的空白空间。

由于偏移角的段很短,因此在拐角处创建非平衡图案非常容易:


有时看起来很丑。这让我想起了一个老笑话:

病人:“医生,当我这样做时,会伤到我。”
医生:“那就别那样!”

因此,我将尽量不要这样做。

通常,我不会沿着偏移角度绘制地图比例尺,但是如果需要,我将需要使用拉伸图案的选项,以使地图比例尺适合到角落,而矩形之间没有间隙:


您可以看到,比例矩形的大小明显不同。也就是说,这不是一个很好的选择。(顺便说一句,偏移角度在圆形图案中也有一个bug。后来我修复了它,但是正如我所说,很难做到这一点。)

如果该图案太大而无法适合偏移角度的分段,则该算法会放弃:


这远非理想,但正如我上面所说,“那就不要这样做。” (如果需要,添加压缩或拉伸功能实际上并不难。)

如果同时使用偏角和将图案停在角前面的选项会发生什么情况?在这种情况下,我只是在离偏移角不远处停了下来:


在我看来,这是一个合乎逻辑的决定。

Source: https://habr.com/ru/post/zh-CN444190/


All Articles