Tagged: data visualization

thabir – Clock

While the perception of time for Western society is largely linear, the measurement of time is intricately dependent on recurring observable cycles. Day and night, lunar cycles, sleep cycles, seasons, migratory patterns, life cycles, cosmic events, all of these are units with which one can measure intervals of time. The more I thought about the notion of measurement using cycles, the more I realized the arbitrary (yet convenient) nature of the cycles we have chosen to represent the world around us, as well as fact that no natural cycle truly aligns itself perfectly with our canonical system of measurement: the Gregorian Calendar. Its this notion of phasing, of constant cosmic counterpoint, that my clock tries to explore.

My clock is a rectangle packing algorithm, which allows the user to define a number of cosmic cycles of their choice, along with their length in days. It then generates a packing in a manner such that the ratios of the areas of the rectangles are equal to the ratios of the lengths of the respective cycles. Therefore area becomes a dimension for representing the period of a cosmic cycle. Moreover, the packing algorithm is designed to create a radial structure in the clock.

The overall hue palette of the clock is defined by the time of day, cycling through the entire range over the course of 24 hours. Each individual rectangle is initialized to a random epsilon variation in brightness and saturation, with the range of epsilon increasing the further outward you move from the center. However, the central concept of the clock is about the subtle phasing of cosmic cycles, independent of the units we try to bind them in. To try and represent that, I took the cycle lengths of each event and, by artificially syncing them all at the turn of the millennium, I calculated their position in their cycle. In other words, I calculated the phase. All the cycles are represented in terms of days and are unfolding in real time. Since all of them were artificially synced at the year 2000(an arbitrary choice that the user can change), the data I get is not true astronomical data but rather a representation of cosmic phasing. I then take these cyclic motions and map them on to the rectangles by making the hues of the rectangles subtly oscillate to their cosmic frequency.

The clock also has some interactive features. When you open the Processing application, you get a screen like the one below, telling you the various features. On pressing enter, you begin experiencing the clock. If you press the letter ‘l’, you can find out the name of the representative cosmic cycle by hovering over a rectangle. Since the actual phasing of the clock is occurring in real time and is therefore imperceptibly slow, you can speed it up and slow it down by factors of 2 using ‘f’ and ‘s’. My intention for this design was to create a subtly undulating field of hues that vary in an almost subliminal manner. Therefore, the right choice of speed is extremely important. By pressing ‘o’, you get what I believe to be the optimal speed for the given packing. The gif above illustrates the clock at this speed.

A fundamental premise of this design was the separation of units from events, which is reflected in the design of the clock since the change in the speed of the cosmic cycles having no effect on the hue palette. This is solely linked to the time of day. However, by pressing ‘h’ and ‘g’ you can move forwards and backwards respectively through the hue palette of the clock in hourly increments. By pressing ‘z’, the current frame gets exported as a png. The image at the top is an arrangement of such frames taken of the clock at hourly increments, starting at midnight and going till 11pm. The first gif below is a visualization of cycle packings of increasing number. The second gif is a longer demonstration of the entire flow of interaction.

The subliminal nature of this clock makes it a media object you need to live with for longer periods to begin to understand and discern. My main visual inspiration was Mondriaan, (such as his Composition No 11 from 1913), even though my work is extremely different in final form. Another major influence is Steve Reich’s use of phasing in his Clapping Music for Two Musicians. From media art, the project that really struck me was Robert Yang’s ‘Rinse and Repeat’, a first person shower simulator which, among many other things, blurs the lines between “real” time and “in-media” time.

There are many directions in which I feel this clock can and should be pushed. One of the most basic, yet important, is the development of a UI that allows for complete customizability of the clock, i.e. it allows for the user to enter the relevant cycles and the choice of the sync year, as well as providing fine-grained control of the speed parameters. I would also love for the clock to be able to scale and resize the rectangles to fit a constant, specified canvas size. This is the easiest adjustment to implement, the only difficulty being some rounding errors, causing gaps between the rectangles. A much harder improvement would be the efficiency of the packing algorithm. As it stands, the algorithm is pretty inefficient and approaches the problem using brute force. It is too slow to represent clocks with greater than 12 cycles. A major part of a redesign will be to optimize this so that I can visualize more data. Ideally I would be able to represent up to 50 cycles, all phasing with each other. However, by far the most important extension to this project would be the addition of an accompanying data sonification. This piece is intended to be a temporal, meditative, subliminal experience of cosmic evolution and is inherently multimodal in its conception.

Since my clock is something that communicates not through a single visual act of transmission, but through the slow process of experiencing it, (an attempt at the visual analog to Brian Eno’s ambient music), it requires a space that allows for extended uninterrupted meditation. In a gallery, I could see it as a projection in a dark room, where viewers are encouraged to enter and deprive themselves of all other sensory stimulation for a while, losing all of our daily markers for the passage of time. In a non-gallery context I could possibly see it projected on a wall, or used as a desktop background or screensaver, but its impact would be greatly reduced in my opinion. This is another reason why sound is so integral to the clock, since it is the most effective medium at isolating and suspending people, both physically and mentally, inside of an experience, acting as a buffer between them and the rest of the world.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
class Cycle {
  int x;
  int y;
  int Width;
  int Height;
  float period;
  String label;
  color c;
  float epsilon;
 
  Cycle(int a, int b, int c, int d, float e, String f) {
    x = a;
    y = b;
    Width = c;
    Height = d;
    period = e;
    label = f;
  }
 
  void rotate90(int canvasHeight) {
    int t = this.y;
    this.y = this.x;
    this.x = canvasHeight - (t+this.Height);
    t = this.Width;
    this.Width = this.Height;
    this.Height = t;
  }
 
  float calculateCyclePos(int syncYear, int year, int month, int day, int hour, int minute) {
    float numDays = (year - syncYear) * 365;
 
    if(month==1) numDays = numDays + day;
    else if(month==2) numDays = numDays + day + 31;
    else if(month==3) numDays = numDays + day + 59;
    else if(month==4) numDays = numDays + day + 90;
    else if(month==5) numDays = numDays + day + 120;
    else if(month==6) numDays = numDays + day + 151;
    else if(month==7) numDays = numDays + day + 181;
    else if(month==8) numDays = numDays + day + 212;
    else if(month==9) numDays = numDays + day + 243;
    else if(month==10) numDays = numDays + day + 273;
    else if(month==11) numDays = numDays + day + 304;
    else numDays = numDays + day + 334;
 
    int leapYear = (year - syncYear)/4;
    if(year%4==0 && month>=3) leapYear++;
    numDays = numDays + leapYear;
    numDays = numDays + ((hour*60)+minute)*1.0/1440.0;
 
    float cyclePos = (numDays/this.period) - floor(numDays/this.period);
    cyclePos = cyclePos*2;
    if(cyclePos>1) cyclePos = map(cyclePos, 1, 2, 1, 0);
    cyclePos = map(cyclePos, 0, 1, -0.5, 0.5);
    return cyclePos;
  }
 
  void updateColor(int hour, float cyclePos) {
    colorMode(HSB, 1440, 50, 50);
    this.c = color((hour*60)+minute()+(150*cyclePos), 22+this.epsilon, 36+this.epsilon);
  }
 
  boolean isMouseOver(int a, int b) {
    return(this.x<=a && a<(this.x+this.Width) && this.y<=b && b<(this.y+this.Height));
  }
 
  void showLabel(int canvasWidth, int canvasHeight) {
    noStroke();
    fill(215, 50);
    rect(0, 0, canvasWidth, canvasHeight);
    textAlign(LEFT);
    float Width = textWidth(this.label);
    int xOffset = round((canvasWidth - Width)/2.0);
    fill(255, 255);
    text(this.label, xOffset, round(canvasHeight/2.5));
  }
 
  void drawCycle() {
    noStroke();
    fill(this.c);
    rect(this.x, this.y, this.Width, this.Height);
  }
}
 
int globalWidth = 0;
int globalHeight = 0;
float[] cyclePeriods = new float[12];
String[] Labels = new String[12];
Cycle[] Cycles = (Cycle[])new Cycle[12];
PFont GillSans;
 
//calculate all subsets of the ratios
float[][] allSubsets(float[] ratios) {
  if(ratios.length==0){
    float[][] subsets = new float[1][0];
    return subsets;
  }
  else {
    float last = ratios[ratios.length-1];
    ratios = shorten(ratios);
    float[][] subsets = allSubsets(ratios);
    float[][] finalSubsets = new float[2*subsets.length][];
    for(int i=0; i<subsets.length; i++) {
      finalSubsets[2*i] = subsets[i];
      finalSubsets[(2*i)+1] = append(subsets[i], last);
    }
    return finalSubsets;
  }
}
 
//filters all subsets of cardinality lower than 2
ArrayList filterCardinality(float[][] subsets) {
  ArrayList filteredSubsets = new ArrayList();
  for(int i=0; i<subsets.length; i++) { if(subsets[i].length>=2) {
      FloatList subset = new FloatList(subsets[i]);
      filteredSubsets.add(subset);
    }
  }
  return filteredSubsets;
}
 
//calculate the width addition of a subset
int subsetWidth(float ratio, FloatList ratios) {
  float ratioSum = 1;
  for(int i=1; i<ratios.size(); i++) {
    ratioSum = ratioSum + (ratios.get(i)/ratios.get(0));
  }
  int a = round((globalHeight*1.0)/ratioSum);
  int b = round((ratios.get(0)*globalWidth*globalHeight)/(ratio*a));
  return b;
}
 
//filter ArrayList of subsets based on choice of subset
ArrayList filterSubsetSequence(FloatList subset, ArrayList subsets) {
  for(int i=0; i<subset.size(); i++) {
    IntList toRemove = new IntList();
    for(int j=0; j<subsets.size(); j++) {
      if(((FloatList)subsets.get(j)).hasValue(subset.get(i))) {
        int index = j - toRemove.size();
        toRemove.append(index);
      }
    }
    for(int j=0; j<toRemove.size(); j++) {
      subsets.remove(toRemove.get(j));
    }
  }
  return subsets;
}
 
//depth first search of subset sequence tree, calculating width addition at each step
ArrayList allSubsetSequences(ArrayList subsets) {
  if(subsets.size()==0) {
    return subsets;
  }
  else if(subsets.size()==1) {
    ArrayList subsetSeq = new ArrayList();
    subsetSeq.add(subsets);
    return subsetSeq;
  }
  else {
    ArrayList sequenceList = new ArrayList();
    for(int i=0; i<subsets.size(); i++) {
      FloatList subset = (FloatList)subsets.get(i);
      ArrayList temp = new ArrayList();
      for(int j=0; j<subsets.size(); j++) {
        temp.add((FloatList)subsets.get(j));
      }
      ArrayList newSubsets = filterSubsetSequence(subset, temp);
      ArrayList localSequenceList = allSubsetSequences(newSubsets);
      for(int j=0; j<localSequenceList.size(); j++) {
        ArrayList temp1 = new ArrayList();
        temp1.add(subset);
        temp1.addAll((ArrayList)localSequenceList.get(j));
        localSequenceList.set(j, temp1);
      }
      sequenceList.addAll(localSequenceList);
    }
    return sequenceList;
  }
}
 
//ranks subset sequences by some criteria, returns highest ranking subset sequence
int finalSequenceNumber(ArrayList widthLists) {
  int bestMetric = -1; int bestSequence = -1;
  for(int i=0; i<widthLists.size(); i++) {
    boolean ordered = true;
    for(int j=0; j<((IntList)widthLists.get(i)).size()-1; j++) {
      if(((IntList)widthLists.get(i)).get(j)<((IntList)widthLists.get(i)).get(j+1)) {
        ordered = false;
        break;
      }
    }
    if(ordered==false) continue;
 
    int metric = 0;
    for(int j=0; j<((IntList)widthLists.get(i)).size()-1; j++) {
      metric += pow((((IntList)widthLists.get(i)).get(j)-((IntList)widthLists.get(i)).get(j+1)), 2);
    }
    metric = round((metric * 1.0) / (((IntList)widthLists.get(i)).size()-1));
    metric = round(sqrt(metric));
    if(bestMetric==-1 && bestSequence==-1) {
      bestMetric = metric;
      bestSequence = i;
    }
    if(metric<bestMetric) {
      bestMetric = metric;
      bestSequence = i;
    }
  }
  return bestSequence;
}
 
//checks if a column of rectangles produce a suitable width to height ratio and then initializes them
void addColumn(float ratio, float[] ratios, int placeHolder) {
  float ratioSum = 1;
  for(int i=1; i<ratios.length; i++) {
    ratioSum = ratioSum + (ratios[i]/ratios[0]);
  }
  int a = round((globalHeight*1.0)/ratioSum);
  int b = round((ratios[0]*globalWidth*globalHeight)/(ratio*a));
  int index = 0;
  for(int i=0; i<cyclePeriods.length; i++) {
    if(ratios[0]==cyclePeriods[i]) index = i;
  }
  Cycles[placeHolder] = new Cycle(globalWidth, 0, b, a, ratios[0], Labels[index]);
  int hOffset = a;
  placeHolder++;
  for(int i=1; i<ratios.length-1; i++) {
    a = round((ratios[i]/ratios[0])*a);
    for(int j=0; j<cyclePeriods.length; j++) {
      if(ratios[i]==cyclePeriods[j]) index = j;
    }
    Cycles[placeHolder] = new Cycle(globalWidth, hOffset, b, a, ratios[i], Labels[index]);
    hOffset += a;
    placeHolder++;
  }
  for(int j=0; j<cyclePeriods.length; j++) {
    if(ratios[ratios.length-1]==cyclePeriods[j]) index = j;
  }
  Cycles[placeHolder] = new Cycle(globalWidth, hOffset, b, globalHeight-hOffset, ratios[ratios.length-1], Labels[index]);
  globalWidth += b;
}
 
void makeCanvas() {
  globalWidth = 500;
  globalHeight = 500;
  Cycles[0] = new Cycle(0, 0, globalWidth, globalHeight, cyclePeriods[0], Labels[0]);
 
  float[] temp = new float[cyclePeriods.length-1];
  for(int i=0; i<temp.length; i++) {
    temp[i] = cyclePeriods[i+1];
  }
  ArrayList subsets = filterCardinality(allSubsets(temp));
  ArrayList allSequences = allSubsetSequences(subsets);
  ArrayList widthLists = new ArrayList();
  for(int i=0; i<allSequences.size(); i++) {
    float ratio = cyclePeriods[0];
    IntList widths = new IntList();
    for(int j=0; j<((ArrayList)allSequences.get(i)).size(); j++) {
      widths.append(subsetWidth(ratio, (FloatList)((ArrayList)allSequences.get(i)).get(j)));
      for(int k=0; k<((FloatList)((ArrayList)allSequences.get(i)).get(j)).size(); k++) {
        ratio += ((FloatList)((ArrayList)allSequences.get(i)).get(j)).get(k);
      }
    }
    widthLists.add(widths);
  }
 
  int sequenceNum = finalSequenceNumber(widthLists);
  subsets = (ArrayList)allSequences.get(sequenceNum);
  float ratio = cyclePeriods[0];
  int placeHolder = 1;
  for(int i=0; i<subsets.size(); i++) {
    float[] ratios = ((FloatList)subsets.get(i)).array();
    addColumn(ratio, ratios, placeHolder);
    for(int j=0; j<ratios.length; j++) {
      ratio = ratio + ratios[j];
    }
    placeHolder += ratios.length;
    for(int j=0; j<placeHolder; j++) { Cycles[j].rotate90(globalHeight); } int t = globalWidth; globalWidth = globalHeight; globalHeight = t; } if(globalHeight>globalWidth) {
    for(int j=0; j<Cycles.length; j++) { Cycles[j].rotate90(globalHeight); } int t = globalWidth; globalWidth = globalHeight; globalHeight = t; } /*if(globalWidth>900 || globalHeight>900) {
    int bigSide;
    if(globalWidth>=globalHeight) bigSide = globalWidth;
    else bigSide = globalHeight;
    float multiplier = 900.0/bigSide;
    globalWidth = floor(globalWidth*multiplier);
    globalHeight = floor(globalHeight*multiplier);
    for(int i=0; i<Cycles.length; i++) {
      Cycles[i].x = floor(Cycles[i].x*multiplier);
      Cycles[i].y = floor(Cycles[i].y*multiplier);
      Cycles[i].Width = floor(Cycles[i].Width*multiplier);
      Cycles[i].Height = floor(Cycles[i].Height*multiplier);
    }
  }*/
}
 
int hour = hour();
int minute = minute();
int day = day();
int month = month();
int year = year();
int lastMinute = millis();
int syncYear = 2000;
float speed = 1.0;
int hueHour = 0;
int numKeyPressed = 0;
int drawHourFrame = -1;
int drawSpeedFrame = -1;
boolean drawInstructionFrame = true;
 
void setup() {
  cyclePeriods[0] = 10752.9; Labels[0] = "Saturn Revolution";
  //cyclePeriods[1] = 5595.45; Labels[1] = "Lemmon Comet Period";
  cyclePeriods[1] = 4328.9; Labels[1] = "Jupiter Revolution";
  cyclePeriods[2] = 3836.15; Labels[2] = "Boattini Comet Period";
  cyclePeriods[3] = 3489.4; Labels[3] = "Kowal Comet Period";
  cyclePeriods[4] = 3120.75; Labels[4] = "Catalina Comet Period";
  //cyclePeriods[6] = 3036.8; Labels[6] = "Tenagra Comet Period";
  //cyclePeriods[7] = 2730.2; Labels[7] = "Christensen Comet Period";
  cyclePeriods[5] = 2686.4; Labels[5] = "Lagerkvist Comet Period";
  cyclePeriods[6] = 1981.95; Labels[6] = "Kowalski Comet Period";
  cyclePeriods[7] = 1401.6; Labels[7] = "Stattmayer Comet Period";
  cyclePeriods[8] = 1361.45; Labels[8] = "Meyer Comet Period";
  cyclePeriods[9] = 686.2; Labels[9] = "Mars Revolution";
  cyclePeriods[10] = 521.95; Labels[10] = "IRAS Comet Period";
  cyclePeriods[11] = 365.26; Labels[11] = "Earth Revolution";
 
  makeCanvas();
  Cycles[0].epsilon = 0;
  for(int i=1; i<Cycles.length; i++) {
    float epsilon = random((i-1)*0.5, i*0.5);
    int sign;
    if(i%2==1) sign = 1;
    else sign = -1;
    Cycles[i].epsilon = sign*epsilon;
  }
  surface.setSize(globalWidth, globalHeight);
  GillSans = createFont("GillSans", 24);
  textFont(GillSans);
}
 
void draw() {
  calculateTime();
  for(int i=0; i<Cycles.length; i++) { float cyclePos = Cycles[i].calculateCyclePos(syncYear, year, month, day, hour, minute); Cycles[i].updateColor((hour()+hueHour)%24, cyclePos); Cycles[i].drawCycle(); } if(drawHourFrame>=0) {
    noStroke();
    fill(215, 50);
    rect(0, 0, globalWidth, globalHeight);
    textAlign(LEFT);
    String text;
    if(minute()<10 && (hour()+hueHour)%24<10) text = "0"+((hour()+hueHour)%24)+":0"+minute();
    else if(minute()<10 && (hour()+hueHour)%24>=10) text = ((hour()+hueHour)%24)+":0"+minute();
    else if(minute()>=10 && (hour()+hueHour)%24<10) text = "0"+((hour()+hueHour)%24)+":"+minute(); else text = ((hour()+hueHour)%24)+":"+minute(); float Width = textWidth(text); int xOffset = round((globalWidth - Width)/2.0); fill(255, 255); text(text, xOffset, round(globalHeight/2.5)); drawHourFrame++; if(drawHourFrame>60) drawHourFrame = -1;
  }
 
  if(drawSpeedFrame>=0) {
    noStroke();
    fill(215, 50);
    rect(0, 0, globalWidth, globalHeight);
    textAlign(LEFT);
    String text = "x "+speed;
    float Width = textWidth(text);
    int xOffset = round((globalWidth - Width)/2.0);
    fill(255, 255);
    text(text, xOffset, round(globalHeight/2.5));
    drawSpeedFrame++;
    if(drawSpeedFrame>60) drawSpeedFrame = -1;
  }
 
  if(numKeyPressed%2==1 && drawHourFrame==-1 && drawSpeedFrame==-1) {
    for(int i=0; i<Cycles.length; i++) {
      if(Cycles[i].isMouseOver(mouseX, mouseY)) {
        Cycles[i].showLabel(globalWidth, globalHeight);
      }
    }
  }
 
  if(drawInstructionFrame==true) {
    openingInstructions();
  }
}
 
void keyPressed() {
  if(key=='l') numKeyPressed++;
  if(key=='f') {
    speed = speed*2;
    drawSpeedFrame = 0;
  }
  if(key=='s') {
    speed = speed/2;
    drawSpeedFrame = 0;
  }
  if(key=='h') {
    hueHour++;
    drawHourFrame = 0;
  }
  if(key=='g') {
    hueHour--;
    drawHourFrame = 0;
  }
  if(key=='o') {
    calculateOptimum();
    drawSpeedFrame = 0;
  }
  if(key=='p') {
    speed = 1.0;
    drawSpeedFrame = 0;
  }
  if(key==ENTER || key==RETURN) {
    drawInstructionFrame = false;
    GillSans = createFont("GillSans", 32);
    textFont(GillSans);
  }
  if(key=='z') {
    String filename = Cycles.length + "_Cycles_" + nf(((hour()+hueHour)%24), 2) + "00.png";
    saveFrame("frames/" + filename);
    println("Saved: " + filename); 
  }
}
 
void openingInstructions() {
  noStroke();
  fill(215, 50);
  rect(0, 0, globalWidth, globalHeight);
 
  String text1 = "This clock phases with the cosmos. Through the subtle shifts in hue, watch as the world unfolds in every widening asynchronicity.";
  String text2 = "To toggle the labels of the cosmic cycles:";
  String text3 = "Press 'l' and hover over the rectangles";
  String text4 = "To control the speed of this clock's journey through time:";
  String text5 = "Press 'f' or 's'";
  String text6 = "To reach the optimum speed for the human eye to appreciate the chromatic counterpoint:";
  String text7 = "Press 'o' (and 'p' to undo)";
  String text8 = "To move forwards and backwards in increments of an hour:";
  String text9 = "Press 'h' and 'g' respectively";
  String text10 = "To capture a moment that, in all likelihood, will never return quite the same:";
  String text11 = "Press'z'";
  String text12 = "Press 'Enter' to proceed";
 
  fill(255, 255);
  textAlign(LEFT);
  text(text1, 50, 50, globalWidth-100, globalHeight-100);
  text(text2, 100, 200, globalWidth/2-100, globalHeight-200);
  text(text4, 100, 300, globalWidth/2-100, globalHeight-300);
  text(text6, 100, 400, globalWidth/2-100, globalHeight-400);
  text(text8, 100, 525, globalWidth/2-100, globalHeight-525);
  text(text10, 100, 625, globalWidth/2-100, globalHeight-625);
 
  textAlign(RIGHT);
  text(text3, globalWidth/2, 200, (globalWidth/2)-100, globalHeight-200);
  text(text5, globalWidth/2, 300, (globalWidth/2)-100, globalHeight-300);
  text(text7, globalWidth/2, 400, (globalWidth/2)-100, globalHeight-400);
  text(text9, globalWidth/2, 525, (globalWidth/2)-100, globalHeight-525);
  text(text11, globalWidth/2, 625, (globalWidth/2)-100, globalHeight-625);
  text(text12, globalWidth-50, globalHeight-50);
}
 
void findYearMonthAndDay(int dayIncrement) {
  int[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  dayIncrement = dayIncrement - (monthDays[month-1] - day);
  if(dayIncrement<=0) day = monthDays[month-1] + dayIncrement; else { while(dayIncrement>0) {
      month = (month%12)+1;
      if(month==1) year++;
      if(year%4==0 && month==2) dayIncrement -= 29;
      else dayIncrement -= monthDays[month-1];
    }
    if(year%4==0 && month==2) day = 29 + dayIncrement;
    else day = monthDays[month-1] + dayIncrement;
  }
}
 
void calculateTime() {
  int offset = floor((millis()-lastMinute)*speed/60000);
  if(offset>=1) lastMinute = millis();
  minute += offset;
  if (minute>=60){ hour = hour+(minute/60); minute %= 60;}
  int dayIncrement = 0;
  if(hour>=24) {
    dayIncrement = hour/24; 
    hour %= 24;
  }
  findYearMonthAndDay(dayIncrement);
}
 
void calculateOptimum() {
  int median = floor(cyclePeriods.length/2);
  float optimalSeconds = 10.0;
  speed = 1440*(60/optimalSeconds)*cyclePeriods[median];
  //speed = 31536000.0;
}