На немецком «эйнштейн» звучит как «один камень». Один - «ein», камень - «Stain». Всем известно, что под этой фамилией жил один замечательный человек, и звали его ... Но в статье речь не о нём. Речь о математической задаче по поиску одной плитки, такой чтобы составленная из неё мозаика была непериодической. «Один камень» - это про плитку. В мозаике Пенроуза таких плиток две, а хотелось бы возможности использовать только одну. Не вдаваясь в детали можно сказать, что задача одной плитки в этом году (2023) решена. Получены интересные красивые мозаики.
Сначала была найдена «шляпа эйнштейна» - плитка, похожая на шляпу. Или, по моему скромному мнению, на рубашку. Из неё можно сделать отличную непериодическую мозаику. Только, для построения используются как сами шляпы, так и их зеркальные отражения. Считать ли это одной плиткой? Можно и не считать.
Дальше была найдена плитка «черепаха». Из неё тоже можно сложить непериодическую мозаику, по тем же самым правилам. Эти два вида плиток могут, плавно меняя форму, переходить друг в друга, меняя размер граней и при этом не меняя их направление. Ещё можно сложить непериодическую мозаику одновременно из этих двух плиток. Дальше больше. У такого плавного преобразования существует средний вариант, в котором длина граней одинакова.
Оказалось, такая мозаика, в которой есть одновременно и шляпы и черепахи, при обмене формой в момент, в котором длина граней становится одинаковой, составлена из плиток полностью одинаковой формы. То есть, существует ещё одна непериодическая мозаика, в которой плитка используется уже без своего зеркального отражения. Плитка, у которой грани модифицированы так, что она позволяет только непереодическое сложение названа «Spectre» (призрак). Задача решена, теперь уже точно.
В статье «Тридцать шесть градусов красоты» я описывал как рисовать мозаику Пенроуза. В статье «Два вида последовательного перебора пикселей» описывал закономерности для квадратов. Теперь напишу о том как рисовать эти новые мозаики. Получилась полная серия «36, 90, 120 градусов красоты».
Нарисуем координатную сетку. Сетки из равносторонних шестиугольников и равносторонних треугольников дополняют друг друга так что при их наложении получается сетка из четырёхугольников, с неравными сторонами. Если одну точку четырёхугольника расположить в , вторую в , то третья может быть рассчитана как . Четвёртая как .
У следующего четырёхугольника, если считать против часовой, дальняя точка будет по координате . Остальные координаты симметричны, меняется только знак числа.
Если откладывать получившиеся короткие ребра начиная от центра, то координаты получатся для первого и для второго . Направлений всего шесть, остальные координаты различаются только знаком.
Стоит изменить масштаб, чтобы длина короткого ребра была единичная.
Координаты этих семи точек возрастут в раз и станут:
Координаты можно хранить как целые числа с множителем для x и для y.
В этой системе отсчёта соответствующие координаты будут:
Здесь можно заметить, что чётность значений обеих координат одинаковая. Это значит, что половина всех координат, у которой значений разной чётности, не достижимы из нуля, и значит, они не используются.
Можно координаты дополнительно разделить, хранить отдельно для изменений после больших шагов и отдельно для изменений после малых шагов. Тогда с точностью до знака будет четыре варианта одного шага:
Изменения координаты по порядку поворота
— для большого шага и
— для малого шага.
Если расставить по порядку увеличения угла:
[2,0,0,0],[0,0,1,1],[1,3,0,0],[0,0,0,2],[-1,3,0,0],[0,0,-1,1],
[-2,0,0,0],[0,0,-1,-1],[-1,-3,0,0],[0,0,0,-2],[1,-3,0,0],[0,0,1,-1]
Итак, нарисуем рубашку.
Фигура составлена из четырёх пар четырёхугольников, занимая место внутри тройки соседних шестиугольников. В одном шестиугольнике две пары четырёхугольников, и в двух других по одной паре. Такого описания вполне достаточно.
Так как шестиугольники и треугольники в этом трафарете выступают наравне, то фигуру можно описать и через треугольники. Фигура занимает место внутри пяти треугольников, один занят полностью, в одном только два прилегающих четырёхугольника, и ещё в трёх по одному. При этом четырёхугольники можно сгруппировать в четыре пары.
Чтобы задать фигуру рубашки в программе, нужно выбрать точку отсчёта координат. Простых вариантов для выбора целых три: можно отсчитывать от низа рубашки, можно от расположенных внутри фигуры «пуговиц»: от верхней или от нижней. Я выбрал считать от нижней пуговицы. А обход будем начинать с самого низа рубашки, и идти против часовой.
Шаги по изменению координат будут
[0,0,1,1],[0,0,1,-1],[1,3,0,0],[2,0,0,0],[0,0,0,2],[0,0,-1,1],[0,0,-1,1],
[0,0,-1,-1],[-1,3,0,0],[-2,0,0,0],[0,0,0,-2],[0,0,1,-1],[-1,-3,0,0],[1,-3,0,0]
Тогда координаты будут:
(0,0,-1,-3),(0,0,0,-2),(0,0,1,-3),(1,3,1,-3),(3,3,1,-3),(3,3,1,-1),(3,3,0,0),
(3,3,-1,1),(3,3,-2,0),(2,6,-2,0),(0,6,-2,0),(0,6,-2,-2),(0,6,-1,-3),(-1,3,-1,-3)
Можно приступать к созданию первой версии программы.
Я буду использовать p5.js, это js-версия Processing. Если самостоятельно дописать команды рисования линий, функции прямого обращения к классу Math, и остальное по мелочи — как было сделано в статье про мозаику Пенроуза, то можно обойтись и без этой библиотеки. Но с ней получается быстрее.
Открываем https://editor.p5js.org и в окне редактирования видим две функции, инициализации и покадрового рисования. Кроме их редактирования нужно будет дописать свои функции для создания и отрисовки фигур.
Сначала напишем функцию рисования одной фигуры по координатам.
Вот что получилось.
// Отрисовка мозаики, yurixi, https://habr.com/ru/articles/757132/
//
// Команда отрисовка линии line(x1,y1,x2,y2);
// Но замкнутые многоугольники стоит рисовать через
// fill(r,g,b); stroke(r,g,b); // цвет заливки и линии
// beginShape(); vertex(x1, y1); vertex(x2, y2); vertex(x3, y3); endShape(CLOSE);
// Ширину линии можно выбирать через strokeWeight(h);
let kf, fc, zm, xs, ys;
let color1, color2;
// преднастройка
function setup() {
createCanvas(640, 480);
// коэффициенты
kf = [sqrt(3)/2, 1/2, sqrt(3)/2, 1/2, 0, 0, 0, 0];
// форма плитки
fc = [[0, 0, -1, -3], [0, 0, 0, -2], [0, 0, 1, -3], [1, 3, 1, -3], [3, 3, 1, -3],
[3, 3, 1, -1], [3, 3, 0, 0], [3, 3, -1, 1], [3, 3, -2, 0], [2, 6, -2, 0],
[0, 6, -2, 0], [0, 6, -2, -2], [0, 6, -1, -3], [-1, 3, -1, -3]];
// цвета можно задавать и не в rgb
color1 = color('hsla(220,100%,75%,0.5)');
color2 = color('hsla(200,100%,75%,0.5)');
// масштаб
zm = 30;
// координаты центра отсчёта
xs = 320;
ys = 240;
}
// Пересчёт из целой системы отсчёта в экранную, есть поддержка вращения
function place(p) {
return createVector(
xs + zm * (kf[0] * p[0] + kf[2] * p[2] + kf[4] * p[1] + kf[6] * p[3]),
ys - zm * (kf[1] * p[1] + kf[3] * p[3] - kf[5] * p[0] - kf[7] * p[2])
);
}
// рисуем первую фигуру
function draw_shape() {
liner(); // фоновые линии
strokeWeight(2);
stroke(50)
fill(color1);
beginShape();
for (i in fc) {
let c = place(fc[i]);
vertex(c.x, c.y);
}
endShape(CLOSE);
point(place([0, 0, 0, 2]));
point(place([0, 0, 0, 0]));
}
// функция отрисовки кадра
function draw() {
// очищаем канву после предыдущего кадра
background(240); // аргумент - цвет в градации серого
draw_shape()
// кадр только один, поэтому можно остановить цикл рисования
//noLoop();
// Но если цикл не остановить, то фигура будет переливаться
// из шляпы в черепаху и обратно,
// через изменение коэффициентов для перевода координат
let k3 = sqrt(3) / 2,
k2 = 1 / 2,
k1 = (1 - cos(frameCount / 60 * PI)) / 2;
// приостанавливаясь на центральном положении
k1 = (1 - cos(abs(k1 - 0.5) * PI)) / 2 * Math.sign(k1 - 0.5) + 0.5;
// и на крайних положениях
k1 = ((1 - cos(k1 * PI)) / 2 + k1) / 2;
kf = [
(1 - k1) * k3 + k1 * k2,
(1 - k1) * k2 + k1 * k3 / 3,
(1 - k1) * k3 + k1 * 3 / 2,
(1 - k1) * k2 + k1 * k3,
0, 0, 0, 0
]
}
// разлиновка
function liner() {
// здесь цвет задаётся через rgb-составляющие и прозрачность
stroke(color(180, 180, 180, 60));
// режим в котором замыкание фигуры не приводит к её заливке
noFill();
// данные шести четырёхугольников, которые образуют шестиугольник
fcn = [
[[0, 0, 0, 0], [2, 0, 0, 0], [2, 0, 0, 2], [2, 0, -1, 3], [1, -3, -1, 3]],
[[0, 0, 0, 0], [1, 3, 0, 0], [1, 3, -1, 1], [1, 3, -2, 0], [2, 0, -2, 0]],
[[0, 0, 0, 0], [-1, 3, 0, 0], [-1, 3, -1, -1], [-1, 3, -1, -3], [1, 3, -1, -3]],
[[0, 0, 0, 0], [-2, 0, 0, 0], [-2, 0, 0, -2], [-2, 0, 1, -3], [-1, 3, 1, -3]],
[[0, 0, 0, 0], [-1, -3, 0, 0], [-1, -3, 1, -1], [-1, -3, 2, 0], [-2, 0, 2, 0]],
[[0, 0, 0, 0], [1, -3, 0, 0], [1, -3, 1, 1], [1, -3, 1, 3], [-1, -3, 1, 3]],
];
// сдвиги для шестиугольников
fs = [[0, 0], [4, 0], [2, 6], [-2, 6], [-4, 0], [-2, -6], [2, -6], [8, 0], [6, 6], [6, -6]]
for (let jf in fs) {
let fs2 = fs[jf];
for (let j in fcn) {
let fcm = fcn[j];
beginShape();
for (let i in fcm) {
let fci = fcm[i];
fci = [fci[0] + fs2[0] - 2, fci[1] + fs2[1], fci[2], fci[3]];
c = place(fci);
vertex(c.x, c.y);
}
endShape(CLOSE);
}
}
}
Если вставить этот фрагмент в редактор и запустить, нарисуется эта рубашка, которая была показана выше.
Дальше хочется составить программу, которая не просто делает рисунок, а является интерактивным инструментом для рисования мозаик. Ради красоты.
По своему опыту с мозаикой Пенроуза могу отметить, что подготовка структуры фигур для рисования это не подбор соседних плиток, а разбиение плитки на конфигурацию плиток меньшего размера, с последующим исправления дублирования накладывающихся элементов, которые получены от разбиения соседних плиток. Края одного уровня могут не совпадать с краями другого уровня, и плитки которые должны повторяться у соседей играют роль выравнивающих ушек при сборе пазла.
Для рисования мозаики надо разобраться в алгоритмах разбиения. И уже потом в программу добавить возможность рисовать на плитках узоры, в зависимости от типа плитки, в том числе от того какой частью плитки она была до разбиения.
Для хранения одной плитки известной формы достаточно хранить координаты её центра, направление и тип. И, возможно, ещё информацию о «родительской» плитке на предыдущем уровне разбиения.
Начнём разбираться в алгоритме. Сначала четырёх-фигурный.
Оказывается, что эта «шляпная» мозаика самими шляпами при разбиении становится только на последнем этапе, а во время разбиения она состоит из других фигур, которых четыре типа. Их можно разделять либо на шляпы, либо, для продолжения разбиения, на эти же четыре фигуры.
Эти фигуры следующие:
большой треугольник, объём 4 шляпы
«планка», 2 шл.
«лопасть», 2 шл.
малый треугольник, 1 шл.
Причём, «лопасти» всегда соединяются в «вентилятор» из трёх лопастей.
(Иллюстрации будут ниже, программу пишу одновременно со статьёй)
Для удобства я перевёл данные фигур в формат, в котором грани представляют собой шаги заданной длины и заданного направления. Тогда достаточно указать величину изменения направления, и можно получить координаты концов изображаемых линий при другом повороте.
Начальная позиция обхода контура задаётся через приводящие к ней шаги, начинающиеся от центра - так как шаги, заданные через направление, поворачивать проще, чем координаты.
Следующий фрагмент кода можно добавить к предыдущему и запустить.
Отрисовка большого треугольника
// Вспомогательные функции
// Функция отрисовки фигуры через массив с коорднатами
function draw_vertex(fc)
{
beginShape();
for (let i in fc) {
let c = place(fc[i]);
vertex(c.x, c.y);
}
endShape(CLOSE);
}
// Сложение координат
function add(c, d)
{
let r = [];
for (let i in c)
{
r[i] = c[i] + d[i];
}
return r;
}
function draw_shape() {
liner(); // фоновые линии
// форма плитки
let fw = {
pos: [-3],
steps: [-1, 2, 0, 3, 5, 5, -5, 4, 6, -3, -1, -4, -2, 1]
};
stroke(0);
fill(color1);
draw_vertex(get_shape(fw, 0, 0, [0, 0, 0, 0], 1));
fill(color2);
draw_vertex(get_shape(fw, 2, 1, [0, 6, -3, -3], 1));
draw_vertex(get_shape(fw, 0, 1, [3, -3, -1, -3], 1));
draw_vertex(get_shape(fw, 0, 1, [3, 3, -1, 3], 1));
}
// получить кординаты для заданного поворота
// аргументы: образец фигуры, сдвиг направления, тип отражения, начальные координаты
// и флаг, чтобы сразу рисовать центр фигуры и линию направления.
function get_shape(fw, dv, mr, start, u) {
let c, i, t, v;
let m = 1 - 2 * mr;
let r = [];
// изменение координат в зависимости от направления и типа
const steps = [
[2, 0, 0, 0], [0, 0, 1, 1], [1, 3, 0, 0], [0, 0, 0, 2], [-1, 3, 0, 0], [0, 0, -1, 1],
[-2, 0, 0, 0], [0, 0, -1, -1], [-1, -3, 0, 0], [0, 0, 0, -2], [1, -3, 0, 0], [0, 0, 1, -1]
];
if (start == undefined) {
start = [0, 0, 0, 0];
}
let p = start;
if (u) {
c = place(p);
circle(c.x, c.y, 10);
}
fws = fw.pos;
for (i in fw.pos) {
v = fws[i];
v = (m * (v - dv * 2 + 3) % 12 + 12 + 9) % 12;
if (u && i == 0) {
c = place(p);
x1 = c.x;
y1 = c.y;
}
p = add(p, steps[v]);
if (u && i == 0) {
c = place(p);
line(x1, y1, c.x, c.y);
}
}
r.push(p);
c = place(p);
x1 = c.x;
y1 = c.y;
fws = fw.steps;
for (i in fws) {
v = fws[i];
v = (m * (v - dv * 2 + 3) % 12 + 12 + 9) % 12;
p = add(p, steps[v]);
r.push(p);
c = place(p);
line(x1, y1, c.x, c.y);
x1 = c.x;
y1 = c.y;
}
return r;
}
Результат будет выглядеть так:
Здесь можно обратить внимание, что рубашка в центре и остальные рубашки - между собой зеркально симметричны. (Ладно, буду звать их шляпами, такое название уже во всех статьях. Но для меня это рубашки)
После разбиения большой треугольник будет выглядеть так:
То есть, большой треугольник разбивается на три большие треугольника и один малый треугольник. Но это ещё не всё. Существуют граничные детали, которые одновременно принадлежат двум соседним фигурам предыдущего уровня. Их тоже надо отобразить.
Две детали, «планка» и «лопасть» (изображены как светлые и изумрудные плитки) обе разбиваются на две плитки-шляпы, и в одинаковом сочетании, в этом они неразличимы. Различие состоит только в том, что при разбиении на следующий уровень они становятся разными комбинациями фигур.
Промежуточный слой обволакивает внутренности большого треугольника вокруг по контуру. И даже разрыв на углах заполняется ещё одной плиткой, одинаково на каждом углу. А так как это фигуры, которые принадлежат нескольким соседним фигурам другого уровня, то не удивительно, что замыкающая контур плитка идёт в паре с ещё одной, частью контура другой фигуры. Образуется как раз вторая лопасть вентилятора, которая точно так же может состыковаться с третей лопастью.
Представьте что после сбора картинки-пазла все «ушки», которые через ограничение формы помогали правильно комбинировать детали, вдруг исчезают. Для этой мозаики это происходит так:
Для этой анимации понадобилось составить последовательность граней для каждой фигуры, особо выделив среди них те которые относятся к ушкам и при построении изображения пересчитать их в другую позицию.
Обозначения центров тоже могут быть пересчитаны и перескакивать с нижней на верхнюю пуговицу и обратно:
Оба правых больших треугольника подобны всей схеме. Это значит, что такая схема на другом уровне может быть как правой верхней частью подобного треугольника так и правой нижней. Или входить в остальные три фигуры.
Если составить алгоритм разбиения для каждой из четырёх фигур, то можно соорудить программу автоматического построения. Для треугольников разбиение уже показано, а вот плашки:
И схема мелкого треугольника. При разбиении он становится большим треугольником.
Ниже фрагмент программы, которая была использована для рисования, с комментариями. Этот код не оформлен для запуска, так как реализует рисование мозаики без общего алгоритма, вручную. При этом координаты для расположения деталей приходилось подбирать.
Функция отрисовки фигуры
// Аргументы функции отрисовки фигуры.
// shp - это объект заданной фигуры.
// его формат:
// три массива,
// sh - описание прохождения по контуру
// mr - выделение шагов относящихся к ушкам пазла
// cc - описание где находятся центры фигур, которые нужно отобразить
// Остальные аргументы:
// pos - это начальная позиция отрисовки. Формат координат четыре числа,
// целые координаты для прошедших отдельно для больших и для малых шагов .
// v - направление
// h - размер "ушек", от 0 до 1
// h2 - сдвиг центра фигуры, от 0 до 1
function draw_fig(shp, pos, v, h, h2)
{
// имена для краткого обращения
let sh = shp.sh;
let mr = shp.mr;
// начальная позиция запоминается
let start = pos;
// sh[0] - это указание с какого шага начинается отрисовка,
// так как несколько первых шагов составляют путь
// от центра фигуры до контура, не отрисовываются
shape = [];
for (let i = 1; i < sh[0]; i++)
{
let vn = ((v + sh[i]) % 12 + 12) % 12;
pos = add(pos, steps[vn]);
}
// сохраняется стартовая позиция на контуре
shape.push(pos);
// проход остального контура с сохранением позиций
for (let i = sh[0]; i < sh.length; i++)
{
let vn = ((v + sh[i]) % 12 + 12) % 12;
pos = add(pos, steps[vn]);
shape.push(pos);
}
// Дальше позиции будут переведены в координаты на экране
fig = [];
for (i = 1; i < shape.length; i++)
{
let c1 = place(shape[i]);
let c2;
// если данная позиция относится к "ушку" первого типа,
// то её сдвиг будет к прошлой позиции
if (mr[i] == 1)
{
c2 = place(shape[i - 1]);
}
// если данная позиция относится к "ушку" второго типа,
// то её сдвиг будет к следующей позиции
if (mr[i] == 2)
{
c2 = place(shape[i + 1]);
}
// Расчёт координат, в зависимости от того нужен сдвиг или нет
if (mr[i] > 0)
{
x = lerp(c1.x, c2.x, h);
y = lerp(c1.y, c2.y, h);
}
else
{
x = c1.x;
y = c1.y;
}
// координаты сохраняются
fig.push([x, y]);
}
// массив для сохранения линий,
// которые из границ плиток превращаются в стрелки
let lines = []
// перебираем заданные в фигуре центры
for (let i in shp.cc)
{
sh = shp.cc[i];
// координаты каждого центра заданы в формате перечисления
// шагов которые нужно пройти чтобы добраться до этого центра из общего
// начинаем с центра общей фигуры
pos = start;
let vn;
let posp; // предпоследняя позиция будет сохранена
for (let shv of sh)
{
vn = ((v + shv) % 12 + 12) % 12;
posp = pos;
pos = add(pos, steps[vn]);
}
// расчёт координат предпоследней позиции
c0 = place(posp);
// координаты самого центра
c1 = place(pos);
posl = pos;
// делаем ещё один шаг в том же направлении как последний шаг.
pos = add(pos, steps[vn]);
c2 = place(pos); // координаты смещённого центра
// расчёт кординат в зависимости от смещения
// lerp функция p5js для линейного отображения,
// при h2 = 0 становится c1.x, при h2 = 1 становится c2.x
x = lerp(c1.x, c2.x, h2);
y = lerp(c1.y, c2.y, h2);
// отрисовка линии, ведущей к центру
line(c0.x, c0.y, x, y)
// и центра
circle(x, y, 10);
// превращаются в стрелки только центры 1 и 2
if (i > 0 && i < 3)
{
// позиция центра
pos = posl;
// определяется первая точка
pos = add(pos, steps[(vn + 6 + 12) % 12]);
// в указании направления
// "6" - разворот, так как vn это направление шага к центру,
// а мы от него уходим
// "-2" - направление,
// "+ 12" - добавка чтоб не получилось отрицательное число.
pos = add(pos, steps[(vn + 6 - 2 + 12) % 12]);
// первая точка определена
p1 = pos;
// определяется вторая точка
pos = add(pos, steps[(vn + 6 - 5 + 12) % 12]);
p2 = pos;
// четвёртая точка это шаг от второй
p4 = add(pos, steps[(vn + 6 - 1 + 12) % 12]);
pos = add(p2, steps[(vn + 6 - 3 + 12) % 12]);
// третья точка это тоже шаг от второй
p3 = pos
pos = add(pos, steps[(vn + 6 - 6 + 12) % 12]);
// линии две: составляющая грань и изображающая стрелку
let ln = [[p1, p2, p3, pos], [p4, p2, p2, p3]];
// будет рассчитана линия при плавном переходе
let lns = [];
for (i in ln[0])
{
// первая линия берётся как есть
c1 = place(ln[0][i]);
// стрелка смещена в направление своего указания
c2 = place(add(ln[1][i], steps[(vn - 2 + 12) % 12]));
x = map(h, 0, 1, c1.x, c2.x);
y = map(h, 0, 1, c1.y, c2.y);
lns.push([x, y])
}
lines.push(lns);
}
}
// теперь всё это рисуем
beginShape();
for (i in fig)
{
vertex(fig[i][0], fig[i][1]);
}
endShape(CLOSE);
// яркость линий грани и стрелки различается
// четвёртый аргумент в указании цвета линии - прозрачность
stroke(60, 60, 60, lerp(60, 255, 1 - h));
for (let i in lines)
{
lns = lines[i];
for (let i = 0; i < 3; i++)
{
line(lns[i][0], lns[i][1], lns[i + 1][0], lns[i + 1][1]);
}
}
// возврат обычного цвета линии
stroke(60)
}
// Сами фигуры заданы так:
function set_shapes()
{
// большой треугольник
shp1 = {
sh: [5, -3, -1, 2, 0,
-3, -5, -2, -4, 5, -5, 4, 6, 3, 5, 5, 3, 6, 4, 1, -1, 2, 0, 3, 1, 1, -1, 2, 0, -3, -5, -2, -4, -1, -3],
mr: [0, 0, 1, 0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 1, 0, 2, 0],
cc: [
[-3, 3],
[-3, -1, 2, 0, 3, 5, 5, 3],
[-3, -5, 4, 2, 5, -5],
[-3, -5, -5, -3, 0, -2, 1, 3]
]
}
// центр большого треугольника
shp0 =
{
sh: [2, -3,
-5, 4, 2, 5, 3, 0, -2, 1, -1, -1, -3, 6, -4, 5],
mr: [0, 1, 0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2],
cc: [[-3, 3]]
}
// планка
shp2 = {
sh: [2, -5,
5, 2, 4, 1, -1, -1, -3, 0, -2, -5, -3, -3, -5, -2, -4, -1, 5, 5, 3, 6, 4, 1, 3, 3],
mr: [0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0],
cc: [
[-5, 1],
[-5, -3, -3, -1]
]
}
// лопасть
shp3 = {
sh: [2, -5,
5, 2, 4, 1, -1, -1, -3, 0, -2, -5, -3, -3, -5, -2, -4, 5, 3, 6, 4, 1, 3, 3],
mr: [0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 1, 0, 2, 0, 0],
cc: [
[-5, 1],
[-5, -3, -3, -1]
]
}
// малый треугольник
shp4 = {
sh: [2, -1,
-3, 6, -4, 5, 3, 3, 1, 4, 2, 5, -1, -1, -3, 0, -2, 1, -5, -5],
mr: [0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0],
cc: [
[-1, 5],
]
}
}
Дальше стоит вычислить некоторые характеристики. Например, коэффициент изменения масштаба за один шаг смены уровня разбиения. Я сделал это так: центры вентиляторов (там где встречаются три лопасти заострёнными концами) при смене уровня не сдвигаются, и на новом уровне остаются центрами вентиляторов. Поэтому берём фигуру из двух встречных лопастей разных уровней, вычисляем их размеры и их соотношение.
Особо не заморачиваясь можно вывести формулу для расстояний через отступы в таких координатах. и посчитать:
Размер мелких лопастей
Размер крупных лопастей
Их отношение равно .
Кроме изменения масштаба при смене уровня вся плоскость, похоже, вращается.
Примерно на одну 468-ю всего оборота, примерно 0,76925°, меньше одного градуса.
Казалось бы, характеристики получены, можно строить алгоритм. Но не так быстро. При проверке обнаруживается, что если мы сделаем разбиение на следующий уровень, то изменение масштаба и изменение поворота не совпадут с предыдущими. Или, при повторении множителей — на точку с целыми координатами не попадаешь. Сразу чувствуется странная немонотонность масштабирования.
При разбиении мозаики Пенроуза масштабирование было монотонным, были сразу известны формы плиток при делении. А здесь нам известно только последнее, приближающее к целым координатам, разбиение и мы его реверсим обратно, понимая, что хотя предыдущее и было дальше от точного значения, и следующее ближе к точному значению, но оно всё так же неизвестно, ручное построение хоть и может дать первые несколько уровней, но происходит без точной формулы, к чему всё приближается - неизвестно. В мозаике Пенроуза помогало золотое сечение, а здесь - интересно, что внутри?
Если разбираться по предоставленному изображению: у лопасти точно известна одна точка, узел вентилятора. У пары лопастей известны не только края, но и середина пары, это значит, ещё одна точка на лопасть, две точки. Но по большой паре лопастей от центра до центра не пройти исключительно по парам лопастей: по всей фигуре так прогуляться можно, но при приближении к краю с последней лопасти надо перейти не в точку на противоположной узлу грани, а совсем в другую, пока неизвестную точку. Это значит, что требуется знать размеры плитки в других направлениях.
Здесь становится ясно, что четыре плитки могут быть совсем другими, сохранять примерно ту же форму, но углы и размеры не обязаны накладываться на шестиугольную сетку. Их идеальная форма - другая, и значит, надо искать её.
Скорее всего, и вращения никакого нет. Различие только от разного приближения идеальных форм к шестиугольной сетке. Это похоже на то как числа фибоначчи начинаются с целых, но идеальное их отношение равно золотому сечению. В обратную сторону чем ближе к единицам тем больше отличие от идеального соотношения.
Самый актуальный вопрос здесь такой: какие есть возможности для преобразования этой мозаики, с сохранением структуры границ плиток?
Из-за симметрии направления всех шести граней большого треугольника должны сохраниться, или повернуться на один и тот же угол. Если соотношение длин граней у него поменяется, то поменяется соотношение длин граней у остальных фигур, но, кроме этого, самое заметное, что из этого выйдет: смыкание граней лопастей в узле вентилятора будет под другим углом. Получается, в мозаике будет девять различных направлений граней, три для узлов (для острия лопастей) и шесть для остальных форм. И все они могут не совпадать с направлениями шестиугольной сетки.
Что известно о размерах фигур, если основываться только на схеме соединения? У нас будут две величины для шести сторон большого треугольника. Обозначим их и . Размер граней малого треугольника . У планки размер по одной грани совпадает с , вторую грань с учётом второй известной точки обозначим . У лопасти торцевая грань — как у планки, . Одна длинная грань совпадает с , другая , и две оставшиеся грани около узла должны между собой совпадать, .
В какую именно сторону поворачивается узел вентилятора? Планка по площади больше чем лопасть, но так как они складываются из одинакового количества шляп (и на втором разбиении тоже), они меняются по размеру в сторону сближения. И значит, различие в соотношении уменьшается, узел вращается по часовой.
Более наглядно направление вращения видно на схеме связей уровней на примере лопасти.
Красные отрезки между собой аналогичны, различаются масштабом. Синие отрезки и тёмно-синий тоже отличаются только масштабом. Нам нужно найти точно положение узла большой лопасти, который справа. Для этого надо использовать то, что между соединяющимися красными линиями угол равен точно 60°. (Так как раздельные красные линии в идеале параллельны и односторонний парный угол 120°). Я так понял, что угол в 60° означает, что точка лежит на пересечении синей линии и описанной вокруг треугольника окружности.
Обозначим длину синего отрезка.
И посчитаем, на раз-два-три:
Радиус окружности .
Расстояние от синей линии до центра окружности
Значит, длина тёмно-синего отрезка, выраженная через длину синего отрезка, то есть, точное изменение масштаба при изменении уровня, равно:
Можно заметить, что точный коэффициент немного меньше значения, которое было получено ранее.
И что в этой мозаике тоже проявляется золотое сечение. ????
Теперь будет использоваться как единица второго уровня.
Подсчитаем угол поворота узла вентилятора:
И длину красного отрезка:
Для дальнейших вычислений удобно, что косинус и синус смещённого от 60° угла имеют простые выражения:
Дальше нужно вычислить размер остальных граней. Сначала получаем .
И теперь можно получить значение :
То есть, и
А теперь получаем .
И размер малого треугольника . Все размеры идеального разбиения получены в формате .
При масштабе будет:
Идеальная форма мозаики получена.
Можно немного отдохнуть.
Расскажу зачем делил координаты на собранные из длинных шагов и собранные из мелких. Мозаика составлена из одной фигуры и её зеркального отражения, длинные грани соединены с длинными, короткие с короткими. У формы рубашки-шляпы есть особенность, что каждый шаг по грани имеет среди остальных шагов и возвратный шаг. Если мы разложим шаги по всем направлениям и размерам, добавляя единицу при шаге вдоль этого направления и отнимая при обратном, то после обхода фигуры у нас будут в суммах нули. Поэтому и в мозаике, если её обойти по граням плиток, обойдя вокруг любого их количества, при возвращении эта сумма будет нулевой. Обратные примеры, где нет такого свойства, это треугольник, по которому одна сторона скомпенсирована двумя другими сторонами, но обратного шага для грани нет. И четырёхугольник с двумя парами одинаковых соседних граней. Из-за различия длин граней соответствия между прямым шагом и обратным отсутствует.
Это свойство «компенсации» вместе с тем что вариантов длины всего два приводит к тому что если менять длину у шагов, то мозаика всё равно сложится. В частности, если поменять длины у коротких и длинных граней между собой, получится плитка, из которой точно так же можно собрать мозаику.
Так как координаты изначально для разных шагов хранились отдельно, то в процедуре визуализации можно просто менять коэффициенты и мозаика будет перерисована с другими плитками.
То же самое, с выравниванием по двум верхним точкам:
Тут заметно, что пара точек фигуры большой лопасти шатаются. Это не из-за погрешности выравнивания. Это потому что построенная таким образом фигура имеет отклонение от идеальных длин и углов. Если строить фигуры по узлам, то пропорции приближаются к идеальным с каждым новым уровнем, на первом уровне отличие ещё заметно.
Причём, на больших масштабах это отличие не накапливается. Значит, можно не связывать узлы идеальной мозаики и не идеальной. Можно считать, что все узлы неидеальной мозаики находятся где-то около узлов идеальной мозаики, не совпадая.
Новая фигура из-за явного сходства с черепахой названа «черепахой». Как и шляпа, черепаха строится по шестиугольно-треугольной сетке.
За центральную точку я выбрал нижнюю пуговицу рубашки, так как она при превращении в черепаху остаётся центральной. Верхняя пуговица при смене координат съезжает на одно из трёх мест на контуре, в зависимости от того с какой стороны в эту точку нужно было добираться.
Менять длину граней можно и дальше, вплоть до нуля. Получаются фигуры, в которых остался только один тип граней.
Я попробовал кроме длины поменять ещё и направление. Получилась ещё одна парочка фигур, но для составления из них мозаики требуются сразу обе фигуры. Я для себя называю их «островок» и «лужа».
У островка есть особенность: с пальмой он сохраняет связность через всего одну точку.
Использование двух фигур напомнило, что существует и такая мозаика, в которой есть и шляпа и черепаха одновременно, ниже надо будет рассказать и о ней.
Продолжим выводить алгоритм построения. Он будет состоять из нескольких частей: построение идеальной мозаики, перевод идеальной мозаики в шестиугольную сетку и затем можно добавлять ушки и разломы плашек на плитки, чтобы получилась мозаика из шляп. Или из черепах, из фигур морфинга, из луж и островов.
Так как схемы разбиения уже известны, то построение идеальной мозаики вопрос технический. А с переводом идеальной мозаики на шестиугольную сетку следует разобраться подробнее. Ведь чтобы округлять в сторону ближайших целых координат нужно ещё мозаику правильно растянуть и повернуть.
При отрыве от идеала размеры плашек начинают зависеть от произвольного параметра вместо константы золотого сечения . Причём, не будет равна этому параметру, а будет отдельной величиной, вторым параметром. И для шестиугольной сетки размеры будут такими:
Если выразить через длину основания равнобедренного треугольника в узле вентилятора, которую выразить через параметры, то получится такое выражение:
А если выразить направление относительно верхней грани, через косинус и синус:
В целом, получается, в структуре есть три параметра вариации: это соотношение базовых отрезков, масштаб, который дополняет соотношение, потому что кроме базовых отрезков участвует и единица. И третья вариация - поворот. И первые две вариации вместе с обычным масштабом могут компенсировать третью так что расположение узлов будет оставаться примерно таким же на всей плоскости.
Пример как меняется мозаика при изменении параметров:
Здесь в моменты когда центральная фигура горизонтально выровнена - мозаика на одном направлении изменений имеет идеальные пропорции, а на другом - по шестиугольной сетке.
Если расширить диапазон изменений:
Или чуть аккуратней:
Для выявления угла поворота и величины растяжения придётся разобраться ещё детальней. Придётся выяснять точные размеры фигур при произвольном масштабе.
При шестиугольно-треугольной сетке удобно использовать «треугольные» координаты - это два числа для указания расстояния по направлениям, отстоящих друг от друга на 60°. Размеры синих и красных отрезков большой лопасти в треугольных координатах будут выражены как и . На следующем уровне масштаба эти размеры становятся и . А на предыдущем уровне, то есть, размеры для малой лопасти, равны и .
Проследим как из величин для размеров одного уровня получаются величины для размеров другого уровня.
Для преобразования векторов получилась матрица
Определитель у неё равен единице, и обратная матрица состоит также из целых чисел. С помощью них можно рассчитывать размеры для любого масштаба.
А ещё, калькулятор при расчёте сообщил: существует базис, для которого эта матрица диагональная.
Красиво? И вся работа по выводу закономерностей сделана одним махом.
То что диагональная матрица состоит из величин квадрата золотого сечения и его обращения - значит, что в смещённом базисе изменения при смене уровня происходят через одновременное умножение одних составляющих и деление других составляющих, на одну величину.
Причём, эта величина совпадает с масштабом увеличения. Возможно, это он и есть. А обращённая величина в пределе уменьшается до нуля и, вероятно, составляет отклонение от идеальной мозаики.
При расчёте размеров для ещё одного масштаба, в сторону уменьшения, получаются значения и . Фигуры становятся сами как линии сетки. Эти значения можно использовать для отсчёта угла - на какую величину происходит поворот при идеализации.
Вектор после смещения базиса:
После этого умножаем первые две составляющие на ноль - они при приближении к идеальной форме уменьшаются. Другие две составляющие менять не надо, это стало бы дополнительным масштабированием.
Потом возвращаем базис.
И пересчитаем первую пару в декартовы координаты.
Вот и получился нужный угол.
Думаю, может быть интересно, как выглядит зависимость вектора от масштаба в явном виде:
Всё же, так вычислять, бывает, проще чем протаскивать вектор через три матрицы.
Вторая составляющая с виду отличается от остальных. Но она может быть представлена в аналогичной форме, для этого требуется изменить одновременно и используемую степень и знак перед дробью.
Проявляются закономерности золотого сечения. Выражать значения через него может быть даже проще. Вектор, выраженный через :
И через него же можно определить синий отрезок в зависимости от уровня, используя для выражения величины угла комплексные числа
Если выразить длину через параметры:
То может напомнить о себе одна особенность: вместе с параметрами согласовывается не только поворот, но и масштаб. Для наложения сеток нужно идеальную мозаику сжать на коэффициент . В уменьшенных единицах длина рассчитанного отрезка увеличится и станет сравнима с длиной отрезка для шестиугольной формы.
Вот и разобрались с трансформацией для наложения.
Теперь надо составить схему разбиения. Сначала стоит разобраться с теми деталями, которые при разбиении окружают фигуру по границам. Каждая плашка порождается одновременно двумя фигурами предыдущего уровня, и тут есть два варианта: создавать обе, а потом одну из них удалять, либо как-то разделить плашки на два типа и создавать только плашки одного из них. И оказывается, что схема разбиения это позволяет: часть плашек может быть из схемы удалена, так как соответствующая деталь должна будет создаваться соседней фигурой. Мозаика всё равно сойдётся, прямо как с выступами и выемками элементов пазла.
Для того чтобы обозначить фигуру достаточно задать её тип, координаты центра и направление. Схема разбиения будет показывать как при разбиении меняются эти данные. Направление обязательно даже для симметричных фигур, ведь при превращении в шляпы варианты получаются различными.
Для удобства добавлю новый тип координат. Для одного направления два числа задают: первое - количество единичных шагов и второе - количество шагов с коэффициентом золотого сечения. Всего направлений шесть, но обратные направления задаются отрицательными величинами. А из оставшихся трёх направлений одно раскладывается на два остальных.
Для таких координат поворот достаточно прост. Поворот на одно деление задаётся как
Получилось четыре составляющие - по два на направление.
Так как координаты центра фигуры могут быть полуцелым числом, то для того чтобы значения остались целыми, они дополнительно увеличены в два раза.
Например, первая фигура это большой «треугольный» шестиугольник, координаты его вершин получились:
[[0, -2, -2, -2], [2, 4, -2, -2], [2, 4, 0, -2], [0, -2, 2, 4], [-2, -2, 2, 4], [-2, -2, 0, -2]]
По совпадению, увеличение координат на коэффициент золотого сечения похоже на только что показанный поворот. Различие в знаке перед :
При смене уровня такое увеличение происходит дважды. Координаты модифицируется по принципу:
Поэтому те же координаты на другом масштабе будут:
[[-2, -4, -4, -6], [6, 10, -4, -6], [6, 10, -2, -4], [-2, -4, 6, 10], [-4, -6, 6, 10], [-4, -6, -2, -4]]
По всем фигурам получились такие данные
// Исходные данные о рёбрах фигур
[
[[0, 0, 0, 0], [1, 3, 0, 0], [0, 0, 1, 0], [-1, -3, 1, 3], [-1, 0, 0, 0], [0, 0, -1, -3], [1, 0, -1, 0]],
[[0, 0, 0, 0], [1, 3, 0, 0], [0, 0, 1, 2], [-1, -3, 0, 0], [0, 0, -1, -2]],
[[0, 0, 0, 0], [1, 3, 0, 0], [1, 0, 0, 1], [0, -1, 1, 1], [-2, -2, 0, 0], [0, 0, -1, -2]],
[[0, 0, 0, 0], [0, 0, 0, 3], [0, -3, 0, 0], [0, 3, 0, -3]]
]
// Получившиеся координаты
1, 0: [[0, -2, -2, -2], [2, 4, -2, -2], [2, 4, 0, -2], [0, -2, 2, 4], [-2, -2, 2, 4], [-2, -2, 0, -2]]
1, 1: [[2, 2, -2, -4], [2, 2, 0, 2], [0, 2, 2, 2], [-2, -4, 2, 2], [-2, -4, 0, 2], [0, 2, -2, -4]]
1, 2: [[2, 4, 0, -2], [0, -2, 2, 4], [-2, -2, 2, 4], [-2, -2, 0, -2], [0, -2, -2, -2], [2, 4, -2, -2]]
1, 3: [[0, 2, 2, 2], [-2, -4, 2, 2], [-2, -4, 0, 2], [0, 2, -2, -4], [2, 2, -2, -4], [2, 2, 0, 2]]
1, 4: [[-2, -2, 2, 4], [-2, -2, 0, -2], [0, -2, -2, -2], [2, 4, -2, -2], [2, 4, 0, -2], [0, -2, 2, 4]]
1, 5: [[-2, -4, 0, 2], [0, 2, -2, -4], [2, 2, -2, -4], [2, 2, 0, 2], [0, 2, 2, 2], [-2, -4, 2, 2]]
1, D: [[-2, -4, -4, -6], [6, 10, -4, -6], [6, 10, -2, -4], [-2, -4, 6, 10], [-4, -6, 6, 10], [-4, -6, -2, -4]]
2, 0: [[-1, -3, -1, -2], [1, 3, -1, -2], [1, 3, 1, 2], [-1, -3, 1, 2]]
2, 1: [[1, 2, -2, -5], [1, 2, 0, 1], [-1, -2, 2, 5], [-1, -2, 0, -1]]
2, 2: [[2, 5, -1, -3], [0, -1, 1, 3], [-2, -5, 1, 3], [0, 1, -1, -3]]
2, 3: [[1, 3, 1, 2], [-1, -3, 1, 2], [-1, -3, -1, -2], [1, 3, -1, -2]]
2, 4: [[-1, -2, 2, 5], [-1, -2, 0, -1], [1, 2, -2, -5], [1, 2, 0, 1]]
2, 5: [[-2, -5, 1, 3], [0, 1, -1, -3], [2, 5, -1, -3], [0, -1, 1, 3]]
2, D: [[-4, -7, -3, -5], [4, 7, -3, -5], [4, 7, 3, 5], [-4, -7, 3, 5]]
3, 0: [[-1, -3, -1, -2], [1, 3, -1, -2], [3, 3, -1, 0], [3, 1, 1, 2], [-1, -3, 1, 2]]
3, 1: [[1, 2, -2, -5], [1, 2, 0, 1], [1, 0, 2, 3], [-1, -2, 4, 3], [-1, -2, 0, -1]]
3, 2: [[2, 5, -1, -3], [0, -1, 1, 3], [-2, -3, 3, 3], [-4, -3, 3, 1], [0, 1, -1, -3]]
3, 3: [[1, 3, 1, 2], [-1, -3, 1, 2], [-3, -3, 1, 0], [-3, -1, -1, -2], [1, 3, -1, -2]]
3, 4: [[-1, -2, 2, 5], [-1, -2, 0, -1], [-1, 0, -2, -3], [1, 2, -4, -3], [1, 2, 0, 1]]
3, 5: [[-2, -5, 1, 3], [0, 1, -1, -3], [2, 3, -3, -3], [4, 3, -3, -1], [0, -1, 1, 3]]
3, D: [[-4, -7, -3, -5], [4, 7, -3, -5], [6, 9, -1, -1], [4, 5, 3, 5], [-4, -7, 3, 5]]
4, 0: [[0, 2, 0, -4], [0, 2, 0, 2], [0, -4, 0, 2]]
4, 1: [[0, 4, 0, -2], [0, -2, 0, 4], [0, -2, 0, -2]]
4, 2: [[0, 2, 0, 2], [0, -4, 0, 2], [0, 2, 0, -4]]
4, 3: [[0, -2, 0, 4], [0, -2, 0, -2], [0, 4, 0, -2]]
4, 4: [[0, -4, 0, 2], [0, 2, 0, -4], [0, 2, 0, 2]]
4, 5: [[0, -2, 0, -2], [0, 4, 0, -2], [0, -2, 0, 4]]
4, D: [[2, 4, -4, -8], [2, 4, 2, 4], [-4, -8, 2, 4]]
Теперь можно составить таблицу для разбиения фигур, дополнив координаты номером фигуры и направлением.
// данные о разбиении
fgs = [
[[-2, -2, 2, 4, 1, 0], [0, 1, 3, 5, 2, 5], [4, 7, 1, -1, 3, 5],
[2, 4, 0, -2, 1, 0], [3, 5, -3, -6, 2, 0], [1, -1, -5, -6, 3, 3],
[0, -2, -2, -2, 1, 2], [-3, -6, 0, 1, 2, 1], [-5, -6, 4, 7, 3, 1],
[0, 0, 0, 0, 4, 0]],
[[-5, -7, 1, 2, 3, 1], [-2, -3, -1, -1, 1, 0], [0, 0, 0, 0, 2, 5],
[2, 3, 1, 1, 1, 5], [5, 7, -1, -2, 3, 4]],
[[-5, -7, 1, 2, 3, 1], [-2, -3, -1, -1, 1, 0], [0, 0, 0, 0, 2, 5],
[2, 3, 1, 1, 1, 5], [5, 7, -1, -2, 3, 4], [1, 2, 4, 5, 3, 0]],
[[0, 0, 0, 0, 1, 1]]
];
Содержание этого массива можно визуализировать так:
После построения идеальной мозаики и её поворота дело остаётся за округлением координат. В шестиугольно-треугольной сетке округление характеризуется выбором того, притягивать значение к себе будут центры треугольников или центры шестиугольников. Кроме того, надо учесть варианты, центр координат это центр шестиугольника или центр одного из двух треугольников.
Рабочий вариант, в котором не происходит ошибок округления - когда центр планок находится в шестиугольнике, а центры треугольников в треугольниках.
Сам алгоритм:
Сначала координаты масштабируются, сдвигаются и искажаются, так чтобы два треугольника с общей гранью трансформировались в квадрат с размером один на один.
// три варианта выравнивания центра
let shift = [[0, 0], [2 / 3, 0], [1 / 3, 1 / 3]][ct]
let x = shift[0] + x0 + 0.001
let y = shift[1] + y0 * sqrt(3) / 2 + x0 / 2 + 0.001
У таких координат разделяются целая и дробная части.
let xn = floor(xd); x = x - xn;
let yn = floor(yd); y = y - yn;
Для округления значений к центрам треугольников дробная часть заменяется, в зависимости от того, какая координата больше.
if(x > y) {dx = 2 / 3; dy = 1 / 3;} else {dx = 1 / 3; dy = 2 / 3;}
Для округления значений к центрам шестиугольников условия другие:
if (x + y > 1)
{
if (2 * x < y) {dx = 0; dy = 1;}
else if (2 * y < x) {dx = 1; dy = 0;}
else {dx = 1; dy = 1;}
}
else
{
if (2 * y > x + 1) {dx = 0; dy = 1;}
else if (2 * x > y + 1) {dx = 1; dy = 0;}
else {dx = 0; dy = 0;}
}
Координаты с обновлённой дробной частью преобразуются в обратном порядке.
x = xn + dx;
y = yn + dy;
let x1 = x - shift[0]
let y1 = (y * 2 - x) / sqrt(3) - shift[1];
Вот и разобрались с теорией. А по практике: наконец-то у меня появилась возможность показывать мозаику любого размера, а не только с ручной расстановкой.
Иллюстрации:
Хайрезы: первый, второй, третий.
Думаю, есть такие кто считает, что сама программа полезнее описания. Вот она:
Рисуем мозаику
// оставлены фрагменты для работы с захватчиком кадров ccapture.js
// объявление, параметр количество захватываемых кадров, флаг запуска
let capturer, Nframes = 60, cap = 0;
let kf, zm, xs, ys;
let color1, color2, color3, color4, color5, color6, cf;
let shp0, shp1, shp2, shp3, shp4;
let steps;
// константа золотого сечение
const fi = (Math.sqrt(5) + 1) / 2;
// параметры отображения
let draw_arrow, draw_center, draw_triangle, center_type, qk, qh;
let start_angle, liner_type, lev_counter;
let draw_hat_center, draw_ideal, draw_hex;
function setup()
{
// выбор варианта размера канвы
let R = [[1920, 1080], [640, 480]][1];
createCanvas(R[0], R[1]);
// коэффициенты преобразования целых координат первого типа в экранные
kf = [sqrt(3) / 2, 1 / 2, sqrt(3) / 2, 1 / 2, 0, 0, 0, 0];
// цвета. Заданы в hsl, полупрозрачные
color1 = color('hsla(220,100%,75%,0.5)');
color2 = color('hsla(200,100%,75%,0.5)');
color3 = color('hsla(120,50%,75%,0.5)');
color4 = color('hsla(189,57%,89%,0.5)');
color5 = color('hsla(189,57%,59%,0.5)');
color6 = color('hsla(189,57%,59%,0.1)');
// расстановка цветов по плиткам
cf = [color3, color2, color4, color5, color3];
// управление вариантами отображения
draw_ideal = 1; // нарисовать идеальную мозаику
draw_hex = 1; // нарисовать полученную мозаику
draw_center = 0; // вывести позиции центров до и после округления
lev_counter = 3; // количество уровней разбиения
start_angle = 3; // поворот базовой фигуры
qk = 1; // сглаживание ушек фигуры
draw_hat_center = 1; // показывать центр плитки
qh = 1; // сдвиг показываемого центра плитки
draw_triangle = 1; // рисовать центр большого треугольника
draw_arrow = 1; // рисовать стрелки, они же дополнительные границы
// тип центра
center_type = ([1, 0, 0, 1][lev_counter % 4] + start_angle) % 2;
// при смене уровня это то один треугольник то другой
// смена чётности угла поворота тоже влияет
// тип разлиновки. Сделано так чтоб совпадал с типом центра
liner_type = center_type;
// инициализация форм
set_shapes();
// выбор масштаба
zm = [8, 12, 32, 24, 6, 2][0]
// координаты начала отсчёта, установлены в центр
xs = width / 2;
ys = height / 2;
// шаги, 12 направлений в целых координатах первого типа
steps = [[2, 0, 0, 0], [0, 0, 1, 1], [1, 3, 0, 0], [0, 0, 0, 2], [-1, 3, 0, 0], [0, 0, -1, 1], [-2, 0, 0, 0], [0, 0, -1, -1], [-1, -3, 0, 0], [0, 0, 0, -2], [1, -3, 0, 0], [0, 0, 1, -1]];
// работа с захватом кадров
if (cap) {capturer = new CCapture({verbose: false, display: true, framerate: 60, motionBlurFrames: 0, quality: 99, format: "png", timeLimit: 20, frameLimit: 0, autoSaveTime: 0});}
}
// Пересчёт из координат первого типа в пизицию на экране, поддерживается поворот
function place(p)
{
return {
x: xs + zm * (kf[0] * p[0] + kf[2] * p[2] + kf[4] * p[1] + kf[6] * p[3]),
y: ys - zm * (kf[1] * p[1] + kf[3] * p[3] - kf[5] * p[0] - kf[7] * p[2])
};
}
// функция отрисовки кадра
function draw()
{
// при первом кадре включение захвата кадров
if (cap) {if (frameCount == 1) {capturer.start(); console.log('start recording.');}}
// очищаем канву после предыдущего кадра
background(240);
// аргумент - цвет в градации серого
// рисование кадра
draw_shape();
// завершение захвата кадров
if (cap) {capturer.capture(document.getElementById('defaultCanvas0')); if (frameCount > Nframes) {noLoop(); console.log('finished recording.'); capturer.stop(); capturer.save();}}
}
//Функция отрисовки фигуры через массив с координатами
function draw_vertex(fc)
{
beginShape(); for (let i in fc) {let c = place(fc[i]); vertex(c.x, c.y);}; endShape(CLOSE);
}
// Сложение координат
function add(c, d)
{
let r = []; for (i in c) {r[i] = c[i] + d[i]; }; return r;
}
// Копирование координат
function crd(c)
{
return [c[0], c[1], c[2], c[3]];
}
// разлиновка на произвольный размер площади
function liner(size, type)
{
if (!size) {size = 10;}
if (!type) {type = 0;}
// сдвиг сетки
let dd = [[2, 2], [0, 4], [0, 0]][type];
let dx = dd[0], dy = dd[1];
strokeWeight(1);
stroke(color(180, 180, 180, 60));
noFill();
// подготовка шестиугольника из четырёхугольников
let stv = [0, 3, 5, 8];
fcn = [];
for (j = 0; j < 3; j++)
{
let stt = [0, 0, 0, 0]; fcn.push([stt]);
for (let i in stv) {let std = steps[(stv[i] + j * 2) % 12]; stt = add(stt, std); fcn[j].push(stt);}
stt = [0, 0, 0, 0]; fcn.push([stt]);
for (let i in stv) {let std = steps[(stv[i] + j * 2 + 6) % 12]; stt = add(stt, std); fcn[j].push(stt);}
}
// рисование центрального шестиугольника, раз он не в ходит в шесть секторов
for (let j in fcn)
{
let fcm = fcn[j];
beginShape();
for (let i in fcm)
{
let fci = fcm[i];
fci = [dx + fci[0], dy + fci[1], fci[2], fci[3]];
c = place(fci);
vertex(c.x, c.y);
}
endShape(CLOSE);
}
// рисование шести секторов
for (let vn = 0; vn < 6; vn++)
{
fs1 = [0, 0, 0, 0];
for (let js = 0; js < size; js++)
{
fcd = steps[vn * 2]; fs1 = add(fs1, fcd); fs1 = add(fs1, fcd); fs2 = fs1;
for (let jf = 0; jf < (js + 1); jf++)
{
fs0 = fs2; fcd = steps[(vn * 2 + 4) % 12]; fs2 = add(fs0, fcd); fs2 = add(fs2, fcd);
for (let j in fcn)
{
let fcm = fcn[j];
beginShape();
for (let i in fcm)
{
let fci = fcm[i];
fci = [dx + fci[0], dy + fci[1] + fs0[1], fci[2] + fs0[0], fci[3]];
c = place(fci);
vertex(c.x, c.y);
}
endShape(CLOSE);
}
}
}
}
}
// Функция отрисовки фигуры.
// Аргументы:
// shp - это объект заданной фигуры.
// его формат:
// три массива,
// sh - описание прохождения по контуру
// mr - выделение шагов, относящихся к ушкам паззла
// cc - описание где находятся центры фигур, которые нужно отобразить
// Остальные аргументы:
// pos - это начальная позиция отрисовки.
// v - направление
// h - размер "ушек", от 0 до 1
// h2 - сдвиг центра фигуры, от 0 до 1
function draw_fig(shp, pos, v, h, h2)
{
// имена для краткого обращения
let sh = shp.sh;
let mr = shp.mr;
// начальная позиция запоминается
let start = pos;
// sh[0] - это указание с какого шага начинается отрисовка,
// так как несколько первых шагов составляют путь
// от центра фигуры до контура, не отрисовываются
shape = [];
for (let i = 1; i < sh[0]; i++)
{
let vn = ((v + sh[i]) % 12 + 12) % 12;
pos = add(pos, steps[vn]);
}
// сохраняется стартовая позиция на контуре
shape.push(pos);
// проход остального контура с сохранением позиций
for (let i = sh[0]; i < sh.length; i++)
{
let vn = ((v + sh[i]) % 12 + 12) % 12;
pos = add(pos, steps[vn]);
shape.push(pos);
}
// Дальше позиции будут переведены в координаты на экране
fig = [];
for (i = 1; i < shape.length; i++)
{
let c1 = place(shape[i]);
let c2;
// если данная позиция относится к "ушку" первого типа,
// то её сдвиг будет к прошлой позиции
if (mr[i] == 1)
{
c2 = place(shape[i - 1]);
}
// если данная позиция относится к "ушку" второго типа,
// то её сдвиг будет к следующей позиции
if (mr[i] == 2)
{
c2 = place(shape[i + 1]);
}
// Расчёт координат, в зависимости от того нужен сдвиг или нет
if (mr[i] > 0)
{
x = lerp(c1.x, c2.x, h);
y = lerp(c1.y, c2.y, h);
}
else
{
x = c1.x;
y = c1.y;
}
// координаты сохраняются
fig.push([x, y]);
}
// массив для сохранения линий,
// которые из границ плиток превращаются в стрелки
let lines = []
// перебираем заданные в фигуре центры
for (let i in shp.cc)
{
sh = shp.cc[i];
// координаты каждого центра заданы в формате перечисления
// шагов которые нужно пройти чтобы добраться до этого центра из общего
// начинаем с центра общей фигуры
pos = start;
// направление
let vn;
// предпоследняя позиция будет сохранена
let posp;
for (let shv of sh)
{
vn = ((v + shv) % 12 + 12) % 12;
posp = pos;
pos = add(pos, steps[vn]);
}
// расчёт координат предпоследней позиции
let c0 = place(posp);
// координаты самого центра
let c1 = place(pos);
posl = pos;
// делаем ещё один шаг в том же направлении как последний шаг.
pos = add(pos, steps[vn]);
let c2 = place(pos); // координаты смещённого центра
// расчёт кординат в зависимости от смещения
// lerp функция p5js для линейного отображения,
// h2=0 становится c1.x, h2=1 становится c2.x
x = lerp(c1.x, c2.x, h2);
y = lerp(c1.y, c2.y, h2);
if(draw_hat_center)
{
// отрисовка линии ведущей к центру
line(c0.x, c0.y, x, y)
// и центра
circle(x, y, 10);
}
// vn это направление шага к центру,
// а мы от него уходим
// разворот направления
vn = (vn + 6) % 12;
// превращаются в стрелки только центры 1 и 2
if (i > 0 && i < 3)
{
// позиция центра
pos = posl;
// определяется первая точка
pos = add(pos, steps[(vn + 12) % 12]);
// в указании направления
// "-2" - направление,
// "+ 12" - добавка чтоб не получилось отрицательное число.
pos = add(pos, steps[(vn - 2 + 12) % 12]);
// первая точка определена
p1 = pos;
// определяется вторая точка
pos = add(pos, steps[(vn - 5 + 12) % 12]);
p2 = pos;
// четвёртая точка это шаг от второй
p4 = add(pos, steps[(vn - 1 + 12) % 12]);
pos = add(p2, steps[(vn - 3 + 12) % 12]);
// третья точка это тоже шаг от второй
p3 = pos
pos = add(pos, steps[(vn - 6 + 12) % 12]);
// линии две: составляющая грань и изображающая стрелку
let ln = [[p1, p2, p3, pos], [p4, p2, p2, p3]];
// будет рассчитана линия при плавном переходе
let lns = [];
for (i in ln[0])
{
// первая линия берётся как есть
c1 = place(ln[0][i]);
// стрелка смещена в направление своего указания
c2 = place(add(ln[1][i], steps[(vn - 8 + 12) % 12]));
x = map(h, 0, 1, c1.x, c2.x);
y = map(h, 0, 1, c1.y, c2.y);
lns.push([x, y])
}
lines.push(lns);
}
}
// теперь всё это рисуем
beginShape();
for (i in fig)
{
vertex(fig[i][0], fig[i][1]);
}
endShape(CLOSE);
if(draw_arrow)
{
// яркость линий грани и стрелки различается
// четвёртый аргумент в указании цвета линии - прозрачность
stroke(60, 60, 60, lerp(60, 255, 1 - h));
for (let i in lines)
{
lns = lines[i];
for (let i = 0; i < 3; i++)
{
line(lns[i][0], lns[i][1], lns[i + 1][0], lns[i + 1][1]);
}
}
}
// возврат обычного цвета линии
stroke(60)
}
// заполнение данных фигур
function set_shapes()
{
// определяются используемые функции
// Превращение рёбер в вершины
function edge_to_vertex(dd)
{
let ps = [];
let p = [0, 0, 0, 0];
for (let d of dd)
{
p = add(p, d);
ps.push(p);
}
return ps
};
// вращение координат второго типа
function rots(p)
{
return [-p[2], -p[3], p[0] + p[2], p[1] + p[3]]
}
// вычисление экранных коордиинат по целым координатам второго типа (использовалась при отладке)
function place2(p)
{
let x, y;
x = (p[0] + p[1] * fi + (p[2] + p[3] * fi) / 2);
y = ((p[2] + p[3] * fi) * sqrt(3) / 2);
x = xs + zm * x;
y = ys - zm * y;
return {
x: x,
y: y
}
}
// Здесь есть небольшое отличие от текста статьи
// Четвёртая фигура была переложена на индекс ноль
// данные о рёбрах фигур
dt = [
[[0, 0, 0, 0], [0, 0, 0, 3], [0, -3, 0, 0], [0, 3, 0, -3]],
[[0, 0, 0, 0], [1, 3, 0, 0], [0, 0, 1, 0], [-1, -3, 1, 3], [-1, 0, 0, 0], [0, 0, -1, -3], [1, 0, -1, 0]],
[[0, 0, 0, 0], [1, 3, 0, 0], [0, 0, 1, 2], [-1, -3, 0, 0], [0, 0, -1, -2]],
[[0, 0, 0, 0], [1, 3, 0, 0], [1, 0, 0, 1], [0, -1, 1, 1], [-2, -2, 0, 0], [0, 0, -1, -2]]
]
// создаются данные о форме
let dtf0 = []
for (fg = 0; fg < dt.length; fg++)
{
let dtf1 = []
for (let rot2 = 0; rot2 < 7; rot2++)
{
let dtf2 = [];
ps = edge_to_vertex(dt[fg]);
// суммирование координат для вычисления центра.
// Кроме третьей фигуры, для неё берётся такой же как у второй фигуры
if (fg == 3)
{
sm = [1 / 2, 3 / 2, 1 / 2, 2 / 2]
}
else
{
sm = [0, 0, 0, 0];
for (let i = 1; i < ps.length; i++) {p0 = ps[i - 1]; sm = add(sm, p0);}
for (i in sm) {sm[i] = sm[i] / (ps.length - 1)}
}
// Центрирование и увеличение в два раза
for (let i = 0; i < ps.length; i++)
for (let j = 0; j < 4; j++)
ps[i][j] = (ps[i][j] - sm[j]) * 2
// расчёт поворотов формы и смены масштаба
for (let i = 1; i < ps.length; i++)
{
p0 = ps[i - 1];
if (rot2 == 6)
{
p0 = [p0[0] + p0[1], p0[0] + 2 * p0[1], p0[2] + p0[3], p0[2] + 2 * p0[3]]
}
else
for (let rot = 0; rot < rot2; rot++)
{
p0 = rots(p0)
}
dtf2.push(p0);
}
dtf1.push(dtf2)
}
dtf0.push(dtf1)
}
dtf = dtf0;
// Четвёртая фигура переставлена на индекс ноль
// данные о разбиении
fgs = [
[
[0, 0, 0, 0, 1, 1]
],
[
[0, 0, 0, 0, 0, 0],
[-2, -2, 2, 4, 1, 0], [0, 1, 3, 5, 2, 5], [4, 7, 1, -1, 3, 5],
[2, 4, 0, -2, 1, 0], [3, 5, -3, -6, 2, 0], [1, -1, -5, -6, 3, 3],
[0, -2, -2, -2, 1, 2], [-3, -6, 0, 1, 2, 1], [-5, -6, 4, 7, 3, 1]
],
[
[-5, -7, 1, 2, 3, 1], [-2, -3, -1, -1, 1, 0], [0, 0, 0, 0, 2, 5],
[2, 3, 1, 1, 1, 5], [5, 7, -1, -2, 3, 4]
],
[ [-5, -7, 1, 2, 3, 1], [-2, -3, -1, -1, 1, 0], [0, 0, 0, 0, 2, 5],
[2, 3, 1, 1, 1, 5], [5, 7, -1, -2, 3, 4], [1, 2, 4, 5, 3, 0]
]
];
// построение нового уровня
function build(lev, lp)
{
let levp = lev[lp];
lev.push([]);
let nl = lev.length - 1;
let levn = lev[nl];
for (let fg of levp)
{
// маштабирование, меняется позиция центра фигуры
p2 = [fg[0] + fg[1], fg[0] + 2 * fg[1], fg[2] + fg[3], fg[2] + 2 * fg[3]]
// берём значение из таблицы разбиения по текущей фигуре
for (let f of fgs[fg[4]])
{
// берём координаты
p = crd(f)
// вращаем координаты
for (let i = 0; i < fg[5]; i++) {p = rots(p);}
// вычисляем новое направление
let v = (f[5] + fg[5]) % 6;
// добавляем сдвиг координат от родительской фигуры
p = add(p, p2);
// кроме координат и направления заполняются тип родительской фигуры и тип родительской для родительской
levn.push([p[0], p[1], p[2], p[3], f[4], v, fg[4], fg[6]])
}
}
// возврат номера нового уровня
return nl;
}
// данные о четырёх фигурах
// большой треугольник
shp1 = {
sh: [5, -3, -1, 2, 0,
-3, -5, -2, -4, 5, -5, 4, 6, 3, 5, 5, 3, 6, 4, 1, -1, 2, 0, 3, 1, 1, -1, 2, 0, -3, -5, -2, -4, -1, -3],
mr: [0, 0, 1, 0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 1, 0, 2, 0],
cc: [
[-3, 3],
[-3, -1, 2, 0, 3, 5, 5, 3],
[-3, -5, 4, 2, 5, -5],
[-3, -5, -5, -3, 0, -2, 1, 3]
]
}
// центр большого треугольника
shp0 =
{
sh: [2, -3,
-5, 4, 2, 5, 3, 0, -2, 1, -1, -1, -3, 6, -4, 5],
mr: [0, 1, 0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2],
cc: [[-3, 3]]
}
// планка
shp2 = {
sh: [2, -5,
5, 2, 4, 1, -1, -1, -3, 0, -2, -5, -3, -3, -5, -2, -4, -1, 5, 5, 3, 6, 4, 1, 3, 3],
mr: [0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0],
cc: [
[-5, 1],
[-5, -3, -3, -1]
]
}
// лопасть
shp3 = {
sh: [2, -5,
5, 2, 4, 1, -1, -1, -3, 0, -2, -5, -3, -3, -5, -2, -4, 5, 3, 6, 4, 1, 3, 3],
mr: [0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 1, 0, 2, 0, 0],
cc: [
[-5, 1],
[-5, -3, -3, -1]
]
}
// малый треугольник
shp4 = {
sh: [2, -1,
-3, 6, -4, 5, 3, 3, 1, 4, 2, 5, -1, -1, -3, 0, -2, 1, -5, -5],
mr: [0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 2, 0, 0],
cc: [
[-1, 5],
]
}
function level_create()
{
// начальная фигура - большой треугольник в центре
let lev = [[[0, 0, 0, 0, 1, start_angle]]];
nl = 0;
for (let i = 0; i < lev_counter; i++)
{
nl = build(lev, nl);
}
return lev;
}
lev = level_create()
}
// используемые функции
// рисование фигуры идеальной мозаики
function dr(fgd)
{
beginShape();
for (let f1 of fgd)
{
let f = add(p, f1);
let c = pl(f); vertex(c.x, c.y);
}
endShape(CLOSE);
}
// расчёт поворота фигуры идеальной мозаики
// в том числе округление
// p - целые коордианты второго типа
// u1 - тип фигуры (0, 1 - треугольники 2, 3 - планки)
// u2 - тип центра координат (три варианта)
function drc(p, u1, u2)
{
let x, y;
// константы поворота
const xa = (8 + 3 * sqrt(5)) / sqrt(5) / 4;
const ya = sqrt(3) / sqrt(5) / 4;
//const za = (7 / 5 + 3 / sqrt(5));
// получаем координаты
x = (2 * p[0] - p[1] + (p[1] - p[0]) * fi +
(2 * p[2] - p[3] + (p[3] - p[2]) * fi) / 2);
y = ((2 * p[2] - p[3] + (p[3] - p[2]) * fi) * sqrt(3) / 2);
// поворот
let xc = x * xa - y * ya;
let yc = y * xa + x * ya;
let xc2 = 0;
let yc2 = 0;
shift = [[1, 3 / 2], [2, 0], [0, 0]][u2];
// подстройка центра
let xd = (xc - shift[0]) / 3;
let yd = (yc - shift[1]) / 3;
// искажение
xd = xd + 0.001;
yd = yd * sqrt(3) / 2 + xd / 2 + 0.001;
// резеделение дробной и целой части
let xd2 = floor(xd); xd = xd - xd2;
let yd2 = floor(yd); yd = yd - yd2;
let dx, dy;
// выраванивание в зависимости от типа фигуры
if (u1 > 1)
{
if(xd + yd > 1)
{
if((2 * xd) < (yd)) {dx = 0; dy = 1;}
else
if((2 * yd) < (xd)) {dx = 1; dy = 0;}
else
{dx = 1; dy = 1;}
}
else
{
if((2 * yd - 1) > xd) {dx = 0; dy = 1;}
else
if((2 * xd - 1) > yd) {dx = 1; dy = 0;}
else
{dx = 0; dy = 0;}
}
}
else
{
if(xd > yd) {dx = 2 / 3; dy = 1 / 3;}
else {dx = 1 / 3; dy = 2 / 3;}
}
xd3 = xd2 + dx;
yd3 = yd2 + dy;
yd3 = yd3 - xd3 / 2;
xc2 = 3 * (xd3) + shift[0];
yc2 = 3 * (yd3) + shift[1];
// Заполнение идёт x <- yc и y <- xc потому что есть дополнительный поворот на 90 градусов
return {
x: xs - zm * yc2 / (sqrt(3)/2),
y: ys - zm * xc2,
x2: xc2,
y2: yc2
}
}
// пересчёт координат с учётом поворота
function plc(p)
{
let x, y;
const xa = (8 + 3 * sqrt(5)) / sqrt(5) / 4;
const ya = sqrt(3) / sqrt(5) / 4;
//const za = (7 / 5 + 3 / sqrt(5));
x = (2 * p[0] - p[1] + (p[1] - p[0]) * fi +
(2 * p[2] - p[3] + (p[3] - p[2]) * fi) / 2);
y = ((2 * p[2] - p[3] + (p[3] - p[2]) * fi) * sqrt(3) / 2);
let xc = x * xa - y * ya;
let yc = y * xa + x * ya;
// Заполнение идёт x <- yc и y <- xc потому что есть дополнительный поворот на 90 градусов
return {x: xs - zm * yc, y: ys - zm * xc}
};
function drawfig(p, fgd)
{
beginShape(); for (let f1 of fgd) {let f = add(p, f1); let c = plc(f); vertex(c.x, c.y);}
endShape(CLOSE);
}
// функция рисования кадра
function draw_shape()
{
// рисуется только один кадр
noLoop();
// список фигур уровня
let fg = lev[nl];
// разлиновка
// первый параметр - количесто шестиугольников от центра
// второй параметр - тип центра сетки
liner(17, liner_type)
stroke(60)
strokeWeight(1)
// идеальная мозаика
if(draw_ideal)
for (let f of fg)
drawfig(crd(f), dtf[f[4]][f[5]]);
for (let f of fg)
{
// тип фигуры
let t = f[4];
let pos;
let p = crd(f);
{
// расчёт центра фигуры
let cu = plc(p);
stroke(60);
// при необходимости он рисуется
if(draw_center)
{
fill(0, 0, 0, 64);
circle(cu.x, cu.y, 7);
}
// расчёт округлённых координат
let c = drc(p, t, center_type);
// перевод в целые координаты первого типа, с округлением
pos = [floor(-c.y2 * 4 / 3 + 1 / 2), floor(c.x2 * 2 + 1 / 2), 0, 0]
// учтён дополнительный поворот на 90 градусов
// задаётся цвет фигуры
fill(cf[f[4]]);
// рисуется новый центр
if(draw_center)
{
circle(c.x, c.y, 7);
line(c.x, c.y, cu.x, cu.y)
// при отладке отображалось как область попадания
//fill(cf[f[4]]); circle(c.x, c.y, zm * 3)
}
// рисование мозаики
if(draw_hex)
{
// выбор фигуры по её номеру
let shp = [shp4, shp1, shp2, shp3][f[4]]
// пересчёт угла поворота из 6 делений в 12 делений
let v = f[5] * 2
// коррекция координат из-за различия позиций центров в разных представлениях фигуры
// поворот тоже различается
if(t == 0) {pos = add(pos, steps[(v + 3) % 12]); v = (v + 4) % 12; }
if(t == 1) {pos = add(pos, steps[(v + 1) % 12]); v = (v + 4) % 12; }
if(t == 2) {pos = add(pos, steps[(v + 10) % 12]); v = (v + 6) % 12;}
if(t == 3) {pos = add(pos, steps[(v + 10) % 12]); v = (v + 6) % 12;}
fill(cf[f[4]]);
draw_fig(shp, pos, v, qk, qh)
// центр большого треугольника
if(t == 1 && draw_triangle)
{
fill(color1);
draw_fig(shp0, pos, v, qk, qh)
}
}
}
}
}
Мозаика нарисована. Только, есть одно замечание. При вычислении координат ребра не были разделены на большие и малые, а значит, изобразить черепах не получится. И другие фигуры тоже. Конечно, можно пройти от центральной плитки до крайних, подсчитывая, какими шагами точки разделены, собрать эти данные, использовать.
Но давайте сделаем кое-что другое. Расставим плитки заново, совсем другим способом. Ведь кроме построения через разбиение на четыре типа фигур существует построение, в котором количество типов фигур всего два. И на этот раз шаги будем различать.
Два типа фигур и два правила замены.
Одна плитка заменяется на восемь, а две плитки на семь, то есть, рост непропорциональный. Но участвующую в правилах группу из двух плиток следует считать единой фигурой. На всех масштабах, кроме начального, этот комплект по площади будет меньше, чем тот который соответствует простой шляпе.
Вместо шляпы буду использовать фигуру, которую я выше называл «лужа». Мозаика тогда состоит из округлых фигур. И удобно для рисования на листке бумаги, по шестиугольной сетке. Название поменяю на «кружка». Бывает же, что весь стол заставлен кружками от кофе, где-то в середине есть свободный островок.
Алгоритм будет состоять из обработки замкнутого контура, данные которого составляет информация о шагах обхода. Основная операция - соединение двух контуров для получения общего контура. При этом на контуре могут быть особые точки, позиция которых при обработке пересчитывается отдельно. Правила разбиения нужно будет перевести в алгоритм подходящих соединений контуров.
Прежде всего особенной точкой будет этот острый уголок снизу. Его смело можно назначать началом обхода контура, и привязываться к другим фигурам через него. Следующая особенная точка - там где две грани смыкаются не образуя угол, где два шага идут в одном направлении подряд. К этой точке можно подвести уголок приставляемой фигуры, тогда образуется узел смыкания трёх фигур.
// Задаём две базовые фигуры через шаги в формате 12 различных направлений
cup = {
// Кружка
c1: [1, 11, 2, 4, 1, 3, 6, 8, 5, 7, 7, 9, 0, 10],
// Пустое место (с кружкой)
c0: [1, 11, 2, 4, 1, 3, 3, 5, 5, 7, 4, 6, 9, 11, 8, 10, 7, 9, 0, 10],
// При этом определяем индексы особых точек пустого места (с кружкой)
p: [5, 7, 9, 11, 13],
// особое место кружки одно
p1: 10,
}
// Каждое из пяти разных добавлений контура имеет свой наклон
let v = [8, 10, 0, 2, 4];
//процесс добавления контуров это простой цикл
for(let i = 0; i < v.length; i++)
{
join_tiles(cup, v[i], cup.p[i], i);
}
// а вот что у него внутри
function join_tiles(cup, v, s, n)
{
// аргументы:
// cup - данные о кружках
// v - поворот присоединяемого контура
// s - индекс особой точки данного присоединения
// n - порядковый номер контура
// для базового и присоединяемого контуров
// используются короткие имена
let c0 = cup.c0;
let c1 = cup.c1;
// получающийся контур
let c2 = [];
// индексы особых точек будут модифицироваться
// новые индексы
let p2 = [];
// инициализируем используемые дальше индексы
let i = 0;
let j = s
// по базовому контуру идём в обратную сторону
for(; j >= 0; j--)
{
// разворачиваем первое направление
v0 = (c0[j] + 6) % 12;
// поворачиваем второе направление
v1 = (c1[i] + v) % 12
// при различии цикл прерывается
if(v0 != v1) {break;}
// иначе по второму контуру продвигаемся вперёд
i++;
}
// количество добавленных граней
let added = j - s;
// при таком удаляющем совмещении будет отрицательным
// копирование контура от начальной точки до точки совмещения
for(let k = 0; k <= j; k++) {c2.push(c0[k]);}
// значение индекса текущей точки
let p3 = s;
// добавление остатка добавляемого контура
for(; i < c1.length; i++)
{
// поворот
c2.push((c1[i] + v) % 12);
// если проходим особую точку кружки, то меняем текущую особую точку на неё.
if(i == cup.p1) {p3 = c2.length - 1;}
// количество добавленных граней увеличивается
added++;
}
// корректировка индексов особых точек
for(let k = 0; k < cup.p.length; k++)
{
let p = cup.p[k];
// текущая особая точка переопределена
if(p == s) {p = p3 - 1;}
// все предстоящие точки сдвигаются на количество добавленных шагов
else
if(p > s) {p += added;}
p2.push(p);
}
// добавление остатка базового контура
for(j = s + 1; j < c0.length; j++) {c2.push(c0[j]);}
Эта процедура сработала на первых трёх кружках, а на четвёртой получился сбой.
Дело в том что тот фрагмент, который я называл «пальмой от островка» при добавлении третьей кружки оставляет в контуре два шага по его форме и тут же возврат этих двух шагов. Это как будто трещина в плитке. Индекс особой точки из-за этого не попадает на нужное место. Придётся добавить процедуру, которая трещину убирает.
for(let i = 1; i < c2.length; i++)
{
// Проверяем последовательные шаги, если обращение одного совпадает с другим
if((c2[i - 1] + 6) % 12 == c2[i])
{
// то удаляем эти два элемента
c2.splice(i - 1, 2);
// чтобы продолжать обрабатывать пока трещин не станет
// откатываем индекс
i -= 2;
// кроме того, индексы особых точек тоже корректируются
for(let i = n + 1; i < p2.length; i++)
p2[i] -= 2;
}
}
// cup.c0 = c2; cup.p = p2;
У нас получилась форма для пустого места (с кружкой) следующего уровня. Теперь нужно найти масштабированную форму самой кружки. Для этого нужно найти шестое особое место, которое выглядит как место для присоединения уголка, и поставить туда кружку. Это место уже известно, это обновлённый вариант особой точки третьей кружки. После добавления придётся затягивать трещину между этой и четвёртой кружкой - и это уже на каждом уровне.
// записываем результат
if(n < 5)
{
cup.c0 = c2; cup.p = p2;
}
else
{
// добавленная шестая кружка обновляет информацию о пустом месте
cup.c1 = c2; cup.p1 = p3;
}
return;
}
// команда на то чтоб поставить шестую кружку с кофе
join_tiles(cup, v[2], cup.p[2], 6);
К определению списка шагов в цикле масштабирования нужно добавить вычисление координат особых точек, для того чтобы потом рассчитывать координаты на основе информации о месте плитки в иерархии.
Целые координаты будут третьего типа. Так же как второй тип, только вместо различия длины на коэффициент золотого сечения будут делиться по типу большие/малые. . Их удобно поворачивать.
let steps3 = [
[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1],
[-1, 1, 0, 0], [0, 0, -1, 1], [-1, 0, 0, 0], [0, 0, -1, 0],
[0, -1, 0, 0], [0, 0, 0, -1], [1, -1, 0, 0], [0, 0, 1, -1],
];
// экранная позиция вычисляется через расчёт для первого типа координат
function place3(pos)
{
return place([pos[0] * 2 + pos[1], pos[1], pos[2] * 2 + pos[3], pos[3]]);
}
function get_mpos(levels)
{
// аргумент levels - сколько рассчитывать уровней.
// сбор данных о позиционировании
let mpos = [];
// текущая позиция
let pos;
// цикл по масштабу
for(let lev = 0; lev <= levels; lev++)
{
// данные для текущего уровня
let mpos1 = [];
// данные для пустого места с кружкой
let mpos2 = [];
pos = [0, 0, 0, 0];
let k = 0;
for(let i = 0; i < cup.c0.length; i++)
{
// смена позиции
pos = add(pos, steps3[cup.c0[i]]);
// для особых точек сохранение позиции
if(i == cup.p[k]) {mpos2.push(pos); k++;}
}
// вносим расчитанное для пустого места
mpos1.push(mpos2);
// и то же самое для кружки
mpos2 = [];
pos = [0, 0, 0, 0];
let k = cup.p1;
for(let i = 0; i < cup.c1.length; i++)
{
// смена позиции
pos = add(pos, steps3[cup.c1[i]]);
// для особой точки сохранение позиции
if(i == k) {mpos2.push(pos);}
}
mpos1.push(mpos2);
// сохраняем оба массива как данные этого уровня
mpos.push(mpos1);
// последний цикл только для добавления координат, не для смены масштаба
if(lev < levels)
{
//процесс добавления контуров кружек
for(let i = 0; i < 5; i++) {join_tiles(cup, v[i], cup.p[i], i);}
// шестая кружка
join_tiles(cup, v[2], cup.p[2], 6);
}
// конец цикла масштаба
}
return mpos;
}
Во время отладки я, конечно, визуализировал контуры. Только, сложно было выбрать в каком формате смотреть: шляпном или кружечном. Так что режимы переключались.
Первый уровень расстановки был получен.
Но на втором слое расставленных кружек, опять на четвёртой, произошёл сбой.
Базовая фигура это «остров пустого места с прилагающейся к ней нулевой кружкой». Оказывается, третья кружка своим левым краем очень похожа на нулевую. И наблюдается сдвиг: четвёртая и пятая кружки присоединяются к особым точкам принадлежащим не к нулевой кружки, а третьей.
Так что теперь задача - выяснить на сколько шагов нужно производить этот сдвиг. Посчитать получилось не через расчёт по имеющимся данным. Способ сработал следующий: нужно поискать уголок, который образовался на краю соединения третьей и нулевой кружки. Сдвиг равен количеству шагов с особого места пятой кружки до этой выемки. И сдвиг для обоих особых мест будет одинаковым, то есть, для четвёртой кружки такой же.
Так что перед записью результата нужно добавить такой фрагмент:
// после добавления третьей кружки
if(n == 2)
{
// если это не нулевой уровень (на котором обошлось без этого сдвига)
if(p2[0] > 5)
{
// то начиная с третьей особой точки
let k = p2[2];
// ищем шаг, который идёт по направлению 11
k = c2.indexOf(11, k)
// а за ним должен идти шаг 8
while(k >= 0 && c2[k + 1] != 8) {k = c2.indexOf(11, k + 1);}
// и сдвигаем точки
p2[3] -= (p2[4] - k);
p2[4] = k;
}
}
И всё это заработало!
На этом можно и завершить.
Мозаика из групп четвёртого уровня выглядит так:
Срок для статьи был этот год, ведь для многих год запомнится именно этим открытием. Про призрака ничего написать не успел. Укажу схему разбиения, там всё примерно так же, только пустое место оказывается черепахой. Если длинные грани и малые грани усреднить, сохраняя направление, то получится заготовка для призрака. Если грани сделать изогнутыми, то получится плитка призрак и иначе чем апериодическую мозаику из него уже не собрать.
Вот схема для самостоятельного изучения:
С новым годом!