Scott Turner continua trabalhando em seu jogo processualmente gerado e agora decidiu resolver o problema de projetar as bordas dos mapas. Para fazer isso, ele precisa resolver vários problemas difíceis e até criar sua própria linguagem para descrever limites.As fronteiras continuavam sendo um elemento importante dos cartões de fantasia, que estavam na minha lista há algum tempo. Os mapas funcionais geralmente têm uma
linha de fronteira simples, mas os mapas de fantasia e os mapas medievais, dos quais os primeiros costumam emprestar idéias, têm limites artísticos e bem pensados. Esses limites deixam claro que o mapa é intencionalmente tornado fantástico e dão ao espectador uma sensação de admiração.
Atualmente, existem algumas maneiras simples de desenhar bordas no meu jogo
Dragons Abound . Ela pode desenhar uma linha simples ou dupla ao redor do perímetro do mapa e adicionar elementos simples nos cantos, como nas figuras:
O jogo também pode adicionar um campo na parte inferior da borda para o nome do mapa. Existem várias variações desse campo em
Dragons Abound , incluindo elementos complexos como cabeças de parafusos falsos:
Há variabilidade nesses campos de nome, mas todos são criados manualmente.
Um aspecto interessante dos limites dos cartões de fantasia é que eles são criativos e modelo. Geralmente, eles consistem em um pequeno número de elementos simples que se combinam de maneiras diferentes para criar um resultado único. Como sempre, o primeiro passo para trabalhar com um novo tópico é estudar uma coleção de exemplos de mapas, criar um catálogo de tipos de elementos de borda e estudar sua aparência.
A borda mais simples é uma linha que percorre as bordas do mapa e indica seus limites. Como eu disse acima, também é chamada de "linha de quadro":
Há também uma variação com a localização das bordas no mapa. Nesta versão, o mapa atinge as bordas da imagem, mas a borda cria uma borda virtual dentro da imagem:
Isso pode ser feito com qualquer tipo de borda, mas geralmente é usado apenas com bordas simples, como a borda de um quadro.
Um conceito de design de cartão de fantasia popular é simular como se fossem desenhados em pergaminho antigo e rasgado. Às vezes, isso é realizado desenhando a borda como a borda áspera do papel:
Aqui está um exemplo mais sofisticado:
Na minha experiência, esse método se tornou menos popular porque as ferramentas digitais foram usadas. Se você deseja que o cartão pareça um pergaminho antigo e rasgado, é mais fácil aplicar a textura do pergaminho a ele do que desenhá-lo à mão.
A ferramenta mais poderosa na criação de bordas do mapa é a repetibilidade. No caso mais simples, basta repetir uma única linha para criar duas linhas:
Você pode adicionar interesse ao mapa variando o estilo do elemento repetido, nesse caso combinando uma linha única grossa com uma única linha fina:
Dependendo do elemento, são possíveis várias variações de estilo. Neste exemplo, a linha se repete, mas a cor muda:
Para criar padrões mais complexos, você pode usar a "repetibilidade repetível". Essa borda consiste em aproximadamente cinco linhas únicas com diferentes larguras e distâncias:
Essa borda repete as linhas, mas as separa para que pareçam duas bordas finas separadas. Nesta parte do post, não falarei sobre o processamento de ângulos, mas ângulos diferentes para as duas linhas também ajudam a criar essa diferença.
Essas duas linhas são quatro ou seis? Eu acho que tudo depende de como você os desenha!
Outro elemento de estilização é preencher o espaço entre os elementos com cor, padrão ou textura. Neste exemplo, a borda ficou mais interessante devido ao preenchimento da cor de destaque entre as duas linhas:
Aqui está um exemplo de como a borda é preenchida com um padrão:
Além disso, os elementos podem ser estilizados para parecerem tridimensionais. Aqui está um mapa no qual a borda é sombreada para parecer volumosa:
Nesse mapa, a borda é sombreada para ter uma aparência tridimensional e isso é combinado com a localização das bordas dentro das bordas do mapa:
Outro elemento de borda comum é a escala na forma de listras coloridas:
Essas faixas formam uma grade (
grade cartográfica ). Em mapas reais, a escala ajuda a determinar distâncias, mas em mapas de fantasia é principalmente um elemento decorativo.
Essas listras são geralmente desenhadas em preto e branco, mas às vezes são adicionadas vermelhas ou outras cores:
Este elemento também pode ser combinado com outros, como neste exemplo, com linhas e escala:
Este exemplo é um pouco incomum. Normalmente, a escala (se houver) é o elemento mais interno da borda.
Neste mapa, existem escalas diferentes com resoluções diferentes (além de notas rúnicas estranhas!):
(No Reddit, o usuário
AbouBenAdhem me informou que as marcas rúnicas são os números 48 e 47 escritos em cuneiforme babilônico. Além disso, “escalas com resoluções diferentes” têm seis divisões divididas em dez divisões menores, o que corresponde ao sistema numérico hexadecimal da Babilônia. Indico as fontes dos mapas, mas há muitas peças pequenas neste post, então não me incomodei. No entanto,
este mapa foi criado por
Thomas Ray para o autor S.E. Boleyn, então, talvez, a ação em seus livros ocorra na comitiva da Babilônia.)
Além de linhas e escala, o elemento mais comum é um padrão geométrico repetitivo. Muitas vezes, consiste em partes como círculos, losangos e retângulos:
Elementos geométricos, como linhas, podem ser sombreados para torná-los tridimensionais:
Limites complexos podem ser criados combinando esses elementos de maneiras diferentes. Aqui está a borda que combina linhas, padrões geométricos e escala:
Os exemplos mostrados acima foram cartões digitais, mas, é claro, o mesmo pode ser feito com cartões manuscritos. Aqui está um exemplo de um padrão geométrico simples criado à mão:
Esses elementos também podem ser combinados de forma flexível de várias maneiras. Aqui está um padrão geométrico combinado com uma “aresta irregular”:
Nos exemplos mostrados acima, o padrão geométrico é bastante simples. Mas você pode criar padrões muito complexos combinando de maneira diferente os elementos geométricos básicos:
Outro elemento popular do padrão é a tecelagem ou o nó celta:
Aqui está uma borda de vime mais complexa contendo cor, escala e outros elementos:
Neste mapa, a tecelagem é combinada com um elemento de borda irregular:
Além dos padrões geométricos e da tecelagem, qualquer padrão repetitivo pode fazer parte da borda do cartão. Aqui está um exemplo usando formas semelhantes a pontas de seta:
E aqui está um exemplo com um padrão de onda repetido:
E, finalmente, runas ou outros elementos do alfabeto de fantasia são adicionados às bordas das cartas de fantasia:
Os exemplos acima são retirados de mapas de fantasia modernos, mas aqui está um exemplo de um mapa histórico (século 18) com linhas e um padrão desenhado à mão:
Obviamente, você pode encontrar exemplos de mapas com muitos outros elementos nas bordas. Algumas das mais belas são totalmente desenhadas à mão e têm decorações tão cuidadosamente elaboradas que podem superar o próprio cartão (
World of Alma , Francesca Baerald):
Também vale a pena conversar um pouco sobre
simetria . Como a repetibilidade, a simetria é uma ferramenta poderosa e as bordas do mapa geralmente são simétricas ou possuem elementos simétricos.
Muitas bordas do mapa são simétricas de dentro para fora, como neste exemplo:
Aqui, a borda é composta de várias linhas com e sem preenchimento, mas de fora para dentro repete idealmente em relação ao centro da borda.
Neste exemplo mais complexo, a borda é simétrica, com exceção das faixas de escala em preto e branco alternadas:
Como duplicar a escala não faz sentido, geralmente é considerado um elemento separado, mesmo que o restante da borda seja simétrico.
Além da simetria interna-externa, as bordas são frequentemente re-simétricas ao longo de seu comprimento. Algumas bordas ilustradas podem ter um design simples que abrange todo o comprimento da borda do mapa, mas na maioria dos casos o padrão é bastante curto e se repete, preenchendo a borda de um canto para outro:
Observe que neste exemplo o padrão contém um elemento que não é simétrico (da esquerda para a direita), mas o padrão geral é simétrico e se repete:
Uma exceção notável a essa regra são as bordas preenchidas com runas ou caracteres alfabéticos. Muitas vezes, eles se tornam únicos, como se alguma mensagem longa fosse escrita ao longo da fronteira:
Obviamente, existem muitos outros exemplos de elementos de borda do mapa que eu não considerei aqui, mas já temos um bom ponto de referência. Nas próximas partes, desenvolverei várias funções em
Dragons Abound para descrever, exibir e gerar bordas de mapa proceduralmente semelhantes a esses exemplos. Na segunda parte, começaremos definindo o idioma para descrever as bordas dos mapas.
Parte 2
Nesta parte, vou criar a versão inicial do MBDL (Map Border Description Language).
Por que gastar tempo criando uma linguagem de descrição de limite de mapa? Primeiro, esse será o objetivo da minha geração processual. Mais tarde, escreverei um algoritmo para criar novas bordas do mapa, e a saída desse algoritmo será uma descrição da nova borda no MBDL. Em segundo lugar, o MBDL servirá como uma representação textual dos limites do mapa. Em particular, preciso ser capaz de salvar e reutilizar meus limites. Para fazer isso, preciso de uma notação de texto que possa ser escrita e usada para recriar a borda do mapa.
Vou começar a criar o MBDL definindo o elemento mais simples: a linha. A linha tem cor e largura. Portanto, no MBDL, apresentarei a linha desta forma:
L(width, color)
Aqui estão alguns exemplos (desculpe pelas minhas habilidades no Photoshop):
A sequência de elementos é renderizada de fora para dentro (*), portanto, assumimos que esta é a borda no topo do mapa:
Veja o segundo exemplo - uma linha com bordas é representada como três elementos de linha separados.
(* Desenhar de fora para dentro era uma escolha arbitrária - pareceu-me que era mais natural do que renderizar de dentro para fora. Infelizmente, como aconteceu muito mais tarde, havia uma boa razão para trabalhar na direção oposta. - antiga, porque levaria muito tempo para refazer todas as ilustrações)Convenientemente, os espaços podem ser representados como linhas sem cor:
Mas seria mais visual ter um elemento espacial vertical específico:
VS (largura)
Os seguintes elementos simples são formas geométricas: listras, losangos e elipses. Supõe-se que as linhas sejam esticadas por todo o comprimento da borda, para que não tenham um comprimento especificado explicitamente. Porém, figuras geométricas não podem preencher a linha inteira; portanto, além da largura (*), cada uma deve ter comprimento, cor de contorno, largura de contorno e cor de preenchimento:
B(width, length, outline, outline width, fill)
D(width, length, outline, outline width, fill)
E(width, length, outline, outline width, fill)
(* Aceitei que considerarei a largura na direção de fora para dentro e o comprimento é medido ao longo da borda.)
Aqui estão exemplos de formas geométricas simples:
Para esses elementos preencherem todo o comprimento da borda, eles devem ser repetidos. Para indicar o grupo de elementos que serão repetidos para preencher o comprimento da borda, use colchetes:
[ element element element ... ]
Aqui está um exemplo de um padrão repetido de retângulos e losangos:
Às vezes, precisarei de um espaço (horizontal) entre os elementos de um padrão repetitivo. Embora você possa usar um elemento sem cores para criar um espaço, será mais inteligente e conveniente ter um elemento de espaço horizontal:
HS(length)
A última função necessária para essa primeira iteração do MBDL é a capacidade de empilhar elementos uns sobre os outros. Aqui está um exemplo de borda:
A maneira mais fácil de descrevê-lo é uma ampla linha amarela sob o padrão superior. Você pode implementar isso de maneiras diferentes (por exemplo, um espaço vertical negativo), mas eu decidi usar chaves para indicar a ordem dos elementos para dentro:
{element element element ...}
De fato, esta entrada diz para você lembrar onde o padrão estava de fora para dentro ao inserir os colchetes e, em seguida, retornar a esse ponto ao deixar os colchetes. Os colchetes também podem ser considerados como uma descrição dos elementos que ocupam um espaço vertical. Portanto, a borda mostrada acima pode ser descrita da seguinte maneira:
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)
Desenhamos uma linha preta, fixamos onde estamos, desenhamos uma linha amarela e, em seguida, retornamos à posição anteriormente fixa, caímos um pouco, desenhamos um padrão de retângulos e losangos, caímos um pouco e depois desenhamos outra linha preta.
Há muito mais a ser feito no MBDL, mas isso é suficiente para descrever os muitos limites dos mapas. A próxima etapa é converter a descrição do limite no MBDL na própria borda. Isso é semelhante à conversão de uma representação escrita de um programa de computador (como Javascript) na execução deste programa. O primeiro estágio é a
análise lexical (análise) da linguagem - a transformação do texto de origem em uma borda real do mapa ou em algum tipo de forma intermediária, que é mais fácil de converter em uma borda.
A análise é uma área bastante estudada da ciência da computação. A análise de um idioma não é muito simples, mas, no nosso caso, é bom (*) que o MBDL seja uma gramática
livre de contexto . As gramáticas sem contexto são analisadas com bastante facilidade e existem muitas
ferramentas de análise de Javascript para elas. Eu
decidi pelo
Nearley.js , que parece bastante maduro e (mais importante) uma ferramenta bem documentada.
(* Isso não é apenas sorte, eu me certifiquei de que a linguagem fosse livre de contexto.)Não apresentarei gramáticas sem contexto, mas a sintaxe de Nearley é bastante simples e você deve entender o significado sem problemas. A gramática Nearley consiste em um conjunto de regras. Cada regra possui um caractere à esquerda, uma seta e a parte direita da regra, que pode ser uma sequência de caracteres e não caracteres, além de várias opções separadas pelo "|" (ou):
border -> element | element border
element -> “ L"
Cada uma das regras diz que o lado esquerdo pode ser substituído por qualquer uma das opções no lado direito. Ou seja, a primeira regra diz que uma borda é um elemento, ou um elemento, seguido por outra borda. Que por si só pode ser um elemento ou um elemento seguido por uma borda e assim por diante. A segunda regra diz que um elemento pode ser apenas uma string "L". Ou seja, juntas essas regras correspondem a esses limites:
L
LLL
e não correspondem a esses limites:
X
L3L
A propósito, se você deseja experimentar esta (ou qualquer outra) gramática em Nearley, existe aqui uma caixa de proteção on-line. Você pode inserir gramática e casos de teste para ver o que corresponde e o que não corresponde.
Aqui está uma definição mais completa de uma primitiva de linha:
@builtin “number.ne"
@builtin “string.ne"
border -> element | element border
element -> “L(" decimal “," dqstring “)"
Nearley possui vários elementos internos comuns e o número é um deles. Portanto, eu posso usá-lo para reconhecer a largura numérica de uma linha primitiva. Para reconhecimento de cores, eu uso outro elemento interno e permito o uso de qualquer string entre aspas duplas.
Seria bom adicionar espaços entre caracteres diferentes, então vamos fazê-lo. Nearley suporta classes de caracteres e
RBNF para "zero ou mais" algo com ": *", para que eu possa usar isso para especificar "zero ou mais espaços" e colá-lo em qualquer lugar para permitir espaços nas descrições:
@builtin "number.ne"
@builtin "string.ne"
border -> element | element border
WS -> [\s]:*
number -> WS decimal WS
color -> WS dqstring WS
element -> "L(" number "," color ")"
No entanto, o uso de WS por todo o lado dificulta a leitura da gramática, então eu os abandonarei, mas imagine que eles são.
Um elemento também pode ser um espaço vertical:
@builtin "number.ne"
@builtin "string.ne"
border -> element | element " " border
number -> decimal
color -> dqstring
element -> "L(" number "," color ")"
element -> "VS(" number ")"
Isso corresponde a esses limites
L(3.5,"black") VS(3.5)
Em seguida, vêm os primitivos de tira, losango e 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 ")"
Combina esses elementos
B(34, 17, "white", 3, "black")
(Observe que as primitivas geométricas não são "elementos" porque não podem estar sozinhas no nível superior. Elas devem estar entre um padrão.)
Também preciso de uma primitiva de espaço horizontal:
@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 ")"
Agora vou adicionar uma operação padrão (repetir). Esta é uma sequência de um ou mais elementos entre colchetes. Vou usar o operador RBNF ": +", que aqui significa "um ou mais".
@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):+ "]"
Observe que o padrão só pode ser preenchido com primitivas geométricas. Não podemos, por exemplo, colocar uma linha dentro de um padrão. O elemento padrão agora corresponderá a algo assim.
[B(34,17,"white",3,"black")E(13,21,"white",3,"rgb(27,0,0)")]
A última parte do idioma é o operador de sobreposição. Este é qualquer número de elementos dentro de chaves.
@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 ):+ "}"
o que nos permite fazer o seguinte:
{L(3.5,"rgb(98,76,15)")VS(3.5)}
(Observe que, diferentemente do operador de repetição, o operador de sobreposição pode ser usado internamente.)
Depois de limpar a descrição e adicionar espaços aos locais necessários, obtemos a seguinte 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 ")"
Portanto, o MBDL agora está definido e criamos uma gramática da linguagem. Pode ser usado com o Nearley para reconhecer cadeias de idiomas. Antes de me aprofundar no MBDL / Nearley, gostaria de implementar as primitivas usadas no MBDL para que o limite descrito no MBDL possa ser exibido. Isso faremos na próxima parte.
Parte 3
Agora começaremos a implementar as próprias primitivas de renderização. (Neste momento, ainda não preciso vincular o analisador às primitivas de renderização. Para testar, vou chamá-las apenas manualmente.)
Vamos começar com a linha primitiva. Lembre-se da aparência:
L(width, color)
Além da largura e cor, há um parâmetro implícito aqui - a distância da borda externa do mapa. (Eu desenho as bordas da borda do mapa para fora. Observe que começamos de uma diferente!). Não deve apontar para o MBDL, porque isso pode ser rastreado pelo intérprete que executa o MBDL para desenhar a borda. No entanto, isso deve ser uma entrada para todas as primitivas de renderização, para que elas saibam onde desenhá-las. Vou chamar esse parâmetro de deslocamento.
Se eu apenas precisasse desenhar uma borda na parte superior do mapa, a primitiva de linha seria muito simples de implementar. No entanto, de fato, precisarei desenhar de cima. inferior, esquerda e direita. (Talvez um dia eu perceba bordas oblíquas ou curvas, mas por enquanto vamos aderir às bordas retangulares padrão.) Além disso, o comprimento e a localização do elemento de linha dependem do tamanho do mapa (assim como do deslocamento). Portanto, como parâmetros, preciso de todos esses dados.
Depois de definir todos esses parâmetros, basta criar uma primitiva de linha e usá-la para desenhar uma linha ao redor do mapa:
(Observe que eu uso várias funções de
Dragons Abound para desenhar a linha "manuscrita".) Vamos tentar criar uma borda mais complexa:
L(3, black) L(10, gold) L(3, black)
É assim:
Muito bom. Observe que há lugares em que as linhas pretas e a linha dourada não estão bem alinhadas devido a flutuações. Se eu quiser me livrar desses pontos, você pode simplesmente reduzir a quantidade de oscilação.
A implementação de uma primitiva de espaço vertical é bastante simples; apenas realiza um incremento de deslocamento. Vamos adicionar um pouco de espaço:
L(3, black) L(10, gold) L(3, black)
VS(5)
L(3, black) L(10, red) L(3, black)
Ao desenhar linhas, os ângulos podem ser realizados traçando entre o deslocamento e o desenho ao longo do mapa no sentido horário. Mas, em geral, preciso implementar o truncamento em cada lado da borda do mapa para criar uma
conexão angular com um chanfro . Isso será necessário para criar bordas com padrões unidos corretamente nos cantos e, no caso geral, eliminará a necessidade de desenhar elementos com bordas em um ângulo que seria necessário. (*)
(Observação: como será dito nas partes a seguir, com o tempo, me recusei a usar regiões de truncamento ao implementar ângulos. O principal motivo é que, para criar ângulos complexos, por exemplo, deslocamentos quadrados:são necessárias áreas de truncamento cada vez mais complexas. Além disso, com o tempo, descobri uma maneira melhor de trabalhar com padrões nos cantos. Em vez de retornar e reescrever esta parte do artigo, decidi deixá-lo para ilustrar o processo de "criatividade".)A idéia principal é truncar cada borda na diagonal e criar quatro áreas truncadas nas quais cada lado da borda será desenhado:
Ao truncar, tudo o que for desenhado na área correspondente será cortado no ângulo desejado.
Infelizmente, isso cria pequenas lacunas ao longo das linhas diagonais, provavelmente porque o navegador executa suavemente a suavização ao longo da borda truncada. O teste mostrou que um fundo brilha através do espaço entre as duas arestas. Foi possível corrigir isso expandindo um pouco as máscaras (metade do pixel parece ser suficiente), mas isso às vezes não resolve o problema.
Em seguida, você precisa implementar formas geométricas. Diferentemente das linhas, elas são repetidas no padrão, preenchendo a lateral da borda do mapa:
Uma pessoa desenharia esse padrão da esquerda para a direita, desenhando um retângulo, um losango e repetindo o mesmo até que toda a borda fosse preenchida. Portanto, isso também pode ser implementado no programa, desenhando um padrão ao longo da borda. No entanto, será mais fácil desenhar primeiro todos os retângulos e depois todos os losangos. Basta desenhar ao longo da borda a mesma figura geométrica em intervalos. E é muito conveniente que cada elemento tenha o mesmo intervalo. Obviamente, uma pessoa não faria isso, porque é muito difícil organizar os elementos nos lugares certos, mas isso não é um problema para o programa.
Ou seja, o procedimento para desenhar formas geométricas simples precisa de parâmetros nos quais todas as dimensões e cores da figura são transmitidas (ou seja, largura, comprimento, espessura da linha, cor e preenchimento da linha), bem como a posição inicial (que por razões que ficarão claras em breve, Vou considerar o centro da figura), o intervalo de espaço horizontal para a transição entre repetições e o número de repetições. Também será conveniente indicar a direção da repetição na forma do vetor [dx, dy], para que possamos executar repetições da esquerda para a direita, da direita para a esquerda, para cima ou para baixo, simplesmente alterando o vetor e o ponto de partida. Junte tudo e obtenha uma faixa de formas repetidas:
Usando esse código várias vezes e renderizando com o mesmo deslocamento, posso combinar as listras em preto e branco para criar a escala do mapa:
Antes de começar a descobrir como aplicar tudo isso à borda real do mapa, vamos primeiro implementar a mesma funcionalidade para elipses e losangos.
Losangos são apenas retângulos com vértices rotacionados, portanto, você só precisa fazer uma pequena alteração no código. Acontece que eu ainda não tenho código pronto para renderizar a elipse, mas é muito fácil obter a
visão paramétrica da elipse e criar uma função que me dê os pontos da elipse:
Aqui está um exemplo (criado manualmente) que usa os recursos implementados acima:
Para uma quantidade tão pequena de código, parece muito bom!
Vamos agora resolver o caso complexo de bordas com elementos repetidos: cantos.
Se houver uma borda com elementos repetidos, existem várias maneiras de resolver o problema com cantos. O primeiro é ajustar as repetições para que sejam executadas nos cantos sem casamento perceptível:
Outra opção é parar a repetição em algum lugar próximo à esquina dos dois lados. Isso geralmente é feito se o padrão não puder ser facilmente “rotacionado” no canto:
A última opção é fechar o padrão com alguma decoração de canto:
Algum dia chegarei às decorações dos cantos, mas por enquanto usaremos a primeira opção. Como fazer com que um padrão de listras ou círculos gire nos cantos do mapa sem espaços?
A idéia principal é colocar o elemento do padrão exatamente no canto, para que metade dele esteja em uma borda do mapa e a outra no lado adjacente. Neste exemplo, o círculo está exatamente no canto e pode ser desenhado de qualquer direção:
Em outros casos, o elemento é meio desenhado em uma direção e metade na outra, mas as arestas coincidem:
Nesse caso, uma faixa branca é desenhada nos dois lados, mas é conectada no canto sem espaços.
Há dois aspectos a serem considerados ao colocar um elemento em um canto.
Primeiro, o elemento de canto será dividido e espelhado em relação à diagonal que passa pelo centro do elemento. Elementos com simetria radial, por exemplo, quadrados, círculos e estrelas, não mudam de forma. Elementos sem simetria radial, por exemplo, retângulos e losangos, mudarão de forma ao espelhar em relação à diagonal.
Em segundo lugar, para que os elementos de canto dos dois lados se conectem corretamente, deve haver um número inteiro de elementos (*) nos dois lados do mapa. Eles não precisam ter o mesmo número, mas deve haver um número inteiro de elementos nos dois lados. Se um número fracionário de padrões estiver contido em um lado, a partir de uma borda, o padrão não coincidirá com o lado adjacente.
(* Em alguns casos, por exemplo, com listras longas, a repetição parcial pode ocorrer com repetição total e os elementos ainda estarão alinhados. No entanto, o elemento de canto resultante será assimétrico e diferirá em comprimento do mesmo elemento nas laterais do mapa. Um exemplo disso pode ser visto aqui:Uma barra branca de escala ocorre com diferentes repetições parciais e, como resultado, é obtido um elemento deslocado em relação ao centro. Para a escala do mapa, esse nem sempre é o caso, porque mostra a distância absoluta e não precisa ser simétrico. Mas para um padrão decorativo, isso geralmente parece ruim.)Aqui está um exemplo mostrando como um número inteiro de repetições é aparado exatamente no canto:
Se você fizer o mesmo dos quatro lados, os cantos coincidirão e o padrão ficará perfeitamente localizado ao longo de toda a extensão da borda:
Após um exame cuidadoso, você notará que o padrão não ocorre exatamente nos cantos. Metade do círculo em cada canto é tirada de cada lado, e essas duas metades são desenhadas à mão independentemente, portanto, não são perfeitas. Mas agora eles estão perto o suficiente disso.
Assim, podemos realizar uma conexão perfeita do padrão nos cantos, escolhendo um número inteiro de repetições para cada aresta. No entanto, a solução para esse problema não é trivial.
Primeiro, suponha que sabemos que o lado tem 866 pixels e queremos repetir o elemento 43 vezes. Em seguida, o elemento deve ser repetido a cada 20,14 pixels. Como definimos o comprimento específico de um elemento (e, no caso geral, um padrão de elementos)? No exemplo acima, adicionei espaço extra entre os círculos. Mas se os círculos se tocaram inicialmente, isso mudará o padrão. Talvez valha a pena esticar os círculos para que eles continuem se tocando?
Agora os elementos estão se tocando, mas os círculos se transformaram em elipses e os cantos têm uma forma estranha. (Lembre-se de que eu disse que elementos sem simetria radial mudam de forma quando refletidos em relação a um ângulo? Para listras, isso não será um grande problema.) Ou talvez valha a pena apertar todos os elementos para que eles se toquem e se ajustem em um comprimento adequado:
Mas, para perceber isso, precisamos tornar os elementos muito menores do que eram originalmente. Nenhuma dessas opções parece perfeita.
O segundo problema ocorre quando os lados do cartão são de tamanhos diferentes. Agora precisamos resolver o problema de encontrar um número inteiro de repetições adequado para ambos os lados. Seria ideal encontrar uma solução adequada para ambos os lados. Mas não quero fazer isso à custa de muitas mudanças de padrão. Talvez seja melhor criar padrões ligeiramente diferentes em ambos os lados se ambos estiverem próximos o suficiente do padrão original.
E finalmente, o terceiro problema surge quando eu uso a função de sobrepor vários elementos um ao outro:Não quero fazer alterações no padrão que destruirão o relacionamento entre os elementos. Penso que, com o dimensionamento adequado, as proporções como um todo permanecerão, mas preciso testar isso.Tarefa interessante, certo? Até agora, não tenho soluções particularmente de alta qualidade para ela. Talvez eles apareçam mais tarde!Parte 4
Portanto, implementamos primitivas para desenhar linhas e formas geométricas. Comecei a trabalhar no uso de formas repetidas para preencher as bordas e falei sobre as dificuldades de colocar padrões arbitrários na borda do mapa para que eles se encaixassem perfeitamente nos cantos. O principal problema é que, no caso geral, é necessário tornar o padrão mais longo (ou mais curto) para que ele se ajuste lateralmente. As opções para alterar o comprimento do padrão - adicionar ou remover espaços, alterar o comprimento dos elementos dos padrões - levam a várias alterações no próprio padrão. Parece que a tarefa de selecionar um padrão de vários elementos é muito difícil!Quando me deparei com essas tarefas aparentemente intransigentes, gosto de começar implementando uma versão simples. Muitas vezes, as tarefas malsucedidas podem ser resolvidas resolvendo repetidamente problemas "simples" até que o resultado seja bom o suficiente. E, às vezes, a implementação de uma versão simples fornece um entendimento que simplifica a solução de um problema mais complexo. Se não melhorar e o problema permanecer desconfortável, pelo menos teremos uma versão simplificada que ainda pode ser útil, embora não exatamente como deveria.A maneira mais fácil é alterar o comprimento do padrão adicionando comprimentos sem alterar nada no padrão. Essencialmente, isso adiciona espaço em branco ao final do padrão. (Nota: é melhor distribuir o espaço vazio entre todos os elementos no padrão.) Vale a pena considerar que essa solução pode apenas prolongar o padrão. Sempre podemos adicionar espaço vazio ao padrão, mas não podemos aceitá-lo se necessário - talvez não haja mais espaço vazio no padrão!Com essa abordagem, o algoritmo de localização do padrão na lateral da placa será muito simples:- Divida o comprimento do lado do cartão pelo comprimento do padrão e arredonde-o para baixo para determinar o número de repetições do padrão que se encaixam nesse lado.
- A distância entre os elementos neste caso será igual ao comprimento do lado dividido pelo número de repetições. (Este é o local mais próximo do local original, pois só podemos adicionar espaço.)
- Desenhe um padrão ao longo do lado, levando em consideração a distância calculada.
Foi difícil implementar esse sistema. Os cantos teimosamente não quiseram coincidir. Levei muito tempo para perceber que, quando o mapa não é quadrado, não consigo desenhar áreas de truncamento para quatro lados do centro do mapa, porque isso cria ângulos de truncamento que não são iguais a 45 graus. De fato, as áreas de truncamento devem se parecer com a parte traseira de um envelope:Quando eu descobri isso, o algoritmo começou a funcionar sem problemas.(Mas não se esqueça da observação anterior de que, com o tempo, abandonei as áreas de truncamento!)Aqui está um exemplo com uma proporção de aproximadamente 2: 1:Nessa escala, é muito difícil perceber, mas os cantos se conectam corretamente e há apenas uma pequena diferença visual entre os lados. Nesse caso, o algoritmo para alinhar os padrões só precisa inserir pixels fracionários, de modo que fica invisível aos olhos, principalmente porque os contornos dos círculos são sobrepostos por um pixel.Aqui está outro exemplo com listras:Este é o topo da borda quadrada. Aqui está a mesma borda em um mapa mais retangular:Aqui você pode ver que no lado do cartão há uma diferença visualmente maior entre as bandas. O algoritmo não deve inserir mais espaço do que o comprimento de um elemento completo; portanto, o pior caso ocorre quando temos elementos longos e um lado curto ligeiramente diferente de um tamanho adequado. Mas na maioria dos casos práticos, o alinhamento não é muito prejudicial.Aqui está um exemplo com um padrão de vários elementos:Aqui as listras se sobrepõem às listras:Você pode ver que, como o mesmo alinhamento é realizado para cada elemento, as faixas permanecem centradas uma em relação à outra.Sugeri que uma boa solução para colocar o padrão ao lado do mapa seria difícil, mas uma abordagem muito simples com a distribuição uniforme do elemento do padrão para preencher o espaço desejado funciona muito bem para muitos padrões. Isso é um lembrete para todos nós: não há necessidade de supor que a decisão deve ser complicada; pode ser mais fácil do que você pensa!No entanto, esta solução não funciona para padrões com elementos tocantes, por exemplo, para escala de mapa. Nesse caso, adicionar espaço muda os elementos:Outra opção para alongar um padrão, que mencionei acima, é esticar os elementos individuais do padrão. É adequado para algo como um padrão de escala, mas ficará ruim em um padrão com elementos simétricos, porque o alongamento os tornará assimétricos.A implementação da opção com alongamento acabou sendo mais difícil do que eu esperava, principalmente porque tive que esticar os elementos em diferentes arestas do mapa por tamanhos diferentes (porque o mapa pode não ser quadrado, mas retangular) e também alterar dinamicamente a disposição dos elementos com base nos novos esticados. tamanhos. Mas depois de algumas horas, consegui isso:Agora eu tenho todos os recursos necessários para desenhar a borda do mapa (embora os próprios elementos da borda sejam criados manualmente):Converti a imagem em escala de cinza, porque não queria me preocupar com a seleção de cores, e o cartão em si é bastante entediante, mas como prova de conceito, as bordas parecem muito bonitas.Parte 5
Na parte 2, desenvolvi a gramática MBDL (Map Border Description Language) e, nas partes 3 e 4, implementei procedimentos para executar todas as primitivas da linguagem. Agora, trabalharei na conexão dessas partes para poder descrever a borda no MBDL e desenhá-la no mapa.Na parte 3, escrevi a gramática MBDL para que funcione com a ferramenta de análise de JavaScript Nearley . A gramática final é assim:@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 " )"
Por padrão, ao analisar com êxito uma regra usando Nearley, a regra retorna uma matriz contendo todos os elementos que correspondem ao lado direito da regra. Por exemplo, se a regratest -> " A" | " B" | " C"
combinado com stringA
Nearley retornará[ " A" ]
Uma matriz com um único valor é a sequência "A" correspondente ao lado direito da regra.O que Nearley retorna quando um elemento é analisado usando esta regra?number -> WS decimal WS
Como existem três partes no lado direito da regra, ele retornará uma matriz com três valores. O primeiro valor será o que retornará a regra para o WS, o segundo valor será o que retornará a regra para decimal e o terceiro valor será o que retornará a regra para o WS. Se, usando a regra acima, pars "57", o resultado será o seguinte:[
[ " " ],
[ "5", "7" ],
[ ]
]
O resultado final da análise de Nearley será uma matriz aninhada de matrizes, que é uma árvore de sintaxe . Em alguns casos, a árvore de sintaxe é uma representação muito útil; em outros casos, não é bem assim. Em Dragons Abound , por exemplo, essa árvore não é particularmente útil.Felizmente, as regras de Nearley podem substituir o comportamento padrão e retornar o que quiserem. De fato, a regra (interna) para decimal não retorna uma lista de números, retorna o número Javascript equivalente, que na maioria dos casos é muito mais útil, ou seja, o valor de retorno da regra numérica tem a forma:[
[ " " ],
57,
[ ]
]
As regras de Nearley redefinem o comportamento padrão adicionando um pós-processador à regra, pegando uma matriz padrão e substituindo-a pelo que você precisa. Um pós-processador é apenas código Javascript entre colchetes especiais no final de uma regra. Por exemplo, na regra do número , nunca estou interessado em espaços possíveis em ambos os lados do número. Portanto, seria conveniente se a regra simplesmente retornasse um número, e não uma matriz de três elementos. Aqui está um pós-processador que executa esta tarefa:number -> WS decimal WS {% default => default[1] %}
Esse pós-processador pega o resultado padrão (a matriz de três elementos mostrada acima) e a substitui pelo segundo elemento da matriz, que é o número Javascript da regra decimal . Então agora a regra numérica retorna o número real.Essa funcionalidade pode ser usada para processar um idioma recebido em um idioma intermediário, mais fácil de trabalhar. Por exemplo, eu posso usar a gramática Nearley para transformar uma string MBDL em uma matriz de estruturas Javascript, cada uma das quais representa uma primitiva identificada por um campo "op". A regra para a linha primitiva será mais ou menos assim:element -> " L(" number " ," color " )" {% data=> {op: " L", width: data[1], color: data[3]} %}
Ou seja, o resultado da análise de "L (13, preto)" será a estrutura Javascript:{op: " L", width: 13, color: " black"}
Após adicionar o pós-processamento apropriado, o resultado retornado da gramática pode ser uma sequência (matriz) de estruturas de operação para a linha de entrada. Ou seja, o resultado da análise da stringL( 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"}]}
]
o que é muito mais fácil de processar para criar uma borda do mapa.Nesse ponto, você pode ter uma pergunta - se o estágio de pós-processamento da regra Nearley pode conter qualquer Javascript, por que não pular a visualização intermediária e simplesmente desenhar a borda do mapa durante o pós-processamento? Para muitas tarefas, essa abordagem seria ideal. Decidi não usá-lo por vários motivos.Primeiramente, no MBDL há alguns (*) componentes que não podem ser executados durante o processo de análise. Por exemplo, não podemos desenhar elementos geométricos repetidos (faixa ou losango) durante o processo de análise, porque precisamos conhecer informações de outros elementos no mesmo padrão. Em particular, precisamos saber o comprimento total do padrão para entender até que ponto precisamos organizar as repetições de cada elemento individual. Ou seja, o elemento do padrão ainda deve criar uma representação intermediária de todos os elementos geométricos.(* Existem outros componentes com limitações semelhantes das quais ainda não falei.)Em segundo lugar, o Javascript em Nearley está incorporado nas regras, portanto, não poderemos passar informações adicionais ao Javascript, exceto para variáveis globais. Por exemplo, para desenhar a borda, preciso saber o tamanho do mapa, as quatro áreas de truncamento usadas etc. Embora eu possa adicionar um código que disponibilize essas informações para os pós-processadores de Nearley, ele ficará um pouco confuso e poderá ser difícil manter esse código.Por esses motivos, estou analisando uma representação intermediária, que é executada para criar a borda do próprio mapa.A próxima etapa é desenvolver um intérprete que receba uma representação intermediária do MBDL e o execute para gerar limites do mapa. Isso não é muito difícil de fazer. Basicamente, a tarefa é definir as condições iniciais (por exemplo, gerar regiões de truncamento para os quatro lados do mapa) e iterar sobre a sequência de estruturas da representação intermediária com cada uma executando.Há alguns momentos escorregadios.Primeiro, preciso passar da renderização de dentro para o desenho de dentro para fora. O motivo é que eu quero que a maioria das bordas não se sobreponha ao mapa, então preciso desenhar as bordas para que as linhas da borda interna coincidam com as bordas do mapa. Se eu desenhar de fora para dentro, preciso saber a largura da borda antes de começar a desenhar para que a borda não se sobreponha ao mapa. Se eu desenhar de dentro para fora, eu apenas começo da borda do mapa e desenho. Também permite que você imponha opcionalmente uma borda no mapa; basta iniciar a borda com um espaço vertical negativo (VS).Outro ponto difícil são os padrões repetidos. Para desenhar padrões repetidos, preciso examinar todos os elementos do padrão e determinar o mais amplo, porque ele definirá a largura de todo o padrão. Também preciso observar e acompanhar o comprimento do padrão para saber quanto tempo falta antes de cada repetição.Aqui está um exemplo de uma borda bastante complexa que eu usei para testar o intérprete:Eu acho que era possível (necessário?) Anexá-lo para teste no analisador, mas para essa borda eu apenas criei uma exibição intermediária 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},
]},
]
Eu criei essa visão por tentativa e erro. Seja como for, o intérprete funciona!Como último passo, deixe-me usar o analisador para criar uma exibição intermediária a partir da versão do MBDL. Não há muito para me mostrar aqui: tive que corrigir alguns nomes de campos, mas, caso contrário, o código funcionou bem. Para a borda, usei uma versão ligeiramente diferente do 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")
Ela desenha a mesma borda, mas de uma maneira um pouco diferente. Também alterei a sintaxe da sobreposição, substituindo os parênteses por chaves, para que fique mais diferente da outra sintaxe.Para mostrar por que eu queria desenhar de dentro para fora e não apenas colocar automaticamente a borda do lado de fora do mapa, posso adicionar um espaço vertical negativo ao início dessa borda para mover a escala do mapa para dentro da borda do mapa:Agora, tenho a maior parte da infraestrutura necessária para a geração procedural de bordas do mapa: uma linguagem de descrição de limites, um analisador de idiomas e procedimentos para executar uma representação intermediária. Resta apenas lidar com a parte difícil - a geração processual!Parte 6
Agora que todo o MBDL foi implementado, pretendi prosseguir para a geração processual das bordas do mapa, mas ainda não tenho certeza de como quero fazer isso, porque vou demorar um pouco e implementar mais alguns recursos do MBDL.Na primeira discussão sobre o processamento de cantos com padrões, falei sobre algumas abordagens diferentes. No final, percebi os cantos chanfrados, mas havia uma segunda opção: interromper o padrão próximo ao canto, como nos exemplos a seguir:Essa solução é frequentemente usada quando o padrão da borda é algum tipo de figura assimétrica, runas ou qualquer outra coisa que não possa ser girada 90 graus, mantendo o alinhamento. Mas é óbvio que isso funcionará com formas geométricas.Essa pode ser a opção que você escolhe antes de gerar a borda, mas você pode adicionar um pouco de flexibilidade se a ativar em uma parte da borda e usar o canto chanfrado na outra. Para fazer isso, tenho que adicionar um novo comando ao MBDL. Eu suspeito que outras opções possam surgir para diferentes partes da borda, então adicionarei um comando de opções gerais:element -> "O(MITER)"
element -> "O(STOPPED)"
element -> "O(STOPPED," number ")"
(Aqui, novamente, para maior clareza, omitimos espaços e outros detalhes.) Até agora, as únicas opções são "MITRE" para cantos chanfrados e "PARADO" para parar nos cantos próximos. Se nenhum valor for transmitido PARADO, o programa interrompe o padrão a uma distância razoável da esquina. Se o valor for transmitido, o padrão será interrompido a essa distância do canto.Se cantos STOPPED forem usados, paro de desenhar o padrão dos cantos. Aqui está o que parece:
Aqui, usei a opção MITRE para o padrão de escala em preto e branco, para que ele espelhe em relação ao ângulo. Para um padrão de círculos vermelhos e quadrados pretos dentro de uma linha dourada (e para um padrão de círculos fora da borda), usei STOPPED. Você pode ver que esses dois padrões terminam perto da esquina.No entanto, existem alguns problemas. Primeiramente, vemos que, à esquerda, o elemento mais próximo ao canto é um quadrado preto e, no topo, um círculo vermelho. Isso aconteceu porque o canto está próximo ao início da repetição, de um lado, e ao final da repetição, de outro. Mas parece estranho. Seria melhor se os cantos fossem simétricos, mesmo que para isso tivéssemos que adicionar outro elemento ao final do padrão. Em segundo lugar, você pode ver que o padrão fora da borda (semicírculos e pontos pretos) também termina em uma repetição no canto. Mas como a duração dessa repetição é muito menor que a duração dos círculos vermelhos / quadrados pretos, eles terminam em lugares diferentes. Provavelmente seria melhor se todos os padrões parassem à mesma distância da esquina.Para corrigir o primeiro problema, você precisa adicionar outra repetição do primeiro elemento do padrão no final de cada lado da borda. Mas, na verdade, é um pouco mais complicado, porque eu poderia usar um deslocamento horizontal negativo dentro do padrão para sobrepor vários elementos (como feito aqui). Você também precisa adicionar outra repetição a qualquer elemento do padrão que tenha o mesmo ponto inicial do primeiro elemento.Agora, o padrão é simétrico em relação ao ângulo e parece muito melhor.Em seguida, preciso rastrear o padrão STOPPED mais longo e parar cada padrão STOPPED a essa distância:Agora, o padrão dos círculos brancos é mais reservado, mas ainda não está alinhado com o padrão dos círculos vermelhos. Porque
Isso aconteceu porque o padrão de círculo branco está mais distante da borda do mapa e o comprimento da borda é maior do que o local onde o padrão de círculo vermelho é desenhado. Para corrigir esse problema, você precisa mover os padrões também, considerando o deslocamento em relação à borda do mapa.Agora tudo está maravilhosamente alinhado.A segunda opção para ângulos são os deslocamentos quadrados nos cantos, por exemplo:Será muito mais difícil implementar isso!No entanto, a gramática desta opção é simples e usa o opcode da opção:element -> "O(SQOFFSET)"
element -> "O(SQOFFSET," number ")"
O número indica o tamanho do deslocamento quadrado do elemento na borda do mapa; Elementos com deslocamentos diferentes devem ser alinhados de acordo. Se não houver número, o programa seleciona o tamanho de deslocamento apropriado. Zerar o número desativa o deslocamento quadrado. Isso permite criar bordas nas quais alguns elementos usam deslocamentos quadrados, enquanto outros não, como nesta borda:A primeira coisa que percebi foi que precisaria de áreas de truncamento adicionais porque uso o truncamento para processar locais onde a borda muda de direção. SQOFFSET exigirá áreas de truncamento mais complexas; Você também precisará de áreas separadas para itens diferentes ao ativar e desativar o SQOFFSET. Dado que as áreas de truncamento adicionam artefatos indesejados de qualquer maneira, isso parece muito trabalho.Quando trabalhei nos padrões passíveis de parada acima, implementei o preenchimento de um padrão assimétrico para adicionar outra repetição a partir de uma extremidade do padrão. Também percebi que isso eliminaria a necessidade de cantos chanfrados. Simplesmente desenharei todos os padrões ao longo da borda no sentido horário, iniciando o padrão em um canto e terminando próximo ao próximo. Isso permitirá que eu me livre das áreas de truncamento.O mais importante nessa nova maneira de trabalhar com cantos é que o primeiro elemento do padrão não é mais "dividido" em dois lados. Se você observar os padrões de escala em preto e branco nos mapas acima, poderá ver que há um retângulo branco passando pelo canto. Agora o retângulo branco fica ao lado do canto:Os mapas são desenhados nos dois sentidos, mas esse não é um problema muito grande.Para iniciantes, implementei compensações para linhas. Para isso, basta girar a linha em relação aos ângulos correspondentes:Como você pode entender, eu posso combinar ângulos com deslocamentos e ângulos regulares, como no mapa acima:Obviamente, virar os padrões ao virar da esquina é mais difícil. A idéia geral é desenhar de um canto para quase o outro, e assim por diante, ao longo da fronteira, até voltarmos ao início. Teoricamente, basta desenhar apenas padrões horizontais e verticais, e tudo deve estar perfeitamente alinhado; rastrear tudo isso é bastante triste. Na verdade, tive que reescrever completamente o código duas vezes e escrever um monte de papel, mas não falarei sobre isso em detalhes. Apenas mostre o resultado:Uma ilusão de ótica irritante surge nos cantos - o elemento do canto parece não centrado perto do lado de fora do canto. De fato, isso não é verdade, mas parece que sim, porque mais perto do lado de dentro da esquina há um espaço visualmente mais vazio.Como os segmentos dos ângulos de deslocamento são bastante curtos, é muito fácil criar um padrão de não equilíbrio no canto:Às vezes parece muito feio. Isso me lembrou uma velha piada:Paciente: "Doutor, quando faço isso, me dói."
Médico: "Então não faça isso!"
Portanto, tentarei não fazer isso.Normalmente, não desenharei a escala do mapa ao longo do ângulo de deslocamento, mas, se necessário, precisarei usar a opção que estica o padrão para que a escala do mapa caiba no canto sem espaços entre os retângulos:Você pode ver que, como resultado, o tamanho dos retângulos da escala varia acentuadamente. Ou seja, essa não é uma opção muito boa. (A propósito, os ângulos de deslocamento também apresentam um erro no padrão dos círculos. Mais tarde, eu o corrigi, mas como disse, é muito difícil fazê-lo.)Se o padrão é muito grande para caber no segmento do ângulo de deslocamento, o algoritmo simplesmente desiste:O que está longe de ser o ideal, mas, como eu disse acima, "então não faça". (Na verdade, não é muito difícil adicionar uma função de compressão ou alongamento, se eu precisar.)O que acontece se eu usar os cantos deslocados e a opção que interrompe os padrões na frente dos cantos? Nesse caso, apenas paro não muito longe dos cantos do deslocamento:Parece-me que esta é uma decisão lógica.