Saturday, June 27, 2026

🪲 Fix in The Works: DRAW statement: Circle Creation: The problem of Asymmetric Pixels

I want to write about a little problem I think I've fixed (in the development version of BASIC Anywhere Machine), and I hope to soon release this fix in a new version of BASIC Anywhere Machine:

🪲 Asymetric pixels in the quadrants or a circle generated with the DRAW statement.

By quadrants, I mean each pie shape created by dividing the top-bottom, and left-right parts of the circle:

By the way, later, keep in mind that from the center coordinate of the circle:

  • travelling directly right, that is at an angle of 0 degrees
  • travelling directly up, that is at an angle of 90 degrees
  • travelling directly left, that is at an angle of 180 degrees
  • travelling directly down, that is at an angle of 270 degrees

Here's a little program using DRAW to create a circle on a small-resolution screen:

d% = 20

SCREEN _NEWIMAGE( d% * 2 + 1, d% * 2 + 1, 17)

FOR A% = 0 TO 359 STEP 1
    COLOR 63
    draw "BM " + d% + "," + d%
    DRAW "TA " + A%
    DRAW "BU" + (d% - 1)
    pset (point(0), point(1)), 61
NEXT A%

The Result:


In each highlighted part of the circle, we find cases where the pixels in the mirror image across the vertical axis, and/or the mirror image across the horizontal axis, do not match.

In the scheme of things, this wouldn't matter in regards to the job of creating circles.  The asymmetry is noticeable at low resolutions, but becomes indistinguishable by the naked eye at high resolutions.

However, this does cause some noticeable problems, even at high resolutions, when using this process to create polygons which ought to have  one or more perfectly horizontal and/or perfectly vertical sides, like this triangle, for example:

d% = 20
SCREEN _NEWIMAGE( d% * 2, d% * 2, 17)
FOR A% = 0 TO 360 STEP 120
    COLOR 63
    draw "BM " + d% + "," + d%
    DRAW "TA " + A%
    DRAW "BU" + (d% - 1)
    NEWX% = POINT(0) : NEWY% = POINT(1)
    IF A% > 0 THEN COLOR 7 : _
                   LINE (NEWX%, NEWY%) TO (LASTX%, LASTY%)
    LASTX% = NEWX% : LASTY% = NEWY%
NEXT A%

The result:


I suspect (without, for lack of interest, researching this thoroughly) that I'm dealing with what might be either a floating point issue, and/or the following, and/or some other related issue(s):

From Google AI's answer to "Using javascript, why would the process of creating a circle of a certain radius not result in symmetric pixels between the quadrants ?":

When you write JavaScript to create a raster (pixel-based) circle, quadrants often lack symmetry because of integer rounding and the discrete nature of pixel grids. Circles rely on continuous math, but monitors have a fixed resolution, creating several common issues: [1, 2, 3]
    • Off-Center Origins: If your radius is an even number (e.g., \(r = 10\)), there is no single middle pixel, forcing the circle to round up or down on the grid. This shifts the center, making left/right or up/down sizes unequal. [1]
    • Coordinate Truncation: Converting float coordinates to whole pixels (using Math.round(), Math.floor(), or Math.ceil()) can bias where lines fall on the grid, stretching or shrinking one quadrant over another. [1]
    • Aspect Ratio: If the canvas CSS scales the element unevenly (e.g., non-square pixels or mismatched width and height settings), what should be a symmetrical equation acts like an oval, shifting pixel densities per quadrant. [1]
    • Algorithm Bias: Algorithms like the Midpoint Circle or Bresenham’s often use directional bias (preferring to step horizontally, vertically, or diagonally) to optimize speed. This "step" decision breaks quadrant reflection. [1]

What I've decided to do (I wanted to solve this on my own without looking for already existing solutions):

Modify BAM's implementation of DRAW such that whatever angle is used to draw (using the TA command to set the angle and/or whatever angles needed to perform commands like U,D,R,L = Up,Down,Right,Left) convert that angle to the equivalent between 0 and 90 degree, figure out the X and Y coordinates for the destination pixel, and then negate the value of X and/or Y (if applicable) to get the symmetric point for the originally intended angle.

For your interest, here's the relevant javascript code to figure out the destination pixel:

  function AngleStep(a) {
       var xstep = 0;
       var ystep = 0;
       var tota = (+draw_angle + +a) % 360;
       var af = tota;
       if (tota > 90 && tota < 180) {af = 180 - tota;}
       else if (tota > 180 && tota < 270) {af = tota - 180;}
       else if (tota > 270) {af = 360 - tota;}
       af = af * Math.PI / 180;
       if (tota % 180 === 0) { xstep = n; }
       else if (tota % 90 === 0)  { ystep = n; }
       else {xstep = n * Math.cos(af); ystep = n * Math.sin(af);}
       xstep=Math.round(xstep); ystep=Math.round(ystep);
       if ((tota % 360 > 90) && (tota % 360 < 270)) { xstep = -xstep;  }
       if ((tota % 360 < 180)) { ystep = -ystep;  }
       Step(xstep,ystep);
     }

  • a is the angle if the pen movement
  • tota is the calculated angle of movement
    • a would be, for example:
      • 0 degrees if the drawing command is Right
      • 90 degrees if the drawing command is Up
      • 180 degrees if the drawing command is Left
      • 270 degrees if the drawing command is Down
    • draw_angle would be an angular offset applied with a TA command; for example, an offset of 5 degrees would result in:
      • 5 degrees if the drawing command is Right
      • 95 degrees if the drawing command is Up
      • 185 degrees if the drawing command is Left
      • 275 degrees if the drawing command is Down
  • af is the tota angle transposed to the first quadrant (between 0 and 90 degrees)
  • n is the distance (the number of pixels) travelled from the current pen position to the destination pen position
  • xstep and ystep are the relative destination coordinates of the pen based on wherever the pen is currently located when the DRAW statement is performed)
Let's say that the "tota" angle is 125 degrees, then the "af" angle will be 55 degrees.  When the function figures out the values of xstep and ystep at 55 degrees: ystep does not change (the value of ystep is the same at 55 degrees and 125 degrees), however xstep is switched to the negative value (for example: if xstep is 100 pixels to the right of the starting pen position when the angle is 55 degrees, then xstep is -100 to the left of the starting pen position when the angle is 125 degrees.)

With this modified implementation of DRAW, the same BASIC programs now result in this circle and this triangle:





WOOHOO !

If you want to try the BASIC programs above, before the next release of BAM:



No comments:

Post a Comment

🪲 Fix in The Works: DRAW statement: Circle Creation: The problem of Asymmetric Pixels

I want to write about a little problem I think I've fixed (in the development version of BASIC Anywhere Machine), and I hope to soon rel...