About this technique →
recursive-polygons/sketch.js
// Recursive Polygons — nested twisted polygons in Oklch hue space
//
// Each level draws a filled polygon, then recurses with a scaled,
// rotated copy — building a depth tunnel of color-shifted geometry.

export const PARAMS = {
  sides:    { value: 5, min: 3, max: 12, step: 1, label: "Polygon Sides", folder: "Structure" },
  depth:    { value: 30, min: 5, max: 80, step: 1, label: "Recursion Depth", folder: "Structure" },
  scale:    { value: 0.92, min: 0.7, max: 0.99, step: 0.005, label: "Scale per Level", folder: "Structure" },
  twist:    { value: 6, min: -45, max: 45, step: 0.5, label: "Twist Per Level (°)", folder: "Structure" },
  hue:      { value: 200, min: 0, max: 360, step: 1, label: "Start Hue", folder: "Appearance" },
  hueShift: { value: 8, min: -30, max: 30, step: 0.5, label: "Hue Shift Per Level", folder: "Appearance" },
  lightness:{ value: 0.55, min: 0.1, max: 0.9, step: 0.01, label: "Lightness", folder: "Appearance" },
  bg:       { value: "#0a0a0a", type: "color", label: "Background", folder: "Appearance" },
};

export const SHARE = {
  bookmarked: ["sides", "depth", "scale", "twist", "hue", "hueShift"],
};

// Parse hex color to [r,g,b] 0-255
function hexToRgb255(hex) {
  const h = hex.replace('#', '');
  return [
    parseInt(h.substring(0, 2), 16),
    parseInt(h.substring(2, 4), 16),
    parseInt(h.substring(4, 6), 16),
  ];
}

// Oklch color conversions (inline — no tng dependency needed)
function srgbToLinear(c) {
  return c < 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
function linearToSrgb(c) {
  return Math.max(0, Math.min(1, c < 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055));
}
function oklchToRgb255(L, C, hDeg) {
  const h = (hDeg * Math.PI) / 180;
  const a = C * Math.cos(h);
  const b = C * Math.sin(h);
  const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
  const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
  const s_ = L - 0.0894841775 * a - 1.291485548 * b;
  const lc = l_ ** 3, mc = m_ ** 3, sc = s_ ** 3;
  const r = linearToSrgb(4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc);
  const g = linearToSrgb(-1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc);
  const bv = linearToSrgb(-0.0041960863 * lc - 0.7034186147 * mc + 1.707614701 * sc);
  return [Math.round(r * 255), Math.round(g * 255), Math.round(bv * 255)];
}

// Compute polygon vertices given center, radius, angle offset (radians)
function polygonVerts(cx, cy, r, sides, angleOffset) {
  const pts = [];
  for (let i = 0; i < sides; i++) {
    const a = angleOffset + (i / sides) * Math.PI * 2;
    pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
  }
  return pts;
}

// Draw a filled polygon using canvas 2D path
function fillPolygon(ctx, verts, r, g, b) {
  ctx.beginPath();
  ctx.moveTo(verts[0][0], verts[0][1]);
  for (let i = 1; i < verts.length; i++) {
    ctx.lineTo(verts[i][0], verts[i][1]);
  }
  ctx.closePath();
  ctx.fillStyle = `rgb(${r},${g},${b})`;
  ctx.fill();
}

export function sketchSetup(ctx, w, h, tng) {
  return {};
}

export function sketchDraw(ctx, w, h, params, tng) {
  // Fill background
  const bgRgb = hexToRgb255(params.bg);
  ctx.fillStyle = `rgb(${bgRgb[0]},${bgRgb[1]},${bgRgb[2]})`;
  ctx.fillRect(0, 0, w, h);

  const cx = w / 2;
  const cy = h / 2;
  // Start radius: inset slightly from canvas edge
  const startRadius = Math.min(w, h) * 0.47;

  const twistRad = (params.twist * Math.PI) / 180;
  // Orient first polygon pointing up
  const startAngle = -Math.PI / 2;

  // Chroma for Oklch — moderate saturation for vivid but not garish colors
  const chroma = 0.18;

  // Draw from outermost to innermost so each level covers what's beneath
  for (let level = 0; level < params.depth; level++) {
    const r = startRadius * Math.pow(params.scale, level);
    if (r < 0.5) break;

    const angle = startAngle + twistRad * level;
    const hue = ((params.hue + params.hueShift * level) % 360 + 360) % 360;
    const [rr, rg, rb] = oklchToRgb255(params.lightness, chroma, hue);

    const verts = polygonVerts(cx, cy, r, params.sides, angle);
    fillPolygon(ctx, verts, rr, rg, rb);
  }
}