← INDEX / 一覧NYAMCO WORKS113 / 117
p5.js sketchworks/kiri/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 113

Fog Comes In

たちこめる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / one night's hourly visibility, Koschmieder contrast, nine rows of a town from 120 m to 18 km

A window over a town, from eight in the evening to eleven the next morning, in one minute. Nine rows of buildings stand at distances from 120 metres to 18 kilometres. The visibility comes from a weather model's hourly values for one real night, interpolated. Each row is seen through the air between it and the window: its contrast is exp(−3.912 d / V), and whatever contrast it loses becomes the colour of the air, which changes with the hour, from a dull city glow at night through dawn to day. The strip below is the whole night's fog, and the three small marks above it are the notices that arrived: a dense fog advisory at 1:28, a thunder advisory at 4:28, and the fog advisory lifted at 8:18. The first notice came at the clearest moment of the night, with the visibility near 23 kilometres and lights reaching to the horizon. The fog itself came later, between five and nine, thickest around six, at about 580 metres, when only the nearest two rows remain. Nothing arrived during it. At the top of the hill in the strip there is no mark at all. By eleven the window looks like an ordinary hazy morning. Without the strip, the window keeps no record that anything happened.

窓から見える町。夜八時から次の朝十一時までを一分で。九列の建物が、120m から 18km までの距離に並んでいる。視程は、ある一晩の気象モデルの毎時の値を補間したもの。どの列も、窓とのあいだの空気を通して見える。コントラストは exp(−3.912 d / V)、失ったぶんは空気の色になる。空気の色は時刻で替わる——夜は町明かりの鈍い色、それから夜明け、それから昼。下の帯は一晩ぶんの霧、その上の小さな印は届いた通知。1:28 に濃霧注意報、4:28 に雷注意報、8:18 に濃霧注意報の解除。最初の通知が来たのは、その夜いちばん晴れていた時刻だった。視程はおよそ 23km、灯りは地平線まで続いている。霧そのものはそのあとに来た。五時から九時、いちばん濃いのは六時ごろで 580m、残るのは手前の二列だけ。そのあいだ、何も届いていない。帯の山のてっぺんには、印が一つもない。十一時の窓は、ふつうの霞んだ朝に見える。帯がなければ、窓には何かあったという記録がひとつも残らない。

下の帯が一晩の霧(高いほど濃い)、その上の三つの小さな三角が届いた通知、白い縦線がいまの時刻。窓は、いまの時刻の視程で見た町。

作る前に書いた予想

  1. いちばん濃い六時には、手前の一列(〜300m)しか残らない。→ だいたい当たり。 残るのは二列(120m で 0.45、224m で 0.22)、三列目(420m)はもう 0.06。
  2. いちばん濃い霧の時刻が、窓の中でいちばん明るい瞬間に見える(夜明けと重なるから)。→ 外れ。 六時は夜明けの途中で、窓は灰色にくすんでいる。六枚並べて、いちばん白く平らに見えたのは四十分あとの六時四十分で、そのころ霧はもう少しだけ薄い。いちばん濃い時刻と、いちばん白い時刻は、四十分ずれていた。
  3. 三つの印はどれも霧の山の外に落ちて、山のてっぺんには印がない。→ 当たり。8:18 は下り坂の肩にいる。
  4. 十一時の窓を見て、目は一晩に何かがあったと読めるか。→ 読めない。いちばん遠い列は十一時でもまだ見えない(視程 7km)けれど、それは霧のあとには見えず、ただの霞に見える。
SOURCE / ソース
// #113 (provisional) — a window over a town, one night, 20:00 -> 11:00.
// Hourly visibility V (metres) from a weather model for that night, interpolated in log.
// Each of nine rows of buildings sits at a distance d (120 m .. 18 km).
// Koschmieder: contrast C = exp(-3.912 d / V); apparent = object * C + air * (1 - C).
// The air colour follows the hour: dull city glow at night, dawn from 5:20, day from 7:00.
// Below: the whole night's visibility as a strip, and three notices as ticks.

export const VIS = [
  // hour (20 = 20:00, 24 = 0:00 next day ...), visibility in metres
  [20, 11720], [21, 20540], [22, 20700], [23, 22140], [24, 22600], [25, 22980],
  [26, 22980], [27, 16620], [28, 12480], [29, 4840], [30, 580], [31, 720],
  [32, 1600], [33, 1800], [34, 6620], [35, 7340],
];
export const NOTICES = [25 + 28 / 60, 28 + 28 / 60, 32 + 18 / 60];

export function visAt(h) {
  if (h <= VIS[0][0]) return VIS[0][1];
  for (let i = 0; i < VIS.length - 1; i++) {
    const [h0, v0] = VIS[i];
    const [h1, v1] = VIS[i + 1];
    if (h <= h1) {
      const u = (h - h0) / (h1 - h0);
      return Math.exp(Math.log(v0) * (1 - u) + Math.log(v1) * u);
    }
  }
  return VIS[VIS.length - 1][1];
}

export const contrast = (d, V) => Math.exp((-3.912 * d) / V);

const mix = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u];
const smooth = (x) => x * x * (3 - 2 * x);
function dayness(h) {
  // 0 = night, 1 = day. Dawn runs 5:20 -> 7:00.
  const a = 29 + 20 / 60, b = 31;
  if (h <= a) return 0;
  if (h >= b) return 1;
  return smooth((h - a) / (b - a));
}

export default function sketch(p) {
  const W = 720, H = 720;
  const WIN_B = 560;           // bottom of the window
  const HORIZON = 318;
  const F = 1000;              // px per metre at 1 m distance
  const ROWS = 9;
  const H0 = 20, H1 = 35;      // 20:00 -> 11:00
  const LOOP = 3600;           // frames per night
  const HOLD = 240;            // frames held at the end before looping

  const AIR_NIGHT = [44, 33, 30];
  const AIR_DAY = [206, 209, 212];
  const SKY_NIGHT = [10, 10, 18];
  const SKY_DAY = [150, 172, 196];
  const BLD_NIGHT = [9, 9, 13];
  const BLD_DAY = [74, 76, 82];
  const LIGHT = [255, 204, 140];

  const rows = [];

  function lightsOn(h) {
    // fraction of windows lit
    if (h < 24) return 0.5 - (h - 20) * 0.06;     // 0.50 -> 0.26
    if (h < 29) return 0.26 - (h - 24) * 0.03;    // -> 0.11
    if (h < 32) return 0.11 + (h - 29) * 0.06;    // -> 0.29
    return Math.max(0.05, 0.29 - (h - 32) * 0.08);
  }

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    p.randomSeed(113);
    for (let i = 0; i < ROWS; i++) {
      const d = 120 * Math.pow(18000 / 120, i / (ROWS - 1));
      const base = HORIZON + 242 * Math.pow(120 / d, 0.5);
      const blds = [];
      let x = -p.random(0, 30);
      while (x < W) {
        const wm = p.random(14, 40);
        const hm = p.random(12, 34) * (p.random() < 0.08 ? 2.2 : 1);
        const w = Math.max(2, (wm * F) / d);
        const hpx = Math.max(1.5, (hm * F) / d);
        const win = Math.max(1, (3.2 * F) / d);
        const wins = [];
        const cols = Math.max(1, Math.floor(w / (win * 1.7)));
        const levels = Math.max(1, Math.floor(hpx / (win * 1.6)));
        for (let c = 0; c < cols; c++) {
          for (let l = 0; l < levels; l++) {
            wins.push({
              x: x + (c + 0.5) * (w / cols) - win * 0.35,
              y: base - hpx + (l + 0.5) * (hpx / levels) - win * 0.3,
              s: Math.max(1, win * 0.7),
              k: p.random(),
            });
          }
        }
        blds.push({ x, w, h: hpx, wins });
        x += w + p.random(0, w * 0.15);
      }
      rows.push({ d, base, blds });
    }
  };

  function hourAt(frame) {
    const f = frame % (LOOP + HOLD);
    return H0 + (H1 - H0) * Math.min(1, f / LOOP);
  }

  function drawWindow(h) {
    const V = visAt(h);
    const dn = dayness(h);
    const air = mix(AIR_NIGHT, AIR_DAY, dn);
    const sky = mix(SKY_NIGHT, SKY_DAY, dn);
    const bld = mix(BLD_NIGHT, BLD_DAY, dn);
    const on = lightsOn(h);

    // sky: far enough that what you see is mostly air, plus a little of the sky above the fog
    const skyC = contrast(4000, V);
    p.noStroke();
    for (let y = 0; y < HORIZON + 40; y += 4) {
      const u = y / (HORIZON + 40);
      const c = mix(mix(sky, air, u * 0.8), air, 1 - skyC);
      p.fill(c[0], c[1], c[2]);
      p.rect(0, y, W, 4);
    }

    for (let i = ROWS - 1; i >= 0; i--) {
      const r = rows[i];
      const C = contrast(r.d, V);
      const b = mix(air, bld, C);
      p.fill(b[0], b[1], b[2]);
      for (const bd of r.blds) p.rect(bd.x, r.base - bd.h, bd.w, bd.h + 2 + (i === 0 ? WIN_B : 0));
      // ground in front of this row, so the next row has something to stand on
      p.rect(0, r.base, W, (i === 0 ? WIN_B : rows[i - 1].base) - r.base + 1);
      const lit = mix(air, LIGHT, C * (1 - 0.85 * dn));
      p.fill(lit[0], lit[1], lit[2]);
      for (const bd of r.blds) for (const w of bd.wins) if (w.k < on) p.rect(w.x, w.y, w.s, w.s);
    }

    // window frame
    p.fill(24, 22, 26);
    p.rect(0, WIN_B, W, H - WIN_B);
  }

  function drawStrip(h) {
    const X0 = 44, X1 = W - 30, Y0 = 600, Y1 = 680;
    const tx = (hh) => X0 + ((hh - H0) / (H1 - H0)) * (X1 - X0);
    const dens = (V) => 1 - Math.max(0, Math.min(1, Math.log(V / 400) / Math.log(25000 / 400)));
    p.noStroke();
    p.fill(205, 208, 212, 150);
    p.beginShape();
    p.vertex(X0, Y1);
    for (let x = X0; x <= X1; x++) {
      const hh = H0 + ((x - X0) / (X1 - X0)) * (H1 - H0);
      p.vertex(x, Y1 - dens(visAt(hh)) * (Y1 - Y0));
    }
    p.vertex(X1, Y1);
    p.endShape(p.CLOSE);

    p.stroke(90, 88, 96);
    p.strokeWeight(1);
    p.line(X0, Y1, X1, Y1);
    p.noStroke();
    p.fill(120, 118, 126);
    p.textSize(9);
    p.textAlign(p.CENTER, p.TOP);
    for (const hh of [21, 24, 27, 30, 33]) {
      p.rect(tx(hh), Y1, 1, 4);
      p.text(String(hh % 24), tx(hh), Y1 + 6);
    }
    // notices
    p.fill(232, 170, 90);
    for (const n of NOTICES) p.triangle(tx(n) - 3, Y0 - 10, tx(n) + 3, Y0 - 10, tx(n), Y0 - 4);
    // now
    p.stroke(235, 235, 240, 200);
    p.line(tx(h), Y0 - 2, tx(h), Y1);
  }

  p.draw = () => {
    const h = hourAt(p.frameCount - 1);
    drawWindow(h);
    drawStrip(h);
  };
}