However, if I comment out the green background, it does work like so, where the shapes are all filled in as you would expect.
After posing the question, I was able to “solve” the issue by tweaking the relevant code segment to look like below:
ctx.beginPath(); // Added this line.
ctx.fillStyle = "black";
ctx.rect(x, y, portWidth, portWidth);
ctx.fill();
ctx.fillStyle = "green";
Basically, my initial attempt was “conceptually” trying to create a green square and then overlay a black shape on top of it. Something was going on weird with that so instead I created one continuous thing (Path? Shape?) composed of two nested things (I don’t know the right canvas terminology) with two separate fills, and then that somehow worked.
I’m not putting this as an answer since it isn’t, but it is how I have accomplished my desired effect for now.
CodePudding user response:
The simple fix for this issue is to replace 
rect, like its related methods, adds to the current subpath.
The distinction between (entire) path and subpath is important, but easily missed: beginPath creates an entirely new path, and moveTo creates a subpath, but fill fills entire paths.
Furthermore, what is filled once, stays filled.
This is like paint: when you paint the canvas once, and are told to fill an overlapping region, you’ll have to paint over your older paint.
This helps explain why these black outlines exist: your port shapes are filled black, but then the same shape — plus a rectangle — are filled green; the green overlays the black. Note that division by 6 isn’t always very precise, so each fill has some transparency towards the edge. That’s why the green doesn’t completely cover the black at the edges.
It is important to note that closePath is not the “opposite” of beginPath.
closePath doesn’t remove or add new paths or subpaths; it does not change which one the “current” path or subpath is.
This is further complicated by the fact that beginPath and closePath are sometimes optional: for instance, a moveTo is only supposed to create a subpath, but if it’s called at the very beginning, when no path exists, a path is automatically created as well, making a beginPath before moveTo redundant.
Let’s go through the script step by step, by considering two iterations one after the other:
- First iteration
ctx.fillStyle = "green";sets the fill color for the nextfillcall to"green".ctx.rect(x, y, portWidth, portWidth);requires a subpath to exist. Since no paths exist at the beginning, a new path is automatically created, and a subpath is created as part of it. Then, a rectangle is added to the current subpath.ctx.fill();fills the interior of the entire current path (the rectangle) green.ctx.fillStyle = "black";sets the fill color for the nextfillcall to"black".ctx.beginPath();discards the previous path and creates a new path. This is now the current path, and the previous rectangle path is no longer accessible.ctx.moveTo(x portSixth, y 2 * portSixth);creates a new subpath within which it moves the current position.- The seven
ctx.lineTo(…);calls add vertices to the current subpath. ctx.closePath();is equivalent to alineToback to the start of the current subpath.ctx.fill();fills the interior of the entire current path (the port shape) black.
- Second iteration (Steps 1 and 4 are no different than in the first iteration)
- (
ctx.fillStyle = "green";) ctx.rect(x, y, portWidth, portWidth);adds a rectangle to the current subpath, which already exists and already includes the port shape of the previous iteration.ctx.fill();fills the interior of the entire current path (the rectangle plus the port shape of the previous iteration) green.- (
ctx.fillStyle = "black";) ctx.beginPath();discards the previous path and creates a new path. Now the rectangle plus port shape are no longer accessible.- Etc.
- (
The second iteration is where the bug happens.
In its third step, fill fills a path that has been opened in the previous iteration using beginPath.
As you found out, you can fix it without fillRect, by simply placing ctx.beginPath(); before ctx.rect(); — or, equivalently, at the start of the iteration.
Now you should understand why.
1: fill does indeed accept a specific Path2D as an argument. Without an argument, it fills the current path of the provided rendering context.



