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 nextfill
call 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 nextfill
call 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 alineTo
back 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.