Skip to content

Instantly share code, notes, and snippets.

@escaroda
Created September 2, 2018 18:44
Show Gist options
  • Save escaroda/4256b61c32402de2cb7dcd33b5b6003a to your computer and use it in GitHub Desktop.
Save escaroda/4256b61c32402de2cb7dcd33b5b6003a to your computer and use it in GitHub Desktop.
Penrose Tile Visualiser
<!--
This is a quick ui that wraps yurixi's code from https://habr.com/post/359244/
It can save PNG and SVG.
All credit goes to him for doing pretty much all the work here.
-->
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="utf-8"/>
<title>Penrose Explorer</title>
<script src="https://rawgit.com/gliffy/canvas2svg/master/canvas2svg.js"></script>
<!--
<script src="https://raw.githubusercontent.com/gliffy/canvas2svg/master/canvas2svg.js"></script>
-->
<style>
* {
margin:0;
padding:0;
}
input[type="color"]{
max-width:50px;
}
</style>
</head>
<body>
<div id="control">
<input type="color" id="clrbg" value="#FFFFFF" onchange="redraw()" >bg</input>
<input type="color" id="clrln" value="#000000" onchange="redraw()" >line</input>
<input type="color" id="clr1" value="#BBCCEE" onchange="redraw()" >1</input>
<input type="color" id="clr2" value="#BBBBEE" onchange="redraw()" >2</input>
<input type="color" id="clr3" value="#EECCEE" onchange="redraw()" >3</input>
<input type="color" id="clr4" value="#EEBBEE" onchange="redraw()" >4</input>
<input type="color" id="clr5" value="#CCEEFF" onchange="redraw()" >5</input>
<input type="color" id="clr6" value="#EEEEFF" onchange="redraw()" >6</input>
<input id="linewidth" type="number" min=0 max=20 step=0.1 value=1 onchange="redraw()">line width px</input>
<input id="levels" type="number" min=0 max=100 step=1 value=7 onchange="redraw()">levels</input>
<label for="zoom">zoom</label>
<input type="range" min="1" max="150" value="30" id="zoom" step="1"
oninput="document.getElementById('zoomval').value = document.getElementById('zoom').value; redraw()">
<output for="zoom" id="zoomval">30</output>
<label for="extent">extent</label>
<input type="range" min="0" max="1" value="1" id="extent" step="0.01"
oninput="document.getElementById('extentval').value = document.getElementById('extent').value; redraw()">
<output for="extent" id="extentval">1</output>
<label for="drawmode">drawmode</label>
<input type="range" min="0" max="13" value="12" id="drawmode" step="1"
oninput="document.getElementById('drawmodeval').value = document.getElementById('drawmode').value; redraw()">
<output for="drawmode" id="drawmodeval">12</output>
<button onclick="doPng()">PNG</button>
<button onclick="doSvg()">SVG</button>
time
<div id="timer" style="display:inline-block"></div>
ms
</div>
<canvas id="canvas" />
<script type="application/javascript">
/* https://habr.com/post/359244 */
/* Получаем «контекст», объектный интерфейс для рисования на плоскости: */
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var svgcontext;
var svg = false;
var colors;
var linecol;
var bgcol;
var linewidth;
var start_ms = 0;
var finish_ms = 0;
// resize the canvas to fill browser window dynamically
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
redraw();
}
resizeCanvas();
function doPng() {
var c=document.getElementById("canvas");
window.open(c.toDataURL('image/png'));
}
function doSvg() {
if(typeof C2S === "undefined"){
alert("unable to load canvas2svg");
return;
}
svg=true;
redraw();
svg=false;
alert("you will need to right click, save page as, then manually type svg filename extension")
var svg_win = window.open("", "svg_win");
var svg_txt=svgcontext.getSerializedSvg();
var parser = new DOMParser();
var svg_doc = parser.parseFromString(svg_txt, "image/svg+xml");
var svg_node = svg_doc.getElementsByTagName('svg')[0];
var transplanted_svg = svg_win.document.importNode(svg_node, true);
var blank_root = svg_win.document.documentElement;
svg_win.document.removeChild(blank_root);
svg_win.document.appendChild(transplanted_svg);
}
function redraw() {
start_ms = Date.now();
bgcol = document.getElementById('clrbg').value
context.lineJoin = 'bevel';
context.beginPath();
context.rect(0,0,canvas.width,canvas.height);
context.fillStyle=bgcol;
context.fill();
if(svg){
svgcontext = new C2S(canvas.width,canvas.height);
svgcontext.lineJoin = 'bevel';
svgcontext.beginPath();
svgcontext.rect(0,0,canvas.width,canvas.height);
svgcontext.fillStyle=(bgcol);
svgcontext.fill();
}
s = prepare();
s[0] = context;
s[7] = 1; // доля последнего уровня
s[8] = canvas.width/2.5; // сдвих координаты x
s[9] = canvas.height/2; // сдвиг координаты y
colors = [
document.getElementById("clr1").value,
document.getElementById("clr2").value,
document.getElementById("clr3").value,
document.getElementById("clr4").value,
document.getElementById("clr5").value,
document.getElementById("clr6").value
]
linecol = document.getElementById("clrln").value
linewidth = document.getElementById("linewidth").value
/* Код заполнения первого уровня */
var f = []; // слои разбиения.
f[0] = [];
f[0].push([0,[ 0, 0, 0, 0],0]);
f[0].push([1,[ 0, 0, 0, 0],0]);
f[0].push([2,[ 0, 0, 0, 0],3]);
f[0].push([3,[ 0, 0, 0, 0],3]);
f[0].push([2,[ 0, 0, 0, 0],7]);
f[0].push([3,[ 0, 0, 0, 0],7]);
/* Код расчета и отрисовки уровней */
fi = s[4]; // берем из констант коэффициент
var levels = document.getElementById('levels').value; // количество расчетных уровней
s[7] = document.getElementById('extent').value; // степень проявления уровня
//s[10] = 24 * 6 * fi * fi; // длина линии
//s[10] = 24 * 6 * fi
//s[10] = 24 * 6;
// для отображения полного поля нужно изменить размер шага, размер канвы, место центра и поменять местами p[0] и p[1] в функциях from() и to().
//s[10] = 24;
s[10]=document.getElementById('zoom').value;
mode = document.getElementById('drawmode').value; // режим рисования
// разбиение
var n = 0, m;
for(; n < levels; n++)
{ m = n + 1;
f[m] = [];
for(var k = 0; k < f[n].length; k++)
zd(f[n][k], s, f[m]);
}
// отображение
n = m - 1;
// предыдущий уровень
if(s[7] != 1)
for(var i = 0; i < f[n].length; i++) {paint(f[n][i], mode, 1);}
// последний уровень
for(var i = 0; i < f[m].length; i++) {paint(f[m][i], mode, 0);}
// Для 11 режима подчеркиваются линии
if(mode == 11) {d = 3; for(var i = 0; i < f[m-d].length; i++) {paint(f[m-d][i], mode, d);}}
}
/* Функции для упрощения команд рисования: */
// начать описание контура
function begin(){
context.beginPath();
if(svg)
svgcontext.beginPath();
}
// начать линию с точки
function from(p) {
context.moveTo(s[8] + p[0], s[9] - p[1]);
if(svg)
svgcontext.moveTo(s[8] + p[0], s[9] - p[1]);
}
// привести линию к точке
function to(p){
context.lineTo(s[8] + p[0], s[9] - p[1]);
if(svg)
svgcontext.lineTo(s[8] + p[0], s[9] - p[1]);
}
// привести линию к начальной точке, не требуется если выполняется только заливка
function close(){
context.closePath();
if(svg)
svgcontext.closePath();
}
// заливка
function fill(color){
context.fillStyle = color; context.fill();
if(svg){
svgcontext.fillStyle = color; svgcontext.fill();
}
}
// обводка контура линией
function line(){
context.strokeStyle = linecol;
context.lineWidth = linewidth;
context.stroke();
if(svg){
svgcontext.strokeStyle = linecol;
svgcontext.lineWidth = linewidth;
svgcontext.stroke();
}
}
function line_white(){
context.strokeStyle = bgcol; context.lineWidth = linewidth; context.stroke();
if(svg){
svgcontext.strokeStyle = bgcol; svgcontext.lineWidth = linewidth; svgcontext.stroke();
}
}
function line_black(){
context.strokeStyle = "#444"; context.lineWidth = linewidth; context.stroke();
if(svg){
svgcontext.strokeStyle = "#444"; svgcontext.lineWidth = linewidth; svgcontext.stroke();
}
}
/* Прежде всего нужно задать используемые константы. */
var s;
function prepare()
{
var sqrt = Math.sqrt;
var fi = (sqrt(5) - 1) / 2;
var fb = (sqrt(5) + 1) / 2;
var f3 = sqrt(3 + fi);
var f2 = sqrt(2 - fi);
//координаты базовых векторов для всех десяти направлений
var vt = [[ 2, 0, 0, 0], [ 1, 1, 0, 1], [ 0, 1, 1, 0], [ 0,-1, 1, 0], [-1,-1, 0, 1],
[-2, 0, 0, 0], [-1,-1, 0,-1], [ 0,-1,-1, 0], [ 0, 1,-1, 0], [ 1, 1, 0,-1]];
// Константы множители координат
var c = [1/2, fi/2, f3/2, f2/2]
// Общий массив констант
// нулевой элемент для контекста
// седьмой для дополнительных данных рисования.
// восьмой и девятый для указания центра рисования
// десятый для размера шага.
var s = [0, vt, c, fi, f3, f2, 0, 0, 0, 0];
return s;
}
/* Функция разбиения */
function zd(a, s, f) {
var t = a[0]; // тип фигуры
var vt = s[1]; // таблица векторов
if (t > 3) t = t - 4; // типы фигур 4 и 5 обрабатываются как 0 и 1
// направление первого шага в зависимости от типа фигуры, в виде смещения направления
sht = [ 1,-1, 2,-2];
var shift = sht[t];
if(t == 0) {t1 = 0; t2 = 3; t3 = 5;} // типы получившихся фигур
else if(t == 1) {t1 = 1; t2 = 2; t3 = 4;}
else if(t == 2) {t1 = 4; t2 = 2;}
else if(t == 3) {t1 = 5; t2 = 3;}
if (t < 2)
{
pos = a[1];
v1 = a[2]; // общее направление
v2 = (v1 + shift + 10) % 10; // направление первого шага
v3 = (v1 - shift + 10) % 10; // направление второго шага
v4 = (v2 + 5) % 10; // обратное направление первому
v5 = (v1 + 5) % 10; // обратное направление общему (не второму)
p1 = add(pos, vt[v2]); // позиция после первого шага
p2 = add(p1, vt[v3]); // позиция после второго шага
p3 = mul(p1,[2,2,0,0]); // масштабирование
p4 = mul(p2,[2,2,0,0]); // масштабирование
f.push([t1, p3, v4]);
f.push([t2, p3, v3]);
f.push([t3, p4, v5]);
}
else
{
pos = a[1];
v1 = a[2];
v2 = (v1 + shift + 10) % 10; // направление первого шага
v3 = (v1 - shift + 10) % 10; // направление второго шага
v4 = (v2 + 5) % 10; // обратное направление первому
v5 = (v3 + 5) % 10; // обратное направление второму
p1 = add(pos, vt[v2]); // позиция после первого шага
p2 = add(p1, vt[v3]); // позиция после второго шага
p3 = mul(p1,[2,2,0,0]); // масштабирование
p4 = mul(p2,[2,2,0,0]); // масштабирование
f.push([t1, p3, v4]);
f.push([t2, p4, v5]);
}
return f;
}
/* Используемые функции сложения и умножения векторов */
function mul(v1, v2) {
mt = [[[1, 0, 0, 0],[0, 1, 0, 0],[ 0, 0, 1, 0],[ 0, 0, 0, 1]],
[[0, 1, 0, 0],[1,-1, 0, 0],[ 0, 0, 0, 1],[ 0, 0, 1,-1]],
[[0, 0, 1, 0],[0, 0, 0, 1],[-3, 1, 0, 0],[-1,-2, 0, 0]],
[[0, 0, 0, 1],[0, 0, 1,-1],[-1,-2, 0, 0],[-2, 1, 0, 0]]]
var v3 = [0, 0, 0, 0];
for(var i = 0; i < 4; i++)
for(var j = 0; j < 4; j++)
for(var k = 0; k < 4; k++)
v3[k] = v3[k] + v1[i] * v2[j] * mt[i][j][k];
for(var i = 0; i < 4; i++) v3[i] = v3[i] / 2;
return v3;
}
function add(v1, v2) {
// нельзя к первому к первому аргументу добавить второй и вернуть,
// потому что аргументы принимаются по ссылке и значит будут изменены.
var v3 = [0, 0, 0, 0];
for(var i = 0; i < 4; i++) v3[i] = v3[i] + v1[i];
for(var i = 0; i < 4; i++) v3[i] = v3[i] + v2[i];
return v3;
}
/* Функция нахождения точки между двумя, с заданным коэффициентом. */
function mean(p1, p2, d) {
var p3 = [(p2[0] - p1[0]) * d + p1[0],(p2[1] - p1[1]) * d + p1[1]];
return p3;
}
/* Функция отрисовки фигуры */
function paint(a, mode, level = 0) {
vt = s[1]; // таблица векторов
c = s[2]; // массив координатных констант
fi = s[3]; // константа фи
pr = s[7]; // доля проявления уровня
b = s[0]; // контекст рисования
var st = s[10];
// шесть цветов на выбор
//colors = ["#BCE","#BBE","#ECE","#EBE","#CEF","#EEF"];
type = a[0]; // оригинальный тип фигуры
tn = type; // тип свернутый до 4
if(tn > 3) tn = tn - 4;
// цвет
color = colors[type];
// направление первого шага, в виде сдвига направления
sht = [ 1,-1, 2,-2];
var shift = sht[tn];
p = a[1]; // точка привязки
v0 = a[2]; // направление
v0 = (10 + v0 % 10) % 10; // направление выравнено в пределы 0-10
v1 = (10 + (v0 + shift) % 10) % 10; // направление первого шага
v2 = (10 + (v0 - shift) % 10) % 10; // направление второго шага
// коэффициенты масштабирования для позциции и для сторон.
var kop = 0;
var koe = 0;
pr1 = 1 - pr; // доля предыдущего уровня.
if(level == 0) {kop = st; koe = pr;}
if(level == 1) {kop = st / fi; koe = pr1 / fi; } // проступание соседнего уровня
if(level == 3) {kop = st / fi / fi / fi; koe = pr / fi / fi / fi; } // линии на три уровня меньше
st = st * koe; // масштабирование фигур.
// координаты начала линии
p0 = [kop * (p[0] * c[0] + p[1] * c[1]), kop * (p[2] * c[2] + p[3] * c[3])]
// координаты конца первой линии
s1 = vt[v1]; p1 = [p0[0] + st * (s1[0] * c[0] + s1[1] * c[1]), p0[1] + st * (s1[2] * c[2] + s1[3] * c[3])];
// координаты конца второй линии
s2 = vt[v2]; p2 = [p1[0] + st * (s2[0] * c[0] + s2[1] * c[1]), p1[1] + st * (s2[2] * c[2] + s2[3] * c[3])];
// таблица рисовать ли фон
modes = [1, 1,1,1,1,1, 0,0,0,0,1, 1,1,1];
y = modes[mode];
// заливка, сразу можно грани рисовать
if(level < 3) // если сдвинутый на три уровень, то фон не отрисовывается, только линии 11 режима.
if(y || mode == 0)
{ begin(); from(p0); to(p1); to(p2); close();
if(y) {fill(color);}
if(mode == 0) line();
if(mode == 12) line_white();
}
// четырехугольники
if(mode == 1)
{ p3 = mean(p0, p2, 0.5);
begin(); from(p0); to(p2); from(p1); to(p3); line_black();
}
// дальняя сторона, фигуры HBS
if(mode == 2)
{ begin(); from(p1); to(p2); line();
}
if(mode == 6) // ромбы
{ begin();
if(tn == 0 || tn == 2)
{ color = colors[tn * 2];
// четвертый угол ромба
p3 = mean(p0, p2, 0.5);
p4 = mean(p1, p3, 2);
from(p0); to(p1); to(p2); to(p4); close(); fill(color);
}
line();
}
// нарезка
if(mode == 3 || mode == 4 || mode == 5) {
p5 = mean(p1, p2, 0.5);
p7 = mean(p0, p1, 0.5);
begin(); from(p5); to(p7);
if(mode > 3) {
from(p0); to(p2);
if(mode == 5) {
from(mean(p2, p5, 0.5)); to(mean(p0, p7, 0.5));
from(mean(p1, p5, 0.5)); to(mean(p1, p7, 0.5));
}
}
line();
}
if(mode == 7) // дельтоиды
{ if(type == 0)
{ // расчет дополнительного угла уголка
p3 = mean(p0, p1, 1 + fi);
p4 = mean(p2, p3, 1 + fi);
begin(); from(p0); to(p1); to(p4); to(p2); close(); fill(colors[0]); line();
}
if(type == 2)
{ // расчет углов фигуры воздушный змей
p3 = mean(p0, p2, 2 + fi)
p4 = [p0[0] + (p2[0] - p1[0]), p0[1] + (p2[1] - p1[1])];
begin(); from(p0); to(p1); to(p3); to(p4); close(); fill(colors[4]); line();
}
}
if(mode == 8) // разные треугольники
{ if(type < 2)
{ begin(); from(p0); to(p1); to(p2); close(); fill(colors[0]); line();
}
if(type == 4 || type == 5)
{ p3 = mean(p0, p1, 1 + fi);
begin(); from(p0); to(p3); to(p2); close(); fill(colors[4]); line();
}
}
if(mode == 9) // ромб и уголки
{ if(type == 0)
{ p3 = mean(p0, p1, 1 + fi);
p4 = mean(p2, p3, 1 + fi);
begin(); from(p0); to(p1); to(p4); to(p2); close(); fill(colors[0]); line();
}
if(type == 2)
{ p3 = [p0[0] - p1[0] + p2[0], p0[1] - p1[1] + p2[1]];
begin(); from(p0); to(p1); to(p2); to(p3); close(); fill(colors[4]); line();
}
if(type == 4)
{ p3 = mean(p2, p1, 1 + fi);
p4 = mean(p0, p3, 1 + fi);
begin(); from(p0); to(p4); to(p1); to(p2); close(); fill(colors[0]); line();
}
}
if(mode == 10)
{
p4 = mean(p1, p0, fi);
p5 = mean(p0, p2, fi);
p6 = mean(p2, p0, 1 / 2 + fi / 2);
p7 = mean(p1, p2, 0.5);
begin(); if(tn < 2) {from(p4); to(p5);} else {from(p6); to(p4);} to(p7); line();
}
if(mode == 11)
{
k1 = 1 / 2;
k2 = (fi + 1) / 2;
k3 = (4 - fi) / 4;
k4 = (fi + 1) / 4;
k5 = (3 - 2 * fi) / 2;
k6 = 1 / 4;
if(tn < 2)
{
p3 = mean(p0, p2, k4);
p4 = mean(p0, p1, k2);
p5 = mean(p1, p2, k1);
p6 = mean(p0, p2, k5);
p7 = mean(p1, p2, k3);
begin(); from(p3); to(p4); to(p5); to(p6); to(p7);
}
else
{
p3 = mean(p2, p1, k3);
p4 = mean(p0, p1, k2);
p5 = mean(p1, p2, k1);
p6 = mean(p2, p0, k6);
begin(); from(p3); to(p4); to(p5); to(p6);
}
line();
}
finish_ms = Date.now();
document.getElementById("timer").innerHTML=finish_ms-start_ms;
}
</script>
<!--
<canvas id="a" width="640" height="640"></canvas>
-->
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment