[Перевод] Создаём границы процедурно генерируемой карты

image


Скотт Тёрнер продолжает работу над своей процедурно генерируемой игрой и теперь решил заняться проблемой оформления границ карт. Для этого ему предстоит решить несколько непростых задач и даже создать собственный язык описания границ.

Важным элементом фэнтезийных карт, который уже довольно долго находился в моём списке, оставались границы. У функциональных карт обычно есть простая линия рамки, но у фэнтезийных карт и средневековых карт, из которых первые часто заимствуют идеи, имеют довольно продуманные и художественные границы. Эти границы дают понять, что карта намеренно сделана фантастической, и вселяют в зрителя ощущение чуда.

В настоящее время в моей игре Dragons Abound есть пара простых способов отрисовки границ. Она может отрисовывать одинарную или двойную линию по периметру карты и добавлять простые элементы в углах, как на этих рисунках:

aed9f8477ef53b5550b742ea1fde36b2.png


cac37ebac81815955dfc940bacdc2eff.png


Также игра может добавлять поле в нижней части границы для названия карты. В Dragons Abound есть несколько вариаций этого поля, в том числе такие сложные элементы, как фальшивые головки винтов:

42ff6fc3adae5b40293bb82d5a1be325.png


В этих полях названий присутствует вариативность, но все они созданы вручную.

Один из интересных аспектов границ фэнтезийных карт заключается в том, что они одновременно и креативны, и шаблонны. Часто они состоят из небольшого количества простых элементов, сочетающихся разными способами для создания уникального результата. Как всегда, первым шагом при работе с новой темой для меня является изучение коллекции примеров карт, создание каталога типов элементов границ и изучение их внешнего вида.
Простейшая граница — это одна линия, идущая вдоль краёв карты и обозначающая её пределы. Как я сказал выше, она также называется «линией рамки»:

99fc81c98e1092077272081464d28752.png


Существует также вариация с расположением границ в пределах карты. В этой версии карта доходит до краёв изображения, но граница создаёт виртуальную рамку внутри изображения:

43faef1db7bf3af195c857d7ed673710.png


Это можно сделать с любым типом границы, но обычно используется только с простыми границами наподобие линии рамки.

Популярная концепция оформления фэнтезийных карт заключается в том, чтобы имитировать, как будто они нарисованы на старом надорванном пергаменте. Иногда это реализуется отрисовкой границы как неровного края бумаги:

907cb2ff125bbd178f9a16717e5e724f.png


Вот более изощрённый пример:

751860d6e43ed2e857cf6f3ba7f766e0.png


По моему опыту, такой способ стал менее популярным, потому что в обиход вошли цифровые инструменты. Если вы хотите, чтобы карта выглядела как старый надорванный пергамент, то проще наложить на неё текстуру пергамента, чем рисовать её вручную.

Самым мощным инструментом в создании границ карты является повторяемость. В простейшем случае достаточно повторить одиночную линию, чтобы создать две линии:

31d139e129fe513e07937c5a2bd88979.png


Карте можно добавить интересности, варьируя стиль повторяемого элемента, в данном случае — скомбинировав толстую одиночную линию с тонкой одиночной линией:

530ccd11872f4aa6a32d4b3271cf94f3.png


В зависимости от элемента возможны различные вариации стиля. В этом примере линия повторяется, но меняется цвет:

fc789e96ccfba58ac4cb595e5ec33fb7.png


Для создания более сложных узоров можно использовать «повторяемую повторяемость». Эта граница состоит из примерно пяти одиночных линий с разной шириной и расстоянием:

796488ef402e59ed7e092e0e14ff2469.png


Эта граница повторяет линии, но отделяет их таким образом, что они выглядят как две отдельные тонкие границы. В этой части поста я не буду говорить про обработку углов, но разные углы для двух линий тоже помогают в создании этого отличия.

97bfd833697b9278660772c1bd04ee46.png


Это две линии, четыре, или шесть? Думаю, всё зависит от того, как их нарисуешь!

Ещё один элемент стилизации — заполнение пространства между элементами цветом, узором или текстуро. В этом примере граница стала более интересной благодаря заливкой между двумя линиями акцентным цветом:

1d8ccf158b9ae0378ddaef1031ba15c9.png


А вот пример того, как граница заполнена узором:

f8737dbb215166947e42f0da1b45cfc7.png


Также элементы можно стилизовать так, чтобы они выглядели трёхмерными. Вот карта, в которой граница затенена, чтобы она выглядела объёмной:

a8025b69549773f8935d2b37680401a3.png


В этой карте граница затенена, чтобы выглядеть трёхмерной, и это скомбинировано с расположением границ внутри краёв карты:

36c8999e74172c962aef40a4a9e3addc.png


Ещё один распространённый элемент границы — масштаб в виде разноцветных полос:

4eef620b830eb0249d019229140e34e6.png


Эти полосы образуют сетку (картографическую сетку). На реальных картах масштаб помогает определять расстояния, однако на фэнтезийных он в основном является декоративным элементом.

Эти полосы обычно отрисовываются чёрным и белым цветами, но иногда добавляется красный или какой-то другой цвет:

450f0ff67ac1fdf5c5e2ffaf8fbe80c0.png


Этот элемент также можно сочетать с другими, как в этом примере с линиями и масштабом:

313d5629d6ec8b8d7c7c4c1ead322a09.png


Этот пример немного необычен. Обычно масштаб (если он есть) является самым внутренним элементом границы.

На этой карте есть разные масштабы с разным разрешением (а также странные рунические пометки!):

74bcd62173160e440a4ee6cfad2128c1.png


(На Reddit пользователь AbouBenAdhem сообщил мне, что рунические пометки — это числа 48 и 47, написанные вавилонской клинописью. Кроме того, «масштабы с разным разрешением» имеют шесть делений, разделённых на десять более мелких делений, что соответствует вавилонской шестидесятиричной системе исчисления. Обычно я указываю источники карт, но в этом посте слишком много маленьких кусков, поэтому я не стал утруждаться. Однако эта карта создана Томасом Реем для автора С.Е. Болейн, так что, возможно, действие в его книгах происходит в антураже Вавилона.)

Кроме линий и масштаба наиболее распространённым элементом является повторяющийся геометрический узор. Часто он состоит из таких частей, как круги, ромбы и прямоугольники:

31c6baf612bf2fa11a13905e4482d82e.png


Геометрические элементы, как и линии, можно затенить, чтобы они выглядели трёхмерными:

43ecc8c33bbf3143ba32c18b878d16e2.png


Сложные границы можно создавать комбинированием этих элементов разными способами. Вот граница, в которой сочетаются линии, геометрические узоры и масштаб:

7b770ec95296ce07e0d18ede205c8798.png


Показанные выше примеры были цифровыми картами, но, разумеется, то же самое можно проделать и с рукописными картами. Вот пример простого геометрического узора, созданного вручную:

6f7c2de3d5db82a9c7e9884d726be98e.png


Эти элементы тоже можно гибко комбинировать разными способами. Вот геометрический узор в сочетании с «оборванным краем»:

0faf1a15052e90a479428c4c495ad44c.png


В показанных выше примерах геометрический узор довольно прост. Но можно создавать и очень сложные узоры, комбинируя различным способом основные геометрические элементы:

38e13ffc2a2aac266aabb291a7d7f0e6.png


Ещё один популярный элемент узора — плетение или кельтский узел:

07e00e8d868b6ca48be746fbe07e61ca.png


Вот более сложная плетёная граница, содержащая цвет, масштаб и другие элементы:

5e158014012fffcbc51e2e3fe71772d6.png


На этой карте плетение сочетается с элементом оборванного края:

e954df58825bc42d408b61804a1287a9.png


Кроме геометрических узоров и плетений частью границы карты может быть любой повторяющийся узор. Вот пример с использованием фигур, напоминающих наконечники стрел:

7be5ce64eb09de2eeefdb18992978d03.png


А вот пример с повторяющимся волновым узором:

841c133c2f1c0f724b5d076e5ea415fb.png


И, наконец, на края фэнтезийных карт иногда добавляют руны или другие элементы фэнтезийного алфавита:

15f6dd3a3e6191da02dc884193b72c82.png


Показанные выше примеры взяты из современных фэнтезийных карт, но вот пример исторической (18 век) карты с линиями и нарисованным от руки узором:

1d7b9f3b8f0c7eb296079c5f6f6b23aa.png


Разумеется, можно найти примеры карт со множеством других элементов на границах. Некоторые из самых красивых полностью нарисованы от руки и имеют настолько тщательно выполненные украшения, что могут превзойти саму карту (World of Alma, Francesca Baerald):

1c7004e186e0d2be63b98539375316ff.png


Также стоит немного поговорить о симметрии. Как и повторяемость, симметрия является мощным инструментом, и границы карты обычно симметричны или имеют симметричные элементы.

Многие границы карт симметричны изнутри наружу, как в этом примере:

796488ef402e59ed7e092e0e14ff2469.png


Здесь граница составлена из нескольких линий с заливкой и без заливки, но снаружи внутрь она идеально повторяется относительно центра границы.

В этом более сложном примере граница симметрична, за исключением перемежающихся чёрно-белых полос масштаба:

5e158014012fffcbc51e2e3fe71772d6.png


Так как дублировать масштаб не имеет смысла, часто он считается отдельным элементом, даже если остальная часть границы симметрична.

Кроме внутренне-наружной симметрии, границы часто повторно симметричны вдоль своей длины. Некоторые иллюстрированные границы могут иметь простой дизайн, растянувшийся на всю длину края карты, но в большинстве случаев паттерн довольно короток и повторяется, заполняя границу от одного угла до другого:

7b770ec95296ce07e0d18ede205c8798.png


Заметьте, что в этом примере паттерн содержит элемент, который не является симметричным (слева направо), но общий паттерн симметричен и повторяется:

7be5ce64eb09de2eeefdb18992978d03.png


Одним из примечательных исключений из этого правила являются границы, заполненные рунами или алфавитными символами. Часто они оказываются уникальными, как будто вдоль границы написано какое-то длинное сообщение:

15f6dd3a3e6191da02dc884193b72c82.png


Разумеется, существует множество других примеров элементов границ карт, которые я здесь не рассмотрел, но у нас уже есть хорошая опорная точка. В следующих нескольких частях я разработаю в Dragons Abound несколько функций для описания, отображения и процедурной генерации границ карт, похожих на эти примеры. Во второй части мы начнём с задания языка для описания границ карт.

Часть 2


В этой части я создам первоначальную версию языка описания границ карт Map Border Description Language (MBDL).

Зачем тратить время на создание языка описания границ карт? Во-первых, это будет целью моей процедурной генерации. Позже я напишу алгоритм для создания новых границ карт, и выходными данными этого алгоритма станет описание новой границы на MBDL. Во-вторых, MBDL будет служить текстовым представлением границ карт. В частности, мне нужно иметь возможность сохранять и повторно использовать понравившиеся границы. Для этого мне потребуется текстовая нотация, которую можно записывать и использовать для воссоздания границы карты.

Создание MBDL я начну с задания простейшего элемента: линии. Линия имеет цвет и ширину. Поэтому в MBDL я представлю линию в таком виде:

L(width, color)


Вот несколько примеров (простите за мои навыки Photoshop):

db2db868ede7282a34c695f8062a691b.png


Последовательность элементов отрендерена снаружи внутрь (*), поэтому будем считать, что это граница сверху карты:

b7c75eb3aefb8f77d1915166c0bdd979.png


Посмотрите на второй пример — линия с границами представлена как три отдельные элемента-линии.

(* Отрисовка снаружи внутрь была произвольным выбором — мне просто показалось, что это естественнее, чем отрисовка изнутри наружу. К сожалению, как выяснилось намного позже, была веская причина работать в обратном направлении. Вскоре я об этом расскажу, но в посте всё оставлено по-старому, потому что на переделку всех иллюстраций ушло бы много времени)

Удобно, что пробелы можно представить как линии без цвета:

0ba1a41719610641d7b5a5e26da0692e.png


Но было бы нагляднее иметь конкретный элемент вертикального пробела:

VS (width)


Следующие простые элементы — это геометрические фигуры: полосы, ромбы и эллипсы. Предполагается, что линии растягиваются на всю длину границы, поэтому у них нет явно заданной длины. Но геометрические фигуры не могут заполнять целую линию, поэтому кроме ширины (*) у каждой должна быть длина, цвет контура, ширина контура и цвет заливки:

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


(* Я принял, что буду считать ширину в направлении снаружи внутрь, а длина измеряется вдоль границы.)

Вот примеры простых геометрических фигур:

a3dfadcf6a5a98a9785b0b487a6e17fc.png


Чтобы эти элементы заполняли всю длину границы, они должны повторяться. Чтобы обозначить группу элементов, которые будут повторяться для заполнения длины границы, я использую квадратные скобки:

[ element element element ... ]


Вот пример повторяющегося узора из прямоугольников и ромбов:

420d8864e72e178252766172dc0320ab.png


Иногда мне будет нужен (горизонтальный) пробел между элементами повторяющегося узора. Хотя для создания пробела можно использовать элемент без цветов, умнее и удобнее будет иметь элемент горизонтального пробела:

HS(length)


Последняя функция, необходимая для этой первой итерации MBDL — это возможность наложения элементов друг на друга. Вот пример границы:

8ea7e93f3040563ef7ec2715ca39a14d.png


Проще всего описать её широкой жёлтой линией под верхним узором. Реализовать это можно разными способами (например отрицательным вертикальным пробелом), но я решил использовать для обозначения порядка элементов по направлению внутрь фигурные скобки:

{element element element ...}


По сути, эта запись приказывает запоминать, где находился узор снаружи внутрь при входе в скобки, а затем возвращаться к этой точке при выходе из скобок. Скобки также можно рассматривать как описание элементов занимающих вертикальное пространство. Поэтому показанную выше границу можно описать так:

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


Мы отрисовываем чёрную линию, фиксируем, где находимся, рисуем жёлтую линию, а затем возвращаемся к зафиксированной ранее позиции, опускаемся немного вниз, отрисовываем паттерн из прямоугольников и ромбов, опускаемся немного вниз, а затем рисуем ещё одну чёрную линию.

В MBDL нужно сделать гораздо больше, но этого достаточно для описания множества границ карт. Следующим шагом будет преобразования описания границы на MBDL в саму границу. Это похоже на преобразование письменного представления компьютерной программы (например, на Javascript) в выполнение этой программы. Первый этап — это лексический анализ (парсинг) языка — преобразование исходного текста в настоящую границу карты или в какой-то промежуточный вид, который проще преобразовать в границу.

Парсинг — это достаточно хорошо изученная область компьютерных наук. Парсинг языка выполнять не очень просто, но в нашем случае хорошо то (*), что MBDL является контекстно-свободной грамматикой. Контекстно-свободные грамматики парсятся достаточно легко, и для них существует множество инструментов парсинга на Javascript. Я остановился на Nearley.js, который кажется достаточно зрелым и (что более важно) хорошо документированным инструментом.

(* Это не просто удача, я позаботился о том, чтобы язык был контекстно-свободным.)

Я не буду знакомить вас с контекстно-свободными грамматиками, но синтаксис Nearley достаточно прост и вы без особых проблем должны понять смысл. Грамматика Nearley состоит из набора правил. Каждое правило имеет символ слева, стрелку и правую часть правила, которая может быть последовательностью символов и не-символов, а также различные опции, разделённые оператором »|» (или):

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


Каждое из правил говорит, что левая часть может быть заменена любой из опций в правой части. То есть первое правило гласит, что граница является элементом, или элементом, за которым идёт ещё одна граница. Которая сама может быть элементом, или элементом, за которым следует граница, и так далее. Второе правило гласит, что элемент может быть только строкой «L». То есть вместе эти правила соответствуют вот таким границам:

L
LLL


и не соответствуют таким границам:

X
L3L


Кстати, если вы захотите поэкспериментировать с этой (или любой другой) грамматикой в Nearley, то для этого есть онлайн-песочница здесь. Можно ввести грамматику и протестировать случаи, чтобы увидеть, чему она соответствует и не соответствует.

Вот более полное определение примитива линии:

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


В Nearley есть несколько общих встроенных элементов, и «number» — один из них. Поэтому я могу использовать его, чтобы распознать численную ширину примитива линии. Для распознавания цвета я использую ещё один встроенный элемент и разрешу использовать любую строку в двойных кавычках.

Было бы неплохо добавить пробелы между разными символами, поэтому давайте сделаем это. Nearley поддерживает классы символов и РБНФ для «нуля или больше» чего-то с помощью »:*», поэтому я могу использовать это для задания «нуля или больше пробелов» и вставлю в любое место, чтобы разрешить пробелы в описаниях:

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


Однако использование повсюду WS усложняет чтение грамматики, поэтому я откажусь от них, но воображайте, что они есть.

Также элемент может быть вертикальным пробелом:

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


Это соответствует таким границам

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


Далее идут примитивы полосы, ромба и эллипса.

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


Это будет соответствовать таким элементам

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


(Учтите, что геометрические примитивы не являются «элементами», потому что они не могут находиться одни на верхнем уровне. Они должны быть заключены в паттерн.)

Также мне нужен примитив горизонтального пробела:

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


Теперь я добавлю операцию паттерна (повторения). Это последовательность одного или нескольких элементов внутри квадратных скобок. Я воспользуюсь РБНФ-оператором »:+», который здесь обозначает «один или больше».

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


Учтите, что паттерн можно заполнить только геометрическими примитивами. Мы не можем, например, поместить внутрь паттерна линию. Элемент паттерна теперь будет соответствовать чему-то подобному

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


Последняя часть языка — это оператор наложения. Это любое количество элементов внутри фигурных скобок.

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


что позволяет нам сделать следующее:

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


(Заметьте, что в отличие от оператора повторения, оператор наложения можно использовать внутри себя.)

Подчистив описание и добавив в нужные места пробелы, мы получим следующую грамматику MBDL:

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


Итак, MBDL теперь определён и мы создали грамматику языка. Её можно использовать с Nearley для распознавания строк языка. Прежде чем углубляться в MBDL/Nearley, я хотел бы реализовать используемые в MBDL примитивы, чтобы можно было отображать описанную на MBDL границу. Этим мы займёмся в следующей части.

Часть 3.


Теперь мы приступим к реализации самих примитивов отрисовки. (На этом этапе мне ещё не обязательно привязывать парсер к примитивам отрисовки. Для тестирования я буду просто вызывать их вручную.)

Начнём с примитива линии. Вспомним, какой он имеет вид:

L(width, color)


В дополнение к ширине и цвету тут есть неявный параметр — расстояние от внешнего края карты. (Я отрисовываю границы с края карты наружу. Заметьте, что начинали мы с другого!) Он не должен указываться на MBDL, потому что это может отслеживать интерпретатор, который выполняет MBDL для отрисовки границы. Однако это должны быть входные данные для всех примитивов отрисовки, чтобы они знали, где их нужно рисовать. Я назову этот параметр смещением.

Если бы мне нужно было только отрисовать границу вдоль верхней части карты, то примитив линии был бы очень прост в реализации. Однако на самом деле мне нужно будет отрисовывать сверху. снизу, слева и справа. (Возможно, когда-нибудь я реализую наклонные или искривлённые границы, но пока мы будем придерживаться стандартных прямоугольных границ.) Кроме того, длина и расположение элемента линии зависят от размеров карты (а также от смещения). Поэтому в качестве параметров мне нужны все эти данные.

Задав все эти параметры, достаточно просто создать примитив линии и использовать его для отрисовки линии вокруг карты:

a5674f78512bef22e569656701203401.png


(Заметьте, что для отрисовки «рукописной» линии я использую различные функции Dragons Abound.) Давайте попробуем создать более сложную границу:

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


Она выглядит вот так:

86aad88494c1c17f993524ef09ef9b2f.png


Довольно неплохо. Заметьте, что есть места, в которых чёрные линии и золотая линия не совсем выровнены из-за колебаний. Если я захочу избавиться от этих пятен, то можно просто уменьшить величину колебаний.

Реализовать примитив вертикального пробела довольно просто; он просто выполняет инкремент смещения. Давайте добавим небольшой пробел:

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


eab471b902eb44e703f460d700936937.png


При отрисовке линий углы можно реализовать отрисовкой между смещением и рисованием вдоль карты по часовой стрелке. Но в общем случае мне нужно реализовать на каждой стороне границы карты усечение, чтобы создать угловое соединение со скосом. Это будет необходимо для создания границ с узорами, правильно стыкующихся на углах, и в общем случае позволит избавиться от необходимости отрисовки элементов с краями под углом, которые бы потребовались в противном случае. (*)

(Примечание: как будет сказано в последующих частях, со временем я отказался от использования областей усечения при реализации углов. Основная причина заключается в том, что для создания сложных углов, например, квадратных смещений:

97bfd833697b9278660772c1bd04ee46.png


требуются всё более сложные области усечения. Также со временем я нашёл более хороший способ работы с паттернами в углах. Вместо того, чтобы возвращаться и переписывать эту часть статьи, я решил оставить её, чтобы проиллюстрировать процесс «творчества».)

Основная идея заключается в усечении каждой границы по диагоналям и создании четырёх усечённых областей в которой будет отрисовываться каждая сторона границы:

9fb31acf993cde46b6a7fdc408be6afd.png


При усечении всё, нарисованное в соответствующей области, будет отрезано под нужным углом.

52bf00f026f6979f6425345389fa646a.png


К сожалению, это создаёт небольшие разрывы вдоль диагональных линий, вероятно, потому, что браузер неидеально выполняет сглаживание вдоль усечённого края. Тест показал, что через зазор между двумя краями просвечивает фон. Исправить это удалось, немного расширив одну из масок (похоже, достаточно половины пикселя), но и это иногда не решает проблему.

Следующими нужно реализовать геометрические фигуры. В отличие от линий, они повторяются в паттерне, заполняя сторону границы карты:

420d8864e72e178252766172dc0320ab.png


Человек бы нарисовал этот узор слева направо, отрисовывая прямоугольник, ромб, а затем повторяя так же, пока не заполнится вся граница. Поэтому это можно реализовать так же и в программе, отрисовывая паттерн вдоль границы. Однако будет проще сначала отрисовать все прямоугольники, а затем все ромбы. Достаточно будет просто отрисовать вдоль границы одну и ту же геометрическую фигуру с интервалами. И очень удобно то, что каждый элемент имеет одинаковый интервал. Разумеется, человек так бы делать не стал, потому что слишком сложно располагать элементы в нужных местах, но для программы это не проблема.

То есть процедуре отрисовки простых геометрических фигур нужны параметры, в которые передаются все размеры и цвета фигуры (т.е. ширина, длина, толщина линии, цвет линии и заливки), а также исходная позиция (которую по причинам, которые скоро станут понятны, я буду считать центром фигуры), интервал горизонтального пробела для перехода между повторами, и количество повторов. Удобно также будет указать направление повтора в виде вектора [dx, dy], чтобы мы могли выполнять повторения слева направо, справа налево, вверх или вниз, просто меняя вектор и начальную точку. Соединим всё это вместе, и получим полосу повторяющихся фигур:

409bcdc33756610ac6ace2d0fde0a3a4.png


Использовав этот код несколько раз и выполняя отрисовку с одинаковым смещением, я смогу скомбинировать чёрные и белые полосы для создания масштаба карты:

b888f50d9f164b9319cc8b3cf8d38588.png


Прежде чем я начну разбираться, как применить всё это к настоящей границе карты, давайте сначала реализуем тот же функционал для эллипсов и ромбов.

Ромбы — это просто прямоугольники с повёрнутыми вершинами, поэтому нужно внести только небольшое изменение в код. Оказалось, что у меня ещё нет готового кода для отрисовки эллипса, но очень легко взять параметрический вид эллипса и создать функцию, дающую мне точки эллипса:

3fa74e079dd2c29afa5acb0135d4092f.png


Вот пример (созданный вручную), в котором используются реализованные выше возможности:

bf8bf2d3992b34bc173622dc86c0d8b5.png


Для такого малого объёма кода выглядит довольно неплохо!

Давайте теперь решим сложный случай границ с повторяющимися элементами: углы.

При наличии границы с повторяющимися элементами решить проблему с углами можно несколькими способами. Первый — отрегулировать повторы таким образом, чтобы они выполнялись в углах без заметного брака:

7b770ec95296ce07e0d18ede205c8798.png


Ещё один вариант — останавливать повторение где-то рядом с углом с обеих сторон. Так часто поступают, если паттерн нельзя легко «повернуть» в углу:

15f6dd3a3e6191da02dc884193b72c82.png


Последний вариант — закрыть паттерн каким-нибудь угловым украшением:

7be5ce64eb09de2eeefdb18992978d03.png


Когда-нибудь я доберусь до угловых украшений, но пока воспользуемся первым вариантом. Как сделать так, чтобы паттерн из полос или кругов без разрывов «поворачивал» в углах карты?

Основная идея заключается в том, чтобы поместить элемент паттерна ровно в угол, чтобы одна его половина находилась на одном краю карты, а другая — на соседнем. В этом примере круг находится ровно в углу и может отрисовываться с любого направления:

7b770ec95296ce07e0d18ede205c8798.png


В других случаях элемент наполовину отрисовывается в одном направлении, и наполовину в другом, но края при этом совпадают:

4eef620b830eb0249d019229140e34e6.png


В этом случае белая полоса отрисована с обеих сторон, но без зазоров соединяется в углу.

При размещении элемента в углу стоит учитывать два аспекта.

Во-первых, угловой элемент будет разбит и отзеркален относительно диагонали, проходящей через центр элемента. Элементы с радиальной симметрией, например, квадраты, круги и звёзды, не изменят своей формы. Элементы без радиально симметрии, например, прямоугольники и ромбы — при отзеркаливании относительно диагонали изменят форму.

Во-вторых, чтобы угловые элементы двух сторон соединялись правильно, вдоль обеих сторон карты должно быть целое количество элементов (*). Их необязательно должно быть одинаковое количество, но на обоих сторонах должно быть целое количество элементов. Если на одной стороне содержится дробное количество паттернов, то с одного края паттерн не совпадёт с прилегающей стороной.

(* В некоторых случаях, например, при длинных полосах, частичное повторение может встретиться с полным повторением и элементы всё равно будут выровнены. Однако получившийся угловой элемент будет асимметричным и отличаться длиной от того же элемента на сторонах карты. Пример этого можно увидеть здесь:

263418110aafe94ef8c62d57e8540371.png


Белая полоса масштаба встречается с разными частичными повторами и в результате получается сдвинутый относительно центра элемент. Для масштаба карты это не всегда неверно, потому что он показывает абсолютное расстояние и не обязан быть симметричным. Но для декоративного узора это обычно выглядит плохо.)

Вот пример, показывающий, как целое число повторений обрезается ровно в углу:

8ab8de0a3452332e2813b3771091b165.png


Если сделать то же самое со&nbs

© Habrahabr.ru