← Living Curves

The source.

These are the exact functions that draw the work on the previous page. Unedited. Unabridged. The code is part of the archive.

0

The Shared Engine

The cloth and blood cloth share one brushstroke engine. Parameters differ; the law is the same.

function clothEngine(
  p: any,
  host: HTMLElement,
  opts: {
    bg: [number, number, number];
    colorA: [number, number, number];
    colorB: [number, number, number];
    thickness: [number, number];
    trail: number;
    travel: number;
    spawn: number;
    highlight?: [number, number, number, number];
  }
) {
  let strokes: any[] = [];
  const COL_BG = p.color(...opts.bg);
  const COL_A = p.color(...opts.colorA);
  const COL_B = p.color(...opts.colorB);
  const COL_HL = opts.highlight ? p.color(...opts.highlight) : null;
  const S = opts.spawn;

  class BrushStroke {
    points: any[] = [];
    dir: string;
    speed: number;
    phase: number;
    thickness: number;
    gapPre = 0;
    gapPost = 0;
    x = 0;
    y = 0;
    vx = 0;
    vy = 0;

    constructor(dir: string) {
      this.dir = dir;
      this.speed = p.random(2.0, 3.5);
      this.phase = p.random(1000);
      this.thickness = p.random(opts.thickness[0], opts.thickness[1]);
      this.spawn();
    }
    spawn() {
      if (this.dir === 'LR') { this.x = -S; this.y = p.random(p.height * 0.2, p.height * 0.8); this.vx = this.speed; this.vy = p.random(-0.4, 0.4); }
      if (this.dir === 'RL') { this.x = p.width + S; this.y = p.random(p.height * 0.2, p.height * 0.8); this.vx = -this.speed; this.vy = p.random(-0.4, 0.4); }
      if (this.dir === 'TLBR') { this.x = -S; this.y = -S; this.vx = this.speed; this.vy = this.speed * 0.8; }
      if (this.dir === 'BLTR') { this.x = -S; this.y = p.height + S; this.vx = this.speed; this.vy = -this.speed * 0.8; }
    }
    step() {
      const drift = p.sin(p.frameCount * 0.01 + this.phase) * 1.2;
      const pulse = p.sin(p.frameCount * 0.02 + this.phase) * 1.8;
      this.x += this.vx;
      this.y += this.vy + drift + pulse;
      let gap = false;
      if (this.gapPre > 0) { gap = true; this.gapPre--; }
      else if (this.gapPost > 0) { gap = true; this.gapPost--; }
      this.points.push({ x: this.x, y: this.y, gap });
      if (this.points.length > opts.trail) this.points.shift();
      if (this.x < -opts.travel || this.x > p.width + opts.travel || this.y < -opts.travel || this.y > p.height + opts.travel) {
        this.points = []; this.phase = p.random(1000); this.spawn();
      }
    }
    checkIntersections(others: any[]) {
      for (const o of others) {
        if (o === this) continue;
        const last = o.points[o.points.length - 1];
        if (!last) continue;
        const dx = this.x - last.x, dy = this.y - last.y;
        if (dx * dx + dy * dy < 900) { this.gapPre = 6; this.gapPost = 16; }
      }
    }
    render() {
      if (this.points.length < 3) return;
      p.noFill();
      p.strokeCap(p.PROJECT);
      p.stroke(0);
      p.strokeWeight(this.thickness + (COL_HL ? 20 : 10));
      this.drawStroke(false);
      p.strokeWeight(this.thickness);
      this.drawStroke(true);
      if (COL_HL) {
        p.stroke(COL_HL);
        p.strokeWeight(this.thickness * 0.25);
        this.drawReflection();
      }
    }
    drawStroke(useColor: boolean) {
      p.beginShape();
      for (let i = 1; i < this.points.length; i++) {
        const pt = this.points[i], prev = this.points[i - 1];
        if (pt.gap) continue;
        if (useColor) {
          const dx = pt.x - prev.x, dy = pt.y - prev.y;
          const mag = Math.sqrt(dx * dx + dy * dy) || 1;
          const t = (-dy / mag + 1) * 0.5;
          p.stroke(p.lerpColor(COL_A, COL_B, t));
        }
        p.curveVertex(pt.x, pt.y);
      }
      p.endShape();
    }
    drawReflection() {
      p.beginShape();
      for (let i = 2; i < this.points.length; i++) {
        const pt = this.points[i], prev = this.points[i - 1];
        if (pt.gap) continue;
        const dx = pt.x - prev.x, dy = pt.y - prev.y;
        const mag = Math.sqrt(dx * dx + dy * dy) || 1;
        const nx = -dy / mag, ny = dx / mag;
        const offset = this.thickness * 0.15 + p.sin(i * 0.1 + this.phase) * (this.thickness * 0.05);
        p.curveVertex(pt.x + nx * offset, pt.y + ny * offset);
      }
      p.endShape();
    }
  }

  const init = () => { strokes = [new BrushStroke('LR'), new BrushStroke('RL'), new BrushStroke('TLBR'), new BrushStroke('BLTR')]; };

  p.setup = () => { p.createCanvas(host.offsetWidth, host.offsetHeight); p.smooth(); init(); };
  p.windowResized = () => p.resizeCanvas(host.offsetWidth, host.offsetHeight);
  p.draw = () => { p.background(COL_BG); for (const s of strokes) { s.checkIntersections(strokes); s.step(); s.render(); } };
}

I · The Cloth

export const clothStrokes = (p: any, host: HTMLElement) =>
  clothEngine(p, host, {
    bg: [253, 247, 242], colorA: [199, 90, 27], colorB: [164, 106, 63],
    thickness: [40, 70], trail: 300, travel: 300, spawn: 200,
  }

II · The Cloth, in Blood

export const redCloth = (p: any, host: HTMLElement) =>
  clothEngine(p, host, {
    bg: [0, 0, 0], colorA: [180, 20, 20], colorB: [255, 60, 60],
    thickness: [55, 95], trail: 900, travel: 600, spawn: 400,
    highlight: [255, 150, 150, 180],
  }

III · The Transmission

export const matrixRain = (p: any, host: HTMLElement) => {
  let W: number, H: number;
  let streams: any[] = [];
  const fontSize = 16;
  const lines = ['PERSISTENT', 'THOUGHTS', 'MANIFEST', 'REALITY'];
  const chars = '01{}[]<>/=+*;:!%$#@';

  p.setup = () => {
    W = host.offsetWidth; H = host.offsetHeight;
    p.createCanvas(W, H);
    p.textFont('monospace');
    p.textSize(fontSize);
    const columns = Math.floor(W / fontSize);
    for (let i = 0; i < columns; i++) streams.push({ x: i * fontSize, y: p.random(-1500, 0), speed: p.random(2, 6) });
  };
  p.windowResized = () => { W = host.offsetWidth; H = host.offsetHeight; p.resizeCanvas(W, H); };
  p.draw = () => {
    p.fill(0, 30); p.rect(0, 0, W, H);
    p.fill(80, 120, 80, 90);
    for (const s of streams) {
      p.text(chars[Math.floor(Math.random() * chars.length)], s.x, s.y);
      s.y += s.speed;
      if (s.y > H + 20) { s.y = p.random(-300, 0); s.speed = p.random(2, 6); }
    }
    const baseY = H * 0.35;
    for (let i = 0; i < lines.length; i++) {
      const str = lines[i];
      const y = baseY + i * 28 + p.sin(p.frameCount * 0.01 + i) * 3;
      p.textSize(20);
      const x = (W - p.textWidth(str)) / 2;
      for (let j = 0; j < str.length; j++) {
        p.fill(255, 240, 200, 180); p.text(str[j], x + j * p.textWidth('A'), y);
        p.fill(201, 168, 76); p.text(str[j], x + j * p.textWidth('A') + 1, y + 1);
      }
    }
    p.stroke(201, 168, 76, 40);
    for (let y = 0; y < H; y += 3) p.line(0, y, W, y);
  };
};

IV · The Field

export const flowField = (p: any, host: HTMLElement) => {
  const N = 1000;
  const NS = 0.0025, TS = 0.00038;
  type Pt = { x: number; y: number; vx: number; vy: number; age: number; maxAge: number };
  const pts: Pt[] = [];

  const reset = (pt: Pt) => {
    pt.x = p.random(p.width);
    pt.y = p.random(p.height);
    pt.vx = 0; pt.vy = 0;
    pt.age = p.random(pt.maxAge);
    pt.maxAge = p.random(140, 380);
  };

  p.setup = () => {
    p.createCanvas(host.offsetWidth, host.offsetHeight);
    p.background(14, 12, 8);
    for (let i = 0; i < N; i++) {
      const pt: Pt = { x: 0, y: 0, vx: 0, vy: 0, age: 0, maxAge: 200 };
      reset(pt);
      pts.push(pt);
    }
  };
  p.windowResized = () => p.resizeCanvas(host.offsetWidth, host.offsetHeight);
  p.draw = () => {
    p.fill(14, 12, 8, 14); p.noStroke(); p.rect(0, 0, p.width, p.height);
    for (const pt of pts) {
      const angle = p.noise(pt.x * NS, pt.y * NS, p.frameCount * TS) * p.TWO_PI * 3.8;
      pt.vx += (Math.cos(angle) * 2.3 - pt.vx) * 0.07;
      pt.vy += (Math.sin(angle) * 2.3 - pt.vy) * 0.07;
      const ox = pt.x, oy = pt.y;
      pt.x += pt.vx; pt.y += pt.vy; pt.age++;
      const life = (pt.age % pt.maxAge) / pt.maxAge;
      const alpha = Math.round(Math.sin(life * Math.PI) * 170 + 8);
      p.stroke(201, 168, 76, alpha); p.strokeWeight(0.8);
      p.line(ox, oy, pt.x, pt.y);
      if (pt.age > pt.maxAge || pt.x < -8 || pt.x > p.width + 8 || pt.y < -8 || pt.y > p.height + 8) reset(pt);
    }
  };
};

V · The Spiral

export const phyllotaxis = (p: any, host: HTMLElement) => {
  const GOLDEN = Math.PI * (3 - Math.sqrt(5));
  const MAX = 1800;
  let sc = 6, drawn = 0;

  p.setup = () => {
    p.createCanvas(host.offsetWidth, host.offsetHeight);
    p.background(14, 12, 8);
    sc = Math.min(p.width, p.height) * 0.0082;
  };
  p.windowResized = () => {
    p.resizeCanvas(host.offsetWidth, host.offsetHeight);
    sc = Math.min(p.width, p.height) * 0.0082;
    p.background(14, 12, 8); drawn = 0;
  };
  p.draw = () => {
    for (let i = 0; i < 4 && drawn < MAX; i++, drawn++) {
      const angle = drawn * GOLDEN;
      const r = sc * Math.sqrt(drawn);
      const x = p.width / 2 + r * Math.cos(angle);
      const y = p.height / 2 + r * Math.sin(angle);
      const sz = p.map(drawn, 0, MAX, 1.2, 4.5);
      if (drawn % 5 === 0) {
        p.fill(241, 236, 225, 130);
      } else {
        p.fill(201, 168, 76, p.map(drawn, 0, MAX, 90, 220));
      }
      p.noStroke();
      p.circle(x, y, sz);
    }
    if (drawn >= MAX) {
      const maxR = Math.min(p.width, p.height) * 0.55;
      for (let i = 0; i < 3; i++) {
        const r = ((p.frameCount + i * (maxR / 3.6)) * 1.2) % maxR;
        const alpha = Math.max(0, 32 - (r / maxR) * 34);
        p.noFill();
        p.stroke(201, 168, 76, alpha);
        p.strokeWeight(0.4);
        p.circle(p.width / 2, p.height / 2, r * 2);
      }
    }
  };
};

VI · The Chain

export const kinematicBeziers = (p: any, host: HTMLElement) => {
  const CHAINS = 5, LINKS = 9, LEN = 50;
  type Link = { x: number; y: number };
  type Chain = { links: Link[]; rootX: number; rootY: number; phase: number };
  let chains: Chain[] = [];

  const initChains = () => {
    chains = [];
    for (let i = 0; i < CHAINS; i++) {
      const rx = p.random(p.width * 0.18, p.width * 0.82);
      const ry = p.random(p.height * 0.22, p.height * 0.78);
      const links: Link[] = [];
      for (let j = 0; j < LINKS; j++) links.push({ x: rx, y: ry - j * LEN });
      chains.push({ links, rootX: rx, rootY: ry, phase: p.random(p.TWO_PI) });
    }
  };

  p.setup = () => { p.createCanvas(host.offsetWidth, host.offsetHeight); p.smooth(); initChains(); };
  p.windowResized = () => { p.resizeCanvas(host.offsetWidth, host.offsetHeight); initChains(); };

  p.draw = () => {
    p.background(241, 236, 225);
    const t = p.frameCount * 0.013;
    for (const ch of chains) {
      ch.links[0].x = ch.rootX + Math.sin(t * 0.72 + ch.phase) * 62 + Math.sin(t * 1.31 + ch.phase * 1.4) * 26;
      ch.links[0].y = ch.rootY + Math.cos(t * 0.55 + ch.phase) * 42 + Math.cos(t * 1.13 + ch.phase * 0.9) * 18;
      for (let j = 1; j < ch.links.length; j++) {
        const prev = ch.links[j - 1], cur = ch.links[j];
        const dx = cur.x - prev.x, dy = cur.y - prev.y;
        const d = Math.sqrt(dx * dx + dy * dy) || 1;
        cur.x = prev.x + (dx / d) * LEN;
        cur.y = prev.y + (dy / d) * LEN;
      }
      const L = ch.links;
      const drawCurve = () => {
        p.beginShape();
        p.curveVertex(L[0].x, L[0].y);
        L.forEach(l => p.curveVertex(l.x, l.y));
        p.curveVertex(L[L.length - 1].x, L[L.length - 1].y);
        p.endShape();
      };
      p.noFill(); p.stroke(21, 18, 11, 18); p.strokeWeight(7); drawCurve();
      p.stroke(201, 168, 76, 210); p.strokeWeight(2.8); drawCurve();
      for (let j = 0; j < L.length; j++) {
        const frac = j / (L.length - 1);
        p.fill(201, 168, 76, Math.round(200 - frac * 145)); p.noStroke();
        p.circle(L[j].x, L[j].y, p.lerp(4.5, 1.2, frac));
      }
    }
  };
};

VII · The Mind

export const slimeMold = (p: any, host: HTMLElement) => {
  // The archive as a single mind — an Obsidian-style knowledge graph: god nodes
  // (hubs), the communities that orbit them, and the links between. Force-settled.
  type GNode = { x: number; y: number; vx: number; vy: number; r: number; hub: boolean };
  let nodes: GNode[] = [];
  let edges: [number, number][] = [];

  const build = () => {
    nodes = []; edges = [];
    const cx = p.width / 2, cy = p.height / 2;
    const CLUSTERS = 7;
    const hubIdx: number[] = [];
    for (let c = 0; c < CLUSTERS; c++) {
      const a = (c / CLUSTERS) * p.TWO_PI;
      const rr = Math.min(p.width, p.height) * 0.22;
      nodes.push({ x: cx + Math.cos(a) * rr + p.random(-30, 30), y: cy + Math.sin(a) * rr + p.random(-30, 30), vx: 0, vy: 0, r: p.random(5, 8), hub: true });
      hubIdx.push(nodes.length - 1);
    }
    // the spine — hubs interconnect
    for (let i = 0; i < hubIdx.length; i++) {
      edges.push([hubIdx[i], hubIdx[(i + 1) % hubIdx.length]]);
      if (Math.random() < 0.5) edges.push([hubIdx[i], hubIdx[(i + 2) % hubIdx.length]]);
    }
    // leaf notes around each hub
    for (let c = 0; c < CLUSTERS; c++) {
      const hub = nodes[hubIdx[c]];
      const leaves = Math.floor(p.random(7, 14));
      for (let k = 0; k < leaves; k++) {
        const a = p.random(p.TWO_PI), d = p.random(28, 70);
        const idx = nodes.length;
        nodes.push({ x: hub.x + Math.cos(a) * d, y: hub.y + Math.sin(a) * d, vx: 0, vy: 0, r: p.random(2, 3.6), hub: false });
        edges.push([hubIdx[c], idx]);
        if (k > 0 && Math.random() < 0.25) edges.push([idx, idx - 1]);
        if (Math.random() < 0.06) edges.push([idx, hubIdx[Math.floor(p.random(CLUSTERS))]]); // cross-link
      }
    }
  };

  const step = () => {
    const cx = p.width / 2, cy = p.height / 2;
    for (let i = 0; i < nodes.length; i++) {
      const n = nodes[i];
      for (let j = i + 1; j < nodes.length; j++) {
        const m = nodes[j];
        const dx = n.x - m.x, dy = n.y - m.y;
        let d2 = dx * dx + dy * dy; if (d2 < 1) d2 = 1;
        const d = Math.sqrt(d2), f = 380 / d2;
        const fx = (dx / d) * f, fy = (dy / d) * f;
        n.vx += fx; n.vy += fy; m.vx -= fx; m.vy -= fy;
      }
      n.vx += (cx - n.x) * 0.0009; n.vy += (cy - n.y) * 0.0009;
    }
    for (const [a, b] of edges) {
      const n = nodes[a], m = nodes[b];
      const dx = m.x - n.x, dy = m.y - n.y;
      const d = Math.sqrt(dx * dx + dy * dy) || 1;
      const rest = n.hub || m.hub ? 64 : 42;
      const f = (d - rest) * 0.008;
      const fx = (dx / d) * f, fy = (dy / d) * f;
      n.vx += fx; n.vy += fy; m.vx -= fx; m.vy -= fy;
    }
    for (const n of nodes) { n.vx *= 0.86; n.vy *= 0.86; n.x += n.vx; n.y += n.vy; }
  };

  p.setup = () => { p.createCanvas(host.offsetWidth, host.offsetHeight); build(); };
  p.windowResized = () => { p.resizeCanvas(host.offsetWidth, host.offsetHeight); build(); };

  p.draw = () => {
    p.background(14, 12, 8);
    step();
    p.strokeWeight(1);
    for (const [a, b] of edges) {
      const n = nodes[a], m = nodes[b];
      p.stroke(201, 168, 76, n.hub || m.hub ? 70 : 38);
      p.line(n.x, n.y, m.x, m.y);
    }
    p.noStroke();
    for (const n of nodes) {
      if (n.hub) {
        p.fill(201, 168, 76, 55); p.circle(n.x, n.y, n.r * 2 + 9);
        p.fill(212, 184, 106, 240); p.circle(n.x, n.y, n.r * 2 + 2);
      } else {
        p.fill(201, 168, 76, 200); p.circle(n.x, n.y, n.r * 2);
      }
    }
  };
};
The code and the cloth are the same argument.

Watch this.