// Islamic Geometry
//
// Star-and-lattice tiling in the Islamic geometric tradition.
// For each cell in a grid, draw:
// 1. A bounding square (the base tile)
// 2. An N-pointed star (alternating outer/inner radius)
// 3. The connecting polygon between star points (the "rosette" lattice)
//
// Stroke only — no fills — so the interlocking lines are fully visible.
export const PARAMS = {
starPoints: { value: 8, options: ["6", "8", "10", "12"], label: "Star Points", folder: "Structure", rebuildOnChange: true },
cellSize: { value: 200, min: 80, max: 400, step: 5, label: "Cell Size", folder: "Structure", rebuildOnChange: true },
innerRatio: { value: 0.5, min: 0.2, max: 0.8, step: 0.01, label: "Inner Star Ratio", folder: "Structure" },
fg: { value: "#d4a843", type: "color", label: "Lines", folder: "Appearance" },
bg: { value: "#0d2645", type: "color", label: "Background", folder: "Appearance" },
strokeW: { value: 2, min: 0.5, max: 6, step: 0.1, label: "Stroke Width", folder: "Appearance" },
};
// Parse hex color to CSS rgb string
function hexToRgbStr(hex) {
const h = hex.replace('#', '');
const r = parseInt(h.substring(0, 2), 16);
const g = parseInt(h.substring(2, 4), 16);
const b = parseInt(h.substring(4, 6), 16);
return `rgb(${r},${g},${b})`;
}
// Compute 2*N vertices of a star polygon
// Alternates between outer radius R and inner radius r
function starVerts(cx, cy, N, R, r, startAngle) {
const pts = [];
for (let i = 0; i < 2 * N; i++) {
const angle = startAngle + (Math.PI * i) / N;
const radius = (i % 2 === 0) ? R : r;
pts.push([cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]);
}
return pts;
}
// Draw a closed polygon path from a list of [x,y] points
function drawPolygon(ctx, pts) {
if (pts.length < 2) return;
ctx.beginPath();
ctx.moveTo(pts[0][0], pts[0][1]);
for (let i = 1; i < pts.length; i++) {
ctx.lineTo(pts[i][0], pts[i][1]);
}
ctx.closePath();
ctx.stroke();
}
// Draw one tile cell centered at (cx, cy) with given parameters
function drawCell(ctx, cx, cy, cs, N, innerRatio, strokeStyle, lineWidth) {
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
const R = cs / 2; // outer star radius = half cell size
const r = R * innerRatio; // inner star radius
// Angle offset: rotate so first outer point is at top
const startAngle = -Math.PI / 2;
// Draw the N-pointed star
const starPts = starVerts(cx, cy, N, R, r, startAngle);
drawPolygon(ctx, starPts);
// Draw the inner inscribed polygon connecting every other inner vertex
// (this forms the "rosette" connection pattern linking star inner points)
const innerPts = starPts.filter((_, i) => i % 2 === 1); // inner vertices
drawPolygon(ctx, innerPts);
// Draw lines from each outer tip to the two adjacent inner vertices
// This creates the "kite" facets that fill in the star shape
for (let i = 0; i < N; i++) {
const outer = starPts[i * 2];
const innerLeft = starPts[((i * 2 - 1) + 2 * N) % (2 * N)];
const innerRight = starPts[(i * 2 + 1) % (2 * N)];
// Midpoint between adjacent inner vertices
const mx = (innerLeft[0] + innerRight[0]) / 2;
const my = (innerLeft[1] + innerRight[1]) / 2;
ctx.beginPath();
ctx.moveTo(outer[0], outer[1]);
ctx.lineTo(mx, my);
ctx.stroke();
}
}
export function sketchSetup(ctx, w, h, tng) {
return {};
}
export function sketchDraw(ctx, w, h, params, tng) {
// Fill background
ctx.fillStyle = hexToRgbStr(params.bg);
ctx.fillRect(0, 0, w, h);
const N = parseInt(params.starPoints, 10);
const cs = params.cellSize;
const fgStr = hexToRgbStr(params.fg);
const lw = params.strokeW;
// Tile offset: for N-fold that's a square grid, use square tiling
// For 6/12-fold, offset every other row by cs/2 for hexagonal look
const useHex = (N === 6 || N === 12);
const cols = Math.ceil(w / cs) + 2;
const rows = Math.ceil(h / cs) + 2;
for (let row = -1; row < rows; row++) {
for (let col = -1; col < cols; col++) {
const xOffset = useHex && (row % 2 !== 0) ? cs / 2 : 0;
const cx = col * cs + cs / 2 + xOffset;
const cy = row * (useHex ? cs * Math.sqrt(3) / 2 : cs) + cs / 2;
drawCell(ctx, cx, cy, cs, N, params.innerRatio, fgStr, lw);
}
}
}