
Всем привет! Привожу перевод статьи Penrose Tiling Explained. Мне самому было интересно как устроен алгоритм прорисовки мозаики. Удивился простоте и хочу поделиться.. Помимо почти не исправленного машинного перевода добавил свой перевод предложенного алгоритма на язык TypeScript. Привожу ссылку на песочницу в конце статьи. Версия TypeScript дополнена интерактивной формой изменения параметров алгоритма. Играя с параметрами, можно понять большую часть алгоритма даже без чтения разъяснения.
В оригинальной статье отсутствуют подписи под иллюстрациями.
Спойлет с оговоркой о реализации
Хочу заметить, что реализация алгоритма немного вводит в заблуждение. В Процессе запуска предложенного автором скрипта алгоритм демонстрирует целый прямоугольник заполненный позаикой Пенроуза создавая ощущение бесконечной плоскости. На самом же деле алгоритм заполняет мозаикой лишь предварительно созданный 10-ти угольник и выбирает масштаб таким образом, чтобы область просмотра помещалась внутри него полностью.
На прошлой неделе я опубликовал запутанный код на Python, который генерирует мозаику Пенроуза. Сегодня я объясню базовый алгоритм, лежащий в основе этого скрипта на Python, и поделюсь его не запутанной версией.
Алгоритм работает со списком красных и синих равнобедренных треугольников. У каждого красного треугольника угол при вершине равен 36°, а у каждого синего — 108°.

В Python такие треугольники можно представить в виде кортежей вида (color, A, B, C). Первый элемент, color, принимает значение 0 для красного треугольника и 1 для синего. Остальные элементы кортежа представляют собой координаты вершин A, B и C, выраженные в виде комплексных чисел. Комплексные числа хорошо подходят для этой задачи, поскольку они могут представлять любую точку на двумерной плоскости: действительная часть даёт координату по оси X, а мнимая часть — координату по оси Y.
Как видите, мы рисуем контур по сторонам треугольника, но не по основанию. Это позволяет соединить каждый треугольник с другим треугольником того же цвета, образуя ромбовидные плитки, которые видны на готовой мозаике Пенроуза.

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

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

В результате этого разбиения появляются две новые вершины: Q на ребре BA и R на ребре BC в точках, которые также удовлетворяют золотому сечению. Кроме того, два получившихся треугольника являются зеркальными отражениями друг друга. Я выделил углы каждого треугольника, чтобы было понятно, какие из них зеркальные, а какие нет.
Все вышеперечисленные действия можно выполнить с помощью всего нескольких строк кода на Python. Эта функция принимает список треугольников, представленных в виде кортежей, делит каждый из них на части и возвращает новый список треугольников:
goldenRatio = (1 + math.sqrt(5)) / 2 def subdivide(triangles): result = [] for color, A, B, C in triangles: if color == 0: # Subdivide red triangle P = A + (B - A) / goldenRatio result += [(0, C, P, B), (1, P, C, A)] else: # Subdivide blue triangle Q = B + (A - B) / goldenRatio R = B + (C - B) / goldenRatio result += [(1, R, C, A), (1, Q, R, B), (0, R, Q, A)] return result
Вариант кода на TypeScript
Эквивалентный код на TypeScript для браузера может выглядеть так:
const goldenRatio = (1 + Math.sqrt(5)) / 2; // Реализуется вычисление комплексного числа по по формуле: // A + (B - A) / goldenRatio // Где первым вычисляется слагаемое с операцией деления из-за // приоритета над сложением. А так же используется вызов clone(), // потому что каждая операция сохраняет результат в this. const subdividePoint = (A: Complex2, B: Complex2): Complex2 => B.clone().sub(A).divScalar(goldenRatio).add(A); export function subdivide(triangles: Vertexes): Vertexes { const result: Vertexes = []; for (const [color, A, B, C] of triangles) { if (color === VertexColor.Red) { // Subdivide red triangle const P = subdividePoint(A, B); result.push([VertexColor.Red, C, P, B], [VertexColor.Blue, P, C, A]); } else { // Subdivide blue triangle const Q = subdividePoint(B, A); const R = subdividePoint(B, C); result.push( [VertexColor.Blue, R, C, A], [VertexColor.Blue, Q, R, B], [VertexColor.Red, R, Q, A] ); } } return result; }
TypeScript позволяет описывать типы:
// Для магических чисел 0 и 1 обозначающих тип треуголника просто // написан отдельный тип. enum VertexColor { Red = 0, Blue, } // В TypeScript можно описывать кортежи через массивы. type Vertex = [ Color: VertexColor, A: Complex2, B: Complex2, C: Complex2 ]; type Vertexes = Vertex[];
Простая реализация комплексных чисел для браузера на TypeScript
В JavaScript/TypeScript нет встроенного типа данных для комплексных чисел. Привожу свой простой легковесный вариант удобный для этой статьи и браузера:
export class Complex2 { constructor(public real: number, public imag: number) {} clone() { return new Complex2(this.real, this.imag); } // Прямой аналог Python функции cmath.rect() static fromPolar(r: number, phi: number): Complex2 { return new Complex2(r * Math.cos(phi), r * Math.sin(phi)); } static zero(): Complex2 { return new Complex2(0, 0); } add(x: Complex2): Complex2 { this.real += x.real; this.imag += x.imag; return this; } sub(x: Complex2): Complex2 { this.real -= x.real; this.imag -= x.imag; return this; } mulScalar(x: number): Complex2 { this.real *= x; this.imag *= x; return this; } divScalar(x: number): Complex2 { this.real /= x; this.imag /= x; return this; } abs(): number { return Math.sqrt(this.real ** 2 + this.imag ** 2); } }
А вот код для фактического отображения списка треугольников. Он использует pycairo — оболочку Python для превосходной библиотеки рисования cairo.
# Draw red triangles for color, A, B, C in triangles: if color == 0: cr.move_to(A.real, A.imag) cr.line_to(B.real, B.imag) cr.line_to(C.real, C.imag) cr.close_path() cr.set_source_rgb(1.0, 0.35, 0.35) cr.fill() # Draw blue triangles for color, A, B, C in triangles: if color == 1: cr.move_to(A.real, A.imag) cr.line_to(B.real, B.imag) cr.line_to(C.real, C.imag) cr.close_path() cr.set_source_rgb(0.4, 0.4, 1.0) cr.fill() # Determine line width from size of first triangle color, A, B, C = triangles[0] cr.set_line_width(abs(B - A) / 10.0) cr.set_line_join(cairo.LINE_JOIN_ROUND) # Draw outlines for color, A, B, C in triangles: cr.move_to(C.real, C.imag) cr.line_to(A.real, A.imag) cr.line_to(B.real, B.imag) cr.set_source_rgb(0.2, 0.2, 0.2) cr.stroke()
Реализация отображения на TypeScript/JavaScript
Реализация написана для браузера среди API которого есть Canvas API. Кратко - Canvas API полностью заменяет cairo. Местами cairo API и Canvas API почти одинаковые.
const ctx = canvas.getContext("2d")!; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(canvas.width / 2.0, canvas.height / 2.0); ctx.scale(options.wheelRadius, options.wheelRadius); // Draw red triangles ctx.fillStyle = "rgb(255, 89, 89)"; for (const [color, A, B, C] of triangles) { if (color == VertexColor.Red) { ctx.beginPath(); ctx.moveTo(A.real, A.imag); ctx.lineTo(B.real, B.imag); ctx.lineTo(C.real, C.imag); ctx.fill(); } } // Draw blue triangles ctx.fillStyle = "rgb(102, 102, 255)"; for (const [color, A, B, C] of triangles) { if (color == VertexColor.Blue) { ctx.beginPath(); ctx.moveTo(A.real, A.imag); ctx.lineTo(B.real, B.imag); ctx.lineTo(C.real, C.imag); ctx.fill(); } } // Determine line width from size of first triangle const [color, A, B, C] = triangles[0]; ctx.lineWidth = B.clone().sub(A).abs() / 10.0; ctx.lineJoin = "round"; // Draw outlines ctx.fillStyle = "rgb(51, 51, 51)"; for (const [color, A, B, C] of triangles) { ctx.beginPath(); ctx.moveTo(C.real, C.imag); ctx.lineTo(A.real, A.imag); ctx.lineTo(B.real, B.imag); ctx.stroke(); } ctx.restore();
Используя весь приведённый выше код, мы можем, например, начать с одного красного треугольника, разделить его на несколько частей и отображать результат после каждого деления. Вы можете увидеть, как начинает формироваться узор мозаики:

Вы даже можете начать последовательность с другого списка треугольников. Вот код для создания «колеса», состоящего из 10 красных треугольников:
# Create wheel of red triangles around the origin triangles = [] for i in xrange(10): B = cmath.rect(1, (2*i - 1) * math.pi / 10) C = cmath.rect(1, (2*i + 1) * math.pi / 10) if i % 2 == 0: B, C = C, B # Make sure to mirror every second triangle triangles.append((0, 0j, B, C))
Примечание о cmath.rect()
Здесь cmath.rect() - это просто форма задания комплексного числа в полярных координатах.
Вариант кода на TypeScript
// Create wheel of red triangles around the origin let triangles: Vertexes = []; for (let i = 0; i < 10; ++i) { let B = Complex2.fromPolar(1, ((2 * i - 1) * Math.PI) / 10); let C = Complex2.fromPolar(1, ((2 * i + 1) * Math.PI) / 10); if (i % 2 === 0) { // Make sure to mirror every second triangle} const Z = B; B = C; C = Z; } triangles.push([VertexColor.Red, Complex2.zero(), B, C]); }
Если мы будем многократно разделять эту форму колеса, то получим следующую последовательность мозаик. Обратите внимание, что каждая мозаика обладает высокой степенью симметрии — как зеркальной, так и вращательной вокруг пяти различных осей.

Если вы внимательно изучите верхний или нижний ряд этой последовательности, то заметите, что для каждой плитки, кроме первой, в плитке справа есть перевёрнутая копия. Я нарисовал жёлтые контуры, чтобы это было заметнее. Можно посмотреть на это и с другой стороны: если взять любую из этих мозаек, разделить ее дважды, перевернуть по вертикали и увеличить, то получится, что вы добавили ещё одно кольцо вокруг мозаики. Повторяя этот процесс бесконечно, вы можете увидеть, как с помощью мозаики Пенроуза можно заполнить всю плоскость.
Наконец, вот не запутанный скрипт на Python, который объединяет всё воедино: скачайте penrose.py. Он начинает с узора в виде колеса, делит его на 10 частей и отображает увеличенный и обрезанный результат в виде изображения размером 1000x1000.
Полная реализация TypeScript
Полная реализация представлена исходным кодом в песочнице codesandbox. Или можно сразу открыть web приложение.
Данная реализация сделана на TypeScript и дополнена HTML элементами ввода для интерактивной отзывчивой настройки параметров алгоритма с применением React. Так же добавлены типы TypeScript и простая реализация комплексных чисел. Для простоты чтения кода могу подсказать что алгоритм прорисовки мозаики из статьи находится в файле src/PenroseTiles.ts - остальные файлы можно воспринимать как вспомогательные.
