Accumulate rotation with snapping

For a few weeks I have been having trouble finding a clean way to measure the accumulated angle of a circular drag while snapping the result to seconds on a clock.

This is as close as I have come but it is still not working smoothly. The problem I’m having in this sketch is that the seconds snap to their angle with `round()`, and I wanted the accumulated minutes to snap with `floor()`.

After the second gets to 59.5, the marker snaps to 0, but the accumulated minute doesn’t turn to 1 until the drag goes past 0. So the accumulated drag time shows 00:00 from 00:59.5 to 01:00.

I fully understand why this isn’t working because I have spent a couple of weeks picking apart the problem, but I am hoping someone could suggest another way.

I have been able to make it work 100% as-expected with values that are not snapped to the nearest second, i.e. raw radians. But I can’t see how to do it with all of the values snapped to the nearest `TWO_PI/60`

EDIT: I added accSec to the variable declarations in the code.

``````
float r, offset; // clock radius
PVector c; // clock center
PFont mono, sans;
int textH = 14;
color cRed = color(200, 35, 40);
color cRedT = color(200, 35, 40, 127);
color cYellow = color(240, 210, 50);
color cWhite = color(200, 220, 200);
color cGreen = color(50, 200, 65);
// mouse
PVector m, pm, mo, po;
boolean mouseDrag, cwDrag, ccwDrag, measuring, measurementBegan;

float snapA, dragA, dragASnap, accDragA;
float startA, startASnap, endASnap, accASnap;
int startSec, endSec, accSec, accMin;
void setup() {
size(400, 600);
//pixelDensity(2);
mono = createFont("Menlo", 14);
sans = createFont("Helvetica", 14);
r = width/2 - 40;
c = new PVector(width/2, height-r-40);
offset = -PI/2;
m = new PVector();
pm = new PVector();
snapA = PI*2/60;
}
void draw() {
background(127);
checkMouse();

dragA %= TWO_PI;

dragASnap = snapRound(dragA);
dragASnap %= TWO_PI;

if (mousePressed && !measurementBegan) {
measurementBegan = true;
startA = dragA;
startASnap = dragASnap;
endASnap = dragASnap;
accDragA = 0;
}

startSec = round(startASnap/snapA);
endSec = round(endASnap/snapA);

if (measuring) {
endASnap = dragASnap;
float dragAngDiff = PVector.angleBetween(mo, po);
if (ccwDrag) dragAngDiff = -dragAngDiff;
accDragA += dragAngDiff;
}

accASnap = snapRound(accDragA-(accDragA-dragA));

accSec = (endSec-startSec>0) ? endSec-startSec : endSec-startSec+60;
accSec %= 60;

accMin = floor(accDragA/TWO_PI);

showNeedle(dragA, cWhite, 3);

showNeedle(startA, cGreen, 1);
showMarker(startASnap, 10, cGreen);

showNeedle(endASnap, cRed, 1);
showMarker(endASnap, 10, cRed);

showMarker(accASnap, 7, cYellow);

showFace();
showDragState();
showTextRight();
}
void mouseDragged() {
measuring = true;
}
void showTextRight() {
String dragAStr = "dragA: " + nf(dragA, 2, 3);
String dragASnapStr = "dragASnap: " + nf(dragASnap, 2, 3);
String accDragAStr = "accDragA: " + nf(accDragA, 2, 3);
String startSecStr = "startSec: " + nf(startSec, 2);
String endSecStr = "endSec: " + nf(endSec, 2);
String accSecStr = "accSec: " + nf(accSec, 2);
String accMinStr = "accMin: " + nf(accMin, 2);
String[] strings = { dragAStr, dragASnapStr, startSecStr, endSecStr, accDragAStr, accSecStr, accMinStr };
textFont(mono);
textSize(textH);
textAlign(RIGHT, TOP);
fill(0);
for (int i=0; i<strings.length; i++) {
text(strings[i], width-5, 5+i*textH);
}
}
float snapRound(float a) {
a /= snapA;
a = round(a);
a *= snapA;
return a;
}
void checkMouse() {
m.set(mouseX, mouseY);
mo = PVector.sub(m, c);
mo.rotate(-offset);

pm.set(pmouseX, pmouseY);
po = PVector.sub(pm, c);
po.rotate(-offset);

mouseDrag = PVector.angleBetween(mo, po) > 0;

cross = mo.cross(po).z;
cwDrag = cross < 0;
ccwDrag = cross > 0;

if (!mousePressed) {
measuring = false;
measurementBegan = false;
}
}
void showDragState() {
if (measuring && !mouseDrag) {
fill(cYellow);
} else if (measuring && cwDrag) {
fill(50, 200, 65);
} else if (measuring && ccwDrag) {
fill(cRed);
} else {
fill(50);
}
stroke(0);
strokeWeight(1);
circleCR(c, 3);
}
void showDragA() {
strokeWeight(3);
stroke(cWhite);
lineStartRA(c, width/2 - 5, dragA + offset);
}
void showMarker(float angle, int sz, color clr) {
strokeWeight(1);
stroke(clr);
color fillC = clr;
PVector tickLoc = new PVector();
angle += offset;
float x = cos(angle);
float y = sin(angle);
tickLoc.set(x, y);
tickLoc.mult(r);
drawTick(tickLoc, sz, fillC);
}
void showNeedle(float angle, color clr, int sz) {
strokeWeight(sz);
stroke(clr);
angle += offset;
lineStartRA(c, width/2 - 10, angle);
}
void showFace() {
PVector tickLoc = new PVector();
PVector textLoc = new PVector();
float faceR = r;
color fillC = 75;
float scaleActive = 9;
float scaleStart = 5;
float scaleNormal = 3;
float scale = 1;

strokeWeight(1);
noStroke();
fill(127, 200);
circleCR(c, faceR);

for (int i=0; i<360; i+=6) {
angle += offset;
float x = cos(angle);
float y = sin(angle);

tickLoc.set(x, y);
tickLoc.mult(faceR);

textLoc.set(x, y);
textLoc.mult(faceR+20);

boolean active = snapRound(angle) == snapRound(dragASnap+offset);

scale = scaleNormal;
fillC = color(175);

if (i%30==0) {
int hVal = (i/30+11)%12+1;
String hTextStr = nf(hVal, 1);
fillC = color(50);
stroke(0);
drawTick(tickLoc, scale, fillC);
hourText(hTextStr, textLoc);
} else {
stroke(0);
drawTick(tickLoc, scale, fillC);
}
}
}
void drawTick(PVector loc, float size, color clr) {
stroke(0);
fill(clr);
circleCR(loc, size);
}
void hourText(String s, PVector loc) {
textFont(sans, 18);
textAlign(CENTER, CENTER);
fill(50);
text(s, loc.x, loc.y);
}
void lineStartRA(PVector start, float r, float a) {
PVector end = new PVector(cos(a), sin(a));
end.mult(r);
linePts(start, end);
}
void linePts(PVector start, PVector end) {
line(start.x, start.y, end.x, end.y);
}
void circleCR(PVector loc, float radius) {
}
``````
1 Like

Have a separate variable that tracks minute according to round instead of floor, and track seconds using floor, im sure there should then be a way to concat them, ie convert the mins to secs and add them to you second variable

This is how I’m getting the minute value:

If I use round here, it changes to 1 after half a rotation

The modulus function might help, ie take modulus 60 of the seconds and increment a min variable if its 0. In this scenario you would need 3 variables, one which starts at 0 which you will increment conditionally and another that would track seconds and floors it then another that tracks seconds takes mod 60 and if 0 increments the first variable

There is floor and ceil

Take both

Measure the distance to your initial value

Result is the nearer value : floor or ceil

2 Likes

I don’t see what you mean.

Possibly to try to use a separate function once seconds reaches 59

if(if sec<59.5){
floor(sec);
}
else{
ceiling(floor);
}

but then again that might cause a problem where the minute changes 1/2 a sec too early, in which case just have a second “sec” variable which you use ceiling on which your minute variable will be tied too. you might need to change your sec variable though to extend beyond 59.

Okay, I tried to organize things a bit better to see what is going on. I can see what you mean about taking both ceil and floor, but I’m not sure how to take your suggestion about measuring the distance to the initial value so I can use that to determine which to use based on the direction of the drag.

I did try basing the decision purely on the direction of the drag but it starts wavering when the drag is barely moving or reversed and resulted in a lot of unreliable measurements.

``````// clock face
PVector c;
float r;
int divisions;

// mouse
PVector m, pm, mo, po;
float offset = -HALF_PI, heading, cross;
boolean cwDrag, ccwDrag, measuring, measureBegan;

// math
float snapAngle, dragAngle, dragAngleSnap;
float startAngleOffset, startAngleSnap;
float accDragAngle, accAngleSnap;
int startSec, endSec, accSec, accMin;

void setup() {
size(400, 600);
//pixelDensity(2);
r = width/2 - 70;
c = new PVector(width/2, height - r - 70);
m = new PVector();
pm = new PVector();
divisions = 12;
snapAngle = TWO_PI/divisions;
}
void draw() {
background(127);
checkMouse();

dragAngleSnap = snapRound(dragAngle);

if (mousePressed && !measureBegan) {
measureBegan = true;

startAngleSnap = dragAngleSnap;
startAngleOffset = startAngleSnap - dragAngle;

startSec = round(startAngleSnap/snapAngle);
startSec *= 60/divisions;

endSec = startSec;

accDragAngle = 0;
accDragAngle -= startAngleOffset;

accAngleSnap = 0;
accSec = 0;
accMin = 0;
}

if (measuring) {
endSec = round(dragAngleSnap/snapAngle);
endSec *= 60/divisions;

float dragAngleDiff = PVector.angleBetween(mo, po);
if (ccwDrag) dragAngleDiff = -dragAngleDiff;

accDragAngle += dragAngleDiff;
accAngleSnap = snapRound(accDragAngle);

accSec = round(accAngleSnap/snapAngle);
accSec *= 60/divisions;

float accMinFloor = floor(accSec/60);
float accMinCeil = ceil(accSec/60);

accMin = accMinFloor;
}

showNeedles();
showFace();
showTextLeft();
showTextRight();
}
void mouseDragged() {
measuring = true;
}
void showTextLeft() {
String startAngleSnapStr = nfs(startAngleSnap, 2, 4) + " " + "startAngleSnap";
String dragAngleSnapStr = nfs(dragAngleSnap, 2, 4) + " " + "dragAngleSnap";

String[] strings = {
startAngleSnapStr,
dragAngleSnapStr
};

float textH = 18;
textSize(textH);
textAlign(LEFT, TOP);
fill(0);
for (int i=0; i<strings.length; i++) {
text(strings[i], 5, 5+i*textH);
}
}
void showTextRight() {
String startSecStr = "startSec" + " " + nfs(startSec, 2);
String endSecStr = "endSec" + " " + nfs(endSec, 2);
String accSecStr = "accSec" + " " + nfs(accSec, 2);
String accMinStr = "accMin" + " " + nfs(accMin, 2);

String[] strings = {
startSecStr,
endSecStr,
accSecStr,
accMinStr
};

float textH = 18;
textSize(textH);
textAlign(RIGHT, TOP);
fill(0);
for (int i=0; i<strings.length; i++) {
text(strings[i], width-5, 5+i*textH);
}
}
float snapRound(float a) {
a /= snapAngle;
a = round(a);
a *= snapAngle;
return a;
}
void checkMouse() {
m.set(mouseX, mouseY);
mo = PVector.sub(m, c);
mo.rotate(-offset);

pm.set(pmouseX, pmouseY);
po = PVector.sub(pm, c);
po.rotate(-offset);

cross = mo.cross(po).z;
cwDrag = cross < 0;
ccwDrag = cross > 0;

if (!mousePressed) {
measuring = false;
measureBegan = false;
}
}
void showNeedles() {
showNeedle(dragAngle, color(255), 7);
showNeedle(startAngleSnap, color(50, 200, 65), 5);
showNeedle(dragAngleSnap, color(200, 35, 40), 3);
}
void showNeedle(float angle, color needleColor, int needleSize) {
strokeWeight(needleSize);
stroke(needleColor);
angle += offset;
lineStartRA(c, r+25, angle);
}
void circleCR(PVector loc, float radius) {
}
void lineStartRA(PVector start, float r, float a) {
PVector end = new PVector(cos(a), sin(a));
end.mult(r);
linePts(start, end);
}
void linePts(PVector start, PVector end) {
line(start.x, start.y, end.x, end.y);
}
void showFace() {
PVector tickLoc = new PVector();
PVector textLoc = new PVector();

strokeWeight(1);
noStroke();
fill(127, 200);
circleCR(c, r);

for (int i=0; i<divisions; i++) {
float inc = TWO_PI/divisions;
float angle = i*inc;
angle += offset;
float x = cos(angle);
float y = sin(angle);

tickLoc.set(x, y);
tickLoc.mult(r);

stroke(0);
noFill();
circleCR(tickLoc, 5);

textLoc.set(x, y);
textLoc.mult(r+45);

int numVal = (i+divisions-1)%divisions+1;
String numTextStr = nf(numVal, 1);
fill(0);
textSize(25);
textAlign(CENTER, CENTER);
text(numTextStr, textLoc.x, textLoc.y);
}
}
``````

One thing i would like to say before anything else is, use constants! They would save you a many unneccessary lines. For example :

``````final float DIVISIONS = 12;
final float SNAP_ANGLE = TWO_PI / DIVISIONS;
``````

Since they would only change if you change the Code, using Constants is advised.

That‘s just what i wanted to mention beforehand. Now on to solving your problem

If i don‘t completely misunderstand what you want to do, then the Solution to your problem should be to use floor instead of round… That should pretty much solve it…

As to what Chrisir meant, it should be this :

``````float floor = abs(initialValue-floor(initialValue));
float ceil = abs(initialValue-ceil(initialValue));
float result = floor < ceil ? floor : ceil; // or use a boolean for a later if statement...
``````

I think…

1 Like

As for what happens when the needle is dragged around to mark the seconds, I like the way round works as far as selecting the second is concerned. Especially when divisions is 60, it makes it much easier to get the second you’re trying for.

The main thing I am trying to solve now is getting the accumulated minute `accMin`to count up or down based on floor and ceil.

When dragging clockwise, `accMin = floor(accSec/60) ` works the way I want it to, and for ccw drags, `accMin = ceil(accSec/60)` works.

I’m not clear on what `initialValue` would translate to in my example. Like this? This seems like it almost works but increments the minute immediately instead of waiting for a full revolution.

``````accMin = (accMin < accMinFloor) ? accMinFloor : accMinCeil;
``````

When you want the minute hand of the clock to go smoothly: calculate the angle for previous minute and for next minute and use command map to got from one to next - see reference, use seconds as the amt parameter as described in the reference

Then you could just add a boolean that checks wether it‘s clockwise or counterclockwise, and then use the appropriate floor/ceil depending on that boolean.

Although it might not be the best approach, it would be the simplest i‘d say

It turns out that just using floor for the minutes does work after all.

I did try that, it gets janky when changing directions. There might be a way to make that work but it becomes unnecessarily clunky as far as I can see.

I was able to get this idea to work, too, but it doesn’t seem to work differently than just using floor for what I’m trying to do right now.

I might try something with this but its not what I was trying to solve at the moment.

Thanks for the help, it seems like this is pretty much working the way I wanted now.

``````// clock face
PVector c, c2;
float r, r2;
int divisions;

// mouse
PVector m, pm, mo, po;
float offset = -HALF_PI, heading, cross;
boolean cwDrag, ccwDrag, measuring, measureBegan;

// math
float snapAngle, dragAngle, dragAngleSnap;
float startAngleOffset, startAngleSnap;
float accDragAngle, accAngleSnap, accMinAngle;
int startSec, endSec, accSec, accMin;

PFont mono, sans;
void setup() {
size(400, 600);
mono = createFont("Monospaced", 18);
sans = createFont("Helvetica", 18);
//pixelDensity(2);
r = width/2 - 70;
c = new PVector(width/2, height - r - 70);
c2 = new PVector(50, height-50);
r2 = 30;
m = new PVector();
pm = new PVector();
divisions = 12;
snapAngle = TWO_PI/divisions;
}
void draw() {
background(127);
checkMouse();

dragAngleSnap = snapRound(dragAngle);

if (mousePressed && !measureBegan) {
measureBegan = true;

startAngleSnap = dragAngleSnap;
startAngleOffset = startAngleSnap - dragAngle;

startSec = round(startAngleSnap/snapAngle);
startSec *= 60/divisions;

endSec = startSec;

accDragAngle = 0;
accDragAngle -= startAngleOffset;

accAngleSnap = 0;
accSec = 0;
accMin = 0;
}

if (measuring) {
endSec = round(dragAngleSnap/snapAngle);
endSec *= 60/divisions;

accDragAngle += calcDragAngleDiff();

accAngleSnap = snapRound(accDragAngle);

accSec = round(accAngleSnap/snapAngle);
accSec *= 60/divisions;

accMin = floor(accSec/60);
accMinAngle = accMin*TWO_PI/60;
}

showNeedles();
showFace(c, r);
showFace(c2, r2);
showTextLeft();
showTextRight();
}
void mouseDragged() {
measuring = true;
}
void showTextLeft() {
String startAngleSnapStr = nfs(startAngleSnap, 3, 2) + " " + "startAngleSnap";
String dragAngleStr = nfs(dragAngle, 3, 2) + " " + "dragAngle";
String dragAngleSnapStr = nfs(dragAngleSnap, 3, 2) + " " + "dragAngleSnap";
String accDragAngleStr = nfs(accDragAngle, 3, 2) + " " + "accDragAngle";
String accAngleSnapStr = nfs(accAngleSnap, 3, 2) + " " + "accAngleSnap";
String accMinAngleStr = nfs(accMinAngle, 3, 2) + " " + "accMinAngle";

String[] strings = {
startAngleSnapStr,
dragAngleStr,
dragAngleSnapStr,
accDragAngleStr,
accAngleSnapStr,
accMinAngleStr
};

float textH = 18;
textFont(mono);
textSize(textH);
textAlign(LEFT, TOP);
fill(0);
for (int i=0; i<strings.length; i++) {
text(strings[i], 5, 5+i*textH);
}
}
void showTextRight() {
String newLine = "";
String startSecStr = "startSec" + " " + nfs(startSec, 2);
String endSecStr = "endSec" + " " + nfs(endSec, 2);
String accSecStr = "accSec" + " " + nfs(accSec, 2);
String accMinStr = "accMin" + " " + nfs(accMin, 2);
String accTimeSign = accMin<0 || accSec<0 ? "-" : " ";
String accTimeStr = "accTime" + " " + accTimeSign + nf(abs(accMin), 2) + ":" + nf(abs(accSec%60), 2);

String[] strings = {
startSecStr,
newLine,
endSecStr,
newLine,
accSecStr,
accMinStr,
newLine,
accTimeStr
};

float textH = 18;
textFont(mono);
textSize(textH);
textAlign(RIGHT, TOP);
fill(0);
for (int i=0; i<strings.length; i++) {
text(strings[i], width-5, 5+i*textH);
}
}
float calcDragAngleDiff() {
float diff = PVector.angleBetween(mo, po);
if (ccwDrag) diff = -diff;
return diff;
}
float snapRound(float a) {
a /= snapAngle;
a = round(a);
a *= snapAngle;
return a;
}
void checkMouse() {
m.set(mouseX, mouseY);
mo = PVector.sub(m, c);
mo.rotate(-offset);

pm.set(pmouseX, pmouseY);
po = PVector.sub(pm, c);
po.rotate(-offset);

cross = mo.cross(po).z;
cwDrag = cross < 0;
ccwDrag = cross > 0;

if (!mousePressed) {
measuring = false;
measureBegan = false;
}
}
void showNeedles() {
showNeedle(c, r, dragAngle, color(255), 7);
showNeedle(c, r, startAngleSnap, color(50, 200, 65), 5);
showNeedle(c, r, dragAngleSnap, color(200, 35, 40), 3);

showNeedle(c2, r2, accMinAngle, color(0), 3);
showNeedle(c2, r2, dragAngleSnap, color(200, 35, 40), 3);
}
void showNeedle(PVector loc, float radius, float angle, color needleColor, int needleSize) {
strokeWeight(needleSize);
stroke(needleColor);
angle += offset;
}
void circleCR(PVector loc, float radius) {
}
void lineStartRA(PVector start, float r, float a) {
PVector end = new PVector(cos(a), sin(a));
end.mult(r);
linePts(start, end);
}
void linePts(PVector start, PVector end) {
line(start.x, start.y, end.x, end.y);
}
void showFace(PVector loc, float radius) {
PVector tickLoc = new PVector();
PVector textLoc = new PVector();

strokeWeight(1);
noStroke();
fill(127, 200);

for (int i=0; i<divisions; i++) {
float inc = TWO_PI/divisions;
float angle = i*inc;
angle += offset;
float x = cos(angle);
float y = sin(angle);

tickLoc.set(x, y);

stroke(0);
noFill();

textLoc.set(x, y);

int numVal = (i+divisions-1)%divisions+1;
numVal *= 60/divisions;
String numTextStr = nf(numVal, 1);
fill(0);
textFont(sans);
textAlign(CENTER, CENTER);
text(numTextStr, textLoc.x, textLoc.y);
}
}
``````
1 Like

Yeah, i knew it would get janky if you change direction while its running, but i was under the assumption that you don‘t change direction while it‘s running (like with a clock or a timer, where they only go one direction until they restart…).

The project is to have a clock that keeps real time, but being able to drag the hands then let go and it should resume. Just working on the basics now, it’s really just a bit of self directed learning.

It’s still not working right on the iCompiler for Processing app, which has issues with integer math sometimes. I really wish the developer would keep it alive and work out the bugs, because it sure is nice to use Processing on the iPhone and iPad. It’s almost just like making an iPhone app, with multitouch support, and access to the motion sensors. But it seems like maybe he’s done with it. I don’t know.

Would appreciate it

Thank you!

Just write Processing ICompiler in the App Store. I‘ve been using it for over a year now (Almost 2) and it‘s awesome to be able to program while on the phone.

Also works without Internet Connection, just to mention that. It has some difference to the actual Computer version and is distributed by Frogg.

The last Update was ~ a quarter year ago and added a data support (txt files n stuff, although it‘s not working well for me…).

Also, like i mentioned in another post, some methods are different, like random() which on IOS takes min/max into account, unlike the Computer version.

2 Likes

Thank you very much, very interesting!!

1 Like

It incorporates some text editing intelligence for cursor placement which is now kind of wonky with the changes in iOS 13, like the discontinuation of the magnifying glass, but otherwise it still seems to be working.

There are some glitches with the way mouseReleased() works, so it’s better to just write an alternative. I found a way to make it work 100% if anyone is interested.

Lists are not supported, PShape is not supported, there is no syntax error reporting, it allows the use of variables without having declared them, it sometimes quietly does float math and doesn’t complain if a float expression is stored in an int.

Lots of little quirks that you have to just “figure out.”

It’s pretty impressive, and I’ll be really sad if it ever stops working.

2 Likes

Could you suggest anything to solve the ccw drag problem that I’m only having in the iCompiler?

The newest code I posted works exactly how I want in the official IDE, but in iCompiler, the first tick of a ccw drag beginning at 12 o’clock immediately makes accTime -1:05, then as it is dragged all the way around, accTime shows -1:55, -1:00, then -2:05.

In the IDE, accTime shows -0:05, then as it goes all the way around, it shows -0:55, -1:00, -1:05.

I tried this:

``````float accMinFloorDiff = floor(accSec/60) - abs(accSec/60);
float accMinCeilDiff = ceil(accSec/60) - abs(accSec/60);

accMin = (accMinFloorDiff >= accMinCeilDiff) ? floor(accSec/60) : ceil(accSec/60);
``````

… and then ccw drag works like in the IDE, but cw drag then has the inverse problem. (Only in the iCompiler)