Public
Edited
Jan 22, 2024
Importers
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const width = 800;
const height = 600;
const canvas = htl.html`<canvas width=${width * 2} height=${
height * 2
} style="width: ${width}px;">`;
const ctx = canvas.getContext("2d");
ctx.scale(2, 2);
let x0 = width >> 1,
y0 = height >> 1;
let fillColor = "#77777747";

const toCanvas = ([x, y]) => [x * edgeSize + x0, y * edgeSize + y0];

const toFrame = (frame) => (p) => frame.transform(p);

const draw = (pts, obj) => {
drawPath(ctx, pts.map(toCanvas));
ctx.fillStyle = tileColor[obj.extra] || fillColor;
ctx.fill();
ctx.stroke();
};

function firstChildFrame(result) {
let frame = null;
function f(pts, obj, objFrame) {
if (!frame) {
frame = objFrame;
}
}
result.traverseComponents(new Frame(), f);
return frame;
}

function drawBorder(result, frame) {
ctx.save();
ctx.strokeStyle = "black";
if (frame) {
ctx.globalAlpha = 0.9;
fillColor = "#77777717";
} else {
fillColor = "#77777757";
}
frame = frame || new Frame();
const points = result.points(frame);
let xmin = Infinity,
ymin = Infinity,
xmax = -Infinity,
ymax = -Infinity;
for (let [x, y] of points) {
xmin = Math.min(x, xmin);
ymin = Math.min(y, ymin);
xmax = Math.max(x, xmax);
ymax = Math.max(y, ymax);
}
x0 = -((xmax + xmin) / 2) * edgeSize + width / 2;
y0 = -((ymax + ymin) / 2) * edgeSize + height / 2;
const tof = toFrame(frame);
result.traverseComponents(frame, (pts, obj) => {
draw(pts, obj);
});
ctx.strokeStyle = "#ff000070";
ctx.lineWidth = 4;
drawPath(ctx, points.map(toCanvas));
ctx.stroke();
ctx.strokeStyle = "blue";
drawPath(ctx, points.slice(-2).map(toCanvas));
ctx.stroke();
ctx.restore();
}
if (formula != "") {
try {
const sBorder = stringToBorder(formula);
const frame = firstChildFrame(sBorder);
drawBorder(sBorder, new Frame().rotate(-frame.angle));
ctx.fillStyle = "black";
ctx.fillText(`${sBorder.angles.length} angles`, 20, 20);
} catch (error) {
ctx.fillText("error in formula", 400, 300);
}
}
return canvas;
}
Insert cell
import { vec2, mat2d } from "@esperanc/vec2-utils"
Insert cell
//
// Tells if a and b are approximately equal
//
function eq(a, b) {
return Math.abs(b - a) < 0.0001;
}
Insert cell
//
// A 2d transformation class
//
class Frame {
constructor(m) {
this.m = m || mat2d.create();
}
static fromOriginAngle(origin, angle) {
let f = new Frame();
f.translate(origin);
f.rotate(angle);
return f;
}
invert() {
mat2d.invert(this.m, this.m);
return this;
}
displace(r = 1) {
mat2d.mul(this.m, this.m, mat2d.fromTranslation([], [r, 0]));
return this;
}
translate(vector) {
mat2d.mul(this.m, this.m, mat2d.fromTranslation([], vector));
return this;
}
rotate(degrees) {
mat2d.mul(
this.m,
this.m,
mat2d.fromRotation([], (degrees / 180) * Math.PI)
);
return this;
}
mirror() {
mat2d.mul(this.m, this.m, mat2d.fromScaling([], [-1, 1]));
return this;
}
clone() {
return new Frame(mat2d.clone(this.m));
}
compose(other) {
mat2d.mul(this.m, this.m, other.m);
return this;
}
transform(point) {
return vec2.transformMat2d([], point, this.m);
}
get angle() {
return (Math.atan2(this.m[1], this.m[0]) * 180) / Math.PI;
}
get origin() {
return [this.m[4], this.m[5]];
}
get xvector() {
return [this.m[0], this.m[1]];
}
}
Insert cell
//
// Represents a tile arrangement by its border as a sequence of angles
//
class Border {
constructor(angles, children = null, frame = null, extra = null) {
frame = frame ? frame.clone() : new Frame();
let foundLoop = false;
do {
let result = [];
foundLoop = false;
let flag = false;
for (let a of angles) {
a = (a < 0 ? 360 + a : a) % 360; // Normalize
if (eq(a, 180)) {
flag = true;
foundLoop = true;
} else if (flag) {
result[result.length - 1] =
(result[result.length - 1] + a + 180) % 360;
flag = false;
} else result.push(a);
}
if (flag) {
// link last edge with first
const deleted = result.shift();
result[result.length - 1] =
(result[result.length - 1] + deleted + 180) % 360;
const transf = new Frame().displace(1).rotate(deleted);
const transfInvert = transf.clone().invert();
frame = frame.clone().compose(transf);
for (let child of children) {
child.frame = transfInvert.clone().compose(child.frame);
}
}
angles = result;
} while (foundLoop);
this.angles = angles;
this.totalAngle = angles.reduce((a, b) => a + b, 0) % 360;
this.children = children;
this.frame = frame;
this.extra = extra;
this.shifts = 0;
}
clone() {
const ret = new Border(this.angles, this.children, this.frame, this.extra);
ret.shifts = this.shifts;
return ret;
}
shift(n) {
n = [n + this.angles.length] % this.angles.length; // Circular arithmetic
let children = this.children;
if (children) {
let newFrame = new Frame();
for (let i = 0; i < n; i++) {
newFrame.displace(1);
newFrame.rotate(this.angles[i]);
}
newFrame.invert();
children = children.map((child) => {
child = child.clone();
child.frame = newFrame.clone().compose(child.frame);
return child;
});
}
let angles = [...this.angles.slice(n), ...this.angles.slice(0, n)];
let ret = new Border(angles, children, this.frame.clone(), this.extra);
ret.shifts = this.shifts + n;
return ret;
}
mirror() {
const angles = [...this.angles].reverse();
let children = this.children;
if (children) {
let frame = new Frame().displace(1).mirror();
children = children.map((child) => {
child = child.clone();
child.frame = frame.clone().compose(child.frame);
return child;
});
}
return new Border(angles, children, this.frame.clone(), this.extra);
}
join(other, extra = null) {
const newFrame = new Frame(); //other.frame.clone().compose(this.frame);
for (let i = 0; i < this.angles.length - 1; i++) {
newFrame.displace(1);
newFrame.rotate(this.angles[i]);
}
newFrame.displace(1);
newFrame.rotate(180);
let otherClone = other.clone();
otherClone.frame = newFrame;
this.frame = new Frame();
return new Border(
[
...this.angles.slice(0, -2),
this.angles[this.angles.length - 2] + 180 + other.angles[0],
...other.angles.slice(1, -1),
other.angles[other.angles.length - 1] +
180 +
this.angles[this.angles.length - 1]
],
[this, otherClone],
this.frame.clone(),
extra
);
}
points(frame) {
frame = frame || new Frame();
frame = frame.clone().compose(this.frame);
let pts = [[...frame.origin]];
for (let ang of this.angles) {
frame.displace(/*edgeSize*/ 1);
frame.rotate(ang);
pts.push([...frame.origin]);
}
return pts;
}
edgeFrame(n) {
let m = this.angles.length;
n = (n + m) % m;
let frame = this.frame.clone();
for (let i = 0; i < n; i++) {
frame.displace(1);
frame.rotate(this.angles[i]);
}
return frame;
}
traverseComponents(frame, f) {
let result = [];
function traverse(node, frame) {
if (node.children) {
frame = frame.clone();
frame = frame.compose(node.frame);
traverse(node.children[0], frame);
traverse(node.children[1], frame);
} else {
let pts = node.points(frame);
result.push(pts);
if (f) f(pts, node, frame);
}
}
traverse(this, frame);
return result;
}
}
Insert cell
function drawPath(ctx, points) {
ctx.beginPath();
for (let p of points) ctx.lineTo(...p);
}
Insert cell
alfa = 180 / 5
Insert cell
beta = (alfa +180) / 2
Insert cell
gama = 180 - beta
Insert cell
// A decagon
decagon = new Border(d3.range(10).map((x) => alfa), null, null, "decagon")
Insert cell
// An equilateral triangle
tri = new Border(
d3.range(3).map((x) => 120),
null,
null,
"triangle"
)
Insert cell
// A square
square = new Border(d3.range(4).map((x) => 90),null,null,"square")
Insert cell
// A "tie" shape used in islamic patterns
tie = new Border([beta, alfa, alfa, beta, alfa, alfa], null, null, "tie")
Insert cell
// A "bowtie" shape used in islamic patterns
bowtie = new Border(
[-alfa, alfa + gama, alfa + gama, -alfa, alfa + gama, alfa + gama],
null,
null,
"bowtie"
)
Insert cell
// An hexagon
hexagon = new Border(
d3.range(6).map(() => 360 / 6),
null,
null,
"hexagon"
)
Insert cell
// An octagon
octagon = new Border(
d3.range(8).map(() => 360 / 8),
null,
null,
"octagon"
)
Insert cell
// A dodecagon
dodecagon = new Border(
d3.range(12).map(() => 360 / 12),
null,
null,
"dodecagon"
)
Insert cell
function stringToBorder(s) {
let stack = [];
let number = 0;
const zero = "0".charCodeAt(0);
for (let op of s) {
if (op >= "0" && op <= "9") {
number = number * 10 + op.charCodeAt(0) - zero;
} else
switch (op) {
case "b":
case "B":
stack.push(bowtie);
break;
case "t":
case "T":
stack.push(tie);
break;
case "d":
stack.push(decagon);
break;
case "q":
case "Q":
stack.push(square);
break;
case "e":
case "E":
stack.push(tri);
break;
case "h":
case "H":
stack.push(hexagon);
break;
case "o":
case "O":
stack.push(octagon);
break;
case "k":
case "K":
stack.push(dodecagon);
break;
case ">":
stack.push(stack.pop().shift(number || 1));
number = 0;
break;
case "<":
stack.push(stack.pop().shift(-number || -1));
number = 0;
break;
case "+":
let tmp = stack.pop().shift(-1);
stack.push(stack.pop().join(tmp));
break;
case "|":
stack.push(stack.pop().mirror());
break;
case "=":
stack.push(stack[stack.length - 1].clone());
break;
}
}
return stack[0];
}
Insert cell
tileColor = {
randomColor;
const names = [
"bowtie",
"tie",
"decagon",
"square",
"tri",
"hexagon",
"octagon",
"dodecagon"
];
const obj = {};
for (let n of names) {
obj[n] = `hsla(${Math.random() * 360},80%,50%,0.5)`;
}
return obj;
}
Insert cell
mutable defaultFormula = "qe+=+=+=+>>=<<+"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more