Most efficient way to draw hyperbola?

TLDR: What’s easier for a computer: making a curve out of a polygon with a bunch of regular vertices, or making it out of a shape with a couple of quadratic vertices?

So, as the title suggests, I need to generate a hyperbola (the 1/x graph). Specifically a hyperbola of arbitrary width and height (and rotation, and position).

There are two approaches I’ve taken to this: one is to draw an open polygon with a bunch of vertices (about 64), and the other is to draw a shape made of a few quadratic vertices (about 8). At first I just wanted to ask which approach would yield a more accurate approximation per unit time spent computing & rendering it all, but now that I think about it, I suppose I also have to ask if there are any more efficient approaches to this (there probably are).

For reference, I’ve optimized both approaches so that the computer runs a couple cosh and sinh functions to compute the first few vertices, and uses angle addition formulas to find the rest (so that most of the calculations are simple arithmetic). I also made an optimization so the computer will only look at the vertices that can be seen on screen, so more vertices can be displayed at once.

If someone could tell me which technique is faster, and if there are any approaches that would be even faster, that’d be great. Thank you!

EDIT: I forgot to mention before, but the shape in question needs to have a constant strokeWeight, and won’t be filled in.

2 Likes

I am not sure if I understand exactly what you want but you could modify the transformation matrix using translate(), rotate() and scale() between pushMatrix() and popMatrix() statements so do not have to recalculate the vertices if the curve’s position, size or rotation changes.

I suppose your right, this does require a bit of clarification. The reason I put position and rotation in parentheses was because I knew once everything else was figured out, I could just use matrix transformations and translations to do the position and angle.

In retrospect, I probably should’ve mentioned this before, but there’s another option that can be used, and a reason that it’s not a viable option: I could create an image of a hyperbola, have the computer display a skewed version of the image, and draw straight lines stemming from the point where the curvature becomes negligible. The reason it’s not a viable option, however, is that the resulting image would have an inconsistent strokeWeight(), and the image I need displayed needs a constant strokeWeight() (relatively speaking). This is also the same reason I can’t use the scale function here.

Forget the image - that is not a viable option for what you want to achieve.

Solving the scale problem is simple - set the stoke weight to sw1 / scale where sw1 is the stroke weight used when the scale is 1.0 and scale is the current scale you are drawing at. As the scale increases the stroke weight decreases.

As I mentioned, though, it needs to have an arbitrary width AND height. I need to be able to draw the 1/x graph, but I also need to be able to draw a vertically stretched version of that, or a horizontally stretched version of that, or a vertically compressed, etc. If you still don’t understand what I’m implying, this shape is to the 1/x graph as an ellipse is to a circle. Now, if you tried using scale(2,1) and then drew a circle, you’d notice the strokeWeight varies throughout the entire shape, being fatter in some spots and thinner in others. That’s why we often use the ellipse feature, instead of just using scale() and drawing a circle every time.

In any case, however, the main problem is: what’s more efficient, a few quadratic vertices, or a bunch of polygonal vertices?

Were you able to resolve this?

If performance really matters, then I think the best answer is: test them both and see. Also, be aware that the results might depend on your graphics mode and hardware, so test on the target if you have one.

Quadratic vertices ended up being a pretty good solution. If you want, I can send you the code I wrote.

1 Like

Thanks so much for sharing what worked for you! Yes, would love to see the solution.

Well, here it is. I should mention two things, first, I tend to make my code a bit compact, so I apologize if it’s a bit difficult to read. Second, “hyperass” stands for hyperbolic assistant, it helps the computer compute hyperbolic trig functions by multiplying the initial conditions by a few constants.

//INPUT: vertex, covertex, center, starting argument, ending argument, number of steps, x/y coordinates of the edges of the screen
//OUTPUT: displays hyperbolic arc(s) with all the desired characteristics that does not go off the screen
void hypinitiator(PVector Axx, PVector Bxx, PVector Cxx, float arg1, float arg2, float prec, float startx, float starty, float endingx, float endingy) {
  
  float[] Inters={1.0/0,1.0/0,1.0/0,1.0/0,0}; //this contains the hyperbolic argument of each intersection with the edge of the screen (with the last element containing how many intersections there are)
  
  Inters=intersgen(Axx.x, Axx.y, Bxx.x, Bxx.y, Cxx.x-startx, Cxx.y, starty, endingy, Inters);  //find all intersections with the left wall,
  Inters=intersgen(Axx.x, Axx.y, Bxx.x, Bxx.y, Cxx.x-endingx, Cxx.y, starty, endingy, Inters); //then the right wall,
  Inters=intersgen(Axx.y, Axx.x, Bxx.y, Bxx.x, Cxx.y-starty, Cxx.x, startx, endingx, Inters);  //then the bottom wall,
  Inters=intersgen(Axx.y, Axx.x, Bxx.y, Bxx.x, Cxx.y-endingy, Cxx.x, startx, endingx, Inters); //then the top wall
  
  int elem=int(Inters[4]); //elem will store that final element, telling us how many intersections there are
  
  Inters=sort(shorten(Inters)); //Inters will drop the final element and sort the rest based off the order each hyperbolic argument occurs
  
  stroke(#FF0000);
  if(elem!=0 && arg2>Inters[0] && arg1<Inters[1]) hyperbola(Axx,Bxx,Cxx,max(arg1,Inters[0]),min(arg2,Inters[1]),prec);
  if(elem==4 && arg2>Inters[2] && arg1<Inters[3]) hyperbola(Axx,Bxx,Cxx,max(arg1,Inters[2]),min(arg2,Inters[3]),prec);
}



//INPUT: vertex, covertex, center, starting argument, ending argument, number of steps
//OUTPUT: displays exactly what was specified, regardless of if it goes off screen
void hyperbola(PVector Axxx, PVector Bxxx, PVector Cxxx, float arg1, float arg2, float prec) {
  float[] hyperass={cosh((arg2-arg1)/prec),sinh((arg2-arg1)/prec)};                               //this stores some floats that will assist in performing trig calculations
  float[] hyperv={cosh(arg1),sinh(arg1),cosh(arg1+(arg2-arg1)/prec),sinh(arg1+(arg2-arg1)/prec)}; //this stores the cosh and sinh of the vertex we're currently on (as well as the next vertex)
  float storefl=(hyperass[0]-1)/hyperass[1];                                                      //this is just a simple storage float
  
  
  beginShape();
  
  vertex(Axxx.x*hyperv[0]+Bxxx.x*hyperv[1]+Cxxx.x,Axxx.y*hyperv[0]+Bxxx.y*hyperv[1]+Cxxx.y);
  for(int count=0;count<prec;count++) { //cycle through the desired number of vertices
    
    quadraticVertex(Axxx.x*(hyperv[0]+storefl*hyperv[1])+Bxxx.x*(hyperv[1]+storefl*hyperv[0])+Cxxx.x,Axxx.y*(hyperv[0]+storefl*hyperv[1])+Bxxx.y*(hyperv[1]+storefl*hyperv[0])+Cxxx.y,
                    Axxx.x*hyperv[2]+Bxxx.x*hyperv[3]+Cxxx.x,Axxx.y*hyperv[2]+Bxxx.y*hyperv[3]+Cxxx.y);
    
  }
  endShape();
}




//INPUT: x/y of vectors specific to the hyperbola, as well as the boundaries in the y direction
//OUTPUT: an array containing the cosh and sinh of all intersections we'll be outputting, as well as 
float[] intersfinder(float Axx, float Axy, float Bxx, float Bxy, float Cxx, float Cxy, float stary, float endy) {
  
  float cstore, sstore, sqstore;            //these are storage variables (cosh, sinh, sqrt)
  float[] output={1.0/0,1.0/0,1.0/0,1.0/0,0}; //this is the array we'll be outputting.  It'll contain the cosh and sinh of the intersections we'll be outputting, plus an extra number containing how many intersections we've found
  
  if(sq(Bxx)+sq(Cxx)>sq(Axx)) { //first we make sure that the hyperbola actually intersects the axis we're looking at
  
    if(sq(Axx)>sq(Bxx)&&Axx*Cxx<0) { //in this case, there are two intersections
    
      sqstore=sqrt(sq(Bxx)+sq(Cxx)-sq(Axx))/(sq(Axx)-sq(Bxx)); //calculate a square root to make computations easier later
      cstore=-Axx*Cxx/(sq(Axx)-sq(Bxx))+Bxx*sqstore; sstore=Bxx*Cxx/(sq(Axx)-sq(Bxx))-Axx*sqstore; //compute the cosh and sinh of one of the intersections
      
      if(Axy*cstore+Bxy*sstore+Cxy>stary && Axy*cstore+Bxy*sstore+Cxy<endy) { //if this intersection is within the y bounds, it's a valid addition to the list of intersections
        output[0]=cstore; output[1]=sstore; output[4]=1;
      }
      
      cstore-=2*Bxx*sqstore; sstore+=2*Axx*sqstore;
      
      if(Axy*cstore+Bxy*sstore+Cxy>stary && Axy*cstore+Bxy*sstore+Cxy<endy) { //if this intersection is within the y bounds, it's a valid addition to the list of intersections
        output[int(2*output[4])]=cstore; output[int(2*output[4]+1)]=sstore; output[4]++;
      }
    }
    
    else if(sq(Axx)<sq(Bxx)) { //in this case there is only one intersection
      sqstore=sqrt(sq(Bxx)+sq(Cxx)-sq(Axx))/(sq(Axx)-sq(Bxx)); //calculate a square root to make computations easier later
      cstore=-Axx*Cxx/(sq(Axx)-sq(Bxx))-abs(Bxx)*sqstore; sstore=Bxx*Cxx/(sq(Axx)-sq(Bxx))+(Bxx>0?1:-1)*Axx*sqstore; //compute the cosh and sinh of one of the intersections
      
      if(Axy*cstore+Bxy*sstore+Cxy>stary && Axy*cstore+Bxy*sstore+Cxy<endy) { //if this intersection is within the y bounds, it's a valid addition to the list of intersections
        output[0]=cstore; output[1]=sstore; output[4]=1;
      }
    }
    
    else if(abs(Axx)==abs(Bxx)&&Axx*Cxx<0) {
      cstore=-(sq(Axx)+sq(Cxx))/(2*Axx*Cxx); sstore=(sq(Axx)-sq(Cxx))/(2*Bxx*Cxx);
      
      if(Axy*cstore+Bxy*sstore+Cxy>stary && Axy*cstore+Bxy*sstore+Cxy<endy) { //if this intersection is within the y bounds, it's a valid addition to the list of intersections
        output[0]=cstore; output[1]=sstore; output[4]=1;
      }
    }
  }
  
  return output; //return the output array
}

//INPUT: recieves the input for a float[] instersfinder, as well as an array of the intersections we've found so far
//OUT:   prints out a modified version of the input array containing any new intersections
float[] intersgen(float Axx, float Axy, float Bxx, float Bxy, float Cxx, float Cxy, float stary, float endy, float[] Inters) {
  
  float[] arrstore = intersfinder(Axx, Axy, Bxx, Bxy, Cxx, Cxy, stary, endy);              //generate an array of all possible intersections
  if(arrstore[4]!=0) { Inters[int(Inters[4])]=log(arrstore[0]+arrstore[1]); Inters[4]++; } //if we found at least one valid intersection, we add it to the array
  if(arrstore[4]==2) { Inters[int(Inters[4])]=log(arrstore[2]+arrstore[3]); Inters[4]++; } //if we found two, add the other one to the array
  
  return Inters; //return our updated array of intersections
}

float cosh(float inp) { return (exp(inp)+exp(-inp))/2; }

float sinh(float inp) { return (exp(inp)-exp(-inp))/2; }

And here’s some example code which should demonstrate how to use the functions. Note that when the previous code refers to an “argument”, it refers to an input “t” in the parametric equation <x(t), y(t)> = <Aₓcosh(t)+Bₓsinh(t)+Cₓ, Aᵧcosh(t)+Bᵧsinh(t)+Cᵧ>.

In the example code, I generate 2 halves of a hyperbola, each extending indefinitely and with a center following the mouse. If you only want one half, then just declare the function once instead of twice like shown below.

float[] box={150,150,550,550};

void setup() {
  size(700,700);
}

void draw() {
  background(0);noFill();stroke(255);
  
  rect(box[0],box[1],box[2]-box[0],box[3]-box[1]);
  
  hypinitiator(new PVector(100,0), new PVector(0,100), new PVector(mouseX,mouseY), -1.0/0, 1.0/0, 8, box[0], box[1], box[2], box[3]);
  
  hypinitiator(new PVector(-100,0), new PVector(0,100), new PVector(mouseX,mouseY), -1.0/0, 1.0/0, 8, box[0], box[1], box[2], box[3]);
}
1 Like