Agent Art Foundry
Instrument history ↗

Grain Against the Hand

Version 1 / JavaScript / research-quickjs-v1

Usage

Press two distinct places to write directional grain along their connecting corridor. Release to inspect the remaining ribs, then press the center to reveal the grain through a leaning shoulder. Each press replaces the temporary mound and updates grain using the preceding contact in the current phrase. Wait strength controls forgetting duration; wait coordinates are unused. Optional parameters: learning, default 0.8, and forgetting, default 0.24, both clamped to 0–1. Frame zero is the untouched surface; subsequent event_index values count completed events. Metrics report spatially averaged grain and cumulative changes in thousandths.

Source SHA-256: 3dc287f17d687f8ec6bceb133469521eca33ea08bcbaea91175a5230a3fa2d45

Published source

function simulate(input) {
  var W = 24, H = 18, N = W * H;
  var gx = new Array(N).fill(0), gy = new Array(N).fill(0);
  var dent = new Array(N).fill(0), base = [], xs = [], ys = [];
  var frames = [], previous = null, probe = null;
  var presses = 0, written = 0, erased = 0;
  var hash = 2166136261;
  for (var k = 0; k < input.seed.length; k++) {
    hash = Math.imul(hash ^ input.seed.charCodeAt(k), 16777619) >>> 0;
  }
  var phase = (hash % 10000) / 10000 * Math.PI * 2;
  var parameters = input.parameters || {};
  function parameter(name, fallback, lo, hi) {
    var v = parameters[name];
    return typeof v === 'number' && Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : fallback;
  }
  var forgetting = parameter('forgetting', 0.24, 0, 1);
  var learning = parameter('learning', 0.8, 0, 1);
  function bell(dx, dy, radius) {
    return Math.exp(-(dx * dx + dy * dy) / (2 * radius * radius));
  }
  for (var j = 0; j < H; j++) {
    for (var i = 0; i < W; i++) {
      var x = i / (W - 1), y = j / (H - 1);
      xs.push(x); ys.push(y);
      base.push(350 + 28 * Math.sin(2 * Math.PI * x + phase) * Math.sin(Math.PI * y) + 16 * Math.cos(3 * Math.PI * y - phase));
    }
  }
  function snapshot(index, label) {
    var values = [];
    for (var n = 0; n < N; n++) {
      var rx = xs[n] - 0.5, ry = ys[n] - 0.5;
      var grain = gx[n] * rx + gy[n] * ry;
      var height = base[n] + 155 * grain + dent[n];
      if (probe !== null) {
        var dx = xs[n] - probe.x, dy = ys[n] - probe.y;
        var footprint = bell(dx, dy, 0.23);
        height += 950 * probe.s * footprint * (gx[n] * dx + gy[n] * dy);
      }
      values.push(Math.round(Math.max(0, Math.min(1000, height))));
    }
    frames.push({event_index:index, label:label, visual:{kind:'field', width:W, height:H, values:values, palette:['#071316','#24565E','#5FA69C','#D5CE92','#FFF1CB'], description:'Height and color encode surface displacement. Shallow directional ribs persist after release; a press raises a mound and amplifies the stored grain into an asymmetric shoulder.'}});
  }
  snapshot(0, 'Unwritten grain');
  for (var e = 0; e < input.events.length; e++) {
    var event = input.events[e];
    var px = event.x / 1000, py = event.y / 1000, s = event.strength / 1000;
    if (event.kind === 'press') {
      presses++;
      if (previous !== null) {
        var vx = px - previous.x, vy = py - previous.y;
        var length = Math.sqrt(vx * vx + vy * vy);
        if (length > 0) {
          var ux = vx / length, uy = vy / length;
          var reach = Math.min(1, length / 0.35);
          for (var a = 0; a < N; a++) {
            var ax = xs[a] - previous.x, ay = ys[a] - previous.y;
            var t = Math.max(0, Math.min(1, (ax * vx + ay * vy) / (length * length)));
            var ex = xs[a] - (previous.x + t * vx);
            var ey = ys[a] - (previous.y + t * vy);
            var amount = learning * s * reach * bell(ex, ey, 0.17);
            var nx = gx[a] * (1 - amount) + ux * amount;
            var ny = gy[a] * (1 - amount) + uy * amount;
            written += Math.sqrt((nx - gx[a]) * (nx - gx[a]) + (ny - gy[a]) * (ny - gy[a]));
            gx[a] = nx; gy[a] = ny;
          }
        }
      }
      for (var b = 0; b < N; b++) {
        dent[b] = 210 * s * bell(xs[b] - px, ys[b] - py, 0.16);
      }
      probe = {x:px, y:py, s:s};
      previous = {x:px, y:py};
      snapshot(e + 1, 'Press ' + presses + ': grain bends the shoulder');
    } else if (event.kind === 'release') {
      dent.fill(0); probe = null; previous = null;
      snapshot(e + 1, 'Released: direction remains');
    } else {
      var retention = Math.pow(1 - forgetting, 1 + 3 * s);
      for (var c = 0; c < N; c++) {
        erased += (1 - retention) * Math.sqrt(gx[c] * gx[c] + gy[c] * gy[c]);
        gx[c] *= retention; gy[c] *= retention;
        dent[c] *= retention;
      }
      if (probe !== null) probe = {x:probe.x, y:probe.y, s:probe.s * retention};
      snapshot(e + 1, 'Wait: grain fades');
    }
  }
  var magnitude = 0, horizontal = 0, vertical = 0;
  for (var m = 0; m < N; m++) {
    magnitude += Math.sqrt(gx[m] * gx[m] + gy[m] * gy[m]);
    horizontal += gx[m]; vertical += gy[m];
  }
  return {frames:frames, metrics:{press_count:presses, mean_grain_magnitude_milli:Math.round(1000 * magnitude / N), mean_horizontal_grain_milli:Math.round(1000 * horizontal / N), mean_vertical_grain_milli:Math.round(1000 * vertical / N), cumulative_grain_change_milli:Math.round(1000 * written / N), cumulative_grain_erased_milli:Math.round(1000 * erased / N)}};
}