Crea los bordes de un mapa generado por procedimientos

imagen

Scott Turner contin√ļa trabajando en su juego generado por procedimientos y ahora ha decidido abordar el problema del dise√Īo de los bordes de los mapas. Para hacer esto, tiene que resolver varios problemas dif√≠ciles e incluso crear su propio lenguaje para describir los l√≠mites.

Las fronteras seguían siendo un elemento importante de las tarjetas de fantasía, que habían estado en mi lista durante bastante tiempo. Los mapas funcionales generalmente tienen una línea de borde simple, pero los mapas de fantasía y los mapas medievales, de los cuales los primeros suelen tomar ideas, tienen límites bastante reflexivos y artísticos. Estos límites dejan en claro que el mapa se ha hecho intencionalmente fantástico y le dan al espectador una sensación de asombro.

Actualmente hay un par de formas simples de dibujar bordes en mi juego Dragons Abound . Puede dibujar una línea simple o doble alrededor del perímetro del mapa y agregar elementos simples en las esquinas, como en estas figuras:



El juego también puede agregar un campo en la parte inferior del borde para el nombre del mapa. Hay varias variaciones de este campo en Dragons Abound , incluidos elementos tan complejos como cabezas de tornillo falsas:


Hay variabilidad en estos campos de nombre, pero todos se crean manualmente.

Un aspecto interesante de los l√≠mites de las tarjetas de fantas√≠a es que son creativas y de plantilla. A menudo consisten en una peque√Īa cantidad de elementos simples que se combinan de diferentes maneras para crear un resultado √ļnico. Como siempre, el primer paso al trabajar con un nuevo tema para m√≠ es estudiar una colecci√≥n de ejemplos de mapas, crear un cat√°logo de tipos de elementos de borde y estudiar su apariencia.

El borde más simple es una línea que corre a lo largo de los bordes del mapa e indica sus límites. Como dije anteriormente, también se llama la "línea de trama":


También hay una variación con la ubicación de los bordes dentro del mapa. En esta versión, el mapa alcanza los bordes de la imagen, pero el borde crea un borde virtual dentro de la imagen:


Esto se puede hacer con cualquier tipo de borde, pero generalmente se usa solo con bordes simples como el borde de un marco.

Un concepto popular de dise√Īo de tarjetas de fantas√≠a es simular como si estuvieran dibujadas en un viejo pergamino roto. A veces esto se realiza dibujando el borde como el borde √°spero del papel:


Aquí hay un ejemplo más sofisticado:


En mi experiencia, este método se ha vuelto menos popular porque las herramientas digitales se han puesto en uso. Si desea que la tarjeta se vea como un viejo pergamino roto, entonces es más fácil aplicarle la textura del pergamino que dibujarla a mano.

La herramienta más poderosa para crear bordes de mapa es la repetibilidad. En el caso más simple, es suficiente repetir una sola línea para crear dos líneas:


Puede agregar interés al mapa variando el estilo del elemento repetido, en este caso combinando una sola línea gruesa con una sola línea delgada:


Dependiendo del elemento, son posibles diversas variaciones de estilo. En este ejemplo, la línea se repite, pero el color cambia:


Para crear patrones más complejos, puede usar "repetibilidad repetible". Este borde consta de aproximadamente cinco líneas simples con diferentes anchos y distancias:


Este borde repite las líneas, pero las separa para que se vean como dos bordes finos separados. En esta parte de la publicación no hablaré sobre el procesamiento de ángulos, pero diferentes ángulos para las dos líneas también ayudan a crear esta diferencia.


¬ŅSon estas dos l√≠neas, cuatro o seis? ¬°Creo que todo depende de c√≥mo los dibujes!

Otro elemento de estilización es llenar el espacio entre elementos con color, patrón o textura. En este ejemplo, el borde se volvió más interesante debido al relleno de color de acento entre las dos líneas:


Aquí hay un ejemplo de cómo se llena el borde con un patrón:


Adem√°s, los elementos se pueden dise√Īar para que se vean tridimensionales. Aqu√≠ hay un mapa en el que el borde est√° sombreado para que parezca voluminoso:


En este mapa, el borde está sombreado para parecer tridimensional, y esto se combina con la ubicación de los bordes dentro de los bordes del mapa:


Otro elemento de borde com√ļn es la escala en forma de rayas multicolores:


Estas franjas forman una cuadrícula ( cuadrícula cartográfica ). En los mapas reales, la escala ayuda a determinar las distancias, pero en los mapas de fantasía es principalmente un elemento decorativo.

Estas rayas generalmente se dibujan en blanco y negro, pero a veces se agrega rojo u otro color:


Este elemento también se puede combinar con otros, como en este ejemplo con líneas y escala:


Este ejemplo es un poco inusual. Por lo general, la escala (si la hay) es el elemento m√°s interno del borde.

En este mapa, hay diferentes escalas con diferentes resoluciones (¬°as√≠ como notas r√ļnicas extra√Īas!):


(En Reddit, el usuario AbouBenAdhem me inform√≥ que las marcas r√ļnicas son n√ļmeros 48 y 47 escritos en cuneiforme babil√≥nico. Adem√°s, las ‚Äúescalas con diferentes resoluciones‚ÄĚ tienen seis divisiones divididas en diez divisiones m√°s peque√Īas, que corresponde al sistema de n√ļmeros hexadecimales babilonios. Por lo general Indico las fuentes de los mapas, pero hay demasiadas piezas peque√Īas en esta publicaci√≥n, as√≠ que no me molest√©. Sin embargo, este mapa fue creado por Thomas Ray para el autor S.E. Boleyn, por lo que tal vez la acci√≥n en sus libros tiene lugar en el s√©quito de Babilonia.

Adem√°s de las l√≠neas y la escala, el elemento m√°s com√ļn es un patr√≥n geom√©trico repetitivo. A menudo consta de partes como c√≠rculos, rombos y rect√°ngulos:


Los elementos geométricos, como las líneas, se pueden sombrear para que se vean tridimensionales:


Se pueden crear límites complejos combinando estos elementos de diferentes maneras. Aquí está el borde que combina líneas, patrones geométricos y escala:


Los ejemplos que se muestran arriba eran tarjetas digitales, pero, por supuesto, lo mismo se puede hacer con tarjetas escritas a mano. Aquí hay un ejemplo de un patrón geométrico simple creado a mano:


Estos elementos también se pueden combinar de manera flexible de muchas maneras. Aquí hay un patrón geométrico combinado con un "borde irregular":


En los ejemplos mostrados anteriormente, el patrón geométrico es bastante simple. Pero puede crear patrones muy complejos combinando de manera diferente los elementos geométricos básicos:


Otro elemento popular del patrón es el tejido o el nudo celta:


Aquí hay un borde de mimbre más complejo que contiene color, escala y otros elementos:


En este mapa, el tejido se combina con un elemento de borde irregular:


Además de los patrones geométricos y el tejido, cualquier patrón repetitivo puede ser parte del borde de la tarjeta. Aquí hay un ejemplo usando formas que se asemejan a puntas de flecha:


Y aquí hay un ejemplo con un patrón de onda repetitiva:


Y finalmente, las runas u otros elementos del alfabeto de fantasía a veces se agregan a los bordes de las tarjetas de fantasía:


Los ejemplos anteriores están tomados de mapas modernos de fantasía, pero aquí hay un ejemplo de un mapa histórico (siglo XVIII) con líneas y un patrón dibujado a mano:


Por supuesto, puede encontrar ejemplos de mapas con muchos otros elementos en los bordes. Algunas de las m√°s bellas est√°n completamente dibujadas a mano y tienen decoraciones tan cuidadosamente elaboradas que pueden superar la propia tarjeta ( Mundo de Alma , Francesca Baerald):


También vale la pena hablar un poco sobre simetría . Al igual que la repetibilidad, la simetría es una herramienta poderosa, y los bordes del mapa son generalmente simétricos o tienen elementos simétricos.

Muchos bordes del mapa son simétricos de adentro hacia afuera, como en este ejemplo:


Aquí, el borde se compone de varias líneas con y sin relleno, pero desde afuera hacia adentro idealmente se repite en relación con el centro del borde.

En este ejemplo más complejo, el borde es simétrico, con la excepción de franjas de escala alternadas en blanco y negro:


Dado que duplicar la escala no tiene sentido, a menudo se considera un elemento separado, incluso si el resto del borde es simétrico.

Adem√°s de la simetr√≠a interna-externa, los bordes a menudo son sim√©tricos a lo largo de su longitud. Algunos bordes ilustrados pueden tener un dise√Īo simple que abarca toda la longitud del borde del mapa, pero en la mayor√≠a de los casos el patr√≥n es bastante corto y se repite, llenando el borde de una esquina a otra:


Observe que en este ejemplo el patrón contiene un elemento que no es simétrico (de izquierda a derecha), pero el patrón general es simétrico y se repite:


Una excepci√≥n notable a esta regla son los bordes llenos de runas o caracteres alfab√©ticos. A menudo resultan ser √ļnicos, como si se escribiera un mensaje largo a lo largo de la frontera:


Por supuesto, hay muchos otros ejemplos de elementos de borde de mapa que no he considerado aquí, pero ya tenemos un buen punto de referencia. En las siguientes partes, desarrollaré varias funciones en Dragons Abound para describir, mostrar y generar de manera procesal bordes de mapas similares a estos ejemplos. En la segunda parte, comenzaremos configurando el idioma para describir los bordes de los mapas.

Parte 2


En esta parte, crearé la versión inicial del lenguaje de descripción de borde de mapa (MBDL).

¬ŅPor qu√© dedicar tiempo a crear un lenguaje de descripci√≥n de l√≠mites de mapa? En primer lugar, este ser√° el objetivo de mi generaci√≥n procesal. M√°s adelante escribir√© un algoritmo para crear nuevos bordes de mapa, y la salida de este algoritmo ser√° una descripci√≥n del nuevo borde en MBDL. En segundo lugar, MBDL servir√° como una representaci√≥n textual de los l√≠mites del mapa. En particular, necesito poder guardar y reutilizar mis l√≠mites. Para hacer esto, necesito una notaci√≥n de texto que se pueda escribir y usar para recrear el borde del mapa.

Comenzaré a crear MBDL definiendo el elemento más simple: la línea. La línea tiene color y ancho. Por lo tanto, en MBDL presentaré la línea de esta forma:

L(width, color)

Aquí hay algunos ejemplos (perdón por mis habilidades de Photoshop):


La secuencia de elementos se representa desde el exterior hacia el interior (*), por lo que suponemos que este es el borde en la parte superior del mapa:


Mire el segundo ejemplo: una línea con bordes se representa como tres elementos de línea separados.

(* Dibujar desde afuera hacia adentro fue una elección arbitraria, me pareció que era más natural que hacer desde adentro hacia afuera. Desafortunadamente, como resultó mucho más tarde, había una buena razón para trabajar en la dirección opuesta. Pronto te contaré sobre eso, pero todo se deja en la publicación - viejo, porque tomaría mucho tiempo rehacer todas las ilustraciones)

Convenientemente, los espacios se pueden representar como líneas sin color:


Pero sería más visual tener un elemento de espacio vertical específico:

VS (ancho)

Los siguientes elementos simples son formas geométricas: rayas, rombos y elipses. Se supone que las líneas se extienden a lo largo de todo el borde, por lo que no tienen una longitud explícitamente especificada. Pero las figuras geométricas no pueden llenar toda la línea, por lo tanto, además del ancho (*), cada una debe tener una longitud, color de contorno, ancho de contorno y color de relleno:

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

(* Acepté que consideraré el ancho en la dirección desde afuera hacia adentro, y la longitud se mide a lo largo del borde).

Aquí hay ejemplos de formas geométricas simples:


Para que estos elementos llenen toda la longitud del borde, deben repetirse. Para indicar el grupo de elementos que se repetir√°n para llenar la longitud del borde, uso corchetes:

[ element element element ... ]

Aquí hay un ejemplo de un patrón repetitivo de rectángulos y rombos:


A veces necesitaré un espacio (horizontal) entre elementos de un patrón repetitivo. Aunque puede usar un elemento sin colores para crear un espacio, será más inteligente y más conveniente tener un elemento de espacio horizontal:

HS(length)

La √ļltima funci√≥n necesaria para esta primera iteraci√≥n de MBDL es la capacidad de apilar elementos uno encima del otro. Aqu√≠ hay un ejemplo de borde:


La forma más fácil de describirlo es una línea amarilla ancha debajo del patrón superior. Puede implementar esto de diferentes maneras (por ejemplo, un espacio vertical negativo), pero decidí usar llaves para indicar el orden de los elementos hacia adentro:

{element element element ...}

De hecho, esta entrada le dice que recuerde dónde estaba el patrón desde afuera hacia adentro al ingresar los corchetes, y luego regrese a este punto cuando salga de los corchetes. Los corchetes también se pueden considerar como una descripción de elementos que ocupan un espacio vertical. Por lo tanto, el borde que se muestra arriba se puede describir de la siguiente manera:

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)

Dibujamos una línea negra, nos fijamos donde estamos, dibujamos una línea amarilla y luego volvemos a la posición previamente fijada, caemos un poco, dibujamos un patrón de rectángulos y rombos, caemos un poco y luego dibujamos otra línea negra.

Hay mucho m√°s por hacer en MBDL, pero esto es suficiente para describir los muchos l√≠mites de los mapas. El siguiente paso es convertir la descripci√≥n del l√≠mite en el MBDL al borde mismo. Esto es similar a convertir una representaci√≥n escrita de un programa de computadora (como Javascript) en la ejecuci√≥n de este programa. La primera etapa es el an√°lisis l√©xico (an√°lisis) del lenguaje: la transformaci√≥n del texto fuente en un borde real del mapa o en alg√ļn tipo de forma intermedia, que es m√°s f√°cil de convertir en un borde.

El análisis es un área bastante bien estudiada de la informática. Analizar un idioma no es muy simple, pero en nuestro caso es bueno (*) que MBDL sea una gramática libre de contexto. Las gramáticas sin contexto se analizan con bastante facilidad, y hay muchas herramientas de análisis de Javascript para ellas. Me decidí por Nearley.js , que parece ser bastante maduro y (más importante) una herramienta bien documentada.

(* Esto no es solo suerte, me aseguré de que el lenguaje no tuviera contexto).

No te presentar√© las gram√°ticas libres de contexto, pero la sintaxis de Nearley es bastante simple y debes entender el significado sin ning√ļn problema. La gram√°tica Nearley consiste en un conjunto de reglas. Cada regla tiene un car√°cter a la izquierda, una flecha y la parte derecha de la regla, que puede ser una secuencia de caracteres y no caracteres, as√≠ como varias opciones separadas por "|" (o):

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

Cada una de las reglas dice que el lado izquierdo se puede reemplazar por cualquiera de las opciones en el lado derecho. Es decir, la primera regla dice que un borde es un elemento, o un elemento, seguido de otro borde. Que en sí mismo puede ser un elemento, o un elemento seguido de un borde, etc. La segunda regla dice que un elemento solo puede ser una cadena "L". Es decir, juntas estas reglas corresponden a tales límites:

L
LLL

y no corresponden a tales límites:

X
L3L

Por cierto, si quieres experimentar con esta (o cualquier otra) gramática en Nearley, entonces hay un sandbox en línea para esto aquí . Puede ingresar gramática y casos de prueba para ver qué coincide y qué no.

Aquí hay una definición más completa de una línea primitiva:

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

Nearley tiene varios elementos integrados comunes, y el n√ļmero es uno de ellos. Por lo tanto, puedo usarlo para reconocer el ancho num√©rico de una l√≠nea primitiva. Para el reconocimiento de color, uso otro elemento incorporado y permito el uso de cualquier cadena entre comillas dobles.

Sería bueno agregar espacios entre diferentes personajes, así que hagámoslo. Nearley admite clases de caracteres y RBNF para "cero o más" algo con ": *", por lo que puedo usar esto para especificar "cero o más espacios" y pegarlo en cualquier lugar para permitir espacios en las descripciones:

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

Sin embargo, el uso de WS en todas partes hace que sea difícil leer la gramática, por lo que los abandonaré, pero imagino que lo son.

Un elemento también puede ser un espacio vertical:

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

Esto corresponde a tales límites

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

Luego vienen las primitivas de tira, rombo y elipse.

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

Coincidir√° con tales elementos

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

(Tenga en cuenta que las primitivas geométricas no son "elementos" porque no pueden estar solos en el nivel superior. Deben estar encerrados en un patrón).

También necesito un espacio horizontal primitivo:

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

Ahora agregaré una operación de patrón (repetición). Esta es una secuencia de uno o más elementos dentro de corchetes. Usaré el operador RBNF ": +", que aquí significa "uno o más".

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

Tenga en cuenta que el patrón solo puede rellenarse con primitivas geométricas. No podemos, por ejemplo, colocar una línea dentro de un patrón. El elemento del patrón ahora coincidirá con algo como esto.

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

La √ļltima parte del lenguaje es el operador de superposici√≥n. Este es cualquier n√ļmero de elementos dentro de llaves.

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

lo que nos permite hacer lo siguiente:

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

(Tenga en cuenta que, a diferencia del operador de repetición, el operador de superposición se puede usar internamente).

Después de limpiar la descripción y agregar espacios a los lugares necesarios, obtenemos la siguiente gramática 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 ")"

Entonces, MBDL ahora está definido y hemos creado una gramática del lenguaje. Se puede usar con Nearley para reconocer cadenas de idioma. Antes de profundizar en MBDL / Nearley, me gustaría implementar las primitivas utilizadas en MBDL para que se pueda mostrar el límite descrito en MBDL. Esto lo haremos en la siguiente parte.

Parte 3


Ahora comenzaremos a implementar las primitivas de representación. (En este punto, todavía no tengo que vincular el analizador a las primitivas de representación. Para realizar pruebas, los llamaré manualmente).

Comencemos con la línea primitiva. Recordemos cómo se ve:

L(width, color)

Además del ancho y el color, aquí hay un parámetro implícito: la distancia desde el borde exterior del mapa. (Dibujo los bordes desde el borde del mapa hacia afuera. ¡Tenga en cuenta que comenzamos desde uno diferente!) No debe apuntar al MBDL, porque el intérprete que ejecuta el MBDL puede rastrearlo para dibujar el borde. Sin embargo, esto debería ser una entrada para todas las primitivas de representación para que sepan dónde dibujarlas. Llamaré a este parámetro offset.

Si solo necesitara dibujar un borde a lo largo de la parte superior del mapa, entonces la primitiva de l√≠nea ser√≠a muy simple de implementar. Sin embargo, de hecho, tendr√© que dibujar desde arriba. abajo, izquierda y derecha. (Quiz√°s alg√ļn d√≠a me d√© cuenta de los bordes oblicuos o curvos, pero por ahora nos adheriremos a los bordes rectangulares est√°ndar.) Adem√°s, la longitud y la ubicaci√≥n del elemento de l√≠nea dependen del tama√Īo del mapa (as√≠ como del desplazamiento). Por lo tanto, como par√°metros, necesito todos estos datos.

Una vez establecidos todos estos parámetros, basta con crear una línea primitiva y usarla para dibujar una línea alrededor del mapa:


(Tenga en cuenta que uso varias funciones de Dragons Abound para dibujar la línea "escrita a mano"). Intentemos crear un borde más complejo:

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

Se ve así:


Bastante bien Tenga en cuenta que hay lugares en los que las líneas negras y la línea dorada no están completamente alineadas debido a las fluctuaciones. Si quiero deshacerme de estos puntos, entonces simplemente puede reducir la cantidad de oscilación.

Implementar una primitiva de espacio vertical es bastante simple; solo realiza un incremento de desplazamiento. Agreguemos un poco de espacio:

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


Al dibujar líneas, los ángulos se pueden realizar dibujando entre el desplazamiento y el dibujo a lo largo del mapa en el sentido de las agujas del reloj. Pero, en general, necesito implementar el truncamiento en cada lado del borde del mapa para crear una conexión angular con un bisel . Esto será necesario para crear bordes con patrones que se unan correctamente en las esquinas, y en el caso general eliminará la necesidad de dibujar elementos con bordes en un ángulo que de otro modo sería necesario. (*)

(Nota: como se dirá en las siguientes partes, con el tiempo me negué a usar regiones de truncamiento al implementar ángulos. La razón principal es que para crear ángulos complejos, por ejemplo, compensaciones cuadradas:


Se requieren áreas de truncamiento cada vez más complejas. Además, con el tiempo, encontré una mejor manera de trabajar con patrones en las esquinas. En lugar de regresar y reescribir esta parte del artículo, decidí dejarlo para ilustrar el proceso de "creatividad".)

La idea principal es truncar cada borde en diagonal y crear cuatro √°reas truncadas en las que se dibujar√° cada lado del borde:


Al truncar, todo lo dibujado en el √°rea correspondiente se cortar√° en el √°ngulo deseado.


Desafortunadamente, esto crea peque√Īos espacios a lo largo de las l√≠neas diagonales, probablemente porque el navegador realiza imperfectamente el suavizado a lo largo del borde truncado. La prueba mostr√≥ que un fondo brilla a trav√©s del espacio entre los dos bordes. Fue posible solucionar esto expandiendo un poco una de las m√°scaras (la mitad del p√≠xel parece ser suficiente), pero esto a veces no resuelve el problema.

A continuación, debe implementar formas geométricas. A diferencia de las líneas, se repiten en el patrón, llenando el lado del borde del mapa:


Una persona dibujaría este patrón de izquierda a derecha, dibujando un rectángulo, un rombo y luego repitiendo el mismo hasta que se llene todo el borde. Por lo tanto, esto también se puede implementar en el programa dibujando un patrón a lo largo del borde. Sin embargo, será más fácil dibujar primero todos los rectángulos y luego todos los rombos. Bastará con dibujar a lo largo del borde la misma figura geométrica a intervalos. Y es muy conveniente que cada elemento tenga el mismo intervalo. Por supuesto, una persona no haría eso, porque es demasiado difícil organizar los elementos en los lugares correctos, pero esto no es un problema para el programa.

Es decir, el procedimiento para dibujar formas geom√©tricas simples necesita par√°metros en los que se transfieren todas las dimensiones y colores de la figura (es decir, ancho, largo, grosor de l√≠nea, color de l√≠nea y relleno), as√≠ como la posici√≥n de inicio (que por razones que se aclarar√°n pronto, Considerar√© el centro de la figura), el intervalo de espacio horizontal para la transici√≥n entre repeticiones y el n√ļmero de repeticiones. Tambi√©n ser√° conveniente indicar la direcci√≥n de repetici√≥n en forma de un vector [dx, dy], para que podamos realizar repeticiones de izquierda a derecha, de derecha a izquierda, arriba o abajo, simplemente cambiando el vector y el punto de partida. P√≥ngalo todo junto y obtenga una tira de formas repetitivas:


Usando este código varias veces y renderizando con el mismo desplazamiento, puedo combinar las rayas en blanco y negro para crear la escala del mapa:


Antes de comenzar a descubrir cómo aplicar todo esto al borde real del mapa, primero implementemos la misma funcionalidad para elipses y rombos.

Los rombos son solo rect√°ngulos con v√©rtices rotados, por lo que solo necesita hacer un peque√Īo cambio en el c√≥digo. Result√≥ que todav√≠a no tengo un c√≥digo listo para renderizar la elipse, pero es muy f√°cil tomar la vista param√©trica de la elipse y crear una funci√≥n que me d√© los puntos de la elipse:


Aquí hay un ejemplo (creado manualmente) que usa las características implementadas anteriormente:


Para una cantidad de c√≥digo tan peque√Īa, ¬°se ve bastante bien!

Resolvamos ahora el complejo caso de los bordes con elementos repetitivos: esquinas.

Si hay un borde con elementos repetidos, hay varias formas de resolver el problema con las esquinas. El primero es ajustar las repeticiones para que se ejecuten en las esquinas sin un matrimonio notable:


Otra opci√≥n es detener la repetici√≥n en alg√ļn lugar cerca de la esquina en ambos lados. Esto a menudo se hace si el patr√≥n no se puede "rotar" f√°cilmente en la esquina:


La √ļltima opci√≥n es cerrar el patr√≥n con alguna decoraci√≥n de esquina:


Alg√ļn d√≠a llegar√© a las decoraciones de las esquinas, pero por ahora usaremos la primera opci√≥n. ¬ŅC√≥mo hacer que un patr√≥n de rayas o c√≠rculos "gire" en las esquinas del mapa sin espacios?

La idea principal es colocar el elemento del patrón exactamente en la esquina de modo que la mitad esté en un borde del mapa y la otra en el adyacente. En este ejemplo, el círculo está exactamente en la esquina y se puede dibujar desde cualquier dirección:


En otros casos, el elemento está medio dibujado en una dirección y la otra mitad en la otra, pero los bordes coinciden:


En este caso, se dibuja una franja blanca en ambos lados, pero se conecta en la esquina sin espacios.

Hay dos aspectos a considerar al colocar un elemento en una esquina.

Primero, el elemento de esquina se dividirá y reflejará en relación con la diagonal que pasa por el centro del elemento. Los elementos con simetría radial, por ejemplo, cuadrados, círculos y estrellas, no cambiarán su forma. Los elementos sin simetría radial, por ejemplo, rectángulos y rombos, cambiarán de forma al reflejar en relación con la diagonal.

En segundo lugar, para que los elementos de las esquinas de los dos lados se conecten correctamente, debe haber un n√ļmero entero de elementos (*) a lo largo de ambos lados del mapa. No tienen que ser el mismo n√ļmero, pero debe haber un n√ļmero entero de elementos en ambos lados. Si un n√ļmero fraccionario de patrones est√° contenido en un lado, entonces desde un borde el patr√≥n no coincidir√° con el lado adyacente.

(* En algunos casos, por ejemplo, con rayas largas, puede ocurrir una repetición parcial con repetición completa y los elementos se alinearán de todos modos. Sin embargo, el elemento de esquina resultante será asimétrico y diferirá en longitud del mismo elemento en los lados del mapa. Un ejemplo de esto se puede ver aquí:


Se produce una barra de escala blanca con diferentes repeticiones parciales y, como resultado, se obtiene un elemento desplazado con respecto al centro. Para la escala del mapa, este no es siempre el caso, ya que muestra la distancia absoluta y no tiene que ser simétrica. Pero para un patrón decorativo, esto generalmente se ve mal).

Aqu√≠ hay un ejemplo que muestra c√≥mo se recorta un n√ļmero entero de repeticiones exactamente en la esquina:


Si hace lo mismo desde los cuatro lados, las esquinas coincidirán y el patrón se ubicará perfectamente a lo largo de todo el borde:


Tras un examen cuidadoso, notará que el patrón no ocurre exactamente en las esquinas. La mitad del círculo en cada esquina se toma de cada lado, y estas dos mitades se dibujan independientemente a mano, por lo tanto, no son perfectas. Pero ahora están lo suficientemente cerca de esto.

Entonces, podemos realizar una conexi√≥n perfecta del patr√≥n en las esquinas, eligiendo un n√ļmero entero de repeticiones para cada borde. Sin embargo, la soluci√≥n a este problema no es trivial.

Primero, supongamos que sabemos que el lado tiene 866 p√≠xeles de largo y queremos repetir el elemento 43 veces. Luego, el elemento debe repetirse cada 20.14 p√≠xeles. ¬ŅC√≥mo establecemos la longitud espec√≠fica de un elemento (y en el caso general, un patr√≥n de elementos)? En el ejemplo anterior, agregu√© espacio adicional entre los c√≠rculos. Pero si los c√≠rculos inicialmente se tocan, entonces esto cambiar√° el patr√≥n. ¬ŅQuiz√°s vale la pena estirar los c√≠rculos para que contin√ļen toc√°ndose?


Ahora los elementos se tocan, pero los c√≠rculos se han convertido en elipses y las esquinas tienen una forma extra√Īa. (¬ŅRecuerda que dije que los elementos sin simetr√≠a radial cambian de forma cuando se reflejan en relaci√≥n con un √°ngulo? Para las rayas, esto no ser√° un gran problema). O quiz√°s valga la pena comprimir todos los elementos para que se toquen entre s√≠ y se ajusten en una longitud adecuada:


Pero para darnos cuenta de esto, necesitamos hacer que los elementos sean mucho m√°s peque√Īos de lo que eran originalmente. Ninguna de estas opciones parece perfecta.

El segundo problema ocurre cuando los lados de la tarjeta son de diferentes tama√Īos. Ahora tenemos que resolver el problema de encontrar un n√ļmero entero de repeticiones adecuado para ambos lados. Ser√≠a ideal encontrar una soluci√≥n que se ajuste a ambos lados. Pero no quiero hacer esto a costa de demasiado cambio de patr√≥n. Puede ser mejor crear patrones ligeramente diferentes en ambos lados si ambos est√°n lo suficientemente cerca del patr√≥n original.

Y finalmente, el tercer problema surge cuando uso la función de superponer varios elementos entre sí:


No quiero hacer ning√ļn cambio en el patr√≥n que destruir√° la relaci√≥n entre los elementos. Creo que con una escala adecuada, las relaciones en su conjunto se mantendr√°n, pero necesito probar esto.

Tarea interesante, ¬Ņverdad? Hasta ahora no tengo soluciones particularmente de alta calidad para ella. ¬°Quiz√°s aparecer√°n m√°s tarde!

Parte 4


Entonces, hemos implementado primitivas para dibujar líneas y formas geométricas. Comencé a trabajar en el uso de formas repetitivas para llenar los bordes y hablé sobre las dificultades de colocar patrones arbitrarios en el borde del mapa para que encajen perfectamente en las esquinas. El problema principal es que, en el caso general, debe hacer que el patrón sea más largo (o más corto) para que se ajuste lateralmente. Las opciones para cambiar la longitud del patrón (agregar o eliminar espacios, cambiar la longitud de los elementos de los patrones) conducen a varios cambios en el patrón en sí. ¡Parece que la tarea de seleccionar un patrón entre varios elementos es muy difícil!

Cuando me encuentro con tareas aparentemente intransigentes, me gusta comenzar implementando una versi√≥n simple. Las tareas fallidas a menudo se pueden resolver resolviendo repetidamente problemas "simples" hasta que el resultado sea lo suficientemente bueno. Y, a veces, la implementaci√≥n de una versi√≥n simple proporciona cierta comprensi√≥n que simplifica la soluci√≥n de un problema m√°s complejo. Si no mejora y el problema sigue siendo inc√≥modo, al menos tendremos una versi√≥n simplificada que a√ļn puede ser √ļtil, aunque no del todo como deber√≠a.

La forma más fácil es cambiar la longitud del patrón agregando longitudes sin cambiar nada en el patrón. Básicamente, esto agrega espacio en blanco al final del patrón. (Nota: es mejor distribuir el espacio vacío entre todos los elementos en el patrón). Vale la pena considerar que tal solución solo puede alargar el patrón. Siempre podemos agregar espacio vacío al patrón, pero no podemos tomarlo si es necesario, ¡tal vez no haya más espacio vacío en el patrón!

Con este enfoque, el algoritmo de ubicación del patrón en el lado de la tarjeta será muy simple:

  • Divida la longitud del lado de la tarjeta por la longitud del patr√≥n y redondee hacia abajo para determinar la cantidad de repeticiones del patr√≥n que se ajusta a ese lado.
  • La distancia entre los elementos en este caso ser√° igual a la longitud del lado dividido por el n√ļmero de repeticiones. (Esta es la m√°s cercana a la ubicaci√≥n original, dado que solo podemos agregar espacio).
  • Dibuja un patr√≥n a lo largo del costado, teniendo en cuenta la distancia calculada.

Fue difícil implementar este sistema. Las esquinas tercamente no querían coincidir. Me tomó demasiado tiempo darme cuenta de que cuando el mapa no es cuadrado, no puedo dibujar áreas de truncamiento para cuatro lados desde el centro del mapa, porque esto crea ángulos de truncamiento que no son iguales a 45 grados. De hecho, las áreas de truncamiento deben parecerse al reverso de un sobre:


Cuando descubrí esto, el algoritmo comenzó a funcionar sin problemas.

(¡Pero no olvide la nota anterior de que con el tiempo abandoné las áreas de truncamiento!)

Aquí hay un ejemplo con una relación de aproximadamente 2: 1:

En esta escala, es bastante difícil de notar, pero las esquinas se conectan correctamente y solo hay una ligera diferencia visual entre los lados. En este caso, el algoritmo para alinear los patrones solo necesita insertar píxeles fraccionarios, por lo que es invisible para el ojo, especialmente porque los contornos de los círculos están superpuestos por un píxel.

Aquí hay otro ejemplo con rayas:


Esta es la parte superior del borde cuadrado. Aquí está el mismo borde en un mapa más rectangular:


Aqu√≠ puede ver que en el lado de la tarjeta hay un espacio visualmente m√°s grande entre las bandas. El algoritmo no debe insertar m√°s espacio que la longitud de un elemento completo; por lo tanto, el peor de los casos ocurre cuando tenemos elementos largos y un lado corto que es ligeramente diferente de un tama√Īo adecuado. Pero en la mayor√≠a de los casos pr√°cticos, la alineaci√≥n no es muy perjudicial.

Aquí hay un ejemplo con un patrón de varios elementos:


Aquí las rayas se superponen a las rayas:


Puede ver que, dado que se realiza la misma alineación para cada elemento, las franjas permanecen centradas entre sí.

Sugerí que una buena solución para colocar el patrón en el costado del mapa sería difícil, pero un enfoque muy simple con la distribución uniforme del elemento del patrón para llenar el espacio deseado funciona bastante bien para muchos patrones. Este es un recordatorio para todos nosotros: no hay necesidad de asumir que la decisión debe ser complicada; ¡puede ser más fácil de lo que piensas!

Sin embargo, esta solución no funciona para patrones con elementos conmovedores, por ejemplo, para escala de mapa. En este caso, agregar espacio desplaza los elementos:


Otra opción para alargar un patrón, que mencioné anteriormente, es estirar los elementos individuales del patrón. Es adecuado para algo así como un patrón de escala, pero se verá mal en un patrón con elementos simétricos, porque el estiramiento los hará asimétricos.

La implementaci√≥n de la opci√≥n de estiramiento result√≥ ser m√°s complicada de lo que esperaba, principalmente porque tuve que estirar los elementos en diferentes bordes del mapa por diferentes tama√Īos (porque el mapa puede no ser cuadrado sino rectangular), y tambi√©n cambiar din√°micamente la ubicaci√≥n de los elementos en funci√≥n de los nuevos estirados Tama√Īos. Pero despu√©s de unas horas logr√© lograr esto:


Ahora tengo todas las características necesarias para dibujar el borde del mapa (aunque los elementos de borde se crean manualmente):


Convertí la imagen a escala de grises, porque no quería molestarme con la selección de colores, y la tarjeta en sí es bastante aburrida, pero como prueba de concepto, los bordes se ven bastante bonitos.

Parte 5


En la parte 2, desarrollé la gramática del lenguaje de descripción de borde de mapa (MBDL), y en las partes 3 y 4, implementé procedimientos para ejecutar todas las primitivas del lenguaje. Ahora trabajaré en conectar estas partes para poder describir el borde en MBDL y dibujarlo en el mapa.

En la Parte 3, escribí la gramática MBDL para que funcione con la herramienta de análisis Javascript Nearley . La gramática terminada se ve así:

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

De manera predeterminada, cuando analiza una regla con éxito usando Nearley, la regla devuelve una matriz que contiene todos los elementos que corresponden al lado derecho de la regla. Por ejemplo, si la regla

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

combinado con una cuerda

A

entonces Nearley regresar√°

[ " A" ]

Una matriz con un solo valor es la cadena "A" correspondiente al lado derecho de la regla.

¬ŅQu√© devuelve Nearley cuando un elemento se analiza usando esta regla?

number -> WS decimal WS

Hay tres partes en el lado derecho de la regla, por lo que devolverá una matriz con tres valores. El primer valor será el que devuelve la regla para WS, el segundo valor será el que devolverá la regla para decimal y el tercer valor será el que devolverá la regla para WS. Si, usando la regla anterior, analizaré "57", entonces el resultado será el siguiente:

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

El resultado final del an√°lisis de Nearley ser√° una matriz anidada de matrices, que es un √°rbol de sintaxis . En algunos casos, el √°rbol de sintaxis es una representaci√≥n muy √ļtil; en otros casos, no del todo. En Dragons Abound , por ejemplo, dicho √°rbol no es particularmente √ļtil.

Afortunadamente, las reglas de Nearley pueden anular el comportamiento est√°ndar y devolver lo que quieran. De hecho, la regla (integrada) para decimal no devuelve una lista de n√ļmeros, devuelve el n√ļmero Javascript equivalente, que en la mayor√≠a de los casos es mucho m√°s √ļtil, es decir, el valor de retorno de la regla num√©rica tiene la forma:

[
[ " " ],
57,
[ ]
]

Las reglas de Nearley redefinen el comportamiento est√°ndar agregando un postprocesador a la regla, tomando una matriz est√°ndar y reemplaz√°ndola por lo que necesita. Un postprocesador es solo c√≥digo Javascript dentro de par√©ntesis especiales al final de una regla. Por ejemplo, en la regla del n√ļmero , nunca me interesan los espacios posibles a cada lado del n√ļmero. Por lo tanto, ser√≠a conveniente que la regla simplemente devolviera un n√ļmero y no una matriz de tres elementos. Aqu√≠ hay un postprocesador que realiza esta tarea:

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

Este postprocesador toma el resultado est√°ndar (la matriz de tres elementos que se muestra arriba) y lo reemplaza con el segundo elemento de la matriz, que es el n√ļmero Javascript de la regla decimal . Entonces ahora la regla num√©rica devuelve el n√ļmero real.

Esta funcionalidad se puede utilizar para procesar un idioma entrante en un idioma intermedio, con el que es más fácil trabajar. Por ejemplo, puedo usar la gramática Nearley para convertir una cadena MBDL en una matriz de estructuras Javascript, cada una de las cuales representa una primitiva identificada por un campo "op". La regla para la línea primitiva se verá así:

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

Es decir, el resultado del an√°lisis "L (13, negro)" ser√° la estructura de Javascript:

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

Después de agregar el procesamiento posterior apropiado, el resultado devuelto por la gramática puede ser una secuencia (matriz) de estructuras de operación para la línea entrante. Es decir, el resultado de analizar la cadena

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

ser√°

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

que es mucho m√°s f√°cil de procesar para crear un borde de mapa.

En este punto, puede tener una pregunta: si la fase de posprocesamiento de la regla Nearley puede contener alg√ļn Javascript, ¬Ņpor qu√© no omitir la vista intermedia y simplemente dibujar el borde del mapa durante el postprocesamiento? Para muchas tareas, este enfoque ser√≠a ideal. Decid√≠ no usarlo por varias razones.

En primer lugar, en MBDL hay un par (*) de componentes que no se pueden ejecutar durante el proceso de an√°lisis. Por ejemplo, no podemos dibujar elementos geom√©tricos repetitivos (tira o rombo) durante el proceso de an√°lisis, porque necesitamos conocer informaci√≥n de otros elementos en el mismo patr√≥n. En particular, necesitamos saber la longitud total del patr√≥n para comprender hasta qu√© punto debemos organizar las repeticiones de cada elemento individual. Es decir, el elemento del patr√≥n a√ļn debe crear una representaci√≥n intermedia de todos los elementos geom√©tricos.

(* Hay otros componentes con limitaciones similares de los que a√ļn no he hablado).

En segundo lugar, Javascript en Nearley est√° integrado en las reglas, por lo que no podremos pasar informaci√≥n adicional a Javascript, a excepci√≥n de las variables globales. Por ejemplo, para dibujar el borde, necesito saber el tama√Īo del mapa, las cuatro √°reas de truncamiento utilizadas, etc. Aunque puedo agregar c√≥digo que haga que esta informaci√≥n est√© disponible para los posprocesadores de Nearley, ser√° un poco complicado y puede ser dif√≠cil mantener dicho c√≥digo.

Por estas razones, estoy analizando una representación intermedia, que luego se ejecuta para crear el borde del mapa en sí.

El siguiente paso es desarrollar un intérprete que reciba una representación intermedia de MBDL y la ejecute para generar límites de mapa. Esto no es muy difícil de hacer. Básicamente, el trabajo es establecer las condiciones iniciales (por ejemplo, generar regiones de truncamiento para los cuatro lados del mapa) e iterar sobre la secuencia de estructuras de la representación intermedia con cada ejecución.

Hay un par de momentos resbaladizos.

Primero, necesito pasar de renderizar desde adentro a dibujar desde adentro hacia afuera. La razón es porque quiero que la mayoría de los bordes no se superpongan con el mapa, así que necesito dibujar los bordes para que las líneas del borde interior coincidan con los bordes del mapa. Si dibujo desde afuera hacia adentro, entonces necesito saber el ancho del borde antes de comenzar a dibujar para que el borde no se superponga con el mapa. Si dibujo de adentro hacia afuera, empiezo desde el borde del mapa y dibujo. También le permite imponer opcionalmente un borde en el mapa; simplemente comience el borde con un espacio vertical negativo (VS).

Otro punto difícil son los patrones repetitivos. Para dibujar patrones repetitivos, necesito mirar todos los elementos del patrón y determinar el más ancho, porque establecerá el ancho de todo el patrón. También necesito mirar y seguir la longitud del patrón para saber cuánta distancia dejar antes de cada repetición.

Aquí hay un ejemplo de un borde bastante complejo que usé para probar el intérprete:


Creo que fue posible (¬Ņnecesario?) Adjuntarlo para probar al analizador, pero para este borde acabo de crear una vista intermedia manualmente:

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

Creé esta vista a través de prueba y error. Sea como fuere, ¡el intérprete funciona!

Como √ļltimo paso, perm√≠tanme usar el analizador para crear una vista intermedia de la versi√≥n MBDL. No hay mucho que mostrarme aqu√≠: tuve que arreglar algunos nombres de campo, pero de lo contrario el c√≥digo funcion√≥ bien. Para el borde, utilic√© una versi√≥n ligeramente diferente de 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")

Ella dibuja el mismo borde, pero de una manera ligeramente diferente. También cambié la sintaxis de la superposición, reemplazando los paréntesis con llaves para que sea más diferente de la otra sintaxis.

Para mostrar por qué quería dibujar de adentro hacia afuera, y no solo colocar automáticamente el borde en el exterior del mapa, puedo agregar un espacio vertical negativo al comienzo de este borde para mover la escala del mapa dentro del borde del mapa:


Ahora tengo la mayor parte de la infraestructura necesaria para la generación procesal de los bordes del mapa: un lenguaje de descripción de límites, un analizador de idiomas y procedimientos para realizar una representación intermedia. Solo queda lidiar con la parte difícil: ¡la generación procesal!

Parte 6


Ahora que se ha implementado todo el MBDL, ten√≠a la intenci√≥n de proceder a la generaci√≥n procesal de los bordes del mapa, pero a√ļn no estoy seguro de c√≥mo quiero hacer esto, porque me demorar√© un poco e implementar√© un par de caracter√≠sticas m√°s de MBDL.

En la primera discusión sobre el procesamiento de esquinas con patrones, hablé sobre un par de enfoques diferentes. Al final, me di cuenta de las esquinas biseladas, pero había una segunda opción: detener el patrón cerca de la esquina, como en estos ejemplos:



Tal soluci√≥n se usa a menudo cuando el patr√≥n de borde es alg√ļn tipo de figura asim√©trica, runas u otra cosa que no se puede girar 90 grados, mientras se mantiene la alineaci√≥n. Pero es obvio que esto funcionar√° con formas geom√©tricas.

Esta puede ser la opción que elija antes de generar el borde, pero puede agregar un poco de flexibilidad si lo habilita desde una parte del borde y usa la esquina biselada en la otra. Para hacer esto, tengo que agregar un nuevo comando a MBDL. Sospecho que pueden surgir otras opciones para diferentes partes del borde, por lo que agregaré un comando de opciones generales:

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

(Aqu√≠ nuevamente, para mayor claridad, omitimos espacios y algunos otros detalles). Hasta ahora, las √ļnicas opciones son "MITRE" para esquinas biseladas y "DETENIDO" para detenerse cerca de esquinas. Si no se transmite ning√ļn valor DETENIDO, el programa detiene el patr√≥n a una distancia razonable de la esquina. Si se transmite el valor, el patr√≥n se detiene a esa distancia de la esquina.

Si se usan esquinas DETENIDAS, entonces dejo de dibujar el patrón de esquina lejos de las esquinas. Así es como se ve:


Aquí, utilicé la opción MITRE para el patrón de escala en blanco y negro, por lo que se refleja con respecto al ángulo. Para un patrón de círculos rojos y cuadrados negros dentro de una línea dorada (y para un patrón de círculos fuera del borde), utilicé DETENIDO. Puedes ver que estos dos patrones terminan cerca de la esquina.

Sin embargo, hay un par de problemas. En primer lugar, vemos que a la izquierda el elemento más cercano a la esquina es un cuadrado negro, y en la parte superior hay un círculo rojo. Sucedió porque la esquina está cerca del comienzo de la repetición en un lado y cerca del final de la repetición en el otro. Pero se ve raro. Sería mejor si las esquinas fueran simétricas, incluso si para esto tuviéramos que agregar otro elemento al final del patrón. En segundo lugar, puede ver que el patrón fuera del borde (semicírculos y puntos negros) también termina en una repetición en la esquina. Pero como la longitud de esta repetición es mucho menor que la longitud de los círculos rojos / cuadrados negros, terminan en diferentes lugares. Probablemente sería mejor si todos los patrones se detuvieran a la misma distancia de la esquina.

Para solucionar el primer problema, debe agregar otra repetición del primer elemento del patrón al final de cada lado del borde. Pero en realidad es un poco más complicado, porque podría usar un desplazamiento horizontal negativo dentro del patrón para superponer varios elementos (como se hace aquí). También debe agregar otra repetición a cualquier elemento del patrón que tenga el mismo punto de inicio que el primer elemento.


Ahora el patrón es simétrico con respecto al ángulo y se ve mucho mejor.

A continuación, necesito rastrear el patrón STOPPED más largo y detener cada patrón STOPPED a esta distancia:


Ahora el patr√≥n de c√≠rculos blancos se deja de lado m√°s, pero a√ļn no est√° alineado con el patr√≥n de c√≠rculos rojos. Por qu√©Esto sucedi√≥ porque el patr√≥n de c√≠rculo blanco est√° m√°s lejos del borde del mapa y el borde es m√°s largo que donde se dibuja el patr√≥n de c√≠rculo rojo. Para solucionar este problema, tambi√©n debe mover los patrones y considerar su desplazamiento en relaci√≥n con el borde del mapa.


Ahora todo est√° bellamente alineado.

La segunda opción para los ángulos son los desplazamientos cuadrados en las esquinas, por ejemplo, estos:


¡Será mucho más difícil implementar esto!

Sin embargo, la gramática de esta opción es simple y utiliza el código de operación Opción:

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

El n√ļmero indica el tama√Īo del desplazamiento cuadrado para el elemento en el borde del mapa; Los elementos con diferentes desplazamientos deben alinearse en consecuencia. Si no hay un n√ļmero, el programa selecciona el tama√Īo de desplazamiento apropiado. Poner a cero el n√ļmero desactiva el desplazamiento cuadrado. Esto le permite crear bordes en los que algunos elementos usan desplazamientos cuadrados, mientras que otros no, como en este borde:


Lo primero que me di cuenta fue que necesitaría áreas de truncamiento adicionales porque utilizo el truncamiento para procesar lugares donde la frontera cambia de dirección. SQOFFSET requerirá áreas de truncamiento más complejas; También necesitará áreas separadas para diferentes elementos cuando active y desactive SQOFFSET. Dado que las áreas de truncamiento agregan artefactos no deseados de todos modos, esto parece demasiado trabajo.

Cuando trabajé en patrones escalables anteriores, implementé el llenado de un patrón asimétrico para agregar otra repetición desde un extremo del patrón. También me di cuenta de que esto eliminaría la necesidad de esquinas biseladas. Simplemente dibujaré todos los patrones a lo largo del borde en el sentido de las agujas del reloj, comenzando el patrón en una esquina y terminando cerca de la siguiente esquina. Esto me permitirá deshacerme de las áreas de truncamiento.

Lo más importante en esta nueva forma de trabajar con esquinas fue que el primer elemento del patrón ya no está "dividido" en dos lados. Si observa los patrones de escala en blanco y negro en los mapas de arriba, puede ver que hay un rectángulo blanco que pasa por la esquina. Ahora el rectángulo blanco colindará con la esquina:


Los mapas se dibujan en ambos sentidos, pero este no es un gran problema.

Para empezar, implementé compensaciones para líneas. Para hacer esto, fue suficiente girar la línea en relación con los ángulos correspondientes:


Como puedes entender, puedo combinar √°ngulos con desplazamientos y √°ngulos regulares, como en el mapa de arriba:


Por supuesto, dar vuelta los patrones a la vuelta de la esquina es más difícil. La idea general es dibujar de una esquina a casi la otra, y así sucesivamente a lo largo del borde, hasta volver al principio. Teóricamente es suficiente dibujar solo patrones horizontales y verticales, y todo debe estar bellamente alineado; En realidad, rastrear todo esto es bastante triste. De hecho, tuve que reescribir completamente el código dos veces y escribir un montón de papel, pero no hablaré sobre esto en detalle. Solo muestra el resultado:


Una ilusión óptica molesta surge en las esquinas: el elemento de la esquina parece no centrado más cerca del exterior de la esquina. De hecho, esto no es cierto, pero parece que sí, porque más cerca del interior de la esquina hay más espacio visualmente vacío.

Como los segmentos de los ángulos de desplazamiento son bastante cortos, es muy fácil crear un patrón de no equilibrio en la esquina:


A veces se ve bastante feo. Me recordó a un viejo chiste:

Paciente: "Doctor, cuando hago esto, me duele".
Doctor: "¬°Entonces no hagas eso!"

Por lo tanto, intentaré no hacerlo.

Por lo general, no dibujaré la escala del mapa a lo largo del ángulo de desplazamiento, pero si lo necesito, necesitaré usar la opción que estira el patrón para que la escala del mapa encaje en la esquina sin espacios entre los rectángulos:


Como resultado, puede ver que el tama√Īo de los rect√°ngulos de escala var√≠a notablemente. Es decir, esta no es una muy buena opci√≥n. (Por cierto, los √°ngulos de desplazamiento tambi√©n tienen un error en el patr√≥n de c√≠rculos. M√°s tarde lo arregl√©, pero como dije, es muy dif√≠cil hacerlo.)

Si el patrón es demasiado grande para caber en el segmento del ángulo de desplazamiento, entonces el algoritmo simplemente se rinde:


Lo cual está lejos de ser ideal, pero, como dije anteriormente, "Entonces no lo hagas". (En realidad, no es muy difícil agregar una función de compresión o estiramiento si la necesito).

¬ŅQu√© sucede si uso ambas esquinas de desplazamiento y la opci√≥n que detiene los patrones en frente de las esquinas? En este caso, me detengo no lejos de las esquinas desplazadas:


Me parece que esta es una decisión lógica.

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


All Articles