Public
Edited
Jan 15, 2024
8 stars
Also listed in…
Notebook examples
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawCombintation(
combination,
{
// A5
width = 420,
height = 420,
background = `hsl(0,0%,95%)`,
foreground = `hsl(0,0%,5%)`,

fontFamily = monoFontFamily, //geomFontFamily,
anchorVertex,

debug = false
} = {}
) {
const maxSize = Math.min(width, height);
const margin = maxSize / 7.5;
const graphicSize = maxSize - margin * 2;
const marginX = margin;
const marginY = margin / 1.3333;

const hexRadius = graphicSize / 15;
const grid = generateHexGrid(graphicSize, graphicSize, hexRadius);

const title = patternName(combination, anchorVertex);
const fontSize = "12px";
const padding = margin / 3;
const titleEl = htl.svg`<text
x=${width / 2}
y=${height - marginY}
text-anchor="middle"
style=${{
fontSize,
fontWeight: 500,
letterSpacing: "0.75px"
}}
>
${title}
</text>`;

const tangramHexEl = tangramHexagon({
combination,
cx: 0,
cy: 0,
radius: hexRadius,
anchorVertex
});

const hexagonPatterns = svg`<g class="tangrams" transform="translate(${marginX},${marginY})">
${grid.map(
([cx, cy]) => svg`<g transform="translate(${cx},${cy})">
${tangramHexEl.cloneNode(true)}
</g>`
)}
</g>`;

const debugEls = debug
? svg`<g class="debug" transform="translate(${marginX},${marginY})">
<rect
width=${graphicSize} height=${graphicSize}
fill="none"
stroke="#f0f"
stroke-width="0.5"
></rect>
<g>
${grid.map(
([cx, cy]) => svg`<g>
<circle cx=${cx} cy=${cy} r="1" fill="#f0f" ></circle>
<circle
cx=${cx} cy=${cy} r=${hexRadius}
fill="none"
stroke="#0ff"
stroke-width="0.5"
></circle>
<g transform="translate(${cx},${cy})">
<path
d=${hexagonD(hexRadius)}
fill="none"
stroke="#f0f"
stroke-width="0.5"></path>
</g>
</g>`
)}
</g>
</g>`
: "";

return svg`<svg viewBox=${[0, 0, width, height]}
width=${width} height=${height} font-family=${fontFamily}>
<rect
width=${width} height=${height}
fill=${background}
></rect>
${titleEl}
${hexagonPatterns}
${debugEls}
</svg>`;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function tangramTriangle({
combination = "ABCDEFGH",
x = 0,
y = 0,
angle = 0,
height = 20,
fill = "hsl(0,0%,5%)",

anchorVertex = 0,

debug
} = {}) {
console.log({ anchorVertex });
const side = eqTriangleSideFromHeight(height);

let ptH = [x + height, y];
ptH = rotatePoint(...ptH, angle, x, y);

const p1 = [x, y];
const p2 = [x + height, y - side / 2];
const p3 = [x + height, y + side / 2];

let corners =
anchorVertex === 1
? [p2, p3, p1]
: anchorVertex === 2
? [p3, p1, p2]
: [p1, p2, p3];

corners = corners.map((pt, _, arr) => rotatePoint(...pt, angle, x, y));

let tangramPolys = tangramPolygons(corners, { combination });

const polyEls = svg`<g>
${tangramPolys.map(
(poly) => svg`<path
fill=${fill}
stroke=${debug ? "#f0f" : fill}
stroke-width=${debug ? 1 : 0.5}
d=${`M${poly.join("L")}Z`}
><path>`
)}
</g>`;

const debugEls = debug
? svg`<g>
<circle cx=${x} cy=${y} r=2 fill="#f0f"></circle>
<path
fill="none" stroke="#f0f"
d=${`M${corners.join("L")}Z`}
><path>
</g>`
: "";

return svg`<g>
${polyEls}
${debugEls}
</g>`;
}
Insert cell
function tangramPolygons(corners, { combination = "ABCDEFGH" } = {}) {
const [p1, p2, p3] = corners;
let polygons = [];

if (combination.includes("A")) {
const pA1 = divideLine(...p1, ...p2, 4 / 6);
const pA1o = divideLine(...p1, ...p3, 4 / 6);
const pA2 = divideLine(...pA1, ...pA1o, 1 / 4);
const pA3 = divideLine(...p1, ...p3, 1 / 6);
const polyA = [p1, pA1, pA2, pA3];
polygons.push(polyA);
}

if (combination.includes("B")) {
const pB1 = divideLine(...p1, ...p3, 1 / 6);
const pB22 = divideLine(...p2, ...p3, 1 / 6);
const pB2 = divideLine(...pB1, ...pB22, 2 / 5);
const pB32 = divideLine(...p3, ...p2, 2 / 6);
const pB3 = divideLine(...pB32, ...pB2, 1 / 3);
const pB4 = divideLine(...pB1, ...p3, 2 / 5);
const polyB = [pB1, pB2, pB3, pB4];
polygons.push(polyB);
}

if (combination.includes("C")) {
const pC1 = divideLine(...p1, ...p3, 3 / 6);
const pC2 = divideLine(...p2, ...p3, 3 / 6);
const pC3 = divideLine(...p2, ...p3, 4 / 6);
const pC4 = divideLine(...p1, ...p3, 4 / 6);
const polyC = [pC1, pC2, pC3, pC4];
polygons.push(polyC);
}

if (combination.includes("D")) {
const pD10 = divideLine(...p1, ...p3, 1 / 6);
const pD12 = divideLine(...p2, ...p3, 1 / 6);

const pD1 = divideLine(...pD10, ...pD12, 2 / 5);
const pD2 = divideLine(...pD10, ...pD12, 3 / 5);

const pDBaseA0 = divideLine(...p1, ...p3, 5 / 6);
const pDBaseA2 = divideLine(...p1, ...p2, 5 / 6);

const pD3 = divideLine(...pDBaseA0, ...pDBaseA2, 3 / 5);
const pD4 = divideLine(...pDBaseA0, ...pDBaseA2, 2 / 5);
const polyD = [pD1, pD2, pD3, pD4];
polygons.push(polyD);
}

if (combination.includes("E")) {
const pE1 = divideLine(...p1, ...p3, 4 / 6);
const pE2 = divideLine(...p3, ...p2, 2 / 6);
const polyE = [pE1, pE2, p3];
polygons.push(polyE);
}

if (combination.includes("F")) {
const pFTop0 = divideLine(...p1, ...p3, 5 / 6);
const pFTop2 = divideLine(...p1, ...p2, 5 / 6);

const pF1 = divideLine(...pFTop0, ...pFTop2, 3 / 5);
const pF2 = divideLine(...pFTop0, ...pFTop2, 2 / 5);
const pF3 = divideLine(...p2, ...p3, 3 / 6);
const pF4 = divideLine(...p2, ...p3, 2 / 6);
const polyF = [pF1, pF2, pF3, pF4];
polygons.push(polyF);
}

if (combination.includes("G")) {
const pG0 = divideLine(...p1, ...p2, 4 / 6);
const pG1 = divideLine(...p1, ...p2, 5 / 6);
const pG2 = divideLine(...p3, ...p2, 5 / 6);
const pG3 = divideLine(...p3, ...p2, 4 / 6);

const pG40 = divideLine(...p2, ...p3, 3 / 6);
const pG42 = divideLine(...p1, ...p2, 3 / 6);

const pG4 = divideLine(...pG40, ...pG42, 1 / 3);
const pG5 = divideLine(...pG40, ...pG42, 2 / 3);

const polyG = [pG0, pG1, pG2, pG3, pG4, pG5];
polygons.push(polyG);
}

if (combination.includes("H")) {
const pH1 = divideLine(...p1, ...p2, 5 / 6);
const pH2 = divideLine(...p3, ...p2, 5 / 6);
const polyH = [pH1, p2, pH2];
polygons.push(polyH);
}

return polygons;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function tangramHexagon({
combination = "ABCDEFGH",
cx = 0,
cy = 0,
radius = 20,
fill = "hsl(0,0%,5%)",
anchorVertex,

debug
} = {}) {
const height = eqTriangleHeightFromSide(radius);
const triangleTangrams = math.linspace(7, true).map((u) =>
tangramTriangle({
x: cx,
y: cy,
height,
combination,
angle: u * 2 * Math.PI,
anchorVertex
})
);
const debugEls = debug
? svg`<circle
cx=${cx}
cy=${cy}
r=${radius}
fill="none"
stroke="#f0f"
></circle>`
: "";
return svg`<g class="tangram-hexagon">
${triangleTangrams}
${debugEls}
</g>
`;
}
Insert cell
Insert cell
function generateHexGrid(width, height, radius) {
const radiusDouble = radius * 2;

const dx = radius * Math.sin(Math.PI / 3);
const dy = radius * Math.cos(Math.PI / 3);
const gapX = 2 * (radius - dx);

// These are effective dimensons of cell when hexagons
// are shifted to touch each other
const cellWidth = radiusDouble - gapX;
const cellHeight = radius + dy;

const cols = Math.floor(height / cellWidth);
const rows = Math.floor(width / cellHeight);

let centers = Array.from({ length: rows }).flatMap((_, j) =>
Array.from({ length: j % 2 === 0 ? cols : cols + 1 }).map((_, i) => {
let x = radius + i * cellWidth;

if (j % 2 !== 0) {
x = x - dx;
}

return [x, radius / 2 + j * (radiusDouble - dy)];
})
);

const [minX, maxX] = d3.extent(centers, (d) => d[0]);
const [minY, maxY] = d3.extent(centers, (d) => d[1]);

const offsetX =
-minX + cellWidth / 2 - Math.abs(width - cellWidth * (cols + 1)) / 2;
const offsetY = (width - maxY) / 2 - minY / 2;

centers = centers.map(([x, y]) => [x + offsetX, y + offsetY]);

return centers;
}
Insert cell
// From https://github.com/d3/d3-hexbin/blob/master/src/hexbin.js
function hexagon(radius) {
const thirdPi = Math.PI / 3,
angles = [0, thirdPi, 2 * thirdPi, 3 * thirdPi, 4 * thirdPi, 5 * thirdPi];
let x0 = 0,
y0 = 0;

return angles.map(function (angle) {
var x1 = Math.sin(angle) * radius,
y1 = -Math.cos(angle) * radius,
dx = x1 - x0,
dy = y1 - y0;
(x0 = x1), (y0 = y1);
return [dx, dy];
});
}
Insert cell
function hexagonD(radius) {
return "m" + hexagon(radius).join("l") + "z";
}
Insert cell
Insert cell
function eqTriangleSideFromHeight(height) {
return (2 * height) / Math.sqrt(3);
}
Insert cell
function eqTriangleHeightFromSide(side) {
return (side * Math.sqrt(3)) / 2;
}
Insert cell
svg = htl.svg
Insert cell
geomFontFamily = "Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif"
Insert cell
monoFontFamily = "ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace"
Insert cell
Insert cell
function svgFilename(combination, vertex = 0) {
return `${patternName(combination, vertex).toLowerCase()}.svg`;
}
Insert cell
function patternName(combination, vertex = 0) {
return `${vertex + 1}-${combination.join("")}`;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more