ALEX'S BLOG OF EVERYTHING

Drawing Pretty Equations

Concept


Recently, I had the goal of creating a program that could take in any mathematical function and draw the graph of the equation to the screen, somewhat like Desmos. While doing this, I came across an interesting challenge which I would like to share with you: giving the graph width. I could of course just connect each point in the graph with a line primitive and call it a day, and for a little while I did. But I eventually decided that the look was spindly dissatisfying, and I took on the task of making my graphs *pretty*. The rendering of any equation with the form y = f(x) takes place in 3 core steps for each step of x:
  1. Define the start (x | f(x)) and end (x+Δx | f(x+Δx)) points.
  2. Generate a quad with the width of the given stroke weight.
  3. Adjust the corners of 2 adjacent quads to merge in the center.

Segment Construction


We will take an incremental approach to constructing our continuous line. First, we will define a step size Δx. Then we will iteratively traverse along the x-axis, constructing quadrelateral primitives for each segment as we go.

...
const double x_start = -5;
const double x_end = 5;
const size_t res = 100;
const double dx = (x_end - x_start) / resolution;

for (size_t i = 0; i < res; i++)
{
    const double x = x_start + i * dx;
    ...
            
The first thing we have to do when generating the quad is of course finding the start and end points. This is done by defining two 2D vector objects to represent points, with the first having the values (x | f(x)) and the second
(x+Δx | f(x+Δx)).

...
const Vector2 start(x, func(x));
const Vector2 end(x + dx, func(x + dx));
...
            
By subtracting the start point from the end point, we are able to get the direction from start to end, and we will use this to form our lines width. From linear algebra we know that we can rotate a vector by 90° by swapping its x and y components and negating one of them. We now have a vector facing perpendicular to our line at this position. Using this, we can begin generating our quad. First, we normalise the width vector (give it a length of 1), and then we scale it again by half of our chosen width.

...
const Vector2 length = end - start;
const Vector2 width = Vector2(-length.y, length.x).normalise() * weight * 0.5;
...
            
After this we can gain all four corners of our quad simply by adding and subtracting this width vector from our start and end points. We only assign the 2 start corners of the quad if we are handling the first segment. Otherwise these are set to the end points of the previous segment creating a continuous chain.

...
const Vector2 x0y0 = start + width;
const Vector2 x0y1 = start - width;
const Vector2 x1y0 = end + width;
const Vector2 x1y1 = end - width;

if (i == 0)
{
    quads[i][0].position = x0y0;
    quads[i][1].position = x0y1;
}
else
{
    quads[i][0].position = quads[i - 1][3].position;
    quads[i][1].position = quads[i - 1][2].position;
}

quads[i][2].position = x1y0;
quads[i][3].position = x1y1;
...
            
The following is a demo of this code rendering "cos(x)" at 1000 segments of resolution between -π and π. cos_demo.png image could not load