-
• #2
well,
since I could not find an existing method, I quickly hacked one myself (good enough for my own purposes):
/**** drawRoundedRect ****/ const roundedRectSines = [ 0, Math.sin(15*Math.PI/180), Math.sin(30*Math.PI/180), Math.sin(45*Math.PI/180), Math.sin(60*Math.PI/180), Math.sin(75*Math.PI/180), 1 ]; const roundedRectPoly = Array(56); function prepareRoundedRect (x1,y1, x2,y2, r) { r = Math.min(r || 0, Math.abs(x1-x2), Math.abs(y1-y2)); for (let i = 0, j = 0; i <= 6; i++, j += 2) { roundedRectPoly[j] = x1 + r - r*roundedRectSines[6-i]; roundedRectPoly[j+1] = y1 + r - r*roundedRectSines[i]; } for (let i = 0, j = 14; i <= 6; i++, j += 2) { roundedRectPoly[j] = x2 - r + r*roundedRectSines[i]; roundedRectPoly[j+1] = y1 + r - r*roundedRectSines[6-i]; } for (let i = 0, j = 28; i <= 6; i++, j += 2) { roundedRectPoly[j] = x2 - r + r*roundedRectSines[6-i]; roundedRectPoly[j+1] = y2 - r + r*roundedRectSines[i]; } for (let i = 0, j = 42; i <= 6; i++, j += 2) { roundedRectPoly[j] = x1 + r - r*roundedRectSines[i]; roundedRectPoly[j+1] = y2 - r + r*roundedRectSines[6-i]; } } g.drawRoundedRect = function drawRoundedRect (x1,y1, x2,y2, r) { prepareRoundedRect(x1,y1, x2,y2, r); this.drawPoly(roundedRectPoly,true); } g.fillRoundedRect = function fillRoundedRect (x1,y1, x2,y2, r) { prepareRoundedRect(x1,y1, x2,y2, r); this.fillPoly(roundedRectPoly,true); } g.clear(); g.setColor('#000000'); g.drawRoundedRect(10,20, 160,100, 20); g.fillRoundedRect(40,10, 100,160, 10);
The two functions have been designed to be added to the
g
variable in order to be invoked like any other of its methods - but there is certainly a better alternative.
1 Attachment
-
• #3
Funny, i would have thought Bresenham´s would be a lot faster. It seems to be (i just checked in the emulator, not on the real hardware), but only below a radius of about 12. Most of the code is straight from Wikipedia, no optimization at all:
function bRoundedRectangle (x1,y1,x2,y2,r) { var f = 1 - r; var ddF_x = 0; var ddF_y = -2 * r; var x = 0; var y = r; g.drawLine(x1+r,y1,x2-r,y1); g.drawLine(x1+r,y2,x2-r,y2); g.drawLine(x1,y1+r,x1,y2-r); g.drawLine(x2,y1+r,x2,y2-r); var cx1=x1+r; var cx2=x2-r; var cy1=y1+r; var cy2=y2-r; while(x < y) { if(f >= 0) { y--; ddF_y += 2; f += ddF_y; } x++; ddF_x += 2; f += ddF_x + 1; g.setPixel(cx2 + x, cy2 + y); g.setPixel(cx1 - x, cy2 + y); g.setPixel(cx2 + x, cy1 - y); g.setPixel(cx1 - x, cy1 - y); g.setPixel(cx2 + y, cy2 + x); g.setPixel(cx1 - y, cy2 + x); g.setPixel(cx2 + y, cy1 - x); g.setPixel(cx1 - y, cy1 - x); } }
-
• #4
Could you share the Wikipedia link? I would be interested in reading it.
-
• #5
this code is more compact than mine and doesn't need Math.sin, at least - but I'd have to modify it in order to be able to draw and/or fill a rounded rectangle. Radii below 12 are certainly the most common ones.
Thanks a lot for this contribution!
-
• #6
Here it is . For some reason, only the german Wikipedia page mentions the cirlce algorithm. Look under "Kreisvariante des Algorithmus".
-
• #7
Nice functionaliy, may be you are willing to extend it a little bit:
I'm struggeling with the Moon Phase Widget ( https://github.com/espruino/BangleApps/tree/master/apps/widmp ) to add "the dark side of the moon". Currently it is drawn with a filled circle overlaid by a rectangle to clear it half.
I'm thinking about using your code to draw a half filled circle, for this it would be nice to add a parameter to disable the rounding of one or several edge(s).
I know that there are easier ways to draw a half filled circle but with a small modification this function (when hopefully added to the graphics library) will be "more universal".My proposal is:
function drawRoundedRect (x1,y1, x2,y2, r, edgeMask)
where
edgeMask
is a bit coded parameter to disable the rounding of an edge (1,2,4,8 - in clockwise order):edgeMask = (e.g.) 0x00 -> (_) 0x06 -> (_] 0x09 -> [_) 0x0F -> [_]
On the other way round, maybe
r
androundMask
can be added to the generic drawRect() function (with roundMask to enable rounded edges) but I don't know how Javascript reacts when function parameters are missing. -
• #8
Why not just draw a filled circle, then a larger dark circle on top of that? That is how real moon phases work, after all :)
-
• #9
Here is a version for filled rectangles. Performance is roughly the same, faster for r<=12, slower for larger radii. Performance is an issue here, on my Bangle2, "bFillRoundedRectangle(10,10,160,160,13);" takes about 85ms. For UI use, i think a LUT for rounded corners would be the best approach. All coordinates needed for a circle covering the complete screen would take up just 52 bytes.
function bFillRoundedRectangle (x1,y1,x2,y2,r) { var f = 1 - r; var ddF_x = 0; var ddF_y = -2 * r; var x = 0; var y = r; g.fillRect(x1+r,y1,x2-r,y2); var cx1=x1+r; var cx2=x2-r; var cy1=y1+r; var cy2=y2-r; while(x < y) { if(f >= 0) { y--; ddF_y += 2; f += ddF_y; } x++; ddF_x += 2; f += ddF_x + 1; g.drawLine(cx1-x,cy1-y,cx1-x,cy2+y); g.drawLine(cx1-y,cy1-x,cx1-y,cy2+x); g.drawLine(cx2+x,cy1-y,cx2+x,cy2+y); g.drawLine(cx2+y,cy1-x,cx2+y,cy2+x); } }
-
• #10
Good point.
I was so stuck in the current implementation that fiddles with fillEllipse() that I didn't see that... -
• #11
Here is an implementation of your moon phase image (based on Bresenham's circle algorithm - yes, I'm able to learn!):
let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2; let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2; g.setBgColor('#000000'); g.clear(false); function drawMoonPhase (CenterX,CenterY, Radius, leftFactor,rightFactor) { let x = Radius, y = 0, Error = Radius; g.drawLine(CenterX-leftFactor*x,CenterY, CenterX+rightFactor*x,CenterY); let dx,dy; while (y <= x) { dy = 1 + 2*y; y++; Error -= dy; if (Error < 0) { dx = 1 - 2*x; x--; Error -= dx; } g.drawLine(CenterX-leftFactor*x,CenterY-y, CenterX+rightFactor*x,CenterY-y); g.drawLine(CenterX-leftFactor*x,CenterY+y, CenterX+rightFactor*x,CenterY+y); g.drawLine(CenterX-leftFactor*y,CenterY-x, CenterX+rightFactor*y,CenterY-x); g.drawLine(CenterX-leftFactor*y,CenterY+x, CenterX+rightFactor*y,CenterY+x); } } g.setColor('#FFFFFF'); drawMoonPhase(CenterX,CenterY, 50, 1,-0.5);
leftFactor
andrightFactor
specify the actual phase that is to be shown:- both variables range from -1...+1
- set both to
1
for a full moon - set
rightFactor = 1
and start withleftFactor = -1
for a waxing moon. IncreaseleftFactor
until1
for a full moon - set
leftFactor = 1
and start withrightFactor = 1
for a waning moon. DecreaserightFactor
until-1
(see GitHub for the current source code and an animated demo)
Have fun!
1 Attachment
- both variables range from -1...+1
-
• #12
Great, thank you!
Concerning performance: you are using the C implementation of Bresenham which assumes that multiplications take longer than additions.
However, in Espruino, variable lookup may be more expensive than any kind of addition/multiplication. Thus, the BASIC implementation might be more efficient than yours (this is the one I used for the moon phase visualization).
It would be interesting to analyze that dependency...
-
• #13
Here is my implementation of
drawRoundedRect
using the BASIC version of Bresenham's algorithm as described in Wikipedia.Please note: the Bresenham algorithm makes a few assumptions (e.g.,
x1 <= x2
) which have been taken care of in the following code as well:let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2; let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2; g.setBgColor('#000000'); g.clear(false); g.drawRoundedRect = function drawRoundedRect (x1,y1, x2,y2, r) { let x,y; if (x1 > x2) { x = x1; x1 = x2; x2 = x; } if (y1 > y2) { y = y1; y1 = y2; y2 = y; } r = Math.min(r || 0, (x2-x1)/2, (y2-y1)/2); let cx1 = x1+r, cx2 = x2-r; let cy1 = y1+r, cy2 = y2-r; this.drawLine(cx1,y1, cx2,y1); this.drawLine(cx1,y2, cx2,y2); this.drawLine(x1,cy1, x1,cy2); this.drawLine(x2,cy1, x2,cy2); x = r; y = 0; let dx,dy, Error = 0; while (y <= x) { dy = 1 + 2*y; y++; Error -= dy; if (Error < 0) { dx = 1 - 2*x; x--; Error -= dx; } this.setPixel(cx1 - x, cy1 - y); this.setPixel(cx1 - y, cy1 - x); this.setPixel(cx2 + x, cy1 - y); this.setPixel(cx2 + y, cy1 - x); this.setPixel(cx2 + x, cy2 + y); this.setPixel(cx2 + y, cy2 + x); this.setPixel(cx1 - x, cy2 + y); this.setPixel(cx1 - y, cy2 + x); } }; g.setColor('#FFFFFF'); g.drawRoundedRect(120,50, 50,120, 100);
It would be interesting to see how that performs compared to the C variant.
1 Attachment
-
• #14
Last, but not least: the
fillRoundedRect
variant of the abovementioned code:let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2; let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2; g.setBgColor('#000000'); g.clear(false); g.fillRoundedRect = function fillRoundedRect (x1,y1, x2,y2, r) { let x,y; if (x1 > x2) { x = x1; x1 = x2; x2 = x; } if (y1 > y2) { y = y1; y1 = y2; y2 = y; } r = Math.min(r || 0, (x2-x1)/2, (y2-y1)/2); let cx1 = x1+r, cx2 = x2-r; let cy1 = y1+r, cy2 = y2-r; this.fillRect(x1,cy1, x2,cy2); x = r; y = 0; let dx,dy, Error = 0; while (y <= x) { dy = 1 + 2*y; y++; Error -= dy; if (Error < 0) { dx = 1 - 2*x; x--; Error -= dx; } this.drawLine(cx1 - x, cy1 - y, cx2 + x, cy1 - y); this.drawLine(cx1 - y, cy1 - x, cx2 + y, cy1 - x); this.drawLine(cx1 - x, cy2 + y, cx2 + x, cy2 + y); this.drawLine(cx1 - y, cy2 + x, cx2 + y, cy2 + x); } }; g.setColor('#FFFFFF'); g.fillRoundedRect(120,50, 50,120, 100);
Now I have everything I need - thank you very much for your assistance!
1 Attachment
-
• #15
no, they do not (do not mix up moon phases with lunar eclipses!)
Moon phases appear because the sun shines on one half of the moon and we (on earth) are looking on it from the side...
-
• #16
Right, i was thinking of an eclipse...
-
• #17
Wonderful!
Implementation AND documentation within a few minutes :-)) -
• #18
meanwhile, I've implemented g.drawRoundRect and g.fillRoundRect as external modules which may easily be loaded into your program.
g.drawRoundRect
has already been documented, the docs forg.fillRoundRect
will follow a.s.a.p. -
• #19
Thanks, this is great! It's now part of my "toolkit", :P
-
• #20
This is the code I use for rounded rects in my programs:
function RectRnd(x1,y1,x2,y2,r) { pp = []; pp.push.apply(pp,g.quadraticBezier([x2-r,y1, x2,y1,x2,y1+r])); pp.push.apply(pp,g.quadraticBezier([x2,y2-r,x2,y2,x2-r,y2])); pp.push.apply(pp,g.quadraticBezier([x1+r,y2,x1,y2,x1,y2-r])); pp.push.apply(pp,g.quadraticBezier([x1,y1+r,x1,y1,x1+r,y1])); return pp; } function fillRectRnd(x1,y1,x2,y2,r,c) { g.setColor(c); g.fillPoly(RectRnd(x1,y1,x2,y2,r),1); g.setColor(255,255,255); } function drawRectRnd(x1,y1,x2,y2,r,c) { g.setColor(c); g.drawPoly(RectRnd(x1,y1,x2,y2,r),1); g.setColor(255,255,255); }
-
• #21
Documentation complete
Additionally, there is also a method to draw filled circular rings (g.fillRing) which might become handy when implementing analog clocks
-
• #23
Interesting. I've written one too, but it is very different. I'll share later (currently on my phone).
-
• #24
@Andreas_Rozek - this is my function, implementing an Andres circle. Heavily based on example implementations on French Wikipedia .
I think it can probably be optimised more, and ultimately I also want to draw this a bit at a time. My best approach to that so far is having an array per octant and pushing/unshifting on each iteration for each circle so that I can step through later, but I think I can improve on that.
I spent a lot of thinking about allocating a fixed length array for the coordinates, but I've come to the conclusion that it isn't possible to know the length a priori (at least, I think that is the case).
const drawThickRing = function (x_centre, y_centre, r, thickness) { // Uses the Andres circle drawing algorithm to draw a ring // with outer radius r and a specified thickness. var x, y, d; for (let t = 0; t < thickness; t++) { r--; x = 0; y = r; d = r - 1; while (y >= x) { g.setPixel(x_centre + x, y_centre - y); g.setPixel(x_centre + y, y_centre - x); g.setPixel(x_centre + y, y_centre + x); g.setPixel(x_centre - x, y_centre + y); g.setPixel(x_centre + x, y_centre + y); g.setPixel(x_centre - y, y_centre + x); g.setPixel(x_centre - y, y_centre - x); g.setPixel(x_centre - x, y_centre - y); if (d >= 2 * x) { d -= 2 * x + 1; x++; } else if (d < 2 * (r - y)) { d += 2 * y - 1; y--; } else { d += 2 * (y - x - 1); y--; x++; } } } }; drawThickRing(85, 85, 70, 10);
-
• #25
this looks as if you would draw concentric rings with an increasing radius. How can you prove, that discretisation does not produce small "holes" in your ring?
Addendum: I'm currently reading the french Wikipedia entry (well, after translation into german - my french is not good enough for maths) and it seems to be a characteristic of that algorithm that no holes are left
...I can't find one in the reference - but the layout library draws such rectangles around buttons.