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

What is Charming?

Charming short for Charming Computing, is a free, open-source, creative coding language for computational art with high performance. It has a declarative, concise, yet expressive API inspired by G2.js, D3.js and P5.js. And there are plenty of examples to get started with.

The core of Charming is built on one observation: visualization and generative art are both data-driven to some extent. The difference is that visualization(information visualization more specifically) is typically driven by data derived from daily life activities, while computational art is driven by data generated by algorithms or computing.

For example, let’s draw a clover composed of multiple circles, whose positions are produced by a clover algorithm. Charming provides APIs for the entire drawing pipeline: process data, append and transform shapes. A clear relationship between data and shape attributes can be easily seen through Charming’s declarative API style.

{
  const app = cm.app({
    width: 640,
    height: 640,
  });

  app
    .data(cm.range(120))
    .process(cm.map, (_, i, data) => (i * Math.PI) / data.length)
    .append(cm.circle, {
      x: (t) => Math.cos(t) * Math.cos(t * 3),
      y: (t) => Math.sin(t) * Math.cos(t * 3),
      r: (t) => t,
      fill: "black",
    })
    .transform(cm.mapPosition, { padding: 30 })
    .transform(cm.mapAttrs, { r: { range: [8, 15] } });

  const node = app.render().node();

  display(node);
}

This example is too simple to see the true charm of charming. To solve this, we can draw thousands of circles whose positions, radii, strokes and stroke opacities are computed by some trigonometric functions. This example is a reimplementation of Ricky Reusser’ Instanced WebGL Circles, but with more readable code.

{
  const width = 700,
    height = 700,
    count = 4000,
    theta = cm.range(count, 0, cm.TWO_PI);

  function update(app) {
    const time = app.prop("frameCount") / 50;
    const dist = (d) => 0.6 + 0.2 * Math.cos(d * 6.0 + Math.cos(d * 8.0 + time));

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

    app
      .data(theta)
      .append(cm.circle, {
        x: (d) => Math.cos(d) * dist(d),
        y: (d) => Math.sin(d) * dist(d),
        r: (d) => 0.2 + 0.12 * Math.cos(d * 9.0 - time * 2.0),
        stroke: (d) => {
          const th = 8.0 * d + time * 2.0;
          const r = 0.6 + 0.4 * Math.cos(th);
          const g = 0.6 + 0.4 * Math.cos(th - Math.PI / 3);
          const b = 0.6 + 0.4 * Math.cos(th - (Math.PI * 2.0) / 3.0);
          return cm.rgb(r * 255, g * 255, b * 255);
        },
        fill: "transparent",
        strokeOpacity: (0.15 * 2000) / count,
      })
      .transform(cm.mapPosition, { padding: 100 })
      .transform(cm.mapAttrs, { r: { range: [24, 96] } });
  }

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

  const node = cm.app({ width, height }).on("update", update).call(dispose).start().node();

  display(node);
}

But imagination does not stop here. With the help of a WebGL renderer, GLSL functions and some math, you can even define some shader art without writing shader directly! Here is an example from shader toy.

{
  const width = 640;
  const height = 360;
  const palette = cm.glsl`vec3 palette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.263, 0.416, 0.557);
    return a + b * cos(3.1415926 * 2.0 * (c * t + d));
  }`;

  function update(app) {
    const time = app.prop("frameCount") / 50;
    const fill = cm.glsl`vec4 fill(vec2 coord, vec4 color) {
      vec2 uv = (coord - vec2(${width}, ${height})) / ${height};
      vec2 uv0 = uv;
      vec3 rgb = vec3(0.0);
      for (float i = 0.0; i < 4.0; i++) {
        uv = fract(uv * 1.5) - 0.5;
        float d = length(uv) * exp(-length(uv0));
        vec3 col = ${palette}(length(uv0) + i * 0.4 + ${time} * 0.4);
        d = sin(d * 8.0 + ${time}) / 8.0;
        d = abs(d);
        d = pow(0.01 / d, 1.2);
        rgb += col * d;
      }
      return vec4(rgb, 1.0);
    }`;
    app.append(cm.rect, { x: 0, y: 0, width, height, fill });
  }

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

  const node = cm.app({ renderer: cm.webgl(), width, height }).on("update", update).call(dispose).start().node();

  display(node);
}

The best part is that in addition to the regular computational art, Charming also offers great support for ASCII art. Simply by changing the renderer to a terminal renderer, you can create character-based art in the terminal. For example, to draw fire burning characters:

{
  let fire = null;

  function update(app) {
    const width = app.prop("width");
    const height = app.prop("height");
    if (!fire) fire = createFire(width, height);
    updateFire(fire);
    drawFire(app, fire);
  }

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

  const app = cm.app({
    renderer: await cm.terminal(),
    frameRate: 15,
  });

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

  display(node);
}

Why ASCII art? Life is not only about work, you know. Want more good examples? Here you go!

Appendix

function createFire(width, height) {
  return cm.range(height).map(() => cm.range(width).map(() => 0));
}
function updateFire(fire) {
  const h = fire.length;
  const w = fire[0].length;
  const max = h;
  const noise = cm.randomNoise(0, max);

  for (let y = 0; y < h - 1; y++) {
    for (let x = 0; x < w; x++) {
      const decay = cm.randomInt(0, 3);
      const spread = cm.randomInt(-1, 1);
      const index = Math.min(Math.max(0, x - spread), w - 1);
      const target = fire[y + 1][index];
      fire[y][x] = Math.max(0, target - decay);
    }
  }

  for (let x = 0; x < w; x++) {
    fire[h - 1][x] = noise(x / 10) | 0;
  }
}
function drawFire(app, fire) {
  const max = fire.length;

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

  app
    .data(fire)
    .append(cm.group, { x: 0, y: (_, i) => i })
    .data((d) => d)
    .append(cm.point, {
      y: 0,
      x: (_, i) => i,
      stroke0: (d) => (d === 0 ? " " : cm.randomChar()),
      stroke2: (d) => (d === 0 ? null : d),
    })
    .transform(cm.mapAttrs, {
      stroke2: {
        domain: [0, max],
        interpolate: d3.interpolateCool,
      },
    });
}