const spineLength = 175;      // min=100,max=220,step=1
const spineSamples = 140;     // min=60,max=220,step=1

const curveAmp1 = 18;         // min=0,max=60,step=1
const curveFreq1 = 1.4;       // min=0.2,max=4,step=0.1
const curveAmp2 = 10;         // min=0,max=40,step=1
const curveFreq2 = 3.2;       // min=0.2,max=8,step=0.1
const spineDrift = 0;         // min=-30,max=30,step=1

const cageCount = 34;         // min=8,max=80,step=1
const cageLayers = 12;        // min=3,max=30,step=1
const cageWidth = 34;         // min=8,max=70,step=1
const cageHeight = 13;        // min=4,max=30,step=1
const cageWave = 6;           // min=0,max=20,step=1
const twistTurns = 2.6;       // min=0,max=8,step=0.1
const twistAmount = 0.9;      // min=0,max=2.5,step=0.05
const taper = 0.42;           // min=0,max=0.9,step=0.02

const strandCount = 16;       // min=4,max=40,step=1
const strandSamples = 140;    // min=40,max=240,step=1

const fieldLines = 90;        // min=10,max=180,step=1
const fieldSteps = 95;        // min=20,max=200,step=1
const fieldStepSize = 2.3;    // min=0.5,max=5,step=0.1
const fieldSpread = 78;       // min=20,max=130,step=1
const fieldPull = 0.95;       // min=0,max=3,step=0.05
const fieldSwirl = 1.05;      // min=0,max=3,step=0.05
const fieldDriftX = 0.0;      // min=-1,max=1,step=0.05
const fieldDriftY = 0.15;     // min=-1,max=1,step=0.05

const closestSamples = 100;   // min=20,max=180,step=1
const border = 115;           // min=90,max=140,step=1

Canvas.setpenopacity(0.65);

const turtle = new Turtle();

function clamp(v, a, b) {
  return Math.max(a, Math.min(b, v));
}

function lerp(a, b, t) {
  return a + (b - a) * t;
}

function vec(x, y) {
  return { x, y };
}

function add(a, b) {
  return { x: a.x + b.x, y: a.y + b.y };
}

function sub(a, b) {
  return { x: a.x - b.x, y: a.y - b.y };
}

function mul(a, s) {
  return { x: a.x * s, y: a.y * s };
}

function dot(a, b) {
  return a.x * b.x + a.y * b.y;
}

function len(a) {
  return Math.sqrt(a.x * a.x + a.y * a.y);
}

function norm(a) {
  const l = len(a);
  if (l < 1e-9) return { x: 0, y: 0 };
  return { x: a.x / l, y: a.y / l };
}

function perp(a) {
  return { x: -a.y, y: a.x };
}

function rotateBasis(n, t, ang) {
  const c = Math.cos(ang);
  const s = Math.sin(ang);
  const bx = add(mul(n, c), mul(t, s));   // local x axis
  const by = add(mul(t, c), mul(n, -s));  // local y axis
  return { bx, by };
}

function polyline(points) {
  if (!points || points.length < 2) return;
  turtle.penup();
  turtle.goto(points[0].x, points[0].y);
  turtle.pendown();
  for (let i = 1; i < points.length; i++) {
    turtle.goto(points[i].x, points[i].y);
  }
  turtle.penup();
}

function inBounds(p) {
  return (
    p.x > -border && p.x < border &&
    p.y > -border && p.y < border
  );
}

function spinePoint(t) {
  const y = lerp(-spineLength / 2, spineLength / 2, t);
  let x =
    curveAmp1 * Math.sin(Math.PI * 2 * curveFreq1 * t) +
    curveAmp2 * Math.sin(Math.PI * 2 * curveFreq2 * t + 0.9);
  x += spineDrift * (t - 0.5);
  return vec(x, y);
}

function spineTangent(t) {
  const e = 0.002;
  const t0 = clamp(t - e, 0, 1);
  const t1 = clamp(t + e, 0, 1);
  const p0 = spinePoint(t0);
  const p1 = spinePoint(t1);
  return norm(sub(p1, p0));
}

function closestOnSpine(p) {
  let bestT = 0;
  let bestP = spinePoint(0);
  let bestD2 = 1e18;

  for (let i = 0; i <= closestSamples; i++) {
    const t = i / closestSamples;
    const s = spinePoint(t);
    const dx = p.x - s.x;
    const dy = p.y - s.y;
    const d2 = dx * dx + dy * dy;
    if (d2 < bestD2) {
      bestD2 = d2;
      bestT = t;
      bestP = s;
    }
  }

  const tan = spineTangent(bestT);
  const nor = perp(tan);
  const rel = sub(p, bestP);
  const offset = dot(rel, nor);

  return {
    t: bestT,
    p: bestP,
    tan: tan,
    nor: nor,
    d2: bestD2,
    offset: offset
  };
}

function localSection(u, width, height, wave, phase) {
  // u in [-1, 1]
  let x = Math.sin(Math.PI * u) * width;

  // pinch around center to feel more vertebra-like
  const pinch = 1 - 0.38 * Math.exp(-7 * u * u);
  x *= pinch;

  let y = u * height;
  y += Math.sin(Math.PI * 2 * u + phase) * wave;

  return vec(x, y);
}

function drawCageRibs() {
  for (let i = 0; i < cageCount; i++) {
    const t = i / (cageCount - 1);
    const c = spinePoint(t);
    const tan = spineTangent(t);
    const nor = perp(tan);

    const midBoost = 0.62 + 0.38 * Math.sin(Math.PI * t);
    const endTaper = 1 - taper * Math.abs(t - 0.5) * 2;

    const w0 = cageWidth * midBoost * endTaper;
    const h0 = cageHeight * (0.8 + 0.2 * Math.sin(Math.PI * t));
    const phase = Math.PI * 2 * twistTurns * t;
    const ang = twistAmount * Math.sin(phase);

    const basis = rotateBasis(nor, tan, ang);

    for (let layer = 0; layer < cageLayers; layer++) {
      const v = (cageLayers <= 1) ? 0 : layer / (cageLayers - 1);
      const centered = lerp(-1, 1, v);

      const width = w0 * (1 - 0.52 * v);
      const height = h0 * (1 + 0.18 * centered);
      const wave = cageWave * (1 - 0.65 * v);

      const pts = [];
      const steps = 60;

      for (let s = 0; s <= steps; s++) {
        const u = lerp(-1, 1, s / steps);
        const local = localSection(u, width, height, wave, phase + centered * 0.8);

        // slight layer shift along tangent to get stacked vertebra feel
        const shifted = add(local, vec(0, centered * 2.0));

        const world = add(
          c,
          add(
            mul(basis.bx, shifted.x),
            mul(basis.by, shifted.y)
          )
        );

        pts.push(world);
      }

      polyline(pts);
    }
  }
}

function drawSpineStrands() {
  for (let k = 0; k < strandCount; k++) {
    const band = (strandCount <= 1) ? 0 : k / (strandCount - 1);
    const uBase = lerp(-0.92, 0.92, band);

    const pts = [];

    for (let i = 0; i <= strandSamples; i++) {
      const t = i / strandSamples;
      const c = spinePoint(t);
      const tan = spineTangent(t);
      const nor = perp(tan);

      const midBoost = 0.62 + 0.38 * Math.sin(Math.PI * t);
      const endTaper = 1 - taper * Math.abs(t - 0.5) * 2;

      const width = cageWidth * midBoost * endTaper * (0.72 + 0.28 * Math.cos(uBase * Math.PI));
      const height = cageHeight * (0.9 + 0.1 * Math.sin(Math.PI * t));
      const phase = Math.PI * 2 * twistTurns * t;
      const ang = twistAmount * Math.sin(phase);

      const basis = rotateBasis(nor, tan, ang);

      const u = uBase;
      const local = localSection(u, width, height, cageWave * 0.55, phase + uBase * 1.4);

      const world = add(
        c,
        add(
          mul(basis.bx, local.x),
          mul(basis.by, local.y)
        )
      );

      pts.push(world);
    }

    polyline(pts);
  }
}

function fieldDirection(p, sign) {
  const hit = closestOnSpine(p);
  const d = Math.sqrt(hit.d2) + 1e-6;

  // stronger near the spine
  const near = Math.exp(-d / fieldSpread);

  // pull toward spine centerline
  const pull = (-hit.offset / (fieldSpread * 0.5)) * fieldPull * near;

  // swirl around it
  const swirlPhase = Math.PI * 2 * twistTurns * hit.t + d * 0.08;
  const swirl = Math.sign(hit.offset || 1) * fieldSwirl * (0.35 + 0.65 * near) * Math.sin(swirlPhase);

  let dir = vec(0, 0);
  dir = add(dir, mul(hit.tan, sign * 1.0));
  dir = add(dir, mul(hit.nor, pull + swirl));
  dir = add(dir, vec(fieldDriftX, fieldDriftY));

  return norm(dir);
}

function traceField(seed) {
  const forward = [seed];
  let p = vec(seed.x, seed.y);

  for (let i = 0; i < fieldSteps; i++) {
    const d = fieldDirection(p, 1);
    p = add(p, mul(d, fieldStepSize));
    if (!inBounds(p)) break;
    forward.push(p);
  }

  const backward = [];
  p = vec(seed.x, seed.y);

  for (let i = 0; i < fieldSteps; i++) {
    const d = fieldDirection(p, -1);
    p = add(p, mul(d, fieldStepSize));
    if (!inBounds(p)) break;
    backward.push(p);
  }

  backward.reverse();
  return backward.concat(forward);
}

function drawField() {
  const half = Math.floor(fieldLines / 2);

  for (let i = 0; i < fieldLines; i++) {
    const side = (i % 2 === 0) ? -1 : 1;
    const j = Math.floor(i / 2);
    const t = (half <= 1) ? 0.5 : j / (half - 1);

    const y = lerp(-spineLength / 2 - 18, spineLength / 2 + 18, t);

    // seed lines further away, but not uniformly
    const spread = lerp(fieldSpread * 0.45, fieldSpread, (Math.sin(t * 11.7) * 0.5 + 0.5));
    const x = side * spread;

    const seed = vec(x, y);
    const pts = traceField(seed);
    polyline(pts);
  }
}

function drawCenterSpine() {
  const pts = [];
  for (let i = 0; i <= spineSamples; i++) {
    pts.push(spinePoint(i / spineSamples));
  }
  polyline(pts);
}

drawField();
drawCageRibs();
drawSpineStrands();
drawCenterSpine();