Best way to emulate a rotary encoder wheel?

Hello all. Trying to find the easiest way to capture user input on an encoder wheel. In theory, I would want to track the clockwise / counterclockwise mouse movement within the area representing the encoder. So instead of just moving a slider left to right or up and down, the user can actually rotate their mouse around a center point.

Thanks all!

2 Likes

Please look at the GUI libraries

Such as G4P or controlP5

They should have this

I was hoping for a way without a Library - but I guess this does answer my question on the “easiest” way…

Edit: Also, neither of these libraries have the kind of “knob” or “encoder” I am trying to make.

or something like this

3 Likes

I think this looks more promising!

Thank you!

1 Like

Hello,

Thanks for asking this! Had some fun with it…

Sharing my initial steps…

I did not look at other examples.

  1. Create a PGraphic image. I used random circles but this could be your encoded wheel or anything.
  2. Move the mouse over the canvas to detect colors.
  3. Do something with the detected colors.
  4. The rest is up to your creativity and imagination. I used the same concepts in the past to do something similar.

In my example the mouse location turned red or green depending on color it detected.
You can leave the mouse location and rotate image with key ‘4’ and ‘6’.
You can create a new image with key ‘5’.

Code is not commented so you can go through it.
Ask if you have questions.

Example Code < Expand this!
// Rotary Encoder 
// v1.0.0
// GLV 2020-06-20

// Step one to creating a rotary encoder simulation

PGraphics pg;
color dot;
float angle = 0;

void setup() 
	{
  size(400, 400);	
  pg = createGraphics(300, 300);
  ranCircles();
  }

void draw() 
	{
  background(255);
  push();
  translate(width/2, height/2);
  rotate(angle);
  imageMode(CENTER);
  image(pg, 0, 0);
  pop();	

  int c = get(mouseX, mouseY);
  
  if (c == color(0))
    {
    dot = color(0, 255, 0);
    }
  else
    {
    dot = color(255, 0, 0);  
    }
  noStroke();
  fill(dot);
  circle(mouseX, mouseY, 10);
  }
  
void keyPressed()
  {
  if (key == '5')
    ranCircles();
  if (key == '6')
    angle += TAU/100;
  if (key == '4')
    angle -= TAU/100;
  }

void ranCircles()
  {
  pg.beginDraw();
  pg.imageMode(CENTER);
  pg.background(color(255, 0));
  pg.noStroke();
  pg.fill(0);
  for(int i = 0; i<= 50; i++)
    {
    float theta = random(0, TAU);
    float r = random(0, 140);
    float x = r*cos(theta) + 150;
    float y = r*sin(theta) + 150;
    pg.circle(x, y, 20);
    }
  pg.endDraw();  
  }

I took the code above, added a rotating arm and detected color at a point on the arm as it rotated.

screenX() and screenY() were used in place of mouseX and mouseY.
Just be careful to detect color before you draw over the point you are detecting.

https://processing.org/reference/screenX_.html

:)

rot01

I also used the image from the Wikipedia page directly:

image

See https://processing.org/reference/PImage.html to load and display images.

References:

2 Likes

Do you want an absolute encoder, which gives an angular location value in some range (0-360) – or do you want an incremental encoder which produces an offset value (e.g. 0.1 or -0.05) each frame depending on how fast the mouse moved – and optionally sums those (so you could spin the dial on and on? Or something else – for example, an absolute encoder which rolls over to higher and higher numbers?

1 Like

Wow! Thanks all, Im glad I checked back up on this. I will look at the code above - but to answer your question… I think an Incremental Encoder would be preferred!

This can be tricky for a few reasons –

  1. PVector.angleBetween is unsigned
  2. PVector.angleBetween doesn’t return the minimum arc
  3. display components like arc() have similar behaviors – they are rooted and directed

Here is one example of a solution.

/**
 * IncrementalEncoderDemo
 * Jeremy Douglass 2020-06-23 - Processing 3.5.4
 * https://discourse.processing.org/t/best-way-to-emulate-a-rotary-encoder-wheel/21982/7
 */
IncrementalEncoder ie;

void setup() {
  size(400, 400);
  ie = new IncrementalEncoder(width/2.0, height/2.0, 100);
}

void draw() {
  background(192);
  ie.update(pmouseX, pmouseY, mouseX, mouseY);
  ie.render();
}

class IncrementalEncoder {
  float val;
  float sum;
  PVector center;
  float size;
  float head;
  
  IncrementalEncoder(float w, float h, float size) {
    center = new PVector(w, h);
    this.size = size;
  }
  
  IncrementalEncoder(PVector center, float size) {
    this.center = center;
    this.size = size;
  }
  
  void update(float oldx, float oldy, float newx, float newy) {
    update(new PVector(oldx, oldy), new PVector(newx, newy));
  }

  void update(PVector pold, PVector pnew) {
    float angleOld = PVector.sub(pold, center).heading();
    float angleNew = PVector.sub(pnew, center).heading();
    head = angleNew;
    update(angleOld, angleNew);
  }

  private void update(float angleOld, float angleNew) {
    float diff = angleNew-angleOld;
    if (diff>PI) {
      diff = TWO_PI - diff;
    } else if (diff<-PI) {
      diff = TWO_PI + diff;
    }
    this.val = diff;
    this.sum += diff;
  }

  /**
   * Draw a rotary dial. An indicator points at the current position.
   * An arc highlights the active change and direction (red / blue).
   */
  void render() {
    pushMatrix();
    pushStyle();
    translate(center.x, center.y);
    rotate(head);
    line(0, 0, 50, 0);
    if (val<0) {
      line(0, 0, 50, 0);
      rotate(val);
      fill(color(0, 0, 255));
      arc(0, 0, 100, 100, 0, -val*3);
    }
    if (val>0) {
      fill(color(255, 0, 0));
      arc(0, 0, 100, 100, 0, val);
    }
    noFill();
    ellipse(0, 0, 100, 100);
    popMatrix();
    fill(0);
    textAlign(CENTER);
    text(val, center.x-size/2,center.y+size/2+10);
    text(sum, center.x+size/2,center.y+size/2+10);
    popStyle();
  }
}

Note that it is still hybrid – the knob has an absolute indicator line on it, which is absolutely oriented with “head” based on the last mouse position. If you wanted to make it a pure rotary encoder, it would be non-oriented – drop the head value and indicator line, drop the oriented display of difference with arc, make update(angleOld, angleNew) public, and don’t worry that the sum will drift from the orientation, because there is no orientation.

Screen Shot 2020-06-23 at 11.50.42 AM

2 Likes

revisiting this. This solution is my current implementation, however I am finding that it is a little difficult to stop on a specific number. I tried using modulus on the sum to mimic a “tick”, and only outputting at 0. However, this doesn’t really work well.

Is there another way you could go about only outputting a “tick” every “x” degrees rotated?

Thanks!

When you say “a specific number” what kind of range and sensitivity are you looking for? Are you trying to dial between 0 and 10, or -100 and +100 – are these numbers integers? Do you want the knob to turn a quarter turn for a “tick”, or a tenth of a turn?

That i am not quite sure yet. I was hoping to try a couple different settings and see how it feels. My first gut feeling says somewhere between 5 and 15 degrees.

To clarify, the way I have this working looks like this:

if (diff > 0) { //CLOCKWISE
    encoderOutput = 1;
} else if (diff < 0) { //COUNTER-CLOCKWISE
    encoderOutput = -1;
} else {
    encoderOutput = 0;
}

//Listener event here

Where the listener event is simply sending which way the encoder is rotating for the OSC message.

What is “diff”? Is that myEncoder.sum? Or is it float diff = myEncoder.sum - previousSum … ?

and then under that my code:

if (diff > 0) { //CLOCKWISE
    encoderOutput = 1;
} else if (diff < 0) { //COUNTER-CLOCKWISE
    encoderOutput = -1;
} else {
    encoderOutput = 0;
}

//Listener event here

Okay, so you are generating an OSC event anytime the wheel turns CW or CCW, no matter how little. But you want a minimum mount of turn before sending an event. Is that right?

1 Like

Indeed! Sorry if I wasn’t great at making that clear!

And the events you send – are they just “+1” or “-1” events, or do they contain the myencoder.val or the myencoder.sum value?

They are simply +1 and -1.

A nice example was provided for you! Thanks @jeremydouglass.

I tinkered with his solution and was able to get this to work with 1 deg steps simply by:

  • Convert heading to degrees
  • Cast that to an integer
  • Convert degrees to radians
  • Use that as heading.

It was not a big leap to do this for any increment using the map() function.

I am getting nice steps moving around arm around with mouse.

This was just an exploration for me.

I will not be sharing code.

:)

1 Like

Another way of approaching this problem is to create a Ticker class and feed the raw IncrementalEncoder values into it.

class Ticker {
  float pos;
  float lastTick;
  float step;
  
  Ticker(float start, float step) {
    this.pos = start;
    this.lastTick = start;
    this.step = step;
  }
  
  int update(int newpos) {

    // updates pos with newpos
    // and updates lastTick if there has been a tick;
    // returns 0, -1, or 1

  }
}

Now add a Ticker to the example sketch above, and call myTicker.update(ie.sum); in draw. If the call returns a 1 or -1, send that via your Listener.

One subtle difference is that @glv’s suggestion to scale the output e.g. with map also needs a past comparison value in order to trigger events, but it will stay oriented – the click position will never drift. The Ticker class I suggested you is accumulating floats, so it will slowly drift over time – the knob indicator won’t always click at exactly the same angle after a long period of use. That may not matter to you, but if it does then you might want to extend the IncrementalEncoder class and add the mapping function directly to it.

2 Likes