Showing posts with label coding. Show all posts
Showing posts with label coding. Show all posts

Tuesday, January 24, 2023

I made a joystick. i.e. Why pay $X for something you can spend $(X+Y) building ¯\_(ツ)_/¯

I Made A Joystick.

    I was playing Microsoft Flight Simulator - gorgeous graphics! - and amidst the splendor of... someplace I've never been, I found myself wanting more physical controls. I have a nice joystick - A VKB Gladiator. It even has a little throttle. That's the only analog control other than the stick though. It's far from bad, but I just felt like trying more...


    The first place I checked was amazon. The Thrustmaster TCA "Airbus" controller caught my eye. It was expandable to 4 levers, and there was even a sale. So I bought it...
    Then I thought about checking Reddit. it's not a bad controller, but there was a lot of love for the pricier fare. Of course, I then stumble onto threads of homemade joysticks. "Yeah... I can do that..." I thought - and off I dove into Amazon again, canceling my order, and getting a project box, various switches, wires, and an Arduino Leonardo.


Are you board?

    It should be noted that not every Arduino board can work in a joystick project (at least, not normally to my knowledge). The Leonardo can, and there are a few libraries that will work with it. 

    That takes care of me telling the computer what to do - "button 5 is pressed", or "set that much throttle". Now I need to figure out how to put together the physical switches, potentiometers etc and read them in.


Building!

    I started with the project box - cutting holes for the switches - and a couple radio-control replacement sticks. These are modular joysticks for use in radio-control transmitters - like the type you'd control a model airplane with.

    This simplified the project - I didn't have to develop a gimbal. This has it all built in. It's small - a thumb stick - but this is fine for at least version 1 right?
    NB: looks like there are nicer stick modules about too. "FrSky" has an all-aluminum hall sensor model. Version 2? 😀



    I used sticker paper with a cutout to get the holes right before applying it to the inside of the project box, and using my scroll saw to cut the larger holes and slider-hole. The smaller holes were simply drilled. The texture of the box is the same as my last headset - Rhino-liner.


    
    Measure twice, and cut once. With the holes cut, everything fit quite nicely. Rhinoliner did get pretty scratched while bolting in the buttons so I make another pass with a touch of bronze.



    The underside of the panel looked like it provided plenty of room - Piece of cake right? (The cake is a lie!)
    Meanwhile, I'm messing with the Arduino code, trying to see what it will take to read the values correctly.



    I've got 16 buttons (6 up-down toggle + 4 regular) - I don't think I had enough pins - so the alternative, is to use 8 pins in a 4x4 grid. Loop through one row of pins sending a signal, then check each pin in the column for a response. If it's there, the button is on. Throw in a diode, and you can press multiple buttons without issue.


I ain't 'fraid of no ghosting...





    Turns out you cannot assume an input pin is going to be high or low - it should be pulled to that point - or else you can expect some garbage. Arduino has an input mode for this - "INPUT_PULLUP".
    
    From the grid above, pins 1-4 are read. Pins A-D are scanned.

    So I'm taking "HIGH" as normal (not pressed). If I press a button that closes a switch - say at "C-2" in the diagram, then as I scan the pins - set "C" to low, then read 1-4 - pin 2 will register low, button press. When done with scanning for "C", set "C" again to high.

    The diode setup prevents ghosting - you can smush a bunch of buttons and only those buttons show up as pressed. Without the diode, singular button presses are fine - but if I were to press A1, A2 and B1 - then B2 will show up as pressed as well.


So... many... wires...

Gaaahhh!!


    
    The most annoying bit of this was soldering all the wires in place. The board needed several wires to act as rows and columns. 4 wires for reading, 4 for the scan, and 32 for 16 buttons. Wires tend to come joined - Joined wires appear to put a lot of stress on the shorter wire. I had to resolder a few that pulled away from the board.


    Soldering the wires to the buttons after that was no picnic either!



    
    I thought I had enough space - really. 



    I had to get creative, bending the headers on the Arduino board a bit. Heatshrink was getting in the way, so I went with masking tape and tucked those wires above one of the stick modules. Not the best, but it will do.



    Yay - we have a pulse!



It doesn't look much different, but it's got a brain now! I've tried it in MS Flight Simulator, and Superflight. I still need to try it in Kerbal Space Program 😀.


Code:

#include "Joystick.h"

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_JOYSTICK, 16, 0,
   true,  true,  true, false, false, false,  true, true, false, false, false);
//    X,     Y,     Z,    RX,    RY,    RZ,  rudr, throt, accel, brake, steer);


#define JOYAX1_PIN  A1 // Arduino pin connected to VRX pin
#define JOYAX2_PIN  A2 // Arduino pin connected to VRY pin
#define JOYAX3_PIN  A4 // Arduino pin connected to VRY pin
#define JOYAX4_PIN  A3 // Arduino pin connected to VRY pin
#define JOYAX5_PIN  A0 // Arduino pin connected to VRY pin
#define HIGHPIN 13
#define CHKPIN 12

#define buttchanA 2
#define buttchanB 3
#define buttchanC 4
#define buttchanD 5

#define buttreadA 8
#define buttreadB 9
#define buttreadC 7
#define buttreadD 6

int ButtonState[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

int reRange(int inval, int inmin, int inmax, int neg){
  int range=inmax-inmin;
  float oldval=inval;
  int newval=(((oldval-inmin)*1023)/range);
  newval=min(newval,1023);
  newval=max(newval,0);
  if (neg<0) {
    newval=1023-newval;
  }
  return int(newval);
}

void setup() {
  //Serial.begin(9600);
  pinMode(JOYAX1_PIN, INPUT_PULLUP);
  pinMode(JOYAX2_PIN, INPUT_PULLUP);
  pinMode(JOYAX3_PIN, INPUT_PULLUP);
  pinMode(JOYAX4_PIN, INPUT_PULLUP);
  pinMode(JOYAX5_PIN, INPUT_PULLUP);
 
  pinMode(HIGHPIN, OUTPUT);
  digitalWrite(HIGHPIN, HIGH);
  pinMode(buttchanA, OUTPUT);
  pinMode(buttchanB, OUTPUT);
  pinMode(buttchanC, OUTPUT);
  pinMode(buttchanD, OUTPUT);
  digitalWrite(buttchanA, HIGH);
  digitalWrite(buttchanB, HIGH);
  digitalWrite(buttchanC, HIGH);
  digitalWrite(buttchanD, HIGH);
  pinMode(buttreadA, INPUT_PULLUP);
  pinMode(buttreadB, INPUT_PULLUP);
  pinMode(buttreadC, INPUT_PULLUP);
  pinMode(buttreadD, INPUT_PULLUP);
  pinMode(CHKPIN, INPUT);
  Joystick.begin();
}

void loop() {
  int joy_ax1_val = analogRead(JOYAX1_PIN); // X axis, right stick regx
  int joy_ax2_val = analogRead(JOYAX2_PIN); // y axis, right stick regy
  int joy_ax3_val = analogRead(JOYAX3_PIN); // X axis, right stick rudder
  int joy_ax4_val = analogRead(JOYAX4_PIN); // Y axis, right stick throttle
  int joy_ax5_val = analogRead(JOYAX5_PIN); // Z axis, slider for trim              

  int pinW;
  int pinR;
  for (int idxW = 0; idxW < 4; idxW++){
    if (idxW == 0) {pinW=buttchanA;}
    if (idxW == 1) {pinW=buttchanB;}
    if (idxW == 2) {pinW=buttchanC;}
    if (idxW == 3) {pinW=buttchanD;}
    digitalWrite(pinW, LOW);
    for (int idxR = 0; idxR < 4; idxR++){
      if (idxR == 0) {pinR=buttreadA;}
      if (idxR == 1) {pinR=buttreadB;}
      if (idxR == 2) {pinR=buttreadC;}
      if (idxR == 3) {pinR=buttreadD;}
      int ChkButtonState=!digitalRead(pinR);
      int butnum=(idxW*4)+idxR;
      if (ChkButtonState != ButtonState[butnum]){
        Joystick.setButton(butnum, ChkButtonState);
        ButtonState[butnum] = ChkButtonState;
      }
    }
    digitalWrite(pinW, HIGH);
  }

  joy_ax1_val=reRange(joy_ax1_val,92,977,1);
  joy_ax2_val=reRange(joy_ax2_val,64,928,1);
  joy_ax3_val=reRange(joy_ax3_val,88,985,1);
  joy_ax4_val=reRange(joy_ax4_val,47,920,-1);
  joy_ax5_val=reRange(joy_ax5_val,15,1023,-1);
  /*Serial.println( String(joy_ax1_val)+" "+
                  String(joy_ax2_val)+" "+
                  String(joy_ax3_val)+" "+
                  String(joy_ax4_val)+" "+
                  String(joy_ax5_val)+" ");
  */
  Joystick.setXAxis(joy_ax1_val);
  Joystick.setYAxis(joy_ax2_val);
  Joystick.setRudder(joy_ax3_val);
  Joystick.setThrottle(joy_ax4_val);
  Joystick.setZAxis(joy_ax5_val);

  if (digitalRead(CHKPIN) == LOW){
    Joystick.end();
  }
  delay(5);
}


    Ignore the "HIGHPIN" and "CHKPIN" - those were there for troubleshooting. The "reRange" function may stand out as odd. The purpose of that is to calibrate the analog axis somewhat. It's not handling centering really, but it will invert an axis if needed and spread the values it does read from 0-1023. 
    e.g. A stick may read from 90-950, instead of the expected 0-1023. Taking the read value, subtracting the minimum value, then multiplying by the range I want (1023), and dividing by the range I have (950-90) gives me an effective 0-1023 analog control. 



    I'm pleased with how this turned out. It matches my headset - version 11 🎧. Two 40mm drivers in each ear, David Clark headband... The headphone builds improved with each version. Perhaps this would apply to the controller as well? 😁











Friday, September 9, 2016

Arduino is fun - great for custom Radio Control signal handling

I've been having some fun with arduino. They're quite cheap boards, with arduino nano coming in at ~$3 per board. That's $3 - THREE. That's pretty awesome for a small amount of custom processing power. Unlike a board like the Raspberry PI which has to run a full Linux kernel, this just runs whatever script you write. As such, the timings are very consistent - it's excellent for handling servo PWN signals where you need to measure pulses from ~1000-2000 microseconds accurately. From my experience the resolution was 4μs.

This makes it excellent for managing radiocontrol PWM signals in complex RC vehicles. 



That wrapped up red component in my latest RC vehicle (another hobby, I build RC cars...) is an arduino that reads in steering and another channel and processes it to output a new signal that lets me choose if I want the truck to have 4 wheel steering, or crabwalk (both wheels point the same way making it go mostly sideways).




Definitely one of the more interesting vehicles I've built, and a decent summer project (though I did start last year wrecking the transmission pushing myself in a chair). 

This actually started out with me receiving a non-working unit for handling quadsteer that I had purchased on amazon. When using the unit, the servos lacked power, wouldn't center, and were slow. I used an oscilloscope to diagnose the signal and saw the pulses were 40 milliseconds apart. For Radio control PWM signals, the leading edge should be 20 milliseconds apart. What was essentially happening was the server would get a signal and act on it for 20 milliseconds, and then not do anything for the next 20 milliseconds.

So I turned to arduino nano in the hopes to making my own. Below I've setup that arduino nano on the breadboard to mix signals as I'd like. Each square on the oscilloscope is 10 ms, so my working prototype is showing the proper waveform. This is what was wrapped up in electrical tape in the first picture.




Below is the code I wrote to handle this. The idea is to read two channels - the steering and a spare channel to know if to invert the steering between front and rear. Since I'm reading the time of the pulse, I need a little math to normalize 1000-2000μs to -1 to 1 and back again. This lets me multiply the channels so I can then smoothly transition between crabwalk/quadsteer. 

Should be noted we're using interrupts to gather input channel data. This is a non-blocking method for gathering the input. I just requires us to determine the time of the pulse by subtracting the time the clock had at the signal's leading edge.

Edit: 20200907 - code had some bugs, turning off/on interrupts wasn't needed and led to jerky servo output. Tidied it up a bit.


#include <Servo.h> 

volatile unsigned long leadingedge1;
volatile unsigned long leadingedge2;
volatile int pulsetime1; 
volatile int pulsetime2;


//declare servo pins
int servoin1  = 2; // pin 2 - steering
int servoin2  = 3; // pin 3 - inversion channel
int servoout1 = 9; // output read servo is pin 9, front is pin 2
Servo rearservo;


int ch1_hist[3];
int ch2_hist[3];

int execute=0;
long RServo=1500; //value to write to rear servo

void setup()
{
  pinMode(servoin1, INPUT);      // sets the digital pin 1 as input
  pinMode(servoin2, INPUT);      // sets the digital pin 1 as input
  pinMode(servoout1, OUTPUT);    // sets the digital pin 9 as output
//  pinMode(servoout2, OUTPUT);    // sets the digital pin 9 as output
  leadingedge1 = 0;
  leadingedge2 = 0;
  pulsetime1 = 1500;
  pulsetime2 = 1500;
  attachInterrupt(0, chan1, CHANGE);
  attachInterrupt(1, chan2, CHANGE);
  rearservo.attach(servoout1);
  Serial.begin(115200);        // for debugging
}

int amode(int a[]){ //mode or average of an array - use to smooth glitches
  if (a[0]==a[1]) { return a[0]; }
  if (a[0]==a[2]) { return a[0]; }
  if (a[1]==a[2]) { return a[1]; }
  return (a[0]+a[1]+a[2])/3;
}

int pusharr(int a[],int pushval) {
  a[2]=a[1];
  a[1]=a[0];
  a[0]=pushval;
  return 0;
}

void dostuff(){
  execute=0;
  RServo = 1500+((((long)pulsetime1-1500)*((long)pulsetime2-1500))/500);
  if (RServo > 2050) {
    RServo=2050;
  } else {
    if (RServo < 950) {
      RServo=950;
    }
  }
  Serial.print("P1.");
  Serial.print(pulsetime1);
  Serial.print("--P2.");
  Serial.print(pulsetime2);
  Serial.print("--SRear.");
  Serial.print(RServo);
  Serial.print("\n" );
  execute=1;
}

void chan1()
{
  if(digitalRead(servoin1) == HIGH)
  {
    leadingedge1 = micros();
  } else {
    if (leadingedge1 > 0)
    {
      pulsetime1 = ((volatile long)micros() - leadingedge1)-14;
      //14 us added from other operations? center needed normalizing to 1500
      if ((pulsetime1 > 800 and pulsetime1 < 2200)) {pusharr(ch1_hist,pulsetime1);}
      leadingedge1 = 0;
      pulsetime1= amode(ch1_hist);
    }
  }
}

void chan2()
{
  if(digitalRead(servoin2) == HIGH)
  {
    leadingedge2 = micros();
  }
  else
  {
    if(leadingedge2 > 0)
    {
      pulsetime2 = ((volatile long)micros() - leadingedge2)-14;
      if ((pulsetime2 > 800 and pulsetime2 < 2200)) {pusharr(ch2_hist,pulsetime2);}
      leadingedge2 = 0;
      pulsetime2= amode(ch2_hist);
    }
  }
}

void loop()
{  
  delay(2);  //delay is non blocking
  dostuff();  //do stuff after the receiver has sent all pulses
  if (execute==1) { rearservo.write(RServo);}
}

Sunday, April 12, 2015

Mandelbrot Set Rendering

Benoit Mandelbrot was a Polish-born mathematician. His most well known work is in the field of fractals. He came up with a function describing a set of complex numbers called the Mandelbrot Set.
The Mandlebrot set describes a set of numbers "c" such that the sequence (c1, c2 .... cx) where cn+1 = (cn*cn)+cn does not approach infinity.
So you're looking at c, c²+c, (c²+c)²+c, ((c²+c)²+c)²+c...

A complex number has two parts. It's a sum of real an imaginary components. Let's say we're looking at A + Bi.
A is the real component.
Bi is the imaginary component.
'i' is a symbol representing the √-1 (square root of -1)
i.e., (Bi)² would be -B².

Mandelbrot's equation for describing the model is z'=z²+c.
Let z be our A + Bi from above.
The next element of the sequence is 
If z=(A+Bi)² then z=A²-B²+2*A*Bi


If plotting, we can us X, Y in place of A, and B.
so, z=X²-Y²+2*X*Y(i)
We have real and imaginary components here.
So Xnew=X²-Y², and Ynew=2*X*Y
the "c" is the starting point for Mandelbrot's equation.
Z=Z²+c, so we need to add X0 and Y0 to the above:

Xnew=X²-Y²+X0
Ynew=2*X*Y+Y0
And from here, we keep iterating on this till we hit set limits to number of iterations, or if X/Y are sufficiently large.

So, for a point X, Y,
Repeat the following until x0²+y0²>4 or until set max iterations

Until (max iterations, or x0²+y0²>4)
  x0=0 and y0=0
  x1=x0*x0-y0*y0+X
  y1=2*x0*y0+Y
  x0=x1
  y0=y1
Color point X,Y based on iterations.




Point X,Y is then associated with the number of iterations to break out of that loop.

Using this we can construct a basic Mandelbrot generator. I did this using Javascript, since it's easy to use any browser to display a canvas element. This works in Firefox and Chrome (has worked for a while) and I've tested it in IE11 and it works there too.




<!DOCTYPE HTML>
<html>
<script language="javascript">
  function rgbToHex(r, g, b) { //thanks stackoverflow
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  }
  function putpixel(x,y,r,g,b,ctx){
    ctx.fillStyle = rgbToHex(r,g,b);
    ctx.fillRect(x,y,1,1);
  }
  function paint(cnvname){
    mcanvas = document.getElementById(cnvname);
    ctx = mcanvas.getContext('2d');
    ctx.clearRect(0, 0, 799, 599);
    for (x = 0; x < 799; x++) {
      for (y = 0; y < 599; y++) {
        tx=(x-400)/(200); // tx is "translated x" for graph centering"
        ty=(300-y)/(200); // ty is "translated y" for graph centering"
        xtmp=0;
        ytmp=0;
        xtmp2=0;
        iter=0
        maxiter=1000;
        for (; (iter<maxiter) && (xtmp*xtmp+ytmp*ytmp < 4);) {
          xtmp2=xtmp*xtmp-ytmp*ytmp+tx;
          ytmp=2*xtmp*ytmp+ty;
          xtmp=xtmp2;
          iter++;
        }
        contrast=0.15
        color=Math.floor( (Math.pow(iter,contrast)*255)/Math.pow(maxiter,contrast));
        putpixel(x,y,0,255-color,255-color,ctx);
      }
    }
  }
</script>
<body onload='paint("mcanvas")'>
  <div>
    <canvas id="mcanvas" width="800" height="600"></canvas>
  </div>
</body>
</html>


This is a simple bit of javascript that iterates over the pixels of the canvas created in the body. It applies the function described earlier to determine the max number of iterations to the set.



Pretty right? Of course there's much more to mandelbrot than just the bulb.
We can zoom in on a portion of this by applying a zoom and x/y offset to the above code.

We can add a zoom and offset feature to explore the set.


  for (x = 0; x < 799; x++) {
    for (y = 0; y < 599; y++) {
      zoom=20; xoff=0.6; yoff=0.6;
      tx=-xoff+(x-400)/(zoom*200); // tx is "translated x" for graph centering"
      ty=yoff+(300-y)/(zoom*200); // ty is "translated y" for graph centering"

The above change will produce the following:

This is zoomed in 20x onto the small bulb in the top left portion of the largest bulb. Note the pattern replicates, and there are several tiny bulbs that look a lot like the main one (with slight variations).

Now the fun part - we can color based on x, y or iterations. Apply a sinusoidal function to iterations for red, blue and green, and you can have a never ending palette of color.

e.g.
From the contrast and color line:

//contrast=0.25
//color=Math.floor( (Math.pow(iter,contrast)*255)/Math.pow(maxiter,contrast));
cycle=(maxiter)/(3.14159265359*100);
cr=Math.floor(128+127*Math.sin(0.00+iter/(cycle*1.5000)));
cg=Math.floor(128+127*Math.sin(1.57+iter/(cycle*1.6000)));
cb=Math.floor(128+127*Math.sin(3.14+iter/(cycle*1.5666)));
putpixel(x,y,cr,cg,cb,ctx);

So, what are we saying? A cycle is 2*pi. We're dividing the iterations by 100*pi, so we're looking at at 50 cycles. Red, green and blue values are sinusoidal cycles with different start points - red starts at 128+127*sin(0) (so 128, and increasing). Green is 128+127*sin(1.57) (so 255, 1.57 being half pi, sin(pi/2) is 1) - i.e. max green and heading down to 0. Blue is starting at 128+sin(pi), which means middle intensity and getting darker.
Each sinusoidal cycle has it's own multiplier so each cycle has it's own rate, giving many more colors ans each cycle creates a new palette.

Here's the result:


We're looking at integers since we're only counting max iterations. We can smoothen this by looking at how far past our boundary condition each iteration went.

My script was based on methods described in wikipedia.


log2=Math.log(2)
if (iter<maxiter){ //smoother transitions
  zn=Math.sqrt(xtmp*xtmp+ytmp*ytmp);
  zl=Math.log(Math.log(zn)/log2)/log2;
  iter+=1-zl;
} 






Now consider the interior of the bulb. It's what happens when the iterations have been maxed. We can color the interior by using the same palette. I haven't quite figured out the how to map distance, but we can use the existing xtmp and ytmp variables. Since iterating more doesn't get us greater than the boundary condition, we can assume xtmp and ytmp are small. 

What I tried was several permutations of setting iter to 1/xtmp and 1/ytmp. I settled on using iter=Math.log( (1/(xtmp*xtmp+ytmp*ytmp)) )/(log2);
The idea is that if my values aren't going to break out of the boundary, then they're converging. So I'm using the inverse of the values to dictate what they will be against the existing palette. Not all sub-bulbs show this though.


Here's the overall code for this basic mandelbrot renderer...



<!DOCTYPE HTML>
<html>
<script language="javascript">
  function rgbToHex(r, g, b) { //thanks stackoverflow
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  }
  function putpixel(x,y,r,g,b,ctx){
    ctx.fillStyle = rgbToHex(r,g,b);
    ctx.fillRect(x,y,1,1);
  }
  function paint(cnvname){
    mcanvas = document.getElementById(cnvname);
    ctx = mcanvas.getContext('2d');
    ctx.clearRect(0, 0, 799, 599);
    log2=Math.log(2)
    for (x = 0; x < 799; x++) {
      for (y = 0; y < 599; y++) {
        zoom=20; xoff=0.6; yoff=0.6;
        tx=-xoff+(x-400)/(zoom*200); // tx is "translated x" for graph centering"
        ty=yoff+(300-y)/(zoom*200); // ty is "translated y" for graph centering"
        xtmp=0;
        ytmp=0;
        xtmp2=0;
        iter=0
        maxiter=1000; // increase with larger zoom values
        totes=0;
        for (; (iter<maxiter) && (xtmp*xtmp+ytmp*ytmp < 4);) {
          xtmp2=xtmp*xtmp-ytmp*ytmp+tx;
          ytmp=2*xtmp*ytmp+ty;
          xtmp=xtmp2;
          iter++;
        }
        if (iter<maxiter){ //smoother transitions
          zn=Math.sqrt(xtmp*xtmp+ytmp*ytmp);
          zl=Math.log(Math.log(zn)/log2)/log2;
          iter+=1-zl;
        } else { // for internal color of Mandelbulb
          iter=Math.log( (1/(xtmp*xtmp+ytmp*ytmp)) )/(log2);
        }
        cycle=(maxiter)/(3.14159265359*100); //50 cycles from 0-maxiter
        cr=Math.floor(128+127*Math.sin(0.00+iter/(cycle*1.5000)));
        cg=Math.floor(128+127*Math.sin(1.57+iter/(cycle*1.6000)));
        cb=Math.floor(128+127*Math.sin(3.14+iter/(cycle*1.5666)));
        putpixel(x,y,cr,cg,cb,ctx);
      }
    }
  }
</script>
<body onload='paint("mcanvas")'>
  <div>
    <canvas id="mcanvas" width="800" height="600"></canvas>
  </div>
</body>
</html>


Manipulation of the color palette constants, zoom, offsets, and even the equation can produce some beautiul results.

For example,
Set the initial xtmp to tx and ytmp to ty.

change:
xtmp2=xtmp*xtmp-ytmp*ytmp+tx;
ytmp=2*xtmp*ytmp+ty;


to:
xtmp2=xtmp*xtmp-ytmp*ytmp-0.32;
ytmp=2*xtmp*ytmp+0.6575;


Now you're making a Julia plot.
This is another fractal you can zoom in continuously.

Let's look at zooming in on a portion of the Mandelbrot set.
zoom=1.00; xoff=0; yoff=0;


zoom=5; xoff=0.7348; yoff=0.1798;

zoom=25; xoff=0.8; yoff=0.2;



zoom=125; xoff=0.79; yoff=0.158;


zoom=625; xoff=0.79; yoff=0.162;

zoom=3125; xoff=0.7893; yoff=0.163;


At this point we need to increase the maxiter constant. I raised this to 10000.
zoom=9125; xoff=0.78937; yoff=0.16307;


 zoom=15625; xoff=0.78940; yoff=0.163055;


zoom=78225; xoff=0.78940; yoff=0.163055;
Note the zoom - we're at almost 80000x and as you can see there's a lot more structures here!