Skip to content

Commit 4606d0a

Browse files
fix: rotate drawCircleGradient boundary vertex per call
Two drawCircleGradient calls stacked at the same position would reveal a faint vertical column where the top circle should have painted - the GPU edge-tiebreaker rule (top-left rule or equivalent) appears to assign the boundary line-degenerate triangle's (C, R_0) edge pixels to the degenerate, which has zero area and produces no fragments. The adjacent real fan slice doesn't own those pixels under the rule, so the column goes unpainted and the lower draw shows through. Rotate which rim vertex serves as the boundary by one slice per call. The polygon has 32-fold rotational symmetry so the rendered shape is visually identical, but consecutive calls now have their boundary line-degens on different rim verts - the unpainted columns no longer stack on the same pixel positions. Partial mitigation only - the artifact can still appear in some combinations because both circles' line-degens converge at the shared center vertex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 06caea9 commit 4606d0a

7 files changed

Lines changed: 43 additions & 32 deletions

File tree

dist/littlejs.d.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,16 +1845,7 @@ declare module "littlejsengine" {
18451845
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
18461846
* @memberof Draw */
18471847
export function drawCircle(pos: Vector2, size?: number, color?: Color, lineWidth?: number, lineColor?: Color, useWebGL?: boolean, screenSpace?: boolean, context?: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D): void;
1848-
/** Draw a circle filled with a radial gradient from the center to the rim
1849-
* @param {Vector2} pos
1850-
* @param {number} [size=1] - Diameter
1851-
* @param {Color} [colorInner=WHITE]
1852-
* @param {Color} [colorOuter=CLEAR_WHITE]
1853-
* @param {boolean} [useWebGL=glEnable]
1854-
* @param {boolean} [screenSpace]
1855-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1856-
* @memberof Draw */
1857-
export function drawCircleGradient(pos: Vector2, size?: number, colorInner?: Color, colorOuter?: Color, useWebGL?: boolean, screenSpace?: boolean, context?: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D): void;
1848+
export function drawCircleGradient(pos: any, size: number, colorInner: Color, colorOuter: Color, useWebGL: boolean, screenSpace: boolean, context: any): void;
18581849
/**
18591850
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
18601851
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context

dist/littlejs.esm.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4520,6 +4520,7 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
45204520
* @param {boolean} [screenSpace]
45214521
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
45224522
* @memberof Draw */
4523+
let drawCircleGradientOffset = 0;
45234524
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
45244525
{
45254526
ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4538,17 +4539,21 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
45384539
pos = screenToWorld(pos);
45394540
size /= cameraScale;
45404541
}
4541-
// fan as tristrip; open and close on the rim so back-to-back
4542-
// gradients at the same position bridge as point-degenerates
4543-
// instead of leaking a spoke from center to top
4542+
// fan as tristrip; rotate the boundary vertex by one slice per call
4543+
// so back-to-back gradients at the same position have their hole
4544+
// (from gpu edge-rule on the boundary line-degen) at different rim
4545+
// verts and don't visibly stack
45444546
const sides = glCircleSides;
45454547
const radius = size/2;
45464548
const innerInt = colorInner.rgbaInt();
45474549
const outerInt = colorOuter.rgbaInt();
4548-
const points = [vec2(pos.x, pos.y + radius)], colors = [outerInt];
4550+
const offset = drawCircleGradientOffset++;
4551+
const startA = (offset%sides)/sides*PI*2;
4552+
const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
4553+
const colors = [outerInt];
45494554
for (let i=sides; i--;)
45504555
{
4551-
const a = i/sides*PI*2;
4556+
const a = ((i+offset)%sides)/sides*PI*2;
45524557
points.push(pos);
45534558
colors.push(innerInt);
45544559
points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));

dist/littlejs.esm.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/littlejs.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4520,6 +4520,7 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
45204520
* @param {boolean} [screenSpace]
45214521
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
45224522
* @memberof Draw */
4523+
let drawCircleGradientOffset = 0;
45234524
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
45244525
{
45254526
ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4538,17 +4539,21 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
45384539
pos = screenToWorld(pos);
45394540
size /= cameraScale;
45404541
}
4541-
// fan as tristrip; open and close on the rim so back-to-back
4542-
// gradients at the same position bridge as point-degenerates
4543-
// instead of leaking a spoke from center to top
4542+
// fan as tristrip; rotate the boundary vertex by one slice per call
4543+
// so back-to-back gradients at the same position have their hole
4544+
// (from gpu edge-rule on the boundary line-degen) at different rim
4545+
// verts and don't visibly stack
45444546
const sides = glCircleSides;
45454547
const radius = size/2;
45464548
const innerInt = colorInner.rgbaInt();
45474549
const outerInt = colorOuter.rgbaInt();
4548-
const points = [vec2(pos.x, pos.y + radius)], colors = [outerInt];
4550+
const offset = drawCircleGradientOffset++;
4551+
const startA = (offset%sides)/sides*PI*2;
4552+
const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
4553+
const colors = [outerInt];
45494554
for (let i=sides; i--;)
45504555
{
4551-
const a = i/sides*PI*2;
4556+
const a = ((i+offset)%sides)/sides*PI*2;
45524557
points.push(pos);
45534558
colors.push(innerInt);
45544559
points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));

dist/littlejs.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/littlejs.release.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3840,6 +3840,7 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
38403840
* @param {boolean} [screenSpace]
38413841
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
38423842
* @memberof Draw */
3843+
let drawCircleGradientOffset = 0;
38433844
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
38443845
{
38453846
ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3858,17 +3859,21 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
38583859
pos = screenToWorld(pos);
38593860
size /= cameraScale;
38603861
}
3861-
// fan as tristrip; open and close on the rim so back-to-back
3862-
// gradients at the same position bridge as point-degenerates
3863-
// instead of leaking a spoke from center to top
3862+
// fan as tristrip; rotate the boundary vertex by one slice per call
3863+
// so back-to-back gradients at the same position have their hole
3864+
// (from gpu edge-rule on the boundary line-degen) at different rim
3865+
// verts and don't visibly stack
38643866
const sides = glCircleSides;
38653867
const radius = size/2;
38663868
const innerInt = colorInner.rgbaInt();
38673869
const outerInt = colorOuter.rgbaInt();
3868-
const points = [vec2(pos.x, pos.y + radius)], colors = [outerInt];
3870+
const offset = drawCircleGradientOffset++;
3871+
const startA = (offset%sides)/sides*PI*2;
3872+
const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
3873+
const colors = [outerInt];
38693874
for (let i=sides; i--;)
38703875
{
3871-
const a = i/sides*PI*2;
3876+
const a = ((i+offset)%sides)/sides*PI*2;
38723877
points.push(pos);
38733878
colors.push(innerInt);
38743879
points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));

src/engineDraw.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
736736
* @param {boolean} [screenSpace]
737737
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
738738
* @memberof Draw */
739+
let drawCircleGradientOffset = 0;
739740
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
740741
{
741742
ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -754,17 +755,21 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
754755
pos = screenToWorld(pos);
755756
size /= cameraScale;
756757
}
757-
// fan as tristrip; open and close on the rim so back-to-back
758-
// gradients at the same position bridge as point-degenerates
759-
// instead of leaking a spoke from center to top
758+
// fan as tristrip; rotate the boundary vertex by one slice per call
759+
// so back-to-back gradients at the same position have their hole
760+
// (from gpu edge-rule on the boundary line-degen) at different rim
761+
// verts and don't visibly stack
760762
const sides = glCircleSides;
761763
const radius = size/2;
762764
const innerInt = colorInner.rgbaInt();
763765
const outerInt = colorOuter.rgbaInt();
764-
const points = [vec2(pos.x, pos.y + radius)], colors = [outerInt];
766+
const offset = drawCircleGradientOffset++;
767+
const startA = (offset%sides)/sides*PI*2;
768+
const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
769+
const colors = [outerInt];
765770
for (let i=sides; i--;)
766771
{
767-
const a = i/sides*PI*2;
772+
const a = ((i+offset)%sides)/sides*PI*2;
768773
points.push(pos);
769774
colors.push(innerInt);
770775
points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));

0 commit comments

Comments
 (0)