From 28baf836e67e08db7117e791d9e28626d15fa6d5 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Tue, 23 Aug 2022 01:23:50 -0600 Subject: [PATCH 1/5] Allow non even ovals to be drawn --- runtimes/web/src/framebuffer.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/runtimes/web/src/framebuffer.ts b/runtimes/web/src/framebuffer.ts index 27a5436d..9a2f5b52 100644 --- a/runtimes/web/src/framebuffer.ts +++ b/runtimes/web/src/framebuffer.ts @@ -156,15 +156,21 @@ export class Framebuffer { const strokeColor = (dc1 - 1) & 0x3; const fillColor = (dc0 - 1) & 0x3; + // Get the width and height const a = width >>> 1; const b = height >>> 1; if (a <= 0) return; if (b <= 0) return; + // Calculate the midpoint const x0 = x + a, y0 = y + b; const aa2 = a * a * 2, bb2 = b * b * 2; + // Store even/odd offsets to allow non even ovals + const wo = (width + 1) % 2, ho = (height + 1) % 2; + const we = width % 2, he = height % 2; + { let x = a, y = 0; let dx = (1 - 2 * a) * b * b, dy = a * a; @@ -172,15 +178,15 @@ export class Framebuffer { let e = 0; while (sx >= sy) { - this.drawPointUnclipped(strokeColor, x0 + x, y0 + y); /* I. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 + x, y0 - y); /* II. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 - x, y0 + y); /* III. Quadrant */ + this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ + this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ + this.drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ this.drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ if (dc0 !== 0) { const start = x0 - x + 1; - const end = x0 + x; - this.drawHLineUnclipped(fillColor, start, y0 + y, end); /* I and III. Quadrant */ + const end = x0 + x - wo; + this.drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ this.drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ } @@ -205,9 +211,9 @@ export class Framebuffer { let ddx = 0; while (sy >= sx) { - this.drawPointUnclipped(strokeColor, x0 + x, y0 + y); /* I. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 + x, y0 - y); /* II. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 - x, y0 + y); /* III. Quadrant */ + this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ + this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ + this.drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ this.drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ x++; @@ -219,8 +225,8 @@ export class Framebuffer { if (dc0 !== 0) { const w = x - ddx - 1; const start = x0 - w; - const end = x0 + w + 1; - this.drawHLineUnclipped(fillColor, start, y0 + y, end); /* I and III. Quadrant */ + const end = x0 + w + we; + this.drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ this.drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ } From 32847546f62f99c629a5265a3b329f09f7d55dde Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Tue, 23 Aug 2022 11:11:21 -0600 Subject: [PATCH 2/5] Fix ovals in native runtime as well --- runtimes/native/src/framebuffer.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/runtimes/native/src/framebuffer.c b/runtimes/native/src/framebuffer.c index d1932f3d..e12b2260 100644 --- a/runtimes/native/src/framebuffer.c +++ b/runtimes/native/src/framebuffer.c @@ -389,15 +389,21 @@ void w4_framebufferOval (int x, int y, int width, int height) { uint8_t strokeColor = (dc1 - 1) & 0x3; uint8_t fillColor = (dc0 - 1) & 0x3; + // Get the width and height int a = width >> 1; int b = height >> 1; if (a <= 0) return; if (b <= 0) return; + // Calculate the midpoint int x0 = x + a, y0 = y + b; int aa2 = a * a * 2, bb2 = b * b * 2; + // Store even/odd offsets to allow non even ovals + int wo = (width + 1) % 2, ho = (height + 1) % 2; + int we = width % 2, he = height % 2; + { int x = a, y = 0; int dx = (1 - 2 * a) * b * b, dy = a * a; @@ -405,15 +411,15 @@ void w4_framebufferOval (int x, int y, int width, int height) { int e = 0; while (sx >= sy) { - drawPointUnclipped(strokeColor, x0 + x, y0 + y); /* I. Quadrant */ - drawPointUnclipped(strokeColor, x0 + x, y0 - y); /* II. Quadrant */ - drawPointUnclipped(strokeColor, x0 - x, y0 + y); /* III. Quadrant */ + drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ + drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ + drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ if (dc0 != 0) { int start = x0 - x + 1; - int end = x0 + x; - drawHLineUnclipped(fillColor, start, y0 + y, end); /* I and III. Quadrant */ + int end = x0 + x - wo; + drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ } @@ -438,9 +444,9 @@ void w4_framebufferOval (int x, int y, int width, int height) { int ddx = 0; while (sy >= sx) { - drawPointUnclipped(strokeColor, x0 + x, y0 + y); /* I. Quadrant */ - drawPointUnclipped(strokeColor, x0 + x, y0 - y); /* II. Quadrant */ - drawPointUnclipped(strokeColor, x0 - x, y0 + y); /* III. Quadrant */ + drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ + drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ + drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ x++; @@ -452,8 +458,8 @@ void w4_framebufferOval (int x, int y, int width, int height) { if (dc0 != 0) { int w = x - ddx - 1; int start = x0 - w; - int end = x0 + w + 1; - drawHLineUnclipped(fillColor, start, y0 + y, end); /* I and III. Quadrant */ + int end = x0 + w + we; + drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ } From e3abc2d75e6c741a2d13a53f72babee3d6ee1665 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Wed, 24 Aug 2022 13:05:25 -0600 Subject: [PATCH 3/5] Oval: rewrite to fix issues, explain algorithm --- runtimes/web/src/framebuffer.ts | 147 +++++++++++++++----------------- 1 file changed, 68 insertions(+), 79 deletions(-) diff --git a/runtimes/web/src/framebuffer.ts b/runtimes/web/src/framebuffer.ts index 9a2f5b52..9991f514 100644 --- a/runtimes/web/src/framebuffer.ts +++ b/runtimes/web/src/framebuffer.ts @@ -144,6 +144,19 @@ export class Framebuffer { } } + // Oval drawing function using a variation on the midpoint algorithm. + // TIC-80's ellipse drawing function used as reference. + // https://github.com/nesbox/TIC-80/blob/main/src/core/draw.c + // + // Javatpoint has a in depth academic explanation that mostly went over my head: + // https://www.javatpoint.com/computer-graphics-midpoint-ellipse-algorithm + // + // Draws the eliipse by "scanning" along the edge in one quadrant, and mirroring + // the movement for the other four quadrants. + // + // There are a lot of details to get correct while implementing this algorithm, + // so ensure the edge cases are covered when changing it. Long, thin ellipses + // are particularly susceptible to being drawn incorrectly. drawOval (x: number, y: number, width: number, height: number) { const drawColors = this.drawColors[0]; const dc0 = drawColors & 0xf; @@ -156,87 +169,63 @@ export class Framebuffer { const strokeColor = (dc1 - 1) & 0x3; const fillColor = (dc0 - 1) & 0x3; - // Get the width and height - const a = width >>> 1; - const b = height >>> 1; - - if (a <= 0) return; - if (b <= 0) return; - - // Calculate the midpoint - const x0 = x + a, y0 = y + b; - const aa2 = a * a * 2, bb2 = b * b * 2; - - // Store even/odd offsets to allow non even ovals - const wo = (width + 1) % 2, ho = (height + 1) % 2; - const we = width % 2, he = height % 2; - - { - let x = a, y = 0; - let dx = (1 - 2 * a) * b * b, dy = a * a; - let sx = bb2 * a, sy = 0; - let e = 0; - - while (sx >= sy) { - this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ - - if (dc0 !== 0) { - const start = x0 - x + 1; - const end = x0 + x - wo; - this.drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ - this.drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ - } - - y++; - sy += aa2; - e += dy; - dy += aa2; - if (2 * e + dx > 0) { - x--; - sx -= bb2; - e += dx; - dx += bb2; - } + let a = width; + let b = height; + let b1 = (height + 1) % 2; // Compensates for precision loss when dividing + + let north = y; + north += Math.floor(b / 2); // Precision loss here + let west = x; + let east = x + width - 1; + let south = north - b1; // Compensation here. Moves the bottom line up by + // one (overlapping the top line) for even heights + + // Error increments. Also known as the decision parameters + let dx = 4 * (1 - a) * b * b; + let dy = 4 * (b1 + 1) * a * a; + + // Error of 1 step + let err = dx + dy + b1 * a * a; + + a *= 8 * a; + b1 = 8 * b * b; + + do { + this.drawPointUnclipped(strokeColor, east, north); /* I. Quadrant */ + this.drawPointUnclipped(strokeColor, west, north); /* II. Quadrant */ + this.drawPointUnclipped(strokeColor, west, south); /* III. Quadrant */ + this.drawPointUnclipped(strokeColor, east, south); /* IV. Quadrant */ + const start = west + 1; + const len = east - start; + if (dc0 !== 0 && len > 0) { // Only draw fill if the length from west to east is not 0 + this.drawHLineUnclipped(fillColor, start, north, east); /* I and III. Quadrant */ + this.drawHLineUnclipped(fillColor, start, south, east); /* II and IV. Quadrant */ } - } - - { - let x = 0, y = b; - let dx = b * b, dy = (1 - 2 * b) * a * a; - let sx = 0, sy = aa2 * b; - let e = 0; - let ddx = 0; - - while (sy >= sx) { - this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ - this.drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ - - x++; - sx += bb2; - e += dx; - dx += bb2; - ddx++; - if (2 * e + dy > 0) { - if (dc0 !== 0) { - const w = x - ddx - 1; - const start = x0 - w; - const end = x0 + w + we; - this.drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ - this.drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ - } - - y--; - sy -= aa2; - e += dy; - dy += aa2; - ddx = 0; - } + const err2 = 2 * err; + if (err2 <= dy) { + // Move vertical scan + north += 1; + south -= 1; + dy += a; + err += dy; + } + if (err2 >= dx || 2 * err > dy) { + // Move horizontal scan + west += 1; + east -= 1; + dx += b1; + err += dx; } + } while (west <= east); + + // Make sure north and south have moved the entire way so top/bottom aren't missing + while (north - south < height) { + this.drawPointUnclipped(strokeColor, west - 1, north); /* II. Quadrant */ + this.drawPointUnclipped(strokeColor, east + 1, north); /* I. Quadrant */ + north += 1; + this.drawPointUnclipped(strokeColor, west - 1, south); /* III. Quadrant */ + this.drawPointUnclipped(strokeColor, east + 1, south); /* IV. Quadrant */ + south -= 1; } } From 05113cb21aad4eaad26ca6dc548b8e0108809374 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Wed, 24 Aug 2022 13:13:49 -0600 Subject: [PATCH 4/5] Oval: port to native --- runtimes/native/src/framebuffer.c | 147 ++++++++++++++---------------- 1 file changed, 68 insertions(+), 79 deletions(-) diff --git a/runtimes/native/src/framebuffer.c b/runtimes/native/src/framebuffer.c index e12b2260..67bf4fcc 100644 --- a/runtimes/native/src/framebuffer.c +++ b/runtimes/native/src/framebuffer.c @@ -377,6 +377,19 @@ void w4_framebufferRect (int x, int y, int width, int height) { } } +// Oval drawing function using a variation on the midpoint algorithm. +// TIC-80's ellipse drawing function used as reference. +// https://github.com/nesbox/TIC-80/blob/main/src/core/draw.c +// +// Javatpoint has a in depth academic explanation that mostly went over my head: +// https://www.javatpoint.com/computer-graphics-midpoint-ellipse-algorithm +// +// Draws the eliipse by "scanning" along the edge in one quadrant, and mirroring +// the movement for the other four quadrants. +// +// There are a lot of details to get correct while implementing this algorithm, +// so ensure the edge cases are covered when changing it. Long, thin ellipses +// are particularly susceptible to being drawn incorrectly. void w4_framebufferOval (int x, int y, int width, int height) { uint8_t dc01 = drawColors[0]; uint8_t dc0 = dc01 & 0xf; @@ -389,87 +402,63 @@ void w4_framebufferOval (int x, int y, int width, int height) { uint8_t strokeColor = (dc1 - 1) & 0x3; uint8_t fillColor = (dc0 - 1) & 0x3; - // Get the width and height - int a = width >> 1; - int b = height >> 1; - - if (a <= 0) return; - if (b <= 0) return; - - // Calculate the midpoint - int x0 = x + a, y0 = y + b; - int aa2 = a * a * 2, bb2 = b * b * 2; - - // Store even/odd offsets to allow non even ovals - int wo = (width + 1) % 2, ho = (height + 1) % 2; - int we = width % 2, he = height % 2; - - { - int x = a, y = 0; - int dx = (1 - 2 * a) * b * b, dy = a * a; - int sx = bb2 * a, sy = 0; - int e = 0; - - while (sx >= sy) { - drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ - drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ - drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ - drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ - - if (dc0 != 0) { - int start = x0 - x + 1; - int end = x0 + x - wo; - drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ - drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ - } - - y++; - sy += aa2; - e += dy; - dy += aa2; - if (2 * e + dx > 0) { - x--; - sx -= bb2; - e += dx; - dx += bb2; - } + int a = width; + int b = height; + int b1 = (height + 1) % 2; // Compensates for precision loss when dividing + + int north = y; + north += b / 2; // Precision loss here + int west = x; + int east = x + width - 1; + int south = north - b1; // Compensation here. Moves the bottom line up by + // one (overlapping the top line) for even heights + + // Error increments. Also known as the decision parameters + int dx = 4 * (1 - a) * b * b; + int dy = 4 * (b1 + 1) * a * a; + + // Error of 1 step + int err = dx + dy + b1 * a * a; + + a *= 8 * a; + b1 = 8 * b * b; + + do { + drawPointUnclipped(strokeColor, east, north); /* I. Quadrant */ + drawPointUnclipped(strokeColor, west, north); /* II. Quadrant */ + drawPointUnclipped(strokeColor, west, south); /* III. Quadrant */ + drawPointUnclipped(strokeColor, east, south); /* IV. Quadrant */ + const start = west + 1; + const len = east - start; + if (dc0 != 0 && len > 0) { // Only draw fill if the length from west to east is not 0 + drawHLineUnclipped(fillColor, start, north, east); /* I and III. Quadrant */ + drawHLineUnclipped(fillColor, start, south, east); /* II and IV. Quadrant */ } - } - - { - int x = 0, y = b; - int dx = b * b, dy = (1 - 2 * b) * a * a; - int sx = 0, sy = aa2 * b; - int e = 0; - int ddx = 0; - - while (sy >= sx) { - drawPointUnclipped(strokeColor, x0 + x - wo, y0 + y - ho); /* I. Quadrant */ - drawPointUnclipped(strokeColor, x0 + x - wo, y0 - y); /* II. Quadrant */ - drawPointUnclipped(strokeColor, x0 - x, y0 + y - ho); /* III. Quadrant */ - drawPointUnclipped(strokeColor, x0 - x, y0 - y); /* IV. Quadrant */ - - x++; - sx += bb2; - e += dx; - dx += bb2; - ddx++; - if (2 * e + dy > 0) { - if (dc0 != 0) { - int w = x - ddx - 1; - int start = x0 - w; - int end = x0 + w + we; - drawHLineUnclipped(fillColor, start, y0 + y - ho, end); /* I and III. Quadrant */ - drawHLineUnclipped(fillColor, start, y0 - y, end); /* II and IV. Quadrant */ - } - - y--; - sy -= aa2; - e += dy; - dy += aa2; - ddx = 0; - } + const err2 = 2 * err; + if (err2 <= dy) { + // Move vertical scan + north += 1; + south -= 1; + dy += a; + err += dy; + } + if (err2 >= dx || 2 * err > dy) { + // Move horizontal scan + west += 1; + east -= 1; + dx += b1; + err += dx; } + } while (west <= east); + + // Make sure north and south have moved the entire way so top/bottom aren't missing + while (north - south < height) { + drawPointUnclipped(strokeColor, west - 1, north); /* II. Quadrant */ + drawPointUnclipped(strokeColor, east + 1, north); /* I. Quadrant */ + north += 1; + drawPointUnclipped(strokeColor, west - 1, south); /* III. Quadrant */ + drawPointUnclipped(strokeColor, east + 1, south); /* IV. Quadrant */ + south -= 1; } } From c38bb5d3b202d783d094891015debdd3fc5e485c Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Thu, 1 Sep 2022 18:25:10 -0600 Subject: [PATCH 5/5] Add 2 to b1 instead of 1 I'm not sure why exactly this works, but it makes the ellipses slightly less boxy and makes rendering ovals more consistent. --- runtimes/native/src/framebuffer.c | 2 +- runtimes/web/src/framebuffer.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/runtimes/native/src/framebuffer.c b/runtimes/native/src/framebuffer.c index 67bf4fcc..05e62fa6 100644 --- a/runtimes/native/src/framebuffer.c +++ b/runtimes/native/src/framebuffer.c @@ -415,7 +415,7 @@ void w4_framebufferOval (int x, int y, int width, int height) { // Error increments. Also known as the decision parameters int dx = 4 * (1 - a) * b * b; - int dy = 4 * (b1 + 1) * a * a; + int dy = 4 * (b1 + 2) * a * a; // Error of 1 step int err = dx + dy + b1 * a * a; diff --git a/runtimes/web/src/framebuffer.ts b/runtimes/web/src/framebuffer.ts index 9991f514..89342c25 100644 --- a/runtimes/web/src/framebuffer.ts +++ b/runtimes/web/src/framebuffer.ts @@ -170,7 +170,7 @@ export class Framebuffer { const fillColor = (dc0 - 1) & 0x3; let a = width; - let b = height; + const b = height; let b1 = (height + 1) % 2; // Compensates for precision loss when dividing let north = y; @@ -182,7 +182,7 @@ export class Framebuffer { // Error increments. Also known as the decision parameters let dx = 4 * (1 - a) * b * b; - let dy = 4 * (b1 + 1) * a * a; + let dy = 4 * (b1 + 2) * a * a; // Error of 1 step let err = dx + dy + b1 * a * a;