Erstellen Sie die Ränder einer prozedural generierten Karte

Bild

Scott Turner arbeitet weiterhin an seinem prozedural generierten Spiel und hat sich nun entschlossen, das Problem der Gestaltung der Kartengrenzen anzugehen. Dazu muss er einige schwierige Probleme lösen und sogar eine eigene Sprache zur Beschreibung von Grenzen erstellen.

Grenzen blieben ein wichtiges Element von Fantasy-Karten, die schon seit einiger Zeit auf meiner Liste standen. Funktionale Karten haben normalerweise eine einfache Grenzlinie , aber Fantasy-Karten und mittelalterliche Karten, von denen die ersteren oft Ideen entlehnen, haben ziemlich durchdachte und künstlerische Grenzen. Diese Grenzen machen deutlich, dass die Karte absichtlich fantastisch gemacht wurde, und geben dem Betrachter ein Gefühl des Staunens.

Derzeit gibt es in meinem Spiel Dragons Abound einige einfache Möglichkeiten, Grenzen zu ziehen. Sie kann eine einfache oder doppelte Linie um den Umfang der Karte ziehen und einfache Elemente in die Ecken einfügen, wie in diesen Abbildungen:



Das Spiel kann auch ein Feld am unteren Rand des Rahmens für den Namen der Karte hinzufügen. In Dragons Abound gibt es verschiedene Variationen dieses Feldes, einschließlich so komplexer Elemente wie gefälschter Schraubenköpfe:


Diese Namensfelder sind unterschiedlich, werden jedoch alle manuell erstellt.

Ein interessanter Aspekt der Grenzen von Fantasy-Karten ist, dass sie sowohl kreativ als auch als Vorlage dienen. Oft bestehen sie aus einer kleinen Anzahl einfacher Elemente, die auf unterschiedliche Weise kombiniert werden, um ein einzigartiges Ergebnis zu erzielen. Wie immer besteht der erste Schritt bei der Arbeit mit einem neuen Thema für mich darin, eine Sammlung von Kartenbeispielen zu studieren, einen Katalog von Randelementtypen zu erstellen und deren Erscheinungsbild zu untersuchen.

Der einfachste Rand ist eine Linie, die entlang der Ränder der Karte verläuft und deren Grenzen angibt. Wie ich oben sagte, wird es auch als "Rahmenlinie" bezeichnet:


Es gibt auch eine Variation mit der Position der Ränder innerhalb der Karte. In dieser Version erreicht die Karte die Bildränder, aber der Rand erstellt einen virtuellen Rand innerhalb des Bildes:


Dies kann mit jeder Art von Rahmen durchgeführt werden, wird jedoch normalerweise nur mit einfachen Rändern wie dem Rand eines Rahmens verwendet.

Ein beliebtes Designkonzept für Fantasy-Karten besteht darin, zu simulieren, als ob sie auf altem zerrissenem Pergament gezeichnet wären. Manchmal wird dies durch Zeichnen des Randes als raue Kante des Papiers realisiert:


Hier ist ein komplexeres Beispiel:


Nach meiner Erfahrung ist diese Methode weniger populär geworden, weil digitale Werkzeuge zum Einsatz gekommen sind. Wenn Sie möchten, dass die Karte wie ein altes zerrissenes Pergament aussieht, ist es einfacher, die Textur des Pergaments darauf aufzutragen, als es von Hand zu zeichnen.

Das leistungsstärkste Werkzeug zum Erstellen von Kartenrändern ist die Wiederholbarkeit. Im einfachsten Fall reicht es aus, eine einzelne Zeile zu wiederholen, um zwei Zeilen zu erstellen:


Sie können der Karte Interesse hinzufügen, indem Sie den Stil des wiederholten Elements variieren. In diesem Fall kombinieren Sie eine dicke einzelne Linie mit einer dünnen einzelnen Linie:


Je nach Element sind verschiedene Stilvarianten möglich. In diesem Beispiel wiederholt sich die Linie, aber die Farbe ändert sich:


Um komplexere Muster zu erstellen, können Sie die „wiederholbare Wiederholbarkeit“ verwenden. Dieser Rand besteht aus ungefähr fünf einzelnen Linien mit unterschiedlichen Breiten und Abständen:


Dieser Rand wiederholt die Linien, trennt sie jedoch so, dass sie wie zwei separate dünne Ränder aussehen. In diesem Teil des Beitrags werde ich nicht auf die Eckenverarbeitung eingehen, aber unterschiedliche Winkel für die beiden Linien helfen auch dabei, diesen Unterschied zu erzeugen.


Sind das zwei Zeilen, vier oder sechs? Ich denke, es hängt alles davon ab, wie du sie zeichnest!

Ein weiteres Element der Stilisierung ist das Füllen des Raums zwischen Elementen mit Farbe, Muster oder Textur. In diesem Beispiel wurde der Rand aufgrund der Akzentfarbe zwischen den beiden Linien interessanter:


Hier ist ein Beispiel, wie der Rand mit einem Muster gefüllt wird:


Außerdem können Elemente so gestaltet werden, dass sie dreidimensional aussehen. Hier ist eine Karte, in der der Rand so schattiert ist, dass er voluminös aussieht:


In dieser Karte ist der Rand dreidimensional schattiert und wird mit der Position der Ränder innerhalb der Kartenränder kombiniert:


Ein weiteres häufiges Randelement ist die Skala in Form von mehrfarbigen Streifen:


Diese Streifen bilden ein Raster ( kartografisches Raster ). Auf realen Karten hilft der Maßstab bei der Bestimmung von Entfernungen, auf Fantasy-Karten ist er jedoch hauptsächlich ein dekoratives Element.

Diese Streifen werden normalerweise in Schwarzweiß gezeichnet, aber manchmal wird Rot oder eine andere Farbe hinzugefügt:


Dieses Element kann auch mit anderen kombiniert werden, wie in diesem Beispiel mit Linien und Skalierung:


Dieses Beispiel ist etwas ungewöhnlich. Normalerweise ist die Skala (falls vorhanden) das innerste Element der Grenze.

Auf dieser Karte gibt es verschiedene Maßstäbe mit unterschiedlichen Auflösungen (sowie seltsame Runennoten!):


(Auf Reddit teilte mir Benutzer AbouBenAdhem mit, dass Runenzeichen die Nummern 48 und 47 sind, die in babylonischer Keilschrift geschrieben sind. Außerdem haben „Skalen mit unterschiedlichen Auflösungen“ sechs Unterteilungen, die in zehn kleinere Unterteilungen unterteilt sind, was dem babylonischen Hexadezimalzahlensystem entspricht. Normalerweise Ich gebe die Quellen der Karten an, aber es gibt zu viele kleine Stücke in diesem Beitrag, so dass ich mich nicht darum gekümmert habe. Diese Karte wurde jedoch von Thomas Ray für den Autor S.E. Boleyn erstellt, sodass die Aktion in seinen Büchern möglicherweise im Gefolge Babylons stattfindet.)

Neben Linien und Maßstäben ist das häufigste Element ein sich wiederholendes geometrisches Muster. Oft besteht es aus Teilen wie Kreisen, Rauten und Rechtecken:


Geometrische Elemente können wie Linien schattiert werden, damit sie dreidimensional aussehen:


Komplexe Grenzen können erstellt werden, indem diese Elemente auf unterschiedliche Weise kombiniert werden. Hier ist der Rand, der Linien, geometrische Muster und Skalierungen kombiniert:


Die oben gezeigten Beispiele waren digitale Karten, aber das Gleiche kann natürlich auch mit handgeschriebenen Karten gemacht werden. Hier ist ein Beispiel für ein einfaches geometrisches Muster, das von Hand erstellt wurde:


Diese Elemente können auch auf viele Arten flexibel kombiniert werden. Hier ist ein geometrisches Muster kombiniert mit einer „zerlumpten Kante“:


In den oben gezeigten Beispielen ist das geometrische Muster recht einfach. Sie können jedoch sehr komplexe Muster erstellen, indem Sie die geometrischen Grundelemente auf andere Weise kombinieren:


Ein weiteres beliebtes Element des Musters ist das Weben oder der keltische Knoten:


Hier ist ein komplexerer Weidenrand, der Farbe, Skalierung und andere Elemente enthält:


Auf dieser Karte wird das Weben mit einem ausgefransten Kantenelement kombiniert:


Zusätzlich zu geometrischen Mustern und Webereien kann jedes sich wiederholende Muster Teil des Randes der Karte sein. Hier ist ein Beispiel mit Formen, die Pfeilspitzen ähneln:


Und hier ist ein Beispiel mit einem sich wiederholenden Wellenmuster:


Und schließlich werden Runen oder andere Elemente des Fantasy-Alphabets manchmal an den Rändern von Fantasy-Karten hinzugefügt:


Die obigen Beispiele stammen aus modernen Fantasy-Karten, aber hier ist ein Beispiel einer historischen Karte (18. Jahrhundert) mit Linien und einem handgezeichneten Muster:


Natürlich finden Sie Beispiele für Karten mit vielen anderen Elementen an den Rändern. Einige der schönsten sind vollständig handgezeichnet und so sorgfältig verarbeitet, dass sie die Karte selbst übertreffen können ( World of Alma , Francesca Baerald):


Es lohnt sich auch ein wenig über Symmetrie zu sprechen. Wie die Wiederholbarkeit ist auch die Symmetrie ein leistungsstarkes Werkzeug, und Kartenränder sind normalerweise symmetrisch oder weisen symmetrische Elemente auf.

Viele Kartenränder sind von innen nach außen symmetrisch, wie in diesem Beispiel:


Hier besteht der Rand aus mehreren Linien mit und ohne Füllung, die sich jedoch von außen nach innen idealerweise relativ zur Randmitte wiederholen.

In diesem komplexeren Beispiel ist der Rand symmetrisch, mit Ausnahme abwechselnder schwarzer und weißer Skalenstreifen:


Da das Duplizieren der Skala nicht sinnvoll ist, wird sie häufig als separates Element betrachtet, auch wenn der Rest des Randes symmetrisch ist.

Zusätzlich zur internen und externen Symmetrie sind Ränder häufig entlang ihrer Länge re-symmetrisch. Einige abgebildete Ränder haben möglicherweise ein einfaches Design, das sich über die gesamte Länge des Kartenrandes erstreckt. In den meisten Fällen ist das Muster jedoch recht kurz und wiederholt sich und füllt den Rand von einer Ecke zur anderen:


Beachten Sie, dass in diesem Beispiel das Muster ein Element enthält, das nicht symmetrisch ist (von links nach rechts), das allgemeine Muster jedoch symmetrisch ist und sich wiederholt:


Eine bemerkenswerte Ausnahme von dieser Regel sind Ränder, die mit Runen oder alphabetischen Zeichen gefüllt sind. Oft erweisen sie sich als einzigartig, als ob eine lange Nachricht entlang der Grenze geschrieben worden wäre:


Natürlich gibt es viele andere Beispiele für Kartenrandelemente, die ich hier nicht berücksichtigt habe, aber wir haben bereits einen guten Bezugspunkt. In den nächsten Teilen werde ich in Dragons Abound verschiedene Funktionen zum Beschreiben, Anzeigen und prozeduralen Generieren von Kartenrändern entwickeln, die diesen Beispielen ähneln. Im zweiten Teil legen wir zunächst die Sprache für die Beschreibung der Kartenränder fest.

Teil 2


In diesem Teil werde ich die erste Version der Map Border Description Language (MBDL) erstellen.

Warum Zeit damit verbringen, eine Beschreibungssprache für Kartengrenzen zu erstellen? Erstens wird dies das Ziel meiner prozeduralen Generierung sein. Später werde ich einen Algorithmus zum Erstellen neuer Kartenränder schreiben, und die Ausgabe dieses Algorithmus wird eine Beschreibung des neuen Randes in MBDL sein. Zweitens dient MBDL als Textdarstellung von Kartengrenzen. Insbesondere muss ich in der Lage sein, meine Grenzen zu speichern und wiederzuverwenden. Dazu benötige ich eine Textnotation, die geschrieben und verwendet werden kann, um den Rand der Karte neu zu erstellen.

Ich beginne mit der Erstellung von MBDL, indem ich das einfachste Element definiere: die Zeile. Die Linie hat Farbe und Breite. Daher werde ich in MBDL die Zeile in dieser Form präsentieren:

L(width, color)

Hier einige Beispiele (Entschuldigung für meine Photoshop-Kenntnisse):


Die Reihenfolge der Elemente wird von außen nach innen gerendert (*), daher nehmen wir an, dass dies der Rand oben auf der Karte ist:


Schauen Sie sich das zweite Beispiel an - eine Linie mit Rahmen wird als drei separate Linienelemente dargestellt.

(* Das Zeichnen von außen nach innen war eine willkürliche Entscheidung - es schien mir nur natürlicher zu sein als das Rendern von innen nach außen. Leider gab es, wie sich viel später herausstellte, einen guten Grund, in die entgegengesetzte Richtung zu arbeiten. Bald werde ich Ihnen davon erzählen, aber alles bleibt in der Post - alt, weil es viel Zeit in Anspruch nehmen würde, alle Abbildungen zu wiederholen)

Praktischerweise können Räume als Linien ohne Farbe dargestellt werden:


Es wäre jedoch visueller, ein bestimmtes vertikales Raumelement zu haben:

VS (Breite)

Die folgenden einfachen Elemente sind geometrische Formen: Streifen, Rauten und Ellipsen. Es wird davon ausgegangen, dass die Linien über die gesamte Länge des Rahmens gespannt sind, sodass sie keine explizit angegebene Länge haben. Geometrische Figuren können jedoch nicht die gesamte Linie ausfüllen. Daher sollte jede zusätzlich zur Breite (*) eine Länge, eine Umrissfarbe, eine Umrissbreite und eine Füllfarbe haben:

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

(* Ich habe akzeptiert, dass ich die Breite in der Richtung von außen nach innen berücksichtigen werde und die Länge entlang der Grenze gemessen wird.)

Hier sind Beispiele für einfache geometrische Formen:


Damit diese Elemente die gesamte Länge des Rahmens ausfüllen, müssen sie wiederholt werden. Um die Gruppe von Elementen anzugeben, die wiederholt werden, um die Länge des Rahmens auszufüllen, verwende ich eckige Klammern:

[ element element element ... ]

Hier ist ein Beispiel für ein sich wiederholendes Muster von Rechtecken und Rauten:


Manchmal brauche ich einen (horizontalen) Abstand zwischen Elementen eines sich wiederholenden Musters. Obwohl Sie ein Element ohne Farben verwenden können, um einen Raum zu erstellen, ist es intelligenter und bequemer, ein horizontales Raumelement zu haben:

HS(length)

Die letzte Funktion, die für diese erste Iteration von MBDL benötigt wird, ist die Fähigkeit, Elemente übereinander zu stapeln. Hier ist ein Beispielrand:


Der einfachste Weg, dies zu beschreiben, ist eine breite gelbe Linie unter dem oberen Muster. Sie können dies auf verschiedene Arten implementieren (z. B. in einem negativen vertikalen Raum), aber ich habe mich für geschweifte Klammern entschieden, um die Reihenfolge der Elemente nach innen anzugeben:

{element element element ...}

Tatsächlich fordert Sie dieser Eintrag auf, sich beim Betreten der Klammern zu merken, wo sich das Muster von außen nach innen befand, und beim Verlassen der Klammern zu diesem Punkt zurückzukehren. Klammern können auch als Beschreibung von Elementen betrachtet werden, die einen vertikalen Raum einnehmen. Daher kann der oben gezeigte Rand wie folgt beschrieben werden:

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)

Wir zeichnen eine schwarze Linie, fixieren, wo wir sind, zeichnen eine gelbe Linie und kehren dann zu der zuvor festgelegten Position zurück, lassen uns ein wenig nach unten fallen, zeichnen ein Muster aus Rechtecken und Rauten, lassen ein wenig nach unten fallen und zeichnen dann eine weitere schwarze Linie.

In MBDL gibt es noch viel mehr zu tun, aber dies reicht aus, um die vielen Grenzen von Karten zu beschreiben. Der nächste Schritt besteht darin, die Grenzbeschreibung in der MBDL in die Grenze selbst zu konvertieren. Dies ähnelt der Konvertierung einer schriftlichen Darstellung eines Computerprogramms (z. B. Javascript) in die Ausführung dieses Programms. Die erste Stufe ist die lexikalische Analyse (Analyse) der Sprache - die Umwandlung des Quelltextes in einen realen Rand der Karte oder in eine Zwischenform, die einfacher in einen Rand umzuwandeln ist.

Parsing ist ein ziemlich gut untersuchter Bereich der Informatik. Das Parsen einer Sprache ist nicht sehr einfach, aber in unserem Fall ist es gut (*), dass MBDL eine kontextfreie Grammatik ist. Kontextfreie Grammatiken lassen sich relativ leicht analysieren, und es gibt viele Javascript-Parsing-Tools für sie. Ich habe mich für Nearley.js entschieden , das ziemlich ausgereift und (was noch wichtiger ist) ein gut dokumentiertes Tool zu sein scheint.

(* Dies ist nicht nur Glück, ich habe dafür gesorgt, dass die Sprache kontextfrei ist.)

Ich werde Sie nicht in kontextfreie Grammatiken einführen, aber die Nearley-Syntax ist recht einfach und Sie sollten die Bedeutung ohne Probleme verstehen. Grammatik Nearley besteht aus einer Reihe von Regeln. Jede Regel hat links ein Symbol, einen Pfeil und den rechten Teil der Regel, der eine Folge von Zeichen und Nichtzeichen sein kann, sowie verschiedene Optionen, die durch das "|" getrennt sind. (oder):

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

Jede der Regeln besagt, dass die linke Seite durch eine der Optionen auf der rechten Seite ersetzt werden kann. Das heißt, die erste Regel besagt, dass ein Rand ein Element oder ein Element ist, gefolgt von einem anderen Rand. Welches selbst kann ein Element sein oder ein Element, dem ein Rand folgt, und so weiter. Die zweite Regel besagt, dass ein Element nur eine Zeichenfolge "L" sein kann. Das heißt, zusammen entsprechen diese Regeln solchen Grenzen:

L
LLL

und entsprechen nicht solchen Grenzen:

X
L3L

Übrigens, wenn Sie mit dieser (oder einer anderen) Grammatik in Nearley experimentieren möchten, gibt es hier eine Online-Sandbox dafür. Sie können Grammatik- und Testfälle eingeben, um zu sehen, was übereinstimmt und was nicht.

Hier ist eine vollständigere Definition eines Linienprimitivs:

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

Nearley hat mehrere gemeinsame eingebaute Elemente, und die Nummer ist eines davon. Daher kann ich damit die numerische Breite eines Linienprimitivs erkennen. Für die Farberkennung verwende ich ein anderes integriertes Element und erlaube die Verwendung einer beliebigen Zeichenfolge in doppelten Anführungszeichen.

Es wäre schön, Leerzeichen zwischen verschiedenen Zeichen einzufügen, also lass es uns tun. Nearley unterstützt Zeichenklassen und RBNF für "null oder mehr" von etwas mit ": *", sodass ich damit "null oder mehr Leerzeichen" angeben und an einer beliebigen Stelle einfügen kann, um Leerzeichen in Beschreibungen zuzulassen:

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

Die Verwendung von WS überall macht es jedoch schwierig, die Grammatik zu lesen, daher werde ich sie aufgeben, aber stellen Sie sich vor, dass sie es sind.

Ein Element kann auch ein vertikaler Raum sein:

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

Dies entspricht solchen Grenzen

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

Als nächstes kommen die Grundelemente Streifen, Raute und Ellipse.

@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 ")"

Es wird mit solchen Elementen übereinstimmen

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

(Beachten Sie, dass geometrische Grundelemente keine „Elemente“ sind, da sie auf der obersten Ebene nicht allein sein können. Sie müssen in einem Muster eingeschlossen sein.)

Ich brauche auch ein horizontales Raumprimitiv:

@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 ")"

Jetzt werde ich eine Musteroperation (Wiederholung) hinzufügen. Dies ist eine Folge von einem oder mehreren Elementen in eckigen Klammern. Ich werde den RBNF-Operator ": +" verwenden, was hier "einer oder mehrere" bedeutet.

@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):+ "]"

Beachten Sie, dass das Muster nur mit geometrischen Grundelementen gefüllt werden kann. Wir können zum Beispiel keine Linie innerhalb eines Musters platzieren. Das Musterelement stimmt jetzt mit so etwas überein.

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

Der letzte Teil der Sprache ist der Overlay-Operator. Dies ist eine beliebige Anzahl von Elementen in geschweiften Klammern.

@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 ):+ "}"

Damit können wir Folgendes tun:

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

(Beachten Sie, dass der Overlay-Operator im Gegensatz zum Wiederholungsoperator intern verwendet werden kann.)

Nachdem wir die Beschreibung bereinigt und den erforderlichen Stellen Leerzeichen hinzugefügt haben, erhalten wir die folgende MBDL-Grammatik:

@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 ist nun definiert und wir haben eine Grammatik der Sprache erstellt. Es kann mit Nearley verwendet werden, um Sprachzeichenfolgen zu erkennen. Bevor ich mich mit MBDL / Nearley befasse, möchte ich die in MBDL verwendeten Grundelemente implementieren, damit die in MBDL beschriebene Grenze angezeigt werden kann. Dies werden wir im nächsten Teil tun.

Teil 3


Jetzt werden wir beginnen, die Rendering-Grundelemente selbst zu implementieren. (Zu diesem Zeitpunkt muss ich den Parser noch nicht an die Rendering-Grundelemente binden. Zum Testen rufe ich sie einfach manuell auf.)

Beginnen wir mit der Grundlinie. Erinnern Sie sich, wie es aussieht:

L(width, color)

Neben der Breite und Farbe gibt es hier einen impliziten Parameter - den Abstand vom äußeren Rand der Karte. (Ich zeichne die Ränder vom Rand der Karte nach außen. Beachten Sie, dass wir von einem anderen ausgegangen sind!) Es sollte nicht auf die MBDL zeigen, da dies vom Interpreter verfolgt werden kann, der die MBDL ausführt, um den Rand zu zeichnen. Dies sollte jedoch für alle Rendering-Grundelemente eingegeben werden, damit sie wissen, wo sie gezeichnet werden sollen. Ich werde diesen Parameter Offset nennen.

Wenn ich nur einen Rand am oberen Rand der Karte zeichnen müsste, wäre das Linienprimitiv sehr einfach zu implementieren. Tatsächlich muss ich jedoch von oben zeichnen. unten, links und rechts. (Vielleicht werde ich eines Tages schräge oder gekrümmte Ränder erkennen, aber im Moment werden wir uns an rechteckige Standardränder halten.) Außerdem hängen Länge und Position des Linienelements von der Größe der Karte (sowie vom Versatz) ab. Daher benötige ich als Parameter all diese Daten.

Nachdem Sie alle diese Parameter festgelegt haben, reicht es aus, einfach ein Linienprimitiv zu erstellen und damit eine Linie um die Karte zu zeichnen:


(Beachten Sie, dass ich verschiedene Funktionen von Dragons Abound verwende, um die „handgeschriebene“ Linie zu zeichnen.) Versuchen wir, einen komplexeren Rand zu erstellen:

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

Es sieht so aus:


Ziemlich gut. Beachten Sie, dass es Stellen gibt, an denen die schwarzen und die goldenen Linien aufgrund von Schwankungen nicht richtig ausgerichtet sind. Wenn ich diese Stellen entfernen möchte, können Sie einfach die Schwingung reduzieren.

Das Implementieren eines vertikalen Raumprimitivs ist recht einfach. Es wird nur ein Offset-Inkrement ausgeführt. Fügen wir ein wenig Platz hinzu:

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


Beim Zeichnen von Linien können Winkel realisiert werden, indem zwischen dem Versatz und der Zeichnung entlang der Karte im Uhrzeigersinn gezeichnet wird. Im Allgemeinen muss ich jedoch auf jeder Seite des Kartenrandes eine Kürzung implementieren, um eine Winkelverbindung mit einer Abschrägung herzustellen . Dies ist erforderlich, um Ränder mit Mustern zu erstellen, die an den Ecken korrekt verbunden sind, und im Allgemeinen entfällt die Notwendigkeit, Elemente mit Kanten in einem Winkel zu zeichnen, der sonst erforderlich wäre. (*)

(Hinweis: Wie in den folgenden Abschnitten erwähnt wird, habe ich mich im Laufe der Zeit geweigert, beim Implementieren von Winkeln Kürzungsbereiche zu verwenden. Der Hauptgrund ist, dass komplexe Winkel erstellt werden, z. B. quadratische Offsets:


Es werden immer komplexere Kürzungsbereiche benötigt. Außerdem habe ich im Laufe der Zeit einen besseren Weg gefunden, mit Mustern in Ecken zu arbeiten. Anstatt diesen Teil des Artikels zurückzugeben und neu zu schreiben, habe ich beschlossen, ihn zu belassen, um den Prozess der „Kreativität“ zu veranschaulichen.)

Die Hauptidee besteht darin, jeden Rand diagonal abzuschneiden und vier abgeschnittene Bereiche zu erstellen, in denen jede Seite des Randes gezeichnet wird:


Beim Abschneiden wird alles, was im entsprechenden Bereich gezeichnet wurde, im gewünschten Winkel abgeschnitten.


Leider entstehen dadurch kleine Lücken entlang der diagonalen Linien, wahrscheinlich weil der Browser die Glättung entlang der abgeschnittenen Kante nicht perfekt durchführt. Der Test zeigte, dass ein Hintergrund durch die Lücke zwischen den beiden Kanten scheint. Es war möglich, dies zu beheben, indem eine der Masken etwas erweitert wurde (das halbe Pixel scheint ausreichend zu sein), aber dies löst das Problem manchmal nicht.

Als nächstes müssen Sie geometrische Formen implementieren. Im Gegensatz zu Linien werden sie im Muster wiederholt und füllen die Seite des Kartenrandes aus:


Eine Person zeichnete dieses Muster von links nach rechts, zeichnete ein Rechteck, eine Raute und wiederholte es dann, bis der gesamte Rand gefüllt war. Daher kann dies auch im Programm implementiert werden, indem ein Muster entlang der Grenze gezeichnet wird. Es ist jedoch einfacher, zuerst alle Rechtecke und dann alle Rauten zu zeichnen. Es reicht aus, in Abständen dieselbe geometrische Figur entlang des Randes zu zeichnen. Und es ist sehr praktisch, dass jedes Element das gleiche Intervall hat. Natürlich würde eine Person das nicht tun, weil es zu schwierig ist, die Elemente an den richtigen Stellen anzuordnen, aber dies ist kein Problem für das Programm.

Das heißt, das Verfahren zum Zeichnen einfacher geometrischer Formen erfordert Parameter, in denen alle Abmessungen und Farben der Figur übertragen werden (d. H. Breite, Länge, Liniendicke, Linienfarbe und Füllung) sowie die Startposition (die aus Gründen, die bald klar werden, Ich werde die Mitte der Figur betrachten), das horizontale Raumintervall für den Übergang zwischen Wiederholungen und die Anzahl der Wiederholungen. Es ist auch zweckmäßig, die Wiederholungsrichtung in Form eines Vektors [dx, dy] anzugeben, damit wir Wiederholungen von links nach rechts, von rechts nach links, nach oben oder unten durchführen können, indem wir einfach den Vektor und den Startpunkt ändern. Setze alles zusammen und erhalte einen Streifen sich wiederholender Formen:


Wenn ich diesen Code mehrmals verwende und mit demselben Versatz rendere, kann ich die schwarzen und weißen Streifen kombinieren, um den Kartenmaßstab zu erstellen:


Bevor ich anfange herauszufinden, wie all dies auf den realen Rand der Karte angewendet werden kann, implementieren wir zunächst dieselbe Funktionalität für Ellipsen und Rauten.

Rauten sind nur Rechtecke mit gedrehten Eckpunkten, sodass Sie nur eine kleine Änderung am Code vornehmen müssen. Es stellte sich heraus, dass ich noch keinen vorgefertigten Code zum Rendern der Ellipse habe, aber es ist sehr einfach, die parametrische Ansicht der Ellipse zu übernehmen und eine Funktion zu erstellen, die mir die Punkte der Ellipse gibt:


Hier ist ein Beispiel (manuell erstellt), das die oben implementierten Funktionen verwendet:


Für so eine kleine Menge Code sieht es ziemlich gut aus!

Lösen wir nun den komplexen Fall von Rändern mit sich wiederholenden Elementen: Ecken.

Wenn es einen Rand mit sich wiederholenden Elementen gibt, gibt es verschiedene Möglichkeiten, das Problem mit Ecken zu lösen. Die erste besteht darin, die Wiederholungen so anzupassen, dass sie in den Ecken ausgeführt werden, ohne dass eine Ehe erkennbar ist:


Eine andere Möglichkeit besteht darin, die Wiederholung irgendwo in der Nähe der Ecke auf beiden Seiten zu stoppen. Dies geschieht häufig, wenn das Muster in der Ecke nicht einfach „gedreht“ werden kann:


Die letzte Möglichkeit besteht darin, das Muster mit einer Eckdekoration zu schließen:


Eines Tages werde ich zu den Eckdekorationen kommen, aber jetzt werden wir die erste Option verwenden. Wie kann man ein Muster aus Streifen oder Kreisen in den Ecken der Karte ohne Lücken „drehen“?

Die Hauptidee besteht darin, das Musterelement genau in der Ecke zu platzieren, sodass sich eine Hälfte davon an einer Kante der Karte und die andere an der benachbarten befindet. In diesem Beispiel befindet sich der Kreis genau in der Ecke und kann aus jeder Richtung gezeichnet werden:


In anderen Fällen wird das Element zur Hälfte in die eine und zur Hälfte in die andere Richtung gezeichnet, aber die Kanten fallen zusammen:


In diesem Fall wird auf beiden Seiten ein weißer Streifen gezeichnet, der jedoch in der Ecke lückenlos verbunden ist.

Beim Platzieren eines Elements in einer Ecke sind zwei Aspekte zu berücksichtigen.

Zunächst wird das Eckelement geteilt und relativ zur Diagonale gespiegelt, die durch die Mitte des Elements verläuft. Elemente mit radialer Symmetrie, z. B. Quadrate, Kreise und Sterne, ändern ihre Form nicht. Elemente ohne radiale Symmetrie, z. B. Rechtecke und Rauten, ändern beim Spiegeln relativ zur Diagonale ihre Form.

Zweitens muss eine ganzzahlige Anzahl von Elementen (*) auf beiden Seiten der Karte vorhanden sein, damit die Eckelemente der beiden Seiten korrekt verbunden sind. Sie müssen nicht dieselbe Anzahl haben, aber es muss auf beiden Seiten eine ganzzahlige Anzahl von Elementen geben. Wenn auf einer Seite eine gebrochene Anzahl von Mustern enthalten ist, stimmt das Muster von einer Kante nicht mit der benachbarten Seite überein.

(* In einigen Fällen, z. B. bei langen Streifen, kann es bei vollständiger Wiederholung zu einer teilweisen Wiederholung kommen, und die Elemente werden trotzdem ausgerichtet. Das resultierende Eckelement ist jedoch asymmetrisch und unterscheidet sich in der Länge von demselben Element auf den Seiten der Karte. Ein Beispiel hierfür ist hier zu sehen:


Ein weißer Maßstabsbalken tritt mit verschiedenen Teilwiederholungen auf, und als Ergebnis wird ein Element erhalten, das relativ zur Mitte verschoben ist. Für den Kartenmaßstab ist dies nicht immer der Fall, da er den absoluten Abstand anzeigt und nicht symmetrisch sein muss. Aber für ein dekoratives Muster sieht das normalerweise schlecht aus.)

Hier ist ein Beispiel, das zeigt, wie eine ganzzahlige Anzahl von Wiederholungen genau in der Ecke abgeschnitten wird:


Wenn Sie von allen vier Seiten dasselbe tun, fallen die Ecken zusammen und das Muster wird nahtlos über die gesamte Länge des Rahmens platziert:


Bei sorgfältiger Prüfung werden Sie feststellen, dass das Muster nicht genau in den Ecken auftritt. Die Hälfte des Kreises in jeder Ecke wird von jeder Seite genommen, und diese beiden Hälften werden unabhängig voneinander von Hand gezeichnet, daher sind sie nicht perfekt. Aber jetzt sind sie nah genug dran.

So können wir eine perfekte Verbindung des Musters in den Ecken realisieren, indem wir für jede Kante eine ganzzahlige Anzahl von Wiederholungen auswählen. Die Lösung für dieses Problem ist jedoch nicht trivial.

Angenommen, wir wissen, dass die Seite 866 Pixel lang ist, und wir möchten das Element 43 Mal wiederholen. Dann sollte das Element alle 20,14 Pixel wiederholt werden. Wie legen wir die spezifische Länge eines Elements (und im allgemeinen Fall ein Muster von Elementen) fest? Im obigen Beispiel habe ich zusätzlichen Abstand zwischen den Kreisen hinzugefügt. Wenn sich die Kreise jedoch anfänglich berührten, ändert dies das Muster. Vielleicht lohnt es sich, die Kreise so zu dehnen, dass sie sich weiterhin berühren?


Jetzt berühren sich die Elemente, aber die Kreise haben sich in Ellipsen verwandelt und die Ecken haben eine seltsame Form. (Denken Sie daran, ich sagte, dass Elemente ohne radiale Symmetrie ihre Form ändern, wenn sie relativ zu einem Winkel reflektiert werden. Für Streifen ist dies kein großes Problem.) Oder es lohnt sich vielleicht, alle Elemente so zu komprimieren, dass sie sich berühren und in eine geeignete Länge passen:


Um dies zu realisieren, müssen wir die Elemente jedoch viel kleiner machen als ursprünglich. Keine dieser Optionen scheint perfekt zu sein.

Das zweite Problem tritt auf, wenn die Seiten der Karte unterschiedlich groß sind. Jetzt müssen wir das Problem lösen, eine ganzzahlige Anzahl von Wiederholungen zu finden, die für beide Seiten geeignet sind. Es wäre ideal, eine Lösung zu finden, die zu beiden Seiten passt. Aber ich möchte dies nicht auf Kosten zu vieler Musteränderungen tun. Es ist möglicherweise besser, auf beiden Seiten leicht unterschiedliche Muster zu erstellen, wenn beide nahe genug am ursprünglichen Muster liegen.

Und schließlich tritt das dritte Problem auf, wenn ich die Funktion verwende, mehrere Elemente übereinander zu legen:


Ich möchte keine Änderungen am Muster vornehmen, die die Beziehung zwischen den Elementen zerstören. Ich denke, dass bei richtiger Skalierung die Verhältnisse insgesamt erhalten bleiben, aber ich muss dies testen.

Interessante Aufgabe, oder? Bisher habe ich keine besonders hochwertigen Lösungen für sie. Vielleicht erscheinen sie später!

Teil 4


Daher haben wir Grundelemente zum Zeichnen von Linien und geometrischen Formen implementiert. Ich fing an, sich wiederholende Formen zu verwenden, um die Ränder zu füllen, und sprach über die Schwierigkeiten, beliebige Muster am Rand der Karte zu platzieren, damit sie perfekt in die Ecken passen. Das Hauptproblem besteht darin, dass Sie das Muster im Allgemeinen länger (oder kürzer) machen müssen, damit es seitlich passt. Optionen zum Ändern der Länge des Musters - Hinzufügen oder Entfernen von Leerzeichen, Ändern der Länge der Elemente der Muster - führen zu verschiedenen Änderungen im Muster selbst. Es scheint, dass die Auswahl eines Musters aus mehreren Elementen sehr schwierig ist!

Wenn ich auf solche scheinbar kompromisslosen Aufgaben stoße, beginne ich gerne mit der Implementierung einer einfachen Version. Nicht erfolgreiche Aufgaben können oft durch wiederholtes Lösen "einfacher" Probleme gelöst werden, bis das Ergebnis gut genug ist. Und manchmal gibt die Implementierung einer einfachen Version ein Verständnis, das die Lösung eines komplexeren Problems vereinfacht. Wenn es nicht besser wird und das Problem weiterhin unangenehm ist, werden wir zumindest eine vereinfachte Version haben, die immer noch nützlich sein kann, wenn auch nicht ganz so, wie es sollte.

Am einfachsten ist es, die Länge des Musters zu ändern, indem Sie Längen hinzufügen, ohne etwas im Muster zu ändern. Im Wesentlichen wird dadurch am Ende des Musters ein Leerzeichen hinzugefügt. (Hinweis: Es ist besser, den leeren Raum auf alle Elemente im Muster zu verteilen.) Es ist zu berücksichtigen, dass eine solche Lösung das Muster nur verlängern kann. Wir können dem Muster immer einen leeren Raum hinzufügen, ihn aber bei Bedarf nicht nehmen - vielleicht gibt es keinen leeren Raum mehr im Muster!

Bei diesem Ansatz ist der Musterortungsalgorithmus auf der Seite der Karte sehr einfach:

  • Teilen Sie die Länge der Seite der Karte durch die Länge des Musters und runden Sie sie ab, um die Anzahl der Wiederholungen des Musters zu bestimmen, die auf diese Seite passen.
  • Der Abstand zwischen den Elementen ist in diesem Fall gleich der Länge der Seite geteilt durch die Anzahl der Wiederholungen. (Dies ist der nächstgelegene Ort, da wir nur Speicherplatz hinzufügen können.)
  • Zeichnen Sie ein Muster entlang der Seite unter Berücksichtigung des berechneten Abstands.

Es war schwierig, dieses System zu implementieren. Die Ecken wollten hartnäckig nicht zusammenfallen. Ich habe zu viel Zeit gebraucht, um zu erkennen, dass ich, wenn die Karte nicht quadratisch ist, keine Kürzungsbereiche für vier Seiten von der Kartenmitte aus zeichnen kann, da dadurch Kürzungswinkel erzeugt werden, die nicht gleich 45 Grad sind. In der Tat sollten Kürzungsbereiche der Rückseite eines Umschlags ähneln:


Als ich das herausfand, begann der Algorithmus ohne Probleme zu funktionieren.

(Vergessen Sie jedoch nicht den vorherigen Hinweis, dass ich im Laufe der Zeit Kürzungsbereiche aufgegeben habe!)

Hier ein Beispiel mit einem Verhältnis von ungefähr 2: 1:

Auf dieser Skala ist es ziemlich schwer zu bemerken, aber die Ecken verbinden sich richtig und es gibt nur einen geringen visuellen Unterschied zwischen den Seiten. In diesem Fall muss der Algorithmus zum Ausrichten der Muster nur Bruchpixel einfügen, sodass er für das Auge unsichtbar ist, insbesondere weil die Konturen der Kreise von einem Pixel überlappt werden.

Hier ist ein weiteres Beispiel mit Streifen:


Dies ist die Spitze der quadratischen Grenze. Hier ist derselbe Rand auf einer rechteckigeren Karte:


Hier können Sie sehen, dass auf der Seite der Karte eine visuell größere Lücke zwischen den Bändern besteht. Der Algorithmus sollte nicht mehr Platz als die Länge eines vollständigen Elements einfügen. Daher tritt der schlimmste Fall auf, wenn wir lange Elemente und eine kurze Seite haben, die sich geringfügig von einer geeigneten Größe unterscheidet. In den meisten praktischen Fällen ist die Ausrichtung jedoch nicht sehr schädlich.

Hier ist ein Beispiel mit einem Muster aus mehreren Elementen:


Hier überlappen die Streifen die Streifen:


Sie können sehen, dass die Streifen relativ zueinander zentriert bleiben, da für jedes Element dieselbe Ausrichtung durchgeführt wird.

Ich schlug vor, dass eine gute Lösung zum Platzieren des Musters an der Seite der Karte schwierig wäre, aber ein sehr einfacher Ansatz mit gleichmäßiger Verteilung des Musterelements, um den gewünschten Raum zu füllen, funktioniert für viele Muster recht gut. Dies ist eine Erinnerung an uns alle: Es besteht keine Notwendigkeit anzunehmen, dass die Entscheidung kompliziert sein muss; es kann einfacher sein als du denkst!

Diese Lösung funktioniert jedoch nicht für Muster mit berührenden Elementen, z. B. für den Kartenmaßstab. In diesem Fall werden durch Hinzufügen von Leerzeichen die Elemente verschoben:


Eine andere Option zum Verlängern eines Musters, die ich oben erwähnt habe, ist das Strecken der einzelnen Elemente des Musters. Es ist für so etwas wie ein Skalenmuster geeignet, sieht aber in einem Muster mit symmetrischen Elementen schlecht aus, da sie durch Dehnen asymmetrisch werden.

Die Implementierung der Option mit Dehnung erwies sich als schwieriger als erwartet, hauptsächlich weil ich die Elemente an verschiedenen Kanten der Karte um verschiedene Größen strecken musste (weil die Karte möglicherweise nicht quadratisch, sondern rechteckig ist) und auch die Anordnung der Elemente basierend auf den neuen gedehnten dynamisch ändern musste Größen. Aber nach ein paar Stunden habe ich das geschafft:


Jetzt habe ich alle Funktionen, die zum Zeichnen des Rahmens der Karte erforderlich sind (obwohl die Rahmenelemente selbst manuell erstellt werden):


Ich habe das Bild in Graustufen konvertiert, weil ich mich nicht um die Auswahl der Farben kümmern wollte und die Karte selbst ziemlich langweilig ist, aber als Proof of Concept sehen die Ränder ziemlich hübsch aus.

Teil 5


In Teil 2 habe ich die MBDL-Grammatik (Map Border Description Language) entwickelt und in Teil 3 und 4 Prozeduren implementiert, um alle Sprachprimitive auszuführen. Jetzt werde ich daran arbeiten, diese Teile zu verbinden, damit ich den Rand in MBDL beschreiben und auf der Karte zeichnen kann.

In Teil 3 habe ich die MBDL-Grammatik so geschrieben, dass sie mit dem Nearley Javascript Parsing Tool funktioniert . Die fertige Grammatik sieht folgendermaßen aus:

@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 " )"

Wenn eine Regel erfolgreich mit Nearley analysiert wird, gibt die Regel standardmäßig ein Array zurück, das alle Elemente enthält, die der rechten Seite der Regel entsprechen. Zum Beispiel, wenn die Regel

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

mit Zeichenfolge abgeglichen

A

dann wird Nearley zurückkehren

[ " A" ]

Ein Array mit einem einzelnen Wert ist die Zeichenfolge "A", die der rechten Seite der Regel entspricht.

Was gibt Nearley zurück, wenn ein Element nach dieser Regel analysiert wird?

number -> WS decimal WS

Auf der rechten Seite der Regel befinden sich drei Teile, sodass ein Array mit drei Werten zurückgegeben wird. Der erste Wert ist derjenige, der die Regel für WS zurückgibt, der zweite Wert ist derjenige, der die Regel für Dezimalzahlen zurückgibt, und der dritte Wert ist derjenige, der die Regel für WS zurückgibt. Wenn ich unter Verwendung der obigen Regel "57" analysiere, ist das Ergebnis wie folgt:

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

Das Endergebnis der Nearley-Analyse ist ein verschachteltes Array von Arrays, bei dem es sich um einen Syntaxbaum handelt . In einigen Fällen ist der Syntaxbaum eine sehr nützliche Darstellung, in anderen Fällen nicht ganz. In Dragons Abound zum Beispiel ist ein solcher Baum nicht besonders nützlich.

Glücklicherweise können Nearley-Regeln das Standardverhalten überschreiben und alles zurückgeben, was sie wollen. In der Tat, für die (integrierte) Regel dezimal nicht eine Liste von Zahlen zurückkehrt, gibt es eine entsprechende Anzahl von Javascript ist, dass in den meisten Fällen viel nützlicher ist, das heißt, die Rückgabewert Regel Nummer ist:

[
[ " " ],
57,
[ ]
]

Nearley-Regeln definieren das Standardverhalten neu, indem sie der Regel einen Postprozessor hinzufügen, ein Standardarray verwenden und es durch das ersetzen, was Sie benötigen. Ein Postprozessor ist nur Javascript-Code in speziellen Klammern am Ende einer Regel. Zum Beispiel Regel Nummer ich nie in irgendwelchen Lücken auf beiden Seiten der Zahl interessierte. Daher wäre es praktisch, wenn die Regel einfach eine Zahl und kein Array von drei Elementen zurückgeben würde. Hier ist ein Postprozessor, der diese Aufgabe ausführt:

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

Dieser Postprozessor nimmt das Standardergebnis (das oben gezeigte Array mit drei Elementen) und ersetzt es durch das zweite Element des Arrays, bei dem es sich um die Javascript-Nummer aus der Dezimalregel handelt . So , jetzt regiert Zahl kehrt die reelle Zahl.

Mit dieser Funktion kann eine eingehende Sprache in eine Zwischensprache umgewandelt werden, mit der einfacher gearbeitet werden kann. Zum Beispiel kann ich die Nearley-Grammatik verwenden, um eine MBDL-Zeichenfolge in ein Array von Javascript-Strukturen umzuwandeln, von denen jede ein durch ein "op" -Feld gekennzeichnetes Grundelement darstellt. Die Regel für das Zeilenprimitiv sieht ungefähr so ​​aus:

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

Das heißt, das Ergebnis des Parsens von „L (13, schwarz)“ ist die Javascript-Struktur:

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

Nach dem Hinzufügen der entsprechenden Nachbearbeitung kann das von der Grammatik zurückgegebene Ergebnis eine Folge (Array) von Operationsstrukturen für die eingehende Zeile sein. Das heißt, das Ergebnis des Parsens der Zeichenfolge

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

wird sein

[
{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"}]}
]

Das ist viel einfacher zu verarbeiten, um einen Kartenrand zu erstellen.

An dieser Stelle haben Sie möglicherweise eine Frage: Wenn die Nachbearbeitungsphase der Nearley-Regel Javascript enthalten kann, überspringen Sie dann die Zwischenansicht und zeichnen Sie einfach den Rand der Karte direkt während der Nachbearbeitung. Für viele Aufgaben wäre dieser Ansatz ideal. Ich habe mich aus mehreren Gründen entschieden, es nicht zu verwenden.

Erstens gibt es in MBDL einige (*) Komponenten, die während des Analyseprozesses nicht ausgeführt werden können. Beispielsweise können wir während des Analyseprozesses keine sich wiederholenden geometrischen Elemente (Streifen oder Raute) zeichnen, da wir Informationen von anderen Elementen im selben Muster kennen müssen. Insbesondere müssen wir die Gesamtlänge des Musters kennen, um zu verstehen, wie weit wir die Wiederholungen jedes einzelnen Elements anordnen müssen. Das heißt, das Element des Musters sollte immer noch eine Zwischendarstellung aller geometrischen Elemente erstellen.

(* Es gibt andere Komponenten mit ähnlichen Einschränkungen, über die ich noch nicht gesprochen habe.)

Zweitens ist Javascript in Nearley in die Regeln eingebettet, sodass wir keine zusätzlichen Informationen an Javascript übergeben können, außer für globale Variablen. Um beispielsweise den Rand zu zeichnen, muss ich die Größe der Karte, die vier verwendeten Kürzungsbereiche usw. kennen. Obwohl ich Code hinzufügen kann, der diese Informationen Nearley-Postprozessoren zur Verfügung stellt, ist er etwas chaotisch und es kann schwierig sein, diesen Code zu verwalten.

Aus diesen Gründen analysiere ich eine Zwischendarstellung, die dann ausgeführt wird, um den Rand der Karte selbst zu erstellen.

Der nächste Schritt besteht darin, einen Interpreter zu entwickeln, der eine Zwischendarstellung von MBDL empfängt und ausführt, um Kartengrenzen zu generieren. Dies ist nicht sehr schwer zu tun. Grundsätzlich besteht die Aufgabe darin, die Anfangsbedingungen festzulegen (z. B. Kürzungsbereiche für die vier Seiten der Karte zu generieren) und die Sequenz der Strukturen der Zwischendarstellung zu durchlaufen, wobei jede ausgeführt wird.

Es gibt ein paar rutschige Momente.

Zuerst muss ich vom Rendern von innen zum Zeichnen von innen nach außen übergehen. Der Grund dafür ist, dass die meisten Ränder die Karte nicht überlappen sollen. Daher muss ich die Ränder so zeichnen, dass die Linien des inneren Randes mit den Rändern der Karte übereinstimmen. Wenn ich von außen nach innen zeichne, muss ich die Breite des Rahmens kennen, bevor ich mit dem Zeichnen beginne, damit der Rand die Karte nicht überlappt. Wenn ich von innen nach außen zeichne, beginne ich einfach am Rand der Karte und zeichne heraus. Außerdem können Sie der Karte optional einen Rahmen hinzufügen. Beginnen Sie den Rand einfach mit einem negativen vertikalen Leerzeichen (VS).

Ein weiterer schwieriger Punkt sind die sich wiederholenden Muster. Um sich wiederholende Muster zu zeichnen, muss ich alle Elemente des Musters betrachten und das breiteste bestimmen, da dadurch die Breite des gesamten Musters festgelegt wird. Ich muss auch die Länge des Musters betrachten und verfolgen, damit ich weiß, wie viel Abstand ich vor jeder Wiederholung lassen muss.

Hier ist ein Beispiel für einen ziemlich komplexen Rand, mit dem ich den Interpreter getestet habe:


Ich denke, es war möglich (notwendig?), Es zum Testen an den Parser anzuhängen, aber für diesen Rahmen habe ich nur manuell eine Zwischenansicht erstellt:

[
{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},
]},
]

Ich habe diese Ansicht durch Ausprobieren erstellt. Wie dem auch sei, der Dolmetscher arbeitet!

Lassen Sie mich als letzten Schritt den Parser verwenden, um eine Zwischenansicht aus der MBDL-Version zu erstellen. Hier gibt es nicht viel zu zeigen: Ich musste ein paar Feldnamen korrigieren, aber ansonsten funktionierte der Code einwandfrei. Für den Rand habe ich eine etwas andere Version von MBDL verwendet:

[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")

Sie zeichnet den gleichen Rand, aber auf etwas andere Weise. Ich habe auch die Syntax für das Overlay geändert und die Klammern durch geschweifte Klammern ersetzt, damit sie sich stärker von der anderen Syntax unterscheiden.

Um zu zeigen, warum ich von innen nach außen zeichnen und nicht nur den Rand automatisch außerhalb der Karte platzieren wollte, kann ich am Anfang dieses Rahmens einen negativen vertikalen Abstand einfügen, um den Kartenmaßstab innerhalb des Kartenrandes zu verschieben:


Jetzt habe ich den größten Teil der Infrastruktur, die für die prozedurale Generierung von Kartenrändern erforderlich ist: eine Grenzbeschreibungssprache, einen Sprachparser und Verfahren zum Durchführen einer Zwischendarstellung. Es bleibt nur der schwierige Teil zu behandeln - die prozedurale Generierung!

Teil 6


Nachdem die gesamte MBDL implementiert wurde, wollte ich mit der prozeduralen Generierung von Kartenrändern fortfahren. Ich bin mir jedoch noch nicht sicher, wie ich dies tun möchte, da ich noch ein wenig verweilen und ein paar weitere MBDL-Funktionen implementieren werde.

In der ersten Diskussion der Eckenverarbeitung mit Mustern habe ich über verschiedene Ansätze gesprochen. Am Ende erkannte ich die abgeschrägten Ecken, aber es gab eine zweite Option: Stoppen Sie das Muster in der Nähe der Ecke, wie in diesen Beispielen:



Eine solche Lösung wird häufig verwendet, wenn das Randmuster eine Art asymmetrische Figur, Runen oder etwas anderes ist, das nicht um 90 Grad gedreht werden kann, während die Ausrichtung beibehalten wird. Es ist jedoch offensichtlich, dass dies mit geometrischen Formen funktioniert.

Dies ist möglicherweise die Option, die Sie vor dem Generieren des Rahmens auswählen. Sie können jedoch ein wenig Flexibilität hinzufügen, wenn Sie sie von einem Teil des Rahmens aus aktivieren und die abgeschrägte Ecke auf dem anderen verwenden. Dazu muss ich MBDL einen neuen Befehl hinzufügen. Ich vermute, dass andere Optionen für verschiedene Teile der Grenze auftreten können, daher werde ich einen allgemeinen Optionsbefehl hinzufügen:

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

(Auch hier lassen wir aus Gründen der Übersichtlichkeit Leerzeichen und einige andere Details weg.) Bisher sind die einzigen Optionen "MITRE" für abgeschrägte Ecken und "STOPPED" zum Anhalten in der Nähe von Ecken. Wenn kein Wert STOPPED übertragen wird, stoppt das Programm das Muster in einem angemessenen Abstand von der Ecke. Wenn der Wert übertragen wird, stoppt das Muster in diesem Abstand von der Ecke.

Wenn STOPPED-Ecken verwendet werden, höre ich auf, das Eckmuster von den Ecken weg zu zeichnen. So sieht es aus:


Hier habe ich die MITRE-Option für das Schwarz-Weiß-Skalenmuster verwendet, damit es in Bezug auf den Winkel gespiegelt wird. Für ein Muster aus roten Kreisen und schwarzen Quadraten innerhalb einer goldenen Linie (und für ein Muster aus Kreisen außerhalb des Randes) habe ich STOPPED verwendet. Sie können sehen, dass diese beiden Muster in der Nähe der Ecke enden.

Es gibt jedoch einige Probleme. Erstens sehen wir, dass links das der Ecke am nächsten liegende Element ein schwarzes Quadrat und oben ein roter Kreis ist. Dies geschah, weil sich die Ecke auf der einen Seite nahe dem Beginn der Wiederholung und auf der anderen Seite nahe dem Ende der Wiederholung befindet. Aber es sieht komisch aus. Es wäre besser, wenn die Ecken symmetrisch wären, auch wenn wir dafür am Ende des Musters ein weiteres Element hinzufügen müssten. Zweitens können Sie sehen, dass das Muster außerhalb des Randes (Halbkreise und schwarze Punkte) auch in einer Wiederholung bis zur Ecke endet. Da die Länge dieser Wiederholung jedoch viel geringer ist als die Länge der roten Kreise / schwarzen Quadrate, landen sie an verschiedenen Stellen. Es wäre wahrscheinlich besser, wenn alle Muster im gleichen Abstand von der Ecke anhalten würden.

Um das erste Problem zu beheben, müssen Sie am Ende jeder Seite des Rahmens eine weitere Wiederholung des ersten Elements des Musters hinzufügen. Tatsächlich ist es jedoch etwas komplizierter, da ich einen negativen horizontalen Versatz innerhalb des Musters verwenden könnte, um mehrere Elemente zu überlappen (wie hier ausgeführt). Sie müssen außerdem jedem Element des Musters, das denselben Startpunkt wie das erste Element hat, eine weitere Wiederholung hinzufügen.


Jetzt ist das Muster in Bezug auf den Winkel symmetrisch und sieht viel besser aus.

Als nächstes muss ich das längste STOPPED-Muster verfolgen und jedes STOPPED-Muster in dieser Entfernung anhalten:


Jetzt wird das Muster der weißen Kreise mehr beiseite gelegt, aber es ist immer noch nicht mit dem Muster der roten Kreise ausgerichtet. Warum?Dies geschah, weil das weiße Kreismuster weiter vom Rand der Karte entfernt ist und der Rand länger ist als dort, wo das rote Kreismuster gezeichnet wird. Um dieses Problem zu beheben, müssen Sie auch die Muster verschieben und ihren Versatz relativ zum Rand der Karte berücksichtigen.


Jetzt ist alles schön ausgerichtet.

Die zweite Option für Winkel sind die quadratischen Versätze in den Ecken, zum Beispiel diese:


Es wird viel schwieriger sein, dies umzusetzen!

Die Grammatik dieser Option ist jedoch einfach und verwendet den Options-Opcode:

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

Die Zahl gibt die Größe der quadratischen Verschiebung für das Element am Rand der Karte an. Elemente mit unterschiedlichen Offsets müssen entsprechend ausgerichtet werden. Wenn keine Nummer vorhanden ist, wählt das Programm die entsprechende Versatzgröße aus. Durch Nullstellen der Zahl wird der quadratische Versatz deaktiviert. Auf diese Weise können Sie Rahmen erstellen, in denen einige Elemente quadratische Offsets verwenden, während andere dies nicht tun, wie in diesem Rahmen:


Als erstes wurde mir klar, dass ich zusätzliche Kürzungsbereiche benötigen würde, da ich die Kürzung verwende, um Stellen zu verarbeiten, an denen der Rand die Richtung ändert. SQOFFSET erfordert komplexere Kürzungsbereiche. Sie benötigen auch separate Bereiche für verschiedene Elemente, wenn Sie SQOFFSET aktivieren und deaktivieren. Angesichts der Tatsache, dass Kürzungsbereiche ohnehin unerwünschte Artefakte hinzufügen, scheint dies zu viel Arbeit zu sein.

Als ich oben an stoppbaren Mustern gearbeitet habe, habe ich das Ausfüllen eines asymmetrischen Musters implementiert, um eine weitere Wiederholung von einem Ende des Musters hinzuzufügen. Ich erkannte auch, dass dies die Notwendigkeit abgeschrägter Ecken beseitigen würde. Ich werde einfach alle Muster entlang des Randes im Uhrzeigersinn zeichnen, wobei das Muster in einer Ecke beginnt und in der Nähe der nächsten Ecke endet. Dadurch kann ich Kürzungsbereiche entfernen.

Das Wichtigste bei dieser neuen Art der Arbeit mit Ecken war, dass das erste Element des Musters nicht mehr in zwei Seiten „unterteilt“ ist. Wenn Sie sich die Schwarz-Weiß-Skalenmuster auf den Karten oben ansehen, sehen Sie, dass ein weißes Rechteck durch die Ecke verläuft. Jetzt stößt das weiße Rechteck an die Ecke:


Karten werden auf beide Arten gezeichnet, aber dies ist kein sehr großes Problem.

Für den Anfang habe ich Offsets für Linien implementiert. Dazu genügte es, die Linie relativ zu den entsprechenden Winkeln zu drehen:


Wie Sie verstehen können, kann ich Winkel mit Offsets und regulären Winkeln kombinieren, wie in der obigen Karte:


Natürlich ist es schwieriger, die Muster um die Ecke zu drehen. Die allgemeine Idee ist, von einer Ecke zur anderen zu ziehen und so weiter entlang der Grenze, bis wir zum Anfang zurückkehren. Theoretisch reicht es aus, nur horizontale und vertikale Muster zu zeichnen, und alles sollte schön ausgerichtet sein. Das alles zu verfolgen ist ziemlich trostlos. Tatsächlich musste ich den Code zweimal komplett neu schreiben und ein paar Papiere schreiben, aber ich werde nicht im Detail darüber sprechen. Zeigen Sie einfach das Ergebnis:


In den Ecken entsteht eine störende optische Täuschung - das Eckelement scheint näher an der Außenseite der Ecke nicht zentriert zu sein. In der Tat ist dies nicht wahr, aber es scheint so, weil näher an der Innenseite der Ecke visuell mehr leerer Raum ist.

Da die Segmente der Versatzwinkel ziemlich kurz sind, ist es sehr einfach, ein Nichtgleichgewichtsmuster in der Ecke zu erstellen:


Manchmal sieht es ziemlich hässlich aus. Es erinnerte mich an einen alten Witz:

Patient: "Doktor, wenn ich das mache, tut es mir weh."
Doktor: "Dann mach das nicht!"

Deshalb werde ich versuchen, dies nicht zu tun.

Normalerweise zeichne ich den Kartenmaßstab nicht entlang des Versatzwinkels, aber wenn ich ihn brauche, muss ich die Option verwenden, mit der das Muster so gedehnt wird, dass der Kartenmaßstab ohne Lücken zwischen den Rechtecken in die Ecke passt:


Sie können sehen, dass die Größe der Skalierungsrechtecke daher stark variiert. Das heißt, dies ist keine sehr gute Option. (Übrigens haben die Versatzwinkel auch einen Fehler im Kreismuster. Später habe ich ihn behoben, aber wie gesagt, es ist sehr schwierig, dies zu tun.)

Wenn das Muster zu groß ist, um auf das Segment des Versatzwinkels zu passen, gibt der Algorithmus einfach auf:


Das ist alles andere als ideal, aber wie ich oben sagte: "Dann tu es nicht." (Es ist eigentlich nicht sehr schwierig, eine Komprimierungs- oder Dehnungsfunktion hinzuzufügen, wenn ich sie benötige.)

Was passiert, wenn ich sowohl versetzte Ecken als auch die Option verwende, mit der die Muster vor den Ecken gestoppt werden? In diesem Fall halte ich einfach nicht weit von den versetzten Ecken an:


Es scheint mir, dass dies eine logische Entscheidung ist.

Source: https://habr.com/ru/post/de444190/


All Articles