Path2D and Rounded Rectangles

A Path2D holds a path you build once and fill, stroke, clip or hit-test many times. Its constructor accepts SVG path data, so SVG's shapes move over unchanged; addPath(path, matrix) appends one through a DOMMatrix. roundRect(x, y, w, h, radii), with radii in border-radius order, is Baseline widely available since October 2025.

The BookNest mark from its SVG path data, and a rounded price tagHTML
<canvas id="c" style="width:600px; height:106px"></canvas><script src="hidpi.js"></script>
<script>
  const ctx = hidpi(c), r = devicePixelRatio;
  const colors = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
  const nest = new Path2D('M36 118 Q120 142 204 118 Q196 182 120 184 Q44 182 36 118 Z');
  ctx.fillStyle = '#263238';  ctx.save();  ctx.scale(0.55, 0.55);  ctx.lineWidth = 22;
  [42, 13, 56, 70, 34, 76].forEach((top, i) => {              // spine tops: pages / 4 + 30
    const turn = new DOMMatrix().translate(120, 240).rotate(10 * i - 25).translate(-120, -240);
    const spine = new Path2D();  spine.addPath(new Path2D(`M120 150 V${top}`), turn);
    ctx.strokeStyle = colors[i];  ctx.stroke(spine);
  });
  ctx.fill(nest);
  const inside = ctx.isPointInPath(nest, 66 * r, 88 * r);  ctx.restore();  // bitmap pixels
  ctx.fillStyle = '#e09a10';  ctx.beginPath();  ctx.roundRect(180, 30, 150, 50, [24, 4]);
  ctx.fill();  ctx.fillStyle = '#263238';  ctx.font = 'bold 18px Georgia';
  ctx.fillText('$14.99', 225, 62);  ctx.fillText(`(66, 88) in the nest: ${inside}`, 350, 62);
</script>
Browser output of Listing 2.7
Browser output of 7

isPointInPath(path, x, y) takes the point in bitmap pixels and the path through the current transform, hence the test before restore(). The mark is kept in demos/ch02/booknest-mark.js.

A reusable Path2D from SVG path data, addPath with a DOMMatrix, roundRect and hit testingHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
  canvas { width: 100%; max-width: 600px; height: 130px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<p>Move the pointer over the nest: <span id="hit">outside</span></p>
<script>
  function hidpi(c, r = devicePixelRatio) {
    c.width = c.clientWidth * r;  c.height = c.clientHeight * r;
    const ctx = c.getContext('2d');  ctx.scale(r, r);  return ctx;
  }
  const c = document.getElementById('c');
  const ctx = hidpi(c), r = devicePixelRatio;
  const colors = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
  // Path2D accepts SVG path data, so the BookNest nest moves over unchanged
  const nest = new Path2D('M36 118 Q120 142 204 118 Q196 182 120 184 Q44 182 36 118 Z');
  let over = false;

  function draw() {
    ctx.clearRect(0, 0, 600, 130);
    ctx.save();  ctx.translate(10, 0);  ctx.scale(0.62, 0.62);  ctx.lineWidth = 22;
    [42, 13, 56, 70, 34, 76].forEach((top, i) => {
      // Fan the spines out: rotate each one about a point below the nest
      const turn = new DOMMatrix().translate(120, 240).rotate(10 * i - 25).translate(-120, -240);
      const spine = new Path2D();
      spine.addPath(new Path2D(`M120 150 V${top}`), turn);
      ctx.strokeStyle = colors[i];  ctx.stroke(spine);
    });
    ctx.fillStyle = over ? '#5d4037' : '#263238';  ctx.fill(nest);
    ctx.restore();

    // roundRect radii follow border-radius order: [top-left/bottom-right, top-right/bottom-left]
    ctx.fillStyle = '#e09a10';  ctx.beginPath();  ctx.roundRect(200, 40, 150, 50, [24, 4]);  ctx.fill();
    ctx.fillStyle = '#263238';  ctx.font = 'bold 20px Georgia';  ctx.fillText('$14.99', 240, 72);
    ctx.beginPath();  ctx.roundRect(380, 40, 200, 50, 25);  ctx.strokeStyle = '#1f5f8b';
    ctx.lineWidth = 3;  ctx.stroke();
    ctx.fillStyle = '#1f5f8b';  ctx.font = '16px sans-serif';  ctx.fillText('Add to cart', 437, 71);
  }

  c.addEventListener('pointermove', e => {
    const b = c.getBoundingClientRect();
    // isPointInPath uses bitmap pixels and applies the current transform to the path
    ctx.save();  ctx.translate(10, 0);  ctx.scale(0.62, 0.62);
    over = ctx.isPointInPath(nest, (e.clientX - b.left) * r, (e.clientY - b.top) * r);
    ctx.restore();
    document.getElementById('hit').textContent = over ? 'inside' : 'outside';
    draw();
  });
  draw();
</script>