This is Charming1. For Charming2, go to charmingjs.org. This is v1. For v2, go to charmingjs.org.

White Noise

Ref. https://asciinema.org/a/19919

const app = cm.app({
  renderer: await cm.terminal(),
  fontWeight: "bold",
  frameRate: 20,
});

const width = app.prop("width"),
  height = app.prop("height"),
  characters = cm.cross(cm.range(width), cm.range(height)).map((d) => ({ x: d[0], y: d[1] }));

const textOptions = {
    text: cm.figlet("Charming"),
    x: width / 2,
    y: height / 2,
    textAlign: "center",
    textBaseline: "middle",
    fill: cm.gradientRainBowX(),
  },
  bbox = app.textBBox(textOptions),
  outside = ({ x, y }) => {
    const { x: tx, y: ty, width: tw, height: th } = bbox;
    return x < tx || x > tx + tw || y < ty || y > ty + th;
  };

function update() {
  app.append(cm.clear, { fill: "black" });

  app.append(cm.text, textOptions);

  app
    .data(characters)
    .process(cm.each, (d) => {
      if (d.lifespan) return d.lifespan--;
      const seed = cm.random(32, 127);
      const ch = String.fromCharCode(seed);
      d.stroke = cm.cfb(ch);
      d.lifespan = cm.random(3, 10) | 0;
    })
    .process(cm.filter, (d) => {
      if (outside(d)) return true;
      const p = cm.random(10);
      if (p < 0.5) return true;
      return false;
    })
    .append(cm.point, {
      x: (d) => d.x,
      y: (d) => d.y,
      stroke: (d) => d.stroke,
    });
}

function dispose(app) {
  invalidation.then(() => app.dispose());
}

const node = app.on("update", update).call(dispose).start().node();

display(node);