Skip to content

Chuanghua ​

Chuanghua (窗花) are Chinese window paper-cuts. Here, each is a rose filled as a radial area with a jagged inner edge. Click one to zoom in with d3.interpolateZoom, and click again to zoom out.

js
const jagged = Inputs.toggle({label: "Jagged", value: true});
js
const stroked = Inputs.toggle({label: "Stroked", value: false});
js
const count = Inputs.range([2, 10], {label: "Count", step: 1, value: 7});
js
const width = 640;
const size = width / count;
const vector = d3.range(count);
const cells = d3.cross(vector, vector).filter(([x, y]) => x !== y);
const grid = d3.scaleBand(vector, [0, width]).padding(0.18);
const r = grid.bandwidth() / 2;
const pos = (x) => grid(x) + r;
const center = [width / 2, width / 2, width];
let current = center;
let zooming = false;

function gcd(a, b) {
  return b ? gcd(b, a % b) : a;
}

function rose(n, d) {
  const k = n / d;
  const turns = d / gcd(n, d);
  const random = d3.randomUniform(-0.1, 0.1);
  const points = [];
  for (let a = 0; a < Math.PI * 2 * turns + 0.02; a += 0.02) {
    const r2 = r * Math.cos(k * a);
    const r1 = r2 * (0.8 + (jagged ? random() : 0));
    points.push([a, r1, r2]);
  }
  return d3
    .areaRadial()
    .curve(d3.curveNatural)
    .angle((d) => d[0])
    .innerRadius((d) => d[1])
    .outerRadius((d) => d[2])(points);
}

function zoomTo(end) {
  if (zooming) return;
  zooming = true;
  const interpolator = d3.interpolateZoom(current, end);
  d3.select(view)
    .transition()
    .duration(interpolator.duration * 1.2)
    .attrTween("transform", () => (t) => {
      const [x, y, w] = interpolator(t);
      const k = width / w;
      return `translate(${width / 2 - x * k}, ${width / 2 - y * k}) scale(${k})`;
    })
    .end()
    .then(() => {
      zooming = false;
      current = end;
    });
}

const svg = cm.svg`<svg ${{width, height: width, viewBox: [0, 0, width, width]}}>
  <g>
    <g ${{
      data: cells,
      transform: ([x, y]) => `translate(${pos(x)}, ${pos(y)})`,
      style_cursor: "pointer",
      onclick: (event, node, [x, y]) => zoomTo(current === center ? [pos(x), pos(y), size] : center),
    }}>
      <path ${{d: ([x, y]) => rose(x + 1, y + 1), fill: "red", stroke: stroked ? "black" : "red"}}/>
    </g>
  </g>
</svg>`;
const view = svg.firstElementChild;

document.body.append(svg);