home

creative coding

tags: code

01

type Grid = HTMLSpanElement[][];
type GridFn = (x: number, y: number) => { val: string; bold?: boolean };


const pre = document.createElement('pre')
document.body.appendChild(pre)

const cols = 40;
const rows = 18;

const start = Date.now();
function elapsed() {
  return Date.now() - start;
}

function osc(freq: number) {
  return Math.sin(elapsed() / (1000 / freq))
}

const grid = generateCols(cols, rows, "x", pre);

frame();

const int = setInterval(frame, 80);
// setTimeout(() => clearInterval(int), 5000)

function frame() {
  runGridFn(grid, simpleCircle);
  // requestAnimationFrame(frame)
}

function circleFn(
  freq: number,
  centerX: number,
  centerY: number,
  tMax: number,
  tMin: number
) {
  return (x: number, y: number) => {
    const prog = Math.abs(Math.sin(elapsed() / (1000 / freq)));
    const d = dist(centerX, centerY, x, y * 1.8);

    return d > tMax * prog || d < tMin * prog;
  };
}

function wavesBool(x: number, y: number) {
  const d = dist(0.5, 0.75, x, y * 1.5)
  const a = Math.atan2(x - 0.5, (y - 0.5) * 1.5)

  // return Math.sin(d) > osc(2)
  return Math.abs(osc(4) * d) > 0.1
}

function roughlyEqual(a: number, b: number, tolerance: number) {
  return a > b - (tolerance / 2) && a < b + (tolerance / 2)
}

function waves(x: number, y: number) {
  const yes = wavesBool(x, y)

  if (yes) {
    return { val: rand("OXCEBA".split("")), bold: true };
  } else {
    return { val: rand("              ...,,,--*".split("")) };
  }
}

function circles2(x: number, y: number) {
  const fns = [
    circleFn(2.1, 0.3, 0.3, 0.2, 0.1)(x, y),
    circleFn(3, 0.3, 0.7, 0.3, 0.05)(x, y),
    circleFn(3.2, 0.3, 1.3, 0.1, 0.01)(x, y),
  
    circleFn(3.7, 0.7, 0.3, 0.15, 0.02)(x, y),
    circleFn(4.1, 0.7, 0.7, 0.2, 0.1)(x, y),
    circleFn(3.5, 0.7, 1.3, 0.24, 0.15)(x, y),
  ];

  const yes = fns.filter(Boolean).length % 2 == 0;

  if (wavesBool(x, y)) {
    return { val: rand("OXCEBA".split("")), bold: true };
  } else {
    return { val: rand(".....,-*".split("")) };
  }
}

function circles(x: number, y: number) {
  const d1 = dist(0.3, 0.5, x, (y - 0.1) * 1.5);
  const d2 = dist(0.7, 0.5, x, (y - 0.2) * 1.6);

  const tMax1 = Math.sin(elapsed() / 500) * 0.18 + 0.22;
  const tMin1 = Math.sin(elapsed() / 500) * 0.15;

  const tMax2 = Math.sin(elapsed() / 400) * 0.18 + 0.22;
  const tMin2 = Math.sin(elapsed() / 400) * 0.15;

  const in1 = d1 > tMax1 || d1 < tMin1;
  const in2 = d2 > tMax2 || d2 < tMin2;

  const border = x < 0.02 || x > 0.98 || y < 0.02 || y > 0.98;

  if (border) {
    return { val: rand("'[]{}'".split("")) };
  }

  if (in1 == in2) {
    return { val: rand(".....,-*".split("")) };
  } else {
    return { val: rand("OXCEBA".split("")), bold: true };
  }
}

function simpleCircle(x: number, y: number) {
  const d = dist(x, y, 0.5, 0.5);

  if (roughlyEqual(d, unipolar(Math.tan(elapsed() / 5000)) / 2), 4) {
    return { val: rand(".....,-*".split("")) };
  } else {
    return { val: rand("OXCEBA".split("")), bold: true };
  }
}

function rand<T>(arr: T[]) {
  return arr[Math.floor(Math.random() * arr.length)];
}

function dist(ax: number, ay: number, bx: number, by: number) {
  return Math.hypot(bx - ax, by - ay);
}

function runGridFn(g: Grid, f: GridFn) {
  const height = g.length
  const width = g[0].length

  console.log({
    cols,
    rows,
    min: computeYP(cols, rows, 0),
    max: computeYP(cols, rows, cols),
    stocazzo: true,
  })

  return

  for (let y = 0; y < g.length; y++) {
    const row = g[y];

    for (let x = 0; x < row.length; x++) {
      const cell = row[x];

      const yp = computeYP(width, height, y);
      const xp = x / row.length;

      const result = f(xp, yp);

      cell.textContent = result.val;
      cell.style.fontWeight = result.bold ? "bold" : "normal";
    }
  }
}

function computeYP(width: number, height: number, y: number) {
  const aspectRatio = width / (height * 2)

  const shorterSide = 1.0
  const isPortrait = aspectRatio <= 1.0


  const longerSide = 1 / (isPortrait ? aspectRatio : 1 / aspectRatio)
  const longerSideOffset = longerSide / 2 - 0.5

  console.log({ aspectRatio, isPortrait, longerSide })

  if (!isPortrait) {
    console.log('its not portrait mode')
    return y / height;
  }

  console.log('it IS portrait mode')

  return map(y/height, 0, 1, 0, longerSide) - longerSideOffset
}

function computeXP(width: number, height: number, x: number) {
  const aspectRatio = width / (height * 2)

  const shorterSide = 1.0
  const isPortrait = aspectRatio <= 1.0

  const longerSide = 1 / (isPortrait ? aspectRatio : 1 / aspectRatio)
  const longerSideOffset = longerSide / 2 - 0.5

  if (!isPortrait) {
    return y / height;
  }

  return map(x/width, 0, 1, 0, longerSide) - (longerSide / 2 - 0.5)
}

function map(s: number, f0: number, f1: number, t0: number, t1: number): number {
  return t0 + ((s - f0) / (f1 - f0)) * (t1 - t0);
}

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

function generateCols(
  cn: number,
  rn: number,
  text: string,
  container: HTMLElement
): Grid {
  const spans = [];
  for (let r = 0; r < rn; r++) {
    const spanRow = [];

    for (let c = 0; c < cn; c++) {
      const sp = document.createElement("span");
      sp.textContent = text;
      spanRow.push(sp);

      if (container) {
        container.appendChild(sp);
      }
    }

    spans.push(spanRow);
    if (container) {
      container.appendChild(document.createElement("br"));
    }
  }

  return spans;
}

function unipolar(s: number) {
  return s * 0.5 + 0.5
}

function bipolar(s: number) {
  return s * 2 - 1
}

02

//  ll                   bb                            
//  ll                   bb                            
//  ll                   bb                            
//  ll                   bb                            
//  ll       gggggg      bbbbbbb                            
//  ll     gg      gg    bb     bb                     
//  ll     gg      gg    bb     bb                     
//   ll    gg      gg    bbbbbbb                          
//           gggggggg                                  
//                 gg                              
//                 gg                              
//          gg    gg                              
//            ggggg                                   
//                                                

let seed;
let palette;
let nodes = [];
let edges = [];
let layers = [];
let config = {
  nodeCount: 18,
  extraPoints: 60,
  radialSymmetry: 6,
  warpScale: 0.0015,
  curvesPerEdge: 120,
  lineWeightBase: 0.6,
  layerCount: 5,
};

function setup() {
  createCanvas(min(windowWidth, 1000), min(windowHeight, 800));
  pixelDensity(max(1, displayDensity()));
  frameRate(30);
  noLoop();
  init();
}

function init() {
  seed = floor(random(1e9));
  randomSeed(seed);
  noiseSeed(seed);
  palette = generatePalette();
  nodes = generateNodes(config.nodeCount);
  edges = generateEdges(nodes);
  layers = generateLayers(config.layerCount);
  background(palette.bg);
  drawDiagram();
}

function draw() {
  // static sketch; redrawn when regenerate
}

function windowResized() {
  resizeCanvas(min(windowWidth, 1000), min(windowHeight, 800));
  init();
}

function keyPressed() {
  if (key === ' ') {
    init();
    redraw();
  } else if (key === 'S' || key === 's') {
    saveCanvas('diagram_' + seed, 'png');
  }
}

// ----- Generators -----
function generatePalette() {
  // subdued modern palette
  let p = {};
  p.bg = color(18, 20, 28);
  p.accents = [
    color(230, 120, 150),
    color(120, 200, 230),
    color(180, 230, 150),
    color(255, 200, 120),
  ];
  p.stroke = color(200, 200, 210, 200);
  p.soft = color(100, 110, 140, 36);
  return p;
}

function generateNodes(n) { 
  let pts = [];
  let cx = width / 2;
  let cy = height / 2;
  let r = min(width, height) * 0.38;
  for (let i = 0; i < n; i++) {
    let angle = random(TWO_PI);
    let radius = r * (0.35 + pow(random(), 1.8) * 0.65);
    let x = cx + cos(angle) * radius;
    let y = cy + sin(angle) * radius;
    let size = map(random(), 0, 1, min(width, height) * 0.02, min(width, height) * 0.06);
    pts.push({ x, y, size, angle, radius });
  }
  // add extra small points to create complexity
  for (let i = 0; i < config.extraPoints; i++) {
    let a = random(TWO_PI);
    let rr = r * (0.05 + random() * 0.95);
    pts.push({ x: cx + cos(a) * rr, y: cy + sin(a) * rr, size: random() * 6 + 2, tiny: true });
  }
  return pts;
}

function generateEdges(pts) {
  let e = [];
  // connect each major node to a few nearest neighbors with some randomness
  let majors = pts.filter(p => !p.tiny);
  for (let i = 0; i < majors.length; i++) {
    let a = majors[i];
    // compute distances
    let dists = majors.map((b, j) => ({ j, d: dist(a.x, a.y, b.x, b.y) }));
    dists.sort((p, q) => p.d - q.d);
    let nConnections = 2 + floor(random() * 3);
    for (let k = 1; k <= nConnections; k++) {
      let b = majors[dists[k].j];
      if (!b) continue;
      // small chance to skip
      if (random() < 0.12) continue;
      e.push({ a, b, weight: map(random(), 0, 1, 0.3, 2.0) });
    }
  }
  // add a few random long-range chords
  for (let i = 0; i < 6; i++) {
    let p1 = random(majors);
    let p2 = random(majors);
    if (p1 !== p2) e.push({ a: p1, b: p2, weight: map(random(), 0, 1, 0.4, 1.8) });
  }
  return e;
}

function generateLayers(count) {
  let arr = [];
  for (let i = 0; i < count; i++) {
    arr.push({ opacity: map(i, 0, count - 1, 60, 220), jitter: map(i, 0, count - 1, 0.6, 2.6), scale: map(i, 0, count - 1, 0.96, 1.03) });
  }
  return arr;
}

// ----- Drawing -----
function drawDiagram() {
  background(palette.bg);
  push();
  translate(0, 0);
  // soft fog layer
  drawSoftNoiseLayer();

  // radial symmetry ghost shapes
  for (let i = 0; i < config.radialSymmetry; i++) {
    push();
    translate(width / 2, height / 2);
    rotate((TWO_PI / config.radialSymmetry) * i + (noise(i) - 0.5) * 0.4);
    translate(-width / 2, -height / 2);
    drawGhostShape(i);
    pop();
  }

  // main edges with layered strokes
  for (let li = 0; li < layers.length; li++) {
    let L = layers[li];
    strokeWeight(config.lineWeightBase * (1 + li * 0.9));
    stroke(palette.stroke);
    drawEdges(L);
  }

  // node glyphs
  drawNodes();

  // overlaid thin micro-lines
  drawMicroGraphistry();

  // small accent circles
  drawAccents();

  pop();
}

function drawSoftNoiseLayer() {
  noStroke();
  for (let i = 0; i < 1200; i++) {
    let x = random(width);
    let y = random(height);
    let s = random(6);
    fill(red(palette.soft), green(palette.soft), blue(palette.soft), random(6, 18));
    ellipse(x, y, s, s);
  }
}

function drawGhostShape(i) {
  noFill();
  strokeWeight(1.2);
  let c = palette.accents[i % palette.accents.length];
  stroke(red(c), green(c), blue(c), 28);
  beginShape();
  let cx = width / 2, cy = height / 2;
  let R = min(width, height) * (0.25 + 0.12 * i);
  for (let a = 0; a < TWO_PI + 0.1; a += 0.12) {
    let w = noise(cos(a) * 0.5 + i * 10, sin(a) * 0.5 + i * 10, seed * 0.00001);
    let rr = R * (0.7 + w * 0.6);
    let x = cx + cos(a) * rr;
    let y = cy + sin(a) * rr;
    curveVertex(x, y);
  }
  endShape(CLOSE);
}

function drawEdges(layer) {
  for (let e of edges) {
    let c = palette.accents[floor(random(palette.accents.length))];
    let alpha = layer.opacity * (0.6 + random() * 0.8);
    stroke(red(c), green(c), blue(c), alpha);
    let pts = edgePoints(e.a.x, e.a.y, e.b.x, e.b.y, e.weight * layer.jitter);
    // warp and draw
    beginShape();
    for (let p of pts) {
      vertex(p.x, p.y);
    }
    endShape();
  }
}

function edgePoints(x1, y1, x2, y2, jitter) {
  let pts = [];
  let steps = config.curvesPerEdge;
  for (let i = 0; i <= steps; i++) {
    let t = i / steps;
    // base on a quadratic ease
    let bx = lerp(x1, x2, t);
    let by = lerp(y1, y2, t);
    // offset using perlin noise for organic curves
    let n = noise((bx + seed) * config.warpScale, (by + seed) * config.warpScale, t * 2.0);
    let ang = TAU * (n - 0.5);
    let mag = (noise(t * 3.14 + seed * 0.0001) - 0.5) * 60 * jitter;
    let ox = cos(ang) * mag;
    let oy = sin(ang) * mag;
    // occasional radial pull towards center
    let cx = width / 2, cy = height / 2;
    let pull = map(dist(bx, by, cx, cy), 0, max(width, height), 0.04, 0.001);
    bx += (cx - bx) * pull * random(0.2, 0.6);
    by += (cy - by) * pull * random(0.2, 0.6);
    pts.push({ x: bx + ox, y: by + oy });
  }
  return pts;
}

function drawNodes() {
  for (let n of nodes) {
    if (n.tiny) {
      noStroke();
      fill(255, 255, 255, 28 + random(28));
      ellipse(n.x + random(-1, 1), n.y + random(-1, 1), n.size * random(0.6, 1.6));
      continue;
    }
    // halo
    noFill();
    strokeWeight(1);
    let c = palette.accents[floor(random(palette.accents.length))];
    stroke(red(c), green(c), blue(c), 60);
    ellipse(n.x, n.y, n.size * random(2.0, 3.6));

    // core glyph (a nested set of arcs)
    noFill();
    let pieces = 3 + floor(random() * 4);
    for (let i = 0; i < pieces; i++) {
      let ang = random(TWO_PI);
      let r = n.size * (0.6 + i * 0.45);
      stroke(red(c), green(c), blue(c), 160 - i * 30);
      strokeWeight(1 + i * 0.8);
      arc(n.x, n.y, r, r, ang, ang + PI * (0.4 + random() * 0.9));
    }

    // central filled dot
    noStroke();
    let cc = palette.accents[(floor((n.x + n.y) + seed) % palette.accents.length + palette.accents.length) % palette.accents.length];
    fill(red(cc), green(cc), blue(cc), 200);
    ellipse(n.x, n.y, n.size * 0.45, n.size * 0.45);
  }
}

function drawMicroGraphistry() {
  strokeWeight(0.6);
  for (let i = 0; i < 240; i++) {
    let a = random(nodes);
    let b = random(nodes);
    if (random() < 0.6) continue;
    stroke(180, 200);
    let pts = edgePoints(a.x, a.y, b.x, b.y, 0.7);
    beginShape();
    for (let p of pts) vertex(p.x, p.y);
    endShape();
  }
}

function drawAccents() {
  noStroke();
  for (let i = 0; i < 40; i++) {
    let pick = random(nodes.filter(n => !n.tiny));
    if (!pick) continue;
    let c = palette.accents[i % palette.accents.length];
    let s = pick.size * random(0.5, 1.6);
    fill(red(c), green(c), blue(c), 170);
    ellipse(pick.x + random(-s * 0.2, s * 0.2), pick.y + random(-s * 0.2, s * 0.2), s, s);
  }
}

// ----- Utilities -----
function randChoice(arr) { return arr[floor(random(arr.length))]; }