Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new methods to ImDrawList that support drawing ellipses #2743

Closed
wants to merge 6 commits into from

Conversation

Doohl
Copy link
Contributor

@Doohl Doohl commented Aug 23, 2019

Contained are three new ImDrawList functions that closely mimic the existing AddEllipse/PathArcTo functions:

  • ImDrawList::PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rotation, float a_min, float a_max, int num_segments = 10)
  • ImDrawList::AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 12, float thickness = 1.0f)
  • ImDrawList::AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 12)

Parameters for drawing an ellipse, more or less, were taken from Javascript's Canvas2D ellipse() method. Willing to make any modifications that will improve performance and/or clarity.

Though ellipses can currently be drawn with the existing bezier curve feature set, it does not seem proper - ellipses, I would argue, are primitive shapes that should be included.

CosmicExplorer_2019-08-22_22-04-35
[Example use: drawing 2D kepler orbits]

@ocornut
Copy link
Owner

ocornut commented Aug 23, 2019

Thank you @Doohl !
Could you update the "Custom Rendering" demo to make use of those functions?
Also adding some comments on the PR now.

imgui_draw.cpp Outdated Show resolved Hide resolved
@ebachard
Copy link

ebachard commented Aug 24, 2019

Hello,

Thank you very much for this new method ! Just in time with my need :-) I tested your code (see screenshot). For the record, it works well, but I didn't investigate much. The only issue I have, is that I was not able to make rotation work.

miniDart_0 9 6_canvas06

The code is big and not public yet, since I've created a class named Canvas since, and something similar will be in miniDart 0.9.7 . See https://framagit.org/ericb/miniDart.

Below, the essential to understand:

 float radius_x = 1.0f + ImGui::GetMouseDragDelta().x;
 float radius_y = 1.0f + ImGui::GetMouseDragDelta().y;

 if (fabs(radius_x) <= 0.1f)
      radius_x = 1.0f; // avoid divide by zero

static float rotation = (ImGui::IsKeyPressed(ImGui::GetIO().KeyCtrl) == true) ? radius_y / radius_x : 0.0f;

The preview:

                                    case EMPTY_ELLIPSE:
                                        ImGui::GetOverlayDrawList()->AddEllipse(ImVec2(pTextCanvas->image_pos.x + aDrawnObject.objectPoints[0].x, pTextCanvas->image_pos.y + aDrawnObject.objectPoints[0].y),
                                                                               radius_x,
                                                                               radius_y,
                                                                               aDrawnObject.objBackgroundColor,
                                                                               rotation,
                                                                               32,
                                                                               aDrawnObject.thickness);
                                    break;

                                    case FILLED_ELLIPSE:
                                        ImGui::GetOverlayDrawList()->AddEllipseFilled(ImVec2(pTextCanvas->image_pos.x + aDrawnObject.objectPoints[0].x, pTextCanvas->image_pos.y + aDrawnObject.objectPoints[0].y),
                                                                               radius_x,
                                                                               radius_y,
                                                                               aDrawnObject.objBackgroundColor,
                                                                               rotation,
                                                                               32);
                                    break;

What is drawn:

                  ///////////////////////////////////////////////////////////////////////////////////////////////
                //                                           DRAW ALL
                ///////////////////////////////////////////////////////////////////////////////////////////////

                // clip lines and objects within the canvas (if we resize it, etc.)
                draw_list->PushClipRect(ImVec2(0.0f, 0.0f), pTextCanvas->image_pos + subview_size);

                for (unsigned int i = 0; i < delayTabDrawnObjects.size(); i++)
                {
                    {
                        switch(delayTabDrawnObjects[i].anObjectType)
                        {
                            case EMPTY_RECTANGLE:
                            // already stored
                            draw_list->AddRect( pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[1],
                                                delayTabDrawnObjects[i].objBackgroundColor, 0.0f, ~0 , delayTabDrawnObjects[i].thickness);
                            break;

                            case EMPTY_CIRCLE:
                            draw_list->AddCircle(ImVec2(pTextCanvas->image_pos.x + delayTabDrawnObjects[i].objectPoints[0].x, pTextCanvas->image_pos.y + delayTabDrawnObjects[i].objectPoints[1].y),
                                                 delayTabDrawnObjects[i].P1P4,
                                                 delayTabDrawnObjects[i].objBackgroundColor, 32, delayTabDrawnObjects[i].thickness);
                            break;

                            case FILLED_RECTANGLE:

                            draw_list->AddRectFilled(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0], pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[1], delayTabDrawnObjects[i].objBackgroundColor);
                            break;

                            case FILLED_CIRCLE:
                            draw_list->AddCircleFilled(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                       delayTabDrawnObjects[i].P1P4,
                                                       delayTabDrawnObjects[i].objBackgroundColor,
                                                       32);
                            break;

                            case EMPTY_ELLIPSE:
                            draw_list->AddEllipse(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                  delayTabDrawnObjects[i].radius_x,
                                                  delayTabDrawnObjects[i].radius_y,
                                                  delayTabDrawnObjects[i].objBackgroundColor,
                                                  delayTabDrawnObjects[i].rotation,
                                                  32,
                                                  delayTabDrawnObjects[i].thickness);
                            break;

                            case FILLED_ELLIPSE:
                            draw_list->AddEllipseFilled(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                        delayTabDrawnObjects[i].radius_x,
                                                        delayTabDrawnObjects[i].radius_y,
                                                        delayTabDrawnObjects[i].objBackgroundColor,
                                                        delayTabDrawnObjects[i].rotation,
                                                        32);
                            break;

( and so on )

Thanks !

@Doohl
Copy link
Contributor Author

Doohl commented Aug 25, 2019

Hello @ebachard

I'd say your problem is here:

static float rotation = (ImGui::IsKeyPressed(ImGui::GetIO().KeyCtrl) == true) ? radius_y / radius_x : 0.0f;

Specifically, (ImGui::IsKeyPressed(ImGui::GetIO().KeyCtrl) == true) is probably not the conditional you want to write. ImGui::GetIO().KeyCtrl is returning a bool and ImGui::IsKeyPressed is expecting an integer input (so it's being implicitly converted).

Instead, do:

static float rotation = ImGui::GetIO().KeyCtrl ? radius_y / radius_x : 0.0f;

Which will evaluate as true when the CTRL key is down. Hope you find this helpful!

@ebachard
Copy link

@Doohl : uff, you are right, I was mistaken with KeyCtrl use. Shame on me ...

In fact, in meantime, I identified another visibility issue, and the final working solution is:

float rotation = ImGui::GetIO().KeyCtrl ? radius_y / radius_x : 0.0f;

Thanks a lot for your help, and for your nice feature ! :-)

@ocornut
Copy link
Owner

ocornut commented Aug 27, 2019

@Doohl Question about the API.. while I can see the use for angle something feels odd which is that the rest of the ImDrawList doesn't have this parameter for other primitives (while it perfectly could have the parameter!). What do you think makes Ellipse different there?

@Doohl
Copy link
Contributor Author

Doohl commented Aug 27, 2019

You are probably right, @ocornut there is not an outstanding reason for why I've added rotation to ellipses. It was a combination of my own personal need and the fact that Canvas2D's ellipse() has that argument.

I can remove that parameter for now and open an issue or PR for standardizing rotation for all primitives.

@ocornut
Copy link
Owner

ocornut commented Aug 28, 2019

The rotation is justified because most of the other primitives can be rotated by the user.

AddRect can be replaced by AddQuad with rotated points. Whereas if AddEllipse doesn’t offer the option, there’s no way to achieve a rotated output.

Only exception is AddRectFilledMultiColor() but that’s a rarely used and very simple function.

I guess what I am most curious about is how one may be using ellipses in a codebase. How about you?

@Doohl
Copy link
Contributor Author

Doohl commented Aug 28, 2019

For background, I'm using ImGui's primitives for a quick program to render a 2D model of the solar system. Trying to avoid either writing up my own methods for primitives or using something like SDL_gfx.

Ellipses in my case require rotation, because in the context of a topdown 2D perspective of a Keplerian two-body solar system, the longitude of the periapsis defines the rotation of the ellipse that represents an orbit.

2019-08-28_14-49-13

So we could:

  • Leave the API with rotation included
  • Have overloads for rotation / no rotation (kinda messy!)
  • Look into other solutions, like an ImRotateStart / ImRotateEnd solution like in Issue Rotated text and animated icons #1286.

I speculate most people who would draw ellipses probably won't need them rotated like I do. But you do bring up a good point in that other primitives can already be rotated!

@ocornut
Copy link
Owner

ocornut commented Aug 29, 2019

Trying to avoid either writing up my own methods

You just did :)

My gut feeling right now is that we should go for your initial plan.

@Doohl
Copy link
Contributor Author

Doohl commented Aug 29, 2019

Haha, I meant I was TRYING to avoid writing my own methods. Actually, I think a more performant solution for me would be to draw my ellipses w/ GLSL especially since their x/y radii can go up to like 5000 pixels... but it's always good to have a quick software solution.

I'll leave that last commit in the branch for posterity; if you decide to keep rotation or not. I figure you probably cherry pick commits anyway! Thank you for all the input.

shujaatak pushed a commit to VideosWorks/miniDart-imgui-ffmpeg-opengl that referenced this pull request Apr 5, 2020
@Matheus-Garbelini
Copy link

Matheus-Garbelini commented Sep 3, 2020

Hi, is there some update on this?
The ellipse API with rotation is particularly useful when rendering SVG.

@ocornut
Copy link
Owner

ocornut commented Sep 3, 2020 via email

@Matheus-Garbelini
Copy link

Matheus-Garbelini commented Nov 1, 2020

Hi, @ocornut thanks. I had some issues using those functions manually so I decided to simply add a WebView (CEF) to ImGui. A bit overkill but it looks awesome if combining WebView on top of ImGui. Hopefully, I can provide an ImGui WebView Addon component when I have the time.

webview_renderer_svg

@Sandruz
Copy link

Sandruz commented Sep 7, 2023

Hi guys, what a pity this never got merged. Do you have plans to work on this again?

@ocornut
Copy link
Owner

ocornut commented Sep 7, 2023

Hi guys, what a pity this never got merged. Do you have plans to work on this again?

It's about 15 lines of code that you can yourself add anywhere in your own code/application. I don't think there's any pressure from adding this in the codebase.

In principle I am for it. I think it was a mistake on my part to suggest a version without rotation would be more standard.
I would suggest to use the version before the last commit.

For reference, here's the code (in its version before removing the angle parameter) made external:

#include "imgui_internal.h"

void ImDrawList_PathEllipticalArcTo(ImDrawList* draw_list, const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments)
{
    draw_list->_Path.reserve(draw_list->_Path.Size + (num_segments + 1));

    const float cos_rot = ImCos(rot);
    const float sin_rot = ImSin(rot);
    for (int i = 0; i <= num_segments; i++)
    {
        const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min);
        ImVec2 point(ImCos(a) * radius_x, ImSin(a) * radius_y);
        const float rel_x = (point.x * cos_rot) - (point.y * sin_rot);
        const float rel_y = (point.x * sin_rot) + (point.y * cos_rot);
        point.x = rel_x + center.x;
        point.y = rel_y + center.y;
        draw_list->_Path.push_back(point);
    }
}

void ImDrawList_AddEllipse(ImDrawList* draw_list, const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot, int num_segments, float thickness)
{
    if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2)
        return;

    // Because we are filling a closed shape we remove 1 from the count of segments/points
    const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments;
    ImDrawList_PathEllipticalArcTo(draw_list, center, radius_x, radius_y, rot, 0.0f, a_max, num_segments - 1);
    draw_list->PathStroke(col, true, thickness);
}

void ImDrawList_AddEllipseFilled(ImDrawList* draw_list, const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot, int num_segments)
{
    if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2)
        return;

    // Because we are filling a closed shape we remove 1 from the count of segments/points
    const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments;
    ImDrawList_PathEllipticalArcTo(draw_list, center, radius_x, radius_y, rot, 0.0f, a_max, num_segments - 1);
    draw_list->PathFillConvex(col);
}

I have rebased this locally and added support for auto-tesselation but I'm unsure of the right API. @Doohl themselves link to https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/ellipse which has a start_angle and end_angle exposed in the high-level Ellipse() function.

@ocornut
Copy link
Owner

ocornut commented Sep 7, 2023

Survey of APIs

CanvasRenderingContext2D.ellipse()
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/ellipse

ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle)
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise)

Old GDI
https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-ellipse

BOOL Ellipse(
  [in] HDC hdc,
  [in] int left,
  [in] int top,
  [in] int right,
  [in] int bottom
);

.Net Graphics
https://learn.microsoft.com/fr-fr/dotnet/api/system.drawing.graphics.drawellipse?view=dotnet-plat-ext-7.0

DrawEllipse(Pen, Single, Single, Single, Single)

Processing:
https://processing.org/reference/ellipse_.html

ellipse(a, b, c, d)
Parameters
a(float)x-coordinate of the ellipse
b(float)y-coordinate of the ellipse
c(float)width of the ellipse by default
d(float)height of the ellipse by default

Matplotlib
https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.patches.Ellipse.html

xy : (float, float): xy coordinates of ellipse centre.
width : float : Total length (diameter) of horizontal axis.
height : float: Total length (diameter) of vertical axis.
angle : scalar, optional: Rotation in degrees anti-clockwise.

Allegro 5
https://www.allegro.cc/manual/5/al_draw_ellipse

    cx, cy - Center of the ellipse
    rx, ry - Radii of the ellipse
    color - Color of the ellipse
    thickness - Thickness of the ellipse, pass <= 0 to draw a hairline ellipse

Raylib
https://www.raylib.com/cheatsheet/cheatsheet.html

void DrawEllipse(int centerX, int centerY, float radiusH, float radiusV, Color color);             // Draw ellipse
void DrawEllipseLines(int centerX, int centerY, float radiusH, float radiusV, Color color);        // Draw ellipse outline

SDL2gfx

ellipseColor (SDL_Renderer *renderer, Sint16 x, Sint16 y, Sint16 rx, Sint16 ry, Uint32 color) Draw ellipse with blending. 

What's evident to me is that most API have limitations, the only one that doesn't is JS one, mathlib, and low-level version proposed by this PR. It's also apparent that all API offering an angle/rotation are expressing the ellipsis as center + radiuses instead of bounding box. I believe bounding box is simpler to use in some situations but it doesn't map to rotated ellipsis.

This low-level API I have no issue:

PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0)`

The high-level IHMO have limitations:

void  AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f);
void  AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0);

But they can be lifted using the low-level version, and that's exactly analogous to using AddCircle() vs PathArcTo() so I am fine with them.

I'll merge this now.

ocornut pushed a commit that referenced this pull request Sep 7, 2023
…ed(). (#2743)

Rebased with mods by ocornut: defaults to num_segments==0, supports for auto-tesselation, tweak demo.
@ocornut
Copy link
Owner

ocornut commented Sep 7, 2023

I have pushed the modified version as e3d9b87

For auto-tesseletation I used:

if (num_segments <= 0)
        num_segments = _CalcCircleAutoSegmentCount(ImMax(radius_x, radius_y));

Which is pessimistic and not ideal (maybe counter-productive in some extreme case). I would appreciate if someone can investigate proper auto-tesselation for ellipsis, based on ellipsis perimeter.

@ocornut ocornut closed this Sep 7, 2023
@cfillion
Copy link
Contributor

cfillion commented Mar 17, 2024

Why is center x and y one ImVec2 parameter, but radius x and y are two float parameters? Two different ways of doing the same thing in the same signature seems inconsistent...

@ocornut ocornut reopened this Mar 18, 2024
ocornut pushed a commit that referenced this pull request Mar 19, 2024
…ImVec2 radius in PathEllipticalArcTo(), AddEllipse(), AddEllipseFilled(). (#2743, #7417)
@ocornut
Copy link
Owner

ocornut commented Mar 19, 2024

As per 868facf, please note that the prototype of the ellipse functions have been changed to use a single ImVec2 instead of two floats for the radius.

@ocornut ocornut closed this Mar 19, 2024
pull bot pushed a commit to TeamREPENTOGON/imgui that referenced this pull request Mar 19, 2024
…ImVec2 radius in PathEllipticalArcTo(), AddEllipse(), AddEllipseFilled(). (ocornut#2743, ocornut#7417)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants