About this technique →
islamic-geometry/sketch.js
// 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);
    }
  }
}