Хабрахабр

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

image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Часть 2

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

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

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

L(width, color)

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

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

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

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

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

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

VS(width)

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

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

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

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

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

] [ element element element ...

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

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

HS(length)

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

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

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

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

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

Грамматика 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 ")"

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

5,"black") VS(3. L(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 ):+ "}" 

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

5,"rgb(98,76,15)")VS(3. {L(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 для отрисовки границы. Однако это должны быть входные данные для всех примитивов отрисовки, чтобы они знали, где их нужно рисовать. Я назову этот параметр смещением.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

При внимательном изучении можно заметить, что узор не встречается ровно в углах. Половина круга в каждом углу берётся с каждой из сторон, и эти две половины независимо друг от друга «отрисовываются вручную», поэтому совпадают неидеально. Но теперь они достаточно близки к этому.

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

Тогда элемент должен повторяться каждые 20,14 пикселей. Во-первых, предположим, нам известно, что сторона имеет длину 866 пикселей, и мы хотим повторить элемент 43 раз. В показанном выше примере я добавил между кругами дополнительное пространство. Как же нам задать конкретную длину элемента (а в общем случае — паттерна элементов)? Возможно, стоит растянуть круги, чтобы они продолжали касаться друг друга? Но если круги изначально касались друг друга, то это изменит паттерн.

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

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

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

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

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

Пока у меня нет особо качественных решений для неё. Интересная задача, правда? Возможно, они появятся позже!

Часть 4

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

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

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

При таком подходе алгоритм расположения паттерна на стороне карты будет очень простым:

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

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

Когда я с этим разобрался, алгоритм стал работать без проблем.

(Но не забывайте предыдущее примечание о том, что со временем я отказался от областей усечения!)

Вот пример с соотношением приблизительно 2:1:

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

Вот другой пример, с полосами:

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

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

Вот пример с паттерном из нескольких элементов:

Здесь полосы накладываются на полосы:

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

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

В этом случае добавление пространства смещает элементы: Однако это решение не работает для паттернов с касающимися элементами, например, для масштаба карты.

Ещё один вариант удлинения паттерна, о котором я говорил выше, — растягивание отдельных элементов паттерна. Он подойдёт для чего-то наподобие паттерна масштаба, но будет плохо выглядеть в паттерне с симметричными элементами, потому что растягивание сделает их асимметричными.

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

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

Я преобразовал изображение в градации серого, потому что не хотел заморачиваться подбором цветов, да и сама карта довольно скучная, но как proof of concept границы выглядят достаточно красиво.

Часть 5

В части 2 я разработал грамматику Map Border Description Language (MBDL), а в частях 3 и 4 реализовал процедуры для выполнения всех примитивов языка. Теперь я поработаю над соединением этих частей, чтобы можно было описывать границу на MBDL и отрисовывать её на карте.

Готовая грамматика выглядит так: В части 3 я писал грамматику MBDL так, чтобы она работала с Javascript-инструментом парсинга Nearley.

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

По умолчанию, при успешном парсинге правила с помощью Nearley правило возвращает массив, содержащий все элементы, соответствующие правой части правила. Например, если правило

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

сопоставлено со строкой

A

то Nearley вернёт

"A" ] 

Массив с единственным значением — строку «A», соответствующую правой части правила.

Что же возвращает Nearley, когда с помощью этого правила парсит элемент?

number -> WS decimal WS

В правой части правила есть три части, поэтому она вернёт массив с тремя значениями. Первое значение будет тем, что вернёт правило для WS, второе значение будет тем, что вернёт правило для decimal, а третье значение будет тем, что вернёт правило для WS. Если с помощью приведённого выше правила я выполню парсинг " 57", то результат будет следующим:

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

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

К счастью, правила Nearley могут переопределять стандартное поведение и возвращать то, что им захочется.  На самом деле, (встроенное) правило для decimal не возвращает список цифр, оно возвращает эквивалентное число Javascript, что в большинстве случаев намного полезнее, то есть возвращаемое значение правила number имеет вид:

[
[ " " ],
57,
[ ]
]

Правила Nearley переопределяют стандартное поведение, добавляя к правилу постобработчик, берущий стандартный массив и заменяющий его тем, что нужно. Постобработчик — это просто код Javascript внутри особых скобок в конце правила. Например, в правиле number меня никогда не интересуют возможные пробелы с любой стороны от числа. Поэтому было бы удобно, если бы правило просто возвращало число, а не массив из трёх элементов. Вот постобработчик, который выполняет эту задачу:

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

Этот постобработчик берёт стандартный результат (показанный выше массив из трёх элементов) и заменяет его вторым элементом массива, который является числом Javascript из правила decimal. Итак, теперь правило number возвращает настоящее число.

Например, я могу использовать грамматику Nearley для превращения строки MBDL в массив структур Javascript, каждая из которых представляет примитив, идентифицируемый по полю «op». Этот функционал можно использовать для переработки входящего языка в промежуточный язык, с которым проще работать. Правило для примитива линии будет выглядеть примерно так:

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

То есть результатом парсинга «L(13,black)» будет структура Javascript:

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

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

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

будет

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

что гораздо проще обработать для создания границы карты.

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

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

(* Есть и другие компоненты с похожими ограничениями, о которых я пока не говорил.)

Например, для отрисовки границы мне нужно знать размер карты, четыре используемые области усечения и т.д. Во-вторых, Javascript в Nearley встраивается в правила, поэтому мы никак не сможем передать в Javascript дополнительную информацию, за исключением глобальных переменных. Хоть я и могу добавить код, делающий эту информацию доступной для постобработчиков Nearley, это будет немного неряшливо и, возможно, такой код будет сложно поддерживать.

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

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

Здесь есть пара скользких моментов.

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

Для отрисовки повторяющихся паттернов мне нужно просмотреть все элементы паттерна и определить самый широкий, потому что он будет задавать ширину всего паттерна. Ещё один сложный момент — повторяющиеся паттерны. Также мне нужно просмотреть и отследить длину паттерна, чтобы я знал, какое расстояние оставлять перед каждым повтором.

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

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

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

Я создал это представление путём проб и ошибок. Как бы то ни было, интерпретатор работает!

Здесь мне показать особо нечего: пришлось исправить несколько названий полей, но во всём остальном код работал хорошо. В качестве последнего шага позвольте мне использовать парсер для создания промежуточного представления из версии на MBDL. Для границы я использовал слегка отличающуюся версию 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")

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

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

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

Часть 6

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

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

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

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

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

(Здесь мы снова для понятности опустим пробелы и некоторые другие детали.) Пока единственными вариантами опций являются «MITER» для скошенных углов и «STOPPED» для остановки рядом с углами. Если STOPPED не передаётся никакое значение, то программа останавливает паттерн на каком-то разумном расстоянии от угла. Если значение передаётся, то паттерн останавливается на таком расстоянии до угла.

Вот как это выглядит: Если используются углы STOPPED, то я прекращаю отрисовывать паттерн углов вдали от углов.

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

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

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

Теперь паттерн симметричен относительно угла и выглядит намного лучше.

Далее мне нужно отследить самый длинный паттерн STOPPED и остановить каждый паттерн STOPPED на этом расстоянии:

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

Теперь всё выровнено красиво.

Вторая опция для углов — это квадратные смещения по углам, например такие:

Реализовать это будет гораздо сложнее!

Однако грамматика этой опции проста и использует опкод Option:

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

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

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

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

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

Карты рисуются обоими способами, но это не очень большая проблема.

Для этого достаточно было повернуть линию относительно соответствующих углов: Для начала я реализовал смещения для линий.

Как можно понять, я могу комбинировать углы со смещениями и обычные углы, как на показанной выше карте:

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

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

Так как отрезки смещённых углов достаточно коротки, в углу очень легко создать неравновесный паттерн:

Иногда это выглядит довольно некрасиво. Мне это напомнило старый анекдот:

Пациент: «Доктор, когда я так делаю, мне больно».
Доктор: «Тогда не делайте так!»

Поэтому я постараюсь так не делать.

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

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

Если паттерн слишком велик для размещения на отрезке смещённого угла, то алгоритм просто сдаётся:

Что далеко неидеально, но, как я говорил выше «Тогда не делайте так». (На самом деле не очень сложно будет добавить функцию сжатия или растяжения, если бы она мне понадобилась.)

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

Мне кажется, что это логичное решение.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть