/* @pjs pauseOnBlur="true"; */
/**
* Currently running instance
*/
public static OrbitClock $;
private static final int STEPS = 240;
private CurveFunction curveFunction;
private float centerX;
private float centerY;
private TimeShape minutesShape;
private TimeShape secondsShape;
private TimeShape millisShape;
public void setup() {
if (typeof preSetup == 'function') { preSetup(); }
size(displayWidth, displayHeight);
smooth();
noSmooth();
$ = this;
centerX = width / 2;
centerY = height / 2;
curveFunction = new LemniscateOfBernoulli(width * .30);
strokeCap(SQUARE);
minutesShape = new TimeShape.Minutes();
minutesShape.radius = width * .05;
minutesShape.steps = 60;
minutesShape.width = 10;
minutesShape.mode = OrbMode.ORBIT;
secondsShape = new TimeShape.Seconds();
secondsShape.radius = minutesShape.radius * .5;
secondsShape.steps = 60;
secondsShape.width = minutesShape.width * .2;
secondsShape.mode = OrbMode.ORBIT;
millisShape = new TimeShape.Millis();
millisShape.radius = secondsShape.radius * .4;
millisShape.steps = 100;
millisShape.width = secondsShape.width * .5;
millisShape.mode = OrbMode.ORBIT;
}
public void draw() {
drawBackground();
drawLemniscate();
drawFPS();
}
private void drawBackground() {
background(0, 0);
}
private void drawLemniscate() {
float angle;
angle = hour2angle(hour(), minute());
Point startPoint = curveFunction.calculate(angle);
float startX = centerX + startPoint.x;
float startY = centerY + startPoint.y;
float lastX = startX;
float lastY = startY;
float lastX1 = -1;
float lastY1 = -1;
float lastX2 = -1;
float lastY2 = -1;
for (int currentStep = 0; currentStep <= STEPS; currentStep++) {
angle += TWO_PI / STEPS;
Point point = curveFunction.calculate(angle);
float x = centerX + point.x;
float y = centerY + point.y;
float angleBetween =
TwoDimensional.angleBetween(lastX, lastY, x, y) + HALF_PI;
noStroke();
setFill(currentStep, STEPS);
int lineWidth = 30;
float distX = cos(angleBetween) * lineWidth;
float distY = sin(angleBetween) * lineWidth;
float x1 = x - distX;
float y1 = y - distY;
float x2 = x + distX;
float y2 = y + distY;
if (lastX1 > -1) {
quad(lastX1, lastY1, x1, y1, x2, y2, lastX2, lastY2);
}
lastX1 = x1;
lastY1 = y1;
lastX2 = x2;
lastY2 = y2;
lastX = x;
lastY = y;
}
drawMinutes(startX, startY);
}
private void drawFPS() {
if (mousePressed) {
fill(0);
text(sprintf("%.1f", frameRate), width - 100, height - 100);
}
}
void setStroke(float currentStep, float steps) {
stroke(0, 255 * (1 - currentStep / steps));
}
void setFill(float currentStep, float steps) {
fill(0, 255 * (1 - currentStep / steps));
}
private void drawMinutes(float x, float y) {
minutesShape.draw(x, y);
drawSeconds(x + cos(minutesShape.getStartAngle()) * minutesShape.radius, y
+ sin(minutesShape.getStartAngle()) * minutesShape.radius);
}
private void drawSeconds(float x, float y) {
secondsShape.draw(x, y);
drawMillis(x + cos(secondsShape.getStartAngle()) * secondsShape.radius, y
+ sin(secondsShape.getStartAngle()) * secondsShape.radius);
}
private void drawMillis(float x, float y) {
millisShape.draw(x, y);
}
/**
* 12:00 = PI
* 24:00 = 0 = TWO_PI
* 6:00 = -PI/2 = 1.5 * PI
* 18:00 = PI/2
*
* @param hours
* @param minutes
* @return
*/
private static float hour2angle(int hours, int minutes) {
return map(hours + minutes / 60, 0, 24, TWO_PI, 0);
}
public class LemniscateOfBernoulli extends CurveFunction {
public static final float SQRT_2 = (float) sqrt(2);
public LemniscateOfBernoulli(float resizeFactor) {
super(resizeFactor);
}
public Point calculate(float angle) {
float sin1 = sin(angle);
float divisor = sq(sin1) + 1;
float dividend = resizeFactor * SQRT_2 * cos(angle);
float x = dividend / divisor;
float y = (dividend * sin1) / divisor;
return new Point(x, y);
}
}
public class Point {
public float x;
public float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public String toString() {
return sprintf("%.2f / %.2f", x, y);
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Float.floatToIntBits(x);
result = prime * result + Float.floatToIntBits(y);
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Point other = (Point) obj;
if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x))
return false;
if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y))
return false;
return true;
}
public Float dist(Point point) {
return PApplet.dist(x, y, point.x, point.y);
}
public Point intermediate(Point point) {
return new Point((x + point.x) / 2, (y + point.y) / 2);
}
}
public class TwoDimensional {
private TwoDimensional() {}
public static float angleBetween(float x1, float y1, float x2, float y2) {
double atan2 = Math.atan2(y1 - y2, x1 - x2);
return (float) (atan2 < 0 ? Math.PI * 2 + atan2 : atan2);
}
}
abstract class TimeShape {
public static class Minutes extends TimeShape {
protected float getStartAngle() {
return map(minute() + second() / 60, 0, 60, PI * -.5f, PI * 1.5);
}
}
public static class Seconds extends TimeShape {
protected float getStartAngle() {
return map(second(), 0, 60, PI * -.5f, PI * 1.5);
}
}
public static class Millis extends TimeShape {
protected float getStartAngle() {
return map(Date.now() % 1000, 0, 1000, PI * -.5f, PI * 1.5);
}
}
float radius;
int steps;
float width;
int mode;
public void draw(float x, float y) {
float startAngle = getStartAngle();
float angleSteps = TWO_PI / steps;
switch (mode) {
case OrbMode.ORB:
$.setFill(steps, steps);
$.noStroke();
$.ellipse(x + cos(startAngle) * radius, y + sin(startAngle) * radius, 10 * width, 10 * width);
break;
case OrbMode.ORBIT:
$.noFill();
$.strokeWeight(width);
for (int i = 0; i < steps; i++) {
float angle = startAngle - angleSteps * i;
$.setStroke(i, steps);
$.arc(x, y, radius * 2, radius * 2, angle, angle + angleSteps);
}
}
}
protected abstract float getStartAngle();
}