Help drawing a Superellipse C++ GUI

Pages: 123
I need help with an exercise on drawing a Super-ellipse. This is the exercise specification:


A superellipse is a two-dimensional shape defined by the equation
pow(abs(x/a), m) + pow(abs(y/b), n) = 1, m,n > 0

Look up superellipse on the web to get a better idea of what such shapes
look like. Write a program that draws “starlike” patterns by connecting
points on a superellipse. Take a , b , m , n , and N as arguments. Select N
points on the superellipse defined by a , b , m , and n . Make the points
equally spaced for some definition of “equal.” Connect each of those N
points to one or more other points (if you like you can make the number
of points to which to connect a point another argument or just use N – 1 ,
i.e., all the other points).


Note: the equation is drawn in the book using the bars that signify absolute values. I did it like above because it was easiest for me.

I'm assuming I have to take user input for a, b, m, n, and N. If that's true, then for that and also to ask how to use the above equation to draw the points is the reason why I made this thread.

I'm sorry that I have no code to show right now, aside from just the basic scaffolding required:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Osman Zakir
// 3 / 29 / 2017
// Bjarne Stroustrup: Programming: Principles and Practice Using C++ 2nd Edition
// Chapter 12 Exercise 12
// Exercise Specifications:
/**
 * A superellipse is a two-dimensional shape defined by the equation
 *		pow(abs(x/a), m) + pow(abs(y/b), n) = 1, m,n > 0

 * Look up superellipse on the web to get a better idea of what such shapes
 * look like. Write a program that draws “starlike” patterns by connecting
 * points on a superellipse. Take a , b , m , n , and N as arguments. Select N
 * points on the superellipse defined by a , b , m , and n . Make the points
 * equally spaced for some definition of “equal.” Connect each of those N
 * points to one or more other points (if you like you can make the number
 * of points to which to connect a point another argument or just use N – 1 ,
 * i.e., all the other points).
 */

#include "../../Simple_window.h"
#include "../../Graph.h";

int main()
{
	using namespace Graph_lib;

	Point tl{ 100, 100 };

	Simple_window win{ tl, 800, 600, "Super-ellipse" };

	win.wait_for_button();
}


I've read around and noticed I might need textboxes for this (if I need to take user input). But I'm not sure how to use those.
You'd need to get the equation as a function of x.
Have a look at the explicit equation here: http://fractional-calculus.com/super_ellipse.pdf

Note the ±. For each value of x, you'll have two y values, which explains the symmetry of a superellipse. Similar to how a parabola is symmetrical, as it has two x values which map to the same y value.

1
2
3
4
5
6
7
8
std::vector<double> superellipse_fx( double x, double a, double b, double n )
{   
    // two y-ordinates for every x
    std::vector<double> y{ 0, 0 };
    // y[0] is negative value, y[1] is positive value
    y[0] = -y[1] = b * std::pow( 1 - std::pow( std::abs( x/a ), n ), 1/n );
    return y;
}


edit: The parametric equation is much nicer to use, as you don't have to worry about returning two y-ordinates. I'd recommend you use that instead.
Last edited on
Is the parametric equation the one shown in the book? I can use that to get x and y values?

If I use the explicit equation, I have to get the two y values from the equation and I input everything else? I take a, b, m and n as inputs and try to find y? Then what's the N and the x?

And what about taking user input? Or do I just put in values for a, b, m, n and N as hard-coded numbers? Why does the book say to select N points to connect, though? How do I choose what points to connect?

But yeah, a parabola is the result of graphing the solutions of a quadratic equation, isn't it? The symmetry does make sense.

I'm sorry for asking so many more questions here, though.

Edit: Oh and, also, I'll need to use that function inside a loop, right? And the loop goes for N times, meaning I get N pairs of y values? How do I draw them on a GUI window, though? A Point object takes one x value and one y value, after all.
Last edited on
a parabola is many things :) It might be what you get if you cut an infinite cone a certain way. It might be half an ellipse. It isn't the solutions to the quadratic equation (that is just 0, 1, or 2 numbers depending on the equation). It is the solutions to a 2nd degree polynomial, for all Y.

That is,

2 = x*x; //not a parabola, but this yields 2 values from the quadratic equation.

y = x*x; // is a parabola, as y represents 0, 1, 2, 3, 100000, 2.75, and everything else...!!

You probably were thinking about it correctly. Just being very explicit here.

as far as graphics go, you can iterate every pixel to draw a super smooth curve, or you can draw short line segments. The fewer dots you draw, the more jagged the curve becomes. Its kinda cool how this ties into calculus, where 2 points on a curve approximate the curve for a short distance and yet are also a line... and this lets you draw *everything* with simple geometry, for 3-d, you draw almost everything with triangles, the more you draw, the smoother the object, and in 2-d, its lines.... (almost because sometimes, you draw 1 pixel)
Last edited on
Is the parametric equation the one shown in the book? I can use that to get x and y values?

It's in the link I posted before, on page 2.
Yes, you use the parametric equation to get x and y ordinates, independent of each other. i.e. you don't need x to get y.

If I use the explicit equation, I have to get the two y values from the equation and I input everything else? I take a, b, m and n as inputs and try to find y? Then what's the N and the x?

Yes. For every x ordinate, you'll get two y ordinates.
N is the number of points on the superellipse. We'll deal with that after we draw the superellipse.
-a ≤ x ≤ a
Depending on how smooth you want the curve to be, you'll need a lot of points. We are going to be drawing a lot of lines, rather than one continuous curve. It'll end up looking like a curve.

And what about taking user input? Or do I just put in values for a, b, m, n and N as hard-coded numbers? Why does the book say to select N points to connect, though? How do I choose what points to connect?

You can take user input if you want to, but I think it'll be easier making them constants, preferably the same as the example provided in the link above, so you can see what it's meant to look like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
constexpr double a{ 3.0 }, b{ 2.0 }, n{ 2.0 }, precision{ 1e-2 };
// -a <= x <= a
for( double x{ -a }; x < a; x += precision ) {
    std::vector<double> y0{ superellipse_fx( x, a, b, n ) },
                        // next point
                        y1{ superellipse_fx( x + precision, a, b, n ) };
    // negative y
    // bottom half of superellipse
    Line l0{
        { x, y0[0] },
        { x + precision, y1[0] }
    },
    // positive y
    // top half of superellipse 
    l1{
        { x, y0[1] },
        { x + precision, y1[1] }
    };
}
Last edited on
Okay, I'll try it like that, then. The function being called in the loop is the same as the one you posted earlier, right (just checking)?

Edit: By the way, how do I do this one syntactically correctly?
 
y[0] = -y[1] = b * std::pow( 1 - std::pow( std::abs( x/a ), n ), 1/n );


I don't think the compiler will accept two assignment operators on the same line like that after all.

Something like this, I guess?
1
2
y[0] = -b * pow(1 - std::pow( std::abs( x/a ), n ), 1/n);
y[1] = b * pow(1 - std::pow( std::abs( x/a ), n ), 1/n);
Last edited on
the compiler is quite happy to do multiple assigns / line.

x=y=z=0; //this goes backwards, so all 3 are set to zero.

It does not save you anything to jumble it together like that, though.

however, instead of your code, try
y[0] = ....
y[1] = -y[0]; //don't recompute all that stuff.
Last edited on
Can't those multiple assignments on the same line have undefined behavior, actually? So yeah, I guess I'll really avoid it.

It'd be better to do:
1
2
y[1] = ...
y[0] = -y[1];


For the points, I think, since y[0] is the one meant to be negative.

Anyway, I have this currently:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// Osman Zakir
// 3 / 29 / 2017
// Bjarne Stroustrup: Programming: Principles and Practice Using C++ 2nd Edition
// Chapter 12 Exercise 12
// Exercise Specifications:
/**
 * A superellipse is a two-dimensional shape defined by the equation
 *		pow(abs(x/a), m) + pow(abs(y/b), n) = 1, m,n > 0

 * Look up superellipse on the web to get a better idea of what such shapes
 * look like. Write a program that draws “starlike” patterns by connecting
 * points on a superellipse. Take a , b , m , n , and N as arguments. Select N
 * points on the superellipse defined by a , b , m , and n . Make the points
 * equally spaced for some definition of “equal.” Connect each of those N
 * points to one or more other points (if you like you can make the number
 * of points to which to connect a point another argument or just use N – 1 ,
 * i.e., all the other points).
 */

#include "../../Simple_window.h"
#include "../../Graph.h"
#include <cmath>

std::vector<double> superellipse_calc(const double x, const double a,
	const double b, const double n);

int main()
{
	using namespace Graph_lib;
	using namespace std;

	Point tl{ 100, 100 };
	Simple_window win{ tl, 800, 600, "Super-ellipse" };

	try
	{
		constexpr double a = 3.0;
		constexpr double b = 2.0;
		constexpr double n = 2.0;
		constexpr double precision = 1e-2;

		// -a <= x <= a
		for (double x = -a; x < a; x += precision)
		{
			vector<double> y0 = superellipse_calc(x, a, b, n);
			vector<double> y1 = superellipse_calc(x + precision, a, b, n);

			// negative y
			// bottom half of superellipse
			Line l0
			{
				{int(x), int(y0[0])},
				{int(x + precision), int(y1[1])}
			};

			// positive y
			// top half of superellipse
			Line l1
			{
				{int(x), int(y0[1])},
				{int(x + precision), int(y1[1])}
			};
			l0.set_color(Color::black);
			l1.set_color(Color::black);
			win.attach(l0);
			win.attach(l1);
		}
		win.wait_for_button();
	}
	catch (const runtime_error &e)
	{
		Text err_msg_start{ Point{300, 600}, "Runtime_error: " };
		Text err_msg{ Point{400, 600}, e.what() };
		err_msg_start.set_color(Color::black);
		err_msg.set_color(Color::black);

		win.attach(err_msg_start);
		win.attach(err_msg);
		win.wait_for_button();
	}
}

std::vector<double> superellipse_calc(const double x, const double a,
	const double b, const double n)
{
	using namespace std;

	// two y-coordinates for every x-coordinate
	vector<double> y{ 0, 0 };

	// y[0] is negative value, y[1] is positive value
	y[0] = -b * pow(1 - std::pow(std::abs(x / a), n), 1 / n);
	y[1] = b * pow(1 - std::pow(std::abs(x / a), n), 1 / n);

	return y;
}


It compiles, but I get a blank GUI window.
Last edited on
for (double x = -a; x < a; x += precision)

It's not a good idea to use doubles in a for loop. They might work most of the time but be a little unpredictable because of off by 1 errors. Because they are not exact, you might get an x that is 2e-14 less than a one time, then the next time it will be 2e-14 greater, resulting in a different number of iterations depending on what the actual numbers are.

It's easy to work how many times to loop as an integer, then use a while loop.
Then how would you suggest I do it without losing data (I don't want that warning about possible loss of data).
The range is 6.0 and the precision is 1e-2, so how many times is that ?

I don't want that warning about possible loss of data


1
2
double TimesToLoop = /* .... */ ;
TimesToLoopAsInt = static_cast<unsigned int>(TimesToLoop);


if ( TimesToLoopAsInt < TimesToLoop ) {++TimesToLoopAsInt} // helps with truncation of the double

Edit; fixed copy/paste error
Last edited on
cast the double to an integer and the warning goes away.
the "loss of data" is telling you that putting
3.14
into an integer
give 3.0, which is "loss of data".

Doing it with a cast tells the compiler that you did it on purpose, and the warning goes away.

I know that, but it'd be good if there's no loss of data (though that isn't possible with integers, I guess).

I've changed it into a while loop.

Here's the code now:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// Osman Zakir
// 3 / 29 / 2017
// Bjarne Stroustrup: Programming: Principles and Practice Using C++ 2nd Edition
// Chapter 12 Exercise 12
// Exercise Specifications:
/**
 * A superellipse is a two-dimensional shape defined by the equation
 *		pow(abs(x/a), m) + pow(abs(y/b), n) = 1, m,n > 0

 * Look up superellipse on the web to get a better idea of what such shapes
 * look like. Write a program that draws “starlike” patterns by connecting
 * points on a superellipse. Take a , b , m , n , and N as arguments. Select N
 * points on the superellipse defined by a , b , m , and n . Make the points
 * equally spaced for some definition of “equal.” Connect each of those N
 * points to one or more other points (if you like you can make the number
 * of points to which to connect a point another argument or just use N – 1 ,
 * i.e., all the other points).
 */

#include "../../Simple_window.h"
#include "../../Graph.h"
#include <cmath>

std::vector<double> superellipse_calc(const double x, const double a,
	const double b, const double n);

int main()
{
	using namespace Graph_lib;
	using namespace std;

	Point tl{ 100, 100 };
	Simple_window win{ tl, 800, 600, "Super-ellipse" };

	try
	{
		constexpr double a = 3.0;
		constexpr double b = 2.0;
		constexpr double n = 2.0;
		constexpr double precision = 1e-2;

		// -a <= x <= a
		double x = -a;
		while (x < a)
		{
			vector<double> y0 = superellipse_calc(x, a, b, n);
			vector<double> y1 = superellipse_calc(x + precision, a, b, n);

			// negative y
			// bottom half of superellipse
			Line l0
			{
				{int(x), int(y0[0])},
				{int(x + precision), int(y1[1])}
			};

			// positive y
			// top half of superellipse
			Line l1
			{
				{int(x), int(y0[1])},
				{int(x + precision), int(y1[1])}
			};
			l0.set_color(Color::black);
			l1.set_color(Color::black);
			win.attach(l0);
			win.attach(l1);
			x += precision;
		}
		win.wait_for_button();
	}
	catch (const runtime_error &e)
	{
		Text err_msg_start{ Point{300, 600}, "Runtime_error: " };
		Text err_msg{ Point{400, 600}, e.what() };
		err_msg_start.set_color(Color::black);
		err_msg.set_color(Color::black);

		win.attach(err_msg_start);
		win.attach(err_msg);
		win.wait_for_button();
	}
}

std::vector<double> superellipse_calc(const double x, const double a,
	const double b, const double n)
{
	using namespace std;

	// two y-coordinates for every x-coordinate
	vector<double> y{ 0, 0 };

	// y[0] is negative value, y[1] is positive value
	y[0] = -b * pow(1 - std::pow(std::abs(x / a), n), 1 / n);
	y[1] = b * pow(1 - std::pow(std::abs(x / a), n), 1 / n);

	return y;
}


Now how do I make sure that the super-ellipse shows up on the window? Is this where N (number of points) comes in?
Right, you probably need to iterate from end to end, that is, max and min x value in a x/y coordinate system, for each x get the y, draw the lines, etc.

The code I have now already gives me the x- and y-values, right? I just need a way to display the lines on the window.

So yeah, how should I implement N? And I just use that to determine the number of Point objects I use the lines to connect, right?

Edit:
Drawing the lines on the super-ellipse involves using the Line::draw() function, right? What else do I need to do? Do I need another loop to iterate over each line and draw it?
Last edited on
right... assuming you have a function that draws a line, you do something like this.. (this is pseudocodeish)


for (n = -x; n<= x; x+= someval) //someval is going to govern how many lines to draw, or the smoothness of your ellipse
{
y1 = f(x); //ellipse function
y2 = f(x+someval);
drawline( x,y1, x+someval, y2);
and possibly, draw the mirrored half at the same time, if you like, which is
drawline(x, -y1, x+someval, -y2); //I *think*
}

then you can *decrease* someval to draw *more* lines for more smoothness, right?


closed account (48T7M4Gy)
y = f(x)
range_x = x_max - x_min

no of points to plot = n, dx = range_x/n

So,
1
2
3
4
5
6
for(int i = 0; i < n; ++i ){
        x = i * dx;
        y = f(x);

        plot(x,y); // and maybe plot(x, -y)  whatever
}

both should be correct. I looped over dx, you looped over n, and both work.
Just going back to working out exactly how many times to loop in a for loop, std::round comes in handy to deal with truncation - if the result of 6.0/ 0.01 is 599.99 (or more likely 599.9999999999997) instead of 600:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <cmath>

int main()
{
  double a = 599.99;
 unsigned int b = static_cast<unsigned int>(std::round(a));
  
  std::cout << "b is " << b << "\n";
  
}


http://en.cppreference.com/w/cpp/numeric/math/round

I might have pointed in the wrong direction by mentioning the while loop.

With while (x < a) and later x += precision; , this still might suffer from off by 1 error. If the last value of x is 2.990000000000002, then the next one might be 3.00000000000002 meaning that the value at 3.0 is not calculated. On the other hand, if the last value of x is 2.989999999999992, then the next one might be 2.99999999999992 meaning that the value at 3.0 is calculated. Again, if one changes either the x range or the precision, different things might happen.

There are a lot subtleties with floating point numbers.
jonin wrote:
both should be correct. I looped over dx, you looped over n, and both work.


If looping over dx implies having a double in the for loop, then I have to disagree, as mentioned above. But you did say that you had pseudo code.

If however, ones correctly calculates n as an integer and then uses that in a for loop, that is fine.
Pages: 123