Search code examples
htmlhtml5-canvas

Clip + Arc leads to an unwanted closing of the path, while Clip + Rect shows the expected behavior


Question:

Why does CanvasRenderingContext2D.clip() closes an additional path when applying it to a collection of CanvasRenderingContext2D.arc() sampled along the path of a quadratic curve?

Background

I am trying to create a path of quadratic segments with a longitudinal color split. Based on a comment to the question "Square curve with lengthwise color division" I am trying to accomplish this goal by going through the following steps:

  1. Draw the quadratic path
  2. Sample point on the quadratic curve
  3. Create a clipping region and draw a cycle at each sampled point
let region = new Path2D();
for (j = 0; j < pointsQBez.length; j++) {
    region.arc(pointsQBez[j].x, pointsQBez[j].y, 4, 0, 2 * Math.PI );
}
ctx.clip(region)
  1. Split the canvas into two segments based on the curve
    1. Calculate the intersection of the start- and end-segment with the canvas border
    2. Close the path (first clipping region)
    3. Draw a rectangle over the whole canvas (second clipping region)
  2. Fill in the two regions created in step four

Steps 3, 4, and 5 in pictures:

enter image description here enter image description here enter image description here

Issue

The pink part in the third image above should have the same thickness as the turquoise. But for some strange reason, the whole inner part of the curve gets filled in.

Additional observations

  1. This behaviour does not show when using CanvasRenderingContext2D.rect() instead of CanvasRenderingContext2D.arc():

enter image description here enter image description here enter image description here

  1. When using CanvasRenderingContext2D.arc(), the inner part of the curve that is filled in is not consistent

enter image description here enter image description here enter image description here


Solution

  • Because rect does include a call to closePath() while arc doesn't.

    Two ways of working around that:

    You can call closePath() after each arc:

    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    
    const pointsQBez = [];
    const cx  = 75;
    const cy  = 75;
    const rad = 50;
    for(let i = 0; i < 180; i++) {
      const a = (Math.PI / 180) * i - Math.PI / 2;
      const x = cx + Math.cos(a) * rad;
      const y = cy + Math.sin(a) * rad;
      pointsQBez.push({ x, y });
    }
    
    let region = new Path2D();
    for (const {x, y} of pointsQBez) {
      region.arc(x, y, 4, 0, 2 * Math.PI);
      region.closePath();
    }
    ctx.clip(region);
    
    ctx.fillStyle = "red";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    <canvas></canvas>

    Or you can moveTo() the entry point of your arc:

    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    
    const pointsQBez = [];
    const cx  = 75;
    const cy  = 75;
    const rad = 50;
    for(let i = 0; i < 180; i++) {
      const a = (Math.PI / 180) * i - Math.PI / 2;
      const x = cx + Math.cos(a) * rad;
      const y = cy + Math.sin(a) * rad;
      pointsQBez.push({ x, y });
    }
    
    let region = new Path2D();
    for (const {x, y} of pointsQBez) {
      region.moveTo(x + 4, y); // x + arc radius
      region.arc(x, y, 4, 0, 2 * Math.PI);
    }
    ctx.clip(region);
    
    ctx.fillStyle = "red";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    <canvas></canvas>