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