На немецком «эйнштейн» звучит как «один камень». Один - «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° означает, что точка лежит на пересечении синей линии и описанной вокруг треугольника окружности.
Обозначим длину синего отрезка.
И посчитаем, на раз-два-три:
Радиус окружности .
Расстояние от синей линии до центра окружности
Значит, длина тёмно-синего отрезка, выраженная через длину синего отрезка, то есть, точное изменение масштаба при изменении уровня, равно:
Можно заметить, что точный коэффициент немного меньше значения, которое было получено ранее.
И что в этой мозаике тоже проявляется золотое сечение.