Как рисовать мозаики типа «эйнштейн»

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

На немецком «эйнштейн» звучит как «один камень». Один - «ein», камень - «Stain». Всем известно, что под этой фамилией жил один замечательный человек, и звали его ... Но в статье речь не о нём. Речь о математической задаче по поиску одной плитки, такой чтобы составленная из неё мозаика была непериодической. «Один камень» - это про плитку. В мозаике Пенроуза таких плиток две, а хотелось бы возможности использовать только одну. Не вдаваясь в детали можно сказать, что задача одной плитки в этом году (2023) решена. Получены интересные красивые мозаики.

Сначала была найдена «шляпа эйнштейна» - плитка, похожая на шляпу. Или, по моему скромному мнению, на рубашку. Из неё можно сделать отличную непериодическую мозаику. Только, для построения используются как сами шляпы, так и их зеркальные отражения. Считать ли это одной плиткой? Можно и не считать.

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

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

В статье «Тридцать шесть градусов красоты» я описывал как рисовать мозаику Пенроуза. В статье «Два вида последовательного перебора пикселей» описывал закономерности для квадратов. Теперь напишу о том как рисовать эти новые мозаики. Получилась полная серия «36, 90, 120 градусов красоты».

Нарисуем координатную сетку. Сетки из равносторонних шестиугольников и равносторонних треугольников дополняют друг друга так что при их наложении получается сетка из четырёхугольников, с неравными сторонами. Если одну точку четырёхугольника расположить в (0,0), вторую в (1,0), то третья может быть рассчитана как (1, \tan(30°))=(1,1/\sqrt{3}). Четвёртая как (\cos(60°),\sin(60°))=(1/2,\sqrt{3}/2).

У следующего четырёхугольника, если считать против часовой, дальняя точка будет по координате (0, 2/\sqrt{3}). Остальные координаты симметричны, меняется только знак числа.

Если откладывать получившиеся короткие ребра начиная от центра, то координаты получатся для первого (0, 1/\sqrt{3}) и для второго (-1/2, 1/2\sqrt{3}). Направлений всего шесть, остальные координаты различаются только знаком.

Стоит изменить масштаб, чтобы длина короткого ребра была единичная.

Координаты этих семи точек возрастут в \sqrt{3} раз и станут:

(0,0),(\sqrt{3},0),(\sqrt{3},1),(\sqrt{3}/2,3/2),(0, 2);\;(0,1),(-\sqrt{3}/2,1/2)

Координаты можно хранить как целые числа с множителем \sqrt{3}/2 для x и 1/2 для y.

В этой системе отсчёта соответствующие координаты будут:

(0,0),(2,0),(2,2),(1,3),(0,4);(0,2),(-1,1)

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

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

(2,0,0,0),(1,3,0,0),(0,0,0,2),(0,0,1,1)

Изменения координаты по порядку поворота

[2,0,0,0],[1,3,0,0],[-1,3,0,0],[-2,0,0,0],[-1,-3,0,0],[1,-3,0,0]

— для большого шага и

[0,0,1,1],[0,0,0,2],[0,0,-1,1],[0,0,-1,-1],[0,0,0,-2],[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],
[-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],
    ]
  }
}

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

Особо не заморачиваясь можно вывести формулу для расстояний через отступы в таких координатах. r=\sqrt{\left(x+\frac{y}{2}\right)^2+3\left(\frac{y}{2}\right)^2} и посчитать:

Размер мелких лопастей \sqrt{\left(8+\frac{1}{2}\right)^2+3\left(\frac{1}{2}\right)^2}=\sqrt{73}

Размер крупных лопастей \sqrt{\left(21+\frac{3}{2}\right)^2+3\left(\frac{3}{2}\right)^2}=3\sqrt{57}

Их отношение равно 3\sqrt{\frac{57}{73}}\approx2{,}65\ldots.

Кроме изменения масштаба при смене уровня вся плоскость, похоже, вращается.

{\arctan\left(\frac{\sqrt{3}}{2}\times\frac{3}{21+3/2}\right)-\arctan\left(\frac{\sqrt{3}}{2}\times \frac{1}{8+1/2}\right)}=\\={\arctan\left(\frac{\sqrt{3}}{15}\right)-\arctan\left(\frac{\sqrt{3}}{17}\right)}={\arctan\left(\frac{\sqrt{3}}{129 }\right)}=\frac{1}{467{,}98\ldots}\ldots

Примерно на одну 468-ю всего оборота, примерно 0,76925°, меньше одного градуса.

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

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

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

Здесь становится ясно, что четыре плитки могут быть совсем другими, сохранять примерно ту же форму, но углы и размеры не обязаны накладываться на шестиугольную сетку. Их идеальная форма - другая, и значит, надо искать её.

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

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

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

Что известно о размерах фигур, если основываться только на схеме соединения? У нас будут две величины для шести сторон большого треугольника. Обозначим их 1 и x. Размер граней малого треугольника h=x-1. У планки размер по одной грани совпадает с x, вторую грань с учётом второй известной точки обозначим y=1+2l. У лопасти торцевая грань — как у планки, y. Одна длинная грань совпадает с x, другая 1+y=2+2l, и две оставшиеся грани около узла должны между собой совпадать, r.

В какую именно сторону поворачивается узел вентилятора? Планка по площади больше чем лопасть, но так как они складываются из одинакового количества шляп (и на втором разбиении тоже), они меняются по размеру в сторону сближения. И значит, различие в соотношении x > 1 + y уменьшается, узел вращается по часовой.

Более наглядно направление вращения видно на схеме связей уровней на примере лопасти.

Красные отрезки между собой аналогичны, различаются масштабом. Синие отрезки и тёмно-синий тоже отличаются только масштабом. Нам нужно найти точно положение узла большой лопасти, который справа. Для этого надо использовать то, что между соединяющимися красными линиями угол равен точно 60°. (Так как раздельные красные линии в идеале параллельны и односторонний парный угол 120°). Я так понял, что угол в 60° означает, что точка лежит на пересечении синей линии и описанной вокруг треугольника окружности.

Обозначим длину синего отрезка. a

И посчитаем, на раз-два-три:

  1. Радиус окружности (2/\sqrt{3})a.

  2. Расстояние от синей линии до центра окружности (1/2\sqrt{3})a

  3. Значит, длина тёмно-синего отрезка, выраженная через длину синего отрезка, то есть, точное изменение масштаба при изменении уровня, равно:

z=3/2+\sqrt{(2/\sqrt{3})^2-(1/(2\sqrt{3}))^2}=\frac{3+\sqrt{5}}{2}=2+\varphi=2{,}618\ldots

Можно заметить, что точный коэффициент немного меньше значения, которое было получено ранее.

И что в этой мозаике тоже проявляется золотое сечение.

Источник: https://habr.com/ru/articles/757132/


Интересные статьи

Интересные статьи

Перед вами обновлённая коллекция вредных советов для C++ программистов, которая превратилась в целую электронную книгу. Всего их 60, и каждый сопровождается пояснением, почему на самом деле ему не с...
Насколько популярна сегодня тема атомарных данных, настолько же она обширна для одной статьи. Можно подробно останавливаться на разных аспектах атомарности: например, анализировать memory ordering, ра...
Ранее мы рассказывали о бюджетных «студийниках» и «мониторах» для мультимедиа. Продолжим тему и обсудим примеры недорогих «полочников» для небольших пространств. ...
Продолжаем рассказ о создании мультипарадигменного языка программирования, сочетающего декларативный логический стиль с объектно-ориентированным и функциональным, который был бы удобен пр...
Недавно наши японские коллеги провели опрос пользователей печатной техники на территориях СНГ и очень удивились, узнав, что в России отношение к струйной печати до сих пор во многом строи...