kerjos-clock
Here is my clock:
If you can’t see that, check out this GIF of it:
It’s super slow. Here’s a photo for speed comparison:
And another photo, just so you can see it more fully grown:
Tree rings occurred to me as a logical form that a clock might take; we already use them to count years, even if it is meant to count the life of the tree.
I like my design for its simplicity and its straightforwardness. And I like that when the tree is keeping time based in minutes, it’s so slow that sometimes when you’re looking at it, you’re not sure if it’s actually growing. I think that hammers home an understanding of patience that at least keeps you looking at it for a little longer.
I wish I could have added more assets to this tree, particularly something that keeps time in hours in addition to minutes. I also think that my design needs to live in a more clearly defined context: It looks a bit like a botanical drawing now, and I wish it was more so, or, like something else.
I computed my rings based on how I would draw them in Adobe Illustrator, using bézier curves as so:
Here’s my initial calculations for roots that would appear randomly around the tree. It’s in my code, but I don’t call the draw function because these actually need more complexity before they look good as roots:
And here’s my code:
var prevSec; var millisRolloverTime; var prevHalf; var currHalf; var specialRollover; var prevSeason; var currSeason; var seasonChange = ""; var H; var M; var S; function setup() { createCanvas(400, 400); cx = width/2; cy = height/2; //Form Init Values numAnchors = 8; radius = 200; numRings = 30; //Color Init Values darkColor = color(112,77,104); //lightColor = color(237,190,104); lightColor = color(255); //Time Init Values counter = floor(minute()/(60/numRings)); newMinute = false; newHour = false; millisRolloverTime = 0; //Golan Levin specialRollover = 0; //Starting Function Calls rings = []; getFirstRing(); getOtherRings(); roots = []; getRoots(); } function mousePressed() { rings = []; getFirstRing(); getOtherRings(); roots = []; getRoots(); counter = floor(minute()/(60/numRings)); newMinute = true; millisRolloverTime = 0; specialRollover = 0; } function keyPressed() { if (keyCode === LEFT_ARROW) { counter = 29; } else if (keyCode === RIGHT_ARROW) { counter += 5; } } function Coord(ax,ay,cL1,cL2) { var a = ax - cx; var b = cy - ay; var c = sqrt(sq(a)+sq(b)); var B = atan(b/a); if (a < 0) {B += PI} var D = -1 * (B - PI/2); var cx1 = ax + (cL1 * cos(D)) var cy1 = ay + (cL1 * sin(D)) var cx2 = ax - (cL2 * cos(D)) var cy2 = ay - (cL2 * sin(D)) var coord = { ax: ax, ay: ay, c: c, theta: B, cL1: cL1, cL2: cL2, cx1: cx1, cy1: cy1, cx2: cx2, cy2: cy2, } return coord; } function BarkCoord(x,y,cx,cy) { var coord = { ax: x, ay: y, cx1: cx, cy1: cy, cx2: x, cy2: y, } return coord; } function Ring(coords,fillColor) { var ring = { coords: coords, fillColor: fillColor, } return ring; } function Root(anchor1,anchor2,l,w) { var currRingNum = fetchCurrentRing(); if (currRingNum < 0) {currRingNum = 0} var currRing = rings[currRingNum]; var x; var y; root = { anchor1: anchor1, anchor2: anchor2, x: x, y: y, l: l, w: w, } updateRoot(root,currRing); return root; } function updateRoot(root,currRing) { var anchor1 = root.anchor1; var anchor2 = root.anchor2; var length = root.l; var rootWidth = root.w; var ax1 = currRing.coords[anchor1].ax; var ay1 = currRing.coords[anchor1].ay; fill(255,0,0); //ellipse(ax1,ay1,5,5); var ax2 = currRing.coords[anchor2].ax; var ay2 = currRing.coords[anchor2].ay; fill(0,255,0); //ellipse(ax2,ay2,5,5); var auxX = (ax1 + ax2)/2; var auxY = (ay1 + ay2)/2; var a = auxX - cx; var b = cy - auxY; var c = sqrt(sq(a)+sq(b)); var B = atan(b/a); if (a < 0) {B += PI} //console.log(length); var l = c + length*(counter/60) + 2; root.x = cx + (l*cos(B)); root.y = cy - (l*sin(B)); /*noStroke(); fill(255,0,0); ellipse(root.x,root.y,3,3);*/ var D = 1 * (B - PI/2); var w = rootWidth*(counter/60) + 2; var ax3 = root.x + (w*cos(D)); var ay3 = root.y - (w*sin(D)); var ax4 = root.x - (w*cos(D)); var ay4 = root.y + (w*sin(D)); /*fill(0,255,0); ellipse(ax3,ay3,3,3); fill(0,0,255); ellipse(ax4,ay4,3,3);*/ var B1 = B + PI/15; var B2 = B - PI/15; var cL1 = 10; var cL2 = 40; var cx31 = ax3 + (cL2*cos(B1)); var cy31 = ay3 - (cL2*sin(B1)); var cx32 = ax3 - (cL1*cos(B1)); var cy32 = ay3 + (cL1*sin(B1)); var cx41 = ax4 + (cL2*cos(B2)); var cy41 = ay4 - (cL2*sin(B2)); var cx42 = ax4 - (cL1*cos(B2)); var cy42 = ay4 + (cL1*sin(B2)); /*fill(255,144,0); ellipse(cx31,cy31,3,3); fill(117,227,255); ellipse(cx32,cy32,3,3); fill(215,66,244); ellipse(cx41,cy41,3,3); ellipse(cx42,cy42,3,3);*/ root.ax3 = ax3; root.ay3 = ay3; root.cx31 = cx31; root.cy31 = cy31; root.cx32 = cx32; root.cy32 = cy32; root.ax4 = ax4; root.ay4 = ay4; root.cx41 = cx41; root.cy41 = cy41; root.cx42 = cx42; root.cy42 = cy42; //fill(255,0,0); //ellipse(ax1,ay1,5,5); root.ax1 = ax1; root.ay1 = ay1; root.ax2 = ax2; root.ay2 = ay2; //ellipse(root.ax1,root.ay1,5,5); } function getFirstRing() { var anchors = numAnchors; var r = radius; var theta = -PI/2; var coords = []; for (var i=0; i<anchors; i++) { var rand = random(-r/5,r/5); var ax = cx + rand + (r * cos(theta)); if (ax<0) {ax=0} if (ax>width) {ax=width} var ay = cy + rand + (r * sin(theta)); if(ay<0) {ay=0} if(ay>height) {ay=height} var cL1 = random(r/10,4*r/10); var cL2 = random(r/10,4*r/10); var coord = Coord(ax,ay,cL1,cL2); coords.push(coord); theta += (2*PI)/anchors; } var fillColor = lightColor; var ring = Ring(coords,fillColor); rings.push(ring); } function getOtherRings() { var anchors = numAnchors; var r = radius; var firstRing = rings[0]; for (var i=1; i<numRings; i++) { var coords = []; var lastRing = rings[i-1]; var wetness = random(0.75,1.25); for (var j=0; j<firstRing.coords.length; j++) { var c = lastRing.coords[j].c; var theta = lastRing.coords[j].theta; var thickness = firstRing.coords[j].c/numRings; var d = c - (thickness*wetness); //var rand = random(-thickness/8,thickness/8); var newAX = cx + (d * cos(theta)); var newAY = cy - (d * sin(theta)); var cL1 = lastRing.coords[j].cL1; var cL2 = lastRing.coords[j].cL2; var newCL1 = cL1 - (firstRing.coords[j].cL1/numRings); var newCL2 = cL2 - (firstRing.coords[j].cL2/numRings); var coord = Coord(newAX,newAY,newCL1,newCL2); coords.push(coord); } var ring = Ring(coords,lightColor); rings.push(ring); } } function getInterpolatedRing(mils) { //takes specialSecs var anchors = numAnchors; var milsMax = (60*1000*(30/numRings)-1); var currentRing = numRings - counter - 1; //console.log(currentRing); if (currentRing <= 0) {return} var nextRing = numRings - counter - 2; var coords = []; for (i=0;i<anchors;i++) { var currAX = rings[currentRing].coords[i].ax; var nextAX = rings[nextRing].coords[i].ax; var newAX = map(mils, 0,milsMax, currAX,nextAX); var currAY = rings[currentRing].coords[i].ay; var nextAY = rings[nextRing].coords[i].ay; var newAY = map(mils, 0,milsMax, currAY,nextAY); var currCL1 = rings[currentRing].coords[i].cL1; var nextCL1 = rings[nextRing].coords[i].cL1; var newCL1 = map(mils, 0,milsMax, currCL1,nextCL1); var currCL2 = rings[currentRing].coords[i].cL2; var nextCL2 = rings[nextRing].coords[i].cL2; var newCL2 = map(mils, 0,milsMax, currCL2,nextCL2); var coord = Coord(newAX,newAY,newCL1,newCL2); coords.push(coord); } var fillColor = lightColor; return Ring(coords,fillColor); } function getBark(ring) { var anchors = ring.coords.length; var barkCoords = []; for (var i=0; i<anchors; i++) { var x1 = ring.coords[i].ax; var y1 = ring.coords[i].ay; var cx1 = ring.coords[i].cx1; var cy1 = ring.coords[i].cy1; var j = i+1; if (i == anchors-1) {j = 0;} var cx2 = ring.coords[j].cx2; var cy2 = ring.coords[j].cy2; var x2 = ring.coords[j].ax; var y2 = ring.coords[j].ay; for (var k=0; k<1; k+=0.2) { //For every sharp point of bark: //var t = 0.25; var P = calcPointOnBezier(x1,y1,cx1,cy1,cx2,cy2,x2,y2,k); var x = P[0]; var y = P[1]; //fill(255,0,0); //ellipse(x,y,5,5); var a = x - cx; var b = cy - y; var c = sqrt(sq(a)+sq(b)); var B = atan(b/a); if (a < 0) {B += PI} var d = c + 10*(counter/60) + 2; var newX = cx + (d * cos(B)); var newY = cy - (d * sin(B)); //fill(0,255,0); //ellipse(newX,newY,5,5); var D = -1 * (B - PI/2); var E = D - PI/4; var cL = 4; var newCX = x + (cL * cos(E)); var newCY = y + (cL * sin(E)); //fill(0,0,255); //ellipse(newCX,newCY,5,5); //var newCX2 = x - (cL2 * cos(E)); //var newCY2 = y - (cL2 * sin(E)); var barkCoord = BarkCoord(newX,newY,newCX,newCY); barkCoords.push(barkCoord); } } var fillColor = color(255,0,0); return Ring(barkCoords,fillColor); } function calcPointOnBezier(x1,y1,x2,y2,x3,y3,x4,y4,t) { var storeAX = x4 - 3.0*x3 + 3.0*x2 - x1; var storeAY = y4 - 3.0*y3 + 3.0*y2 - y1; var storeBX = 3.0*x3 - 6.0*x2 + 3.0*x1; var storeBY = 3.0*y3 - 6.0*y2 + 3.0*y1; var storeCX = 3.0*x2 - 3.0*x1; var storeCY = 3.0*y2 - 3.0*y1; var storeDX = x1; var storeDY = y1; var x = ((storeAX*t+storeBX)*t+storeCX)*t+storeDX; var y = ((storeAY*t+storeBY)*t+storeCY)*t+storeDY; return [x,y] } function fetchCurrentRing() { var currRing = numRings - counter - 1; if (currRing == -1) {currRing = 0} if (currRing < -1) { return } else { return currRing } } function getRoots() { var anchors = numAnchors; var numRoots = floor(random(1,8)); //console.log(numRoots); var usedPairs = []; for (i=0;i<numRoots;i++) { var anchor1 = floor(random(0,anchors-1)); var anchor2 = anchor1 + 1; if (anchor2 > anchors-1) {anchor2 = 0} var pair = [anchor1,anchor2]; var containsPair = false; for (i=0;i<usedPairs.length;i++) { var otherPair = usedPairs[i]; if ((otherPair[0] == pair[0]) && (otherPair[1] == pair[1])) { containsPair = true; } } if (containsPair) {continue} //console.log(anchor1 + " " + anchor2); var l = random(50,175); var w = random(70,100); var root = Root(anchor1,anchor2,l,w); roots.push(root); //console.log("Number " + i + " " + roots); usedPairs.push(pair); } } function draw() { background(255); // Fetch the current time H = hour(); M = minute(); S = second(); // Reckon the current millisecond, // particularly if the second has rolled over. if (prevSec != S) { millisRolloverTime = millis(); } prevSec = S; var mils = floor(millis() - millisRolloverTime); //milliseconds in growth period if ((M%(60/numRings)) < ((60/numRings)/2)) { currHalf = 0; } else { currHalf = 1; } if (prevHalf != currHalf) { specialRollover = millis(); } prevHalf = currHalf var specialMils; if (currHalf >= 1) { specialMils = floor(millis() - specialRollover); } else { specialMils = 0; } //console.log(currHalf); fill(0); //text(specialMils, 15, 30); var m = M; if (M < 10) {m = "0"+m} var s = S; if (S < 10) {s = "0"+s} //text(H + " " + m + " " + s + " " + mils,15,15); if ((M%(60/numRings)==0) && (newMinute)) { //Ring is complete. newMinute = false; if (M == 0) { counter = 0 } else { counter += 1 } } if (M%(60/numRings)!=0) { newMinute = true; } //console.log(counter); if ((M==0) && (newHour)) { //Tree is complete. newHour = false; rings = []; getFirstRing(); getOtherRings(); roots = []; getRoots(); } if (M!=0) { newHour = true; } var clr = color(darkColor); var currRing = fetchCurrentRing(); var currRingObject; if (currRing != null) { currRingObject = rings[currRing]; } for (i=0;i<roots.length;i++) { if (currRing != null) { updateRoot(roots[i],currRingObject); } else { updateRoot(roots[i],rings[0]); } //drawBeziers(roots[i],clr); //drawRoot(roots[i],clr); } var barkRing; var interpolatedRing = getInterpolatedRing(specialMils); if (interpolatedRing != null) { //Draw Bark barkRing = getBark(interpolatedRing); if (barkRing != null) { drawBeziers(barkRing,clr); } //Draw Interpolated Ring drawBeziers(interpolatedRing,clr); } else { barkRing = getBark(currRingObject); if (barkRing != null) { drawBeziers(barkRing,clr); } } for (var i=0; i<rings.length; i++) { if (i >= numRings-counter-1) { //clr = 0; drawBeziers(rings[i],clr); } else { //clr = 210; //drawBeziers(rings[i],clr); } } } function drawRoot(root,clr) { stroke(clr); noFill(); var ax1 = root.ax1; var ay1 = root.ay1; var ax2 = root.ax2; var ay2 = root.ay2; //ellipse(ax1,ay1,4,4); bezier(ax2,ay2,ax2,ay2,root.cx32,root.cy32,root.ax3,root.ay3); bezier(root.ax3,root.ay3,root.cx31,root.cy31, root.cx41,root.cy41,root.ax4,root.ay4); bezier(ax1,ay1,ax1,ay1,root.cx42,root.cy42,root.ax4,root.ay4); } function drawBeziers(ring,clr) { stroke(clr); var fillColor = ring.fillColor; fill(fillColor); noFill(); //Just so I don't have fill for now. var sides = ring.coords.length; if (sides>numAnchors) { //It's bark. strokeWeight(4) } else { //It's a ring. strokeWeight(1) } var vertices = []; for (var i=0; i<sides; i++) { var x1 = ring.coords[i].ax; var y1 = ring.coords[i].ay; var x2 = ring.coords[i].cx1; var y2 = ring.coords[i].cy1; var j = i+1; if (i == sides-1) {j = 0;} var x3 = ring.coords[j].cx2; var y3 = ring.coords[j].cy2; var x4 = ring.coords[j].ax; var y4 = ring.coords[j].ay; /*stroke(255,0,0); fill(255,0,0); ellipse(x2,y2,3,3); line(x1,y1,x2,y2); stroke(0,255,0); fill(0,255,0); ellipse(x3,y3,3,3); line(x4,y4,x3,y3); stroke(0); noFill(); line(cx,cy,x1,y1);*/ vertices.push([x1,y1]); bezier(x1,y1,x2,y2,x3,y3,x4,y4); } fillBeziers(vertices,fillColor); } function fillBeziers(vertices,fillColor) { noStroke(); fill(fillColor); noFill(); //Just so I don't have fill for now. beginShape(); for (i=0;i<vertices.length;i++) { var x = vertices[i][0]; var y = vertices[i][1]; vertex(x,y); } endShape(); } |
And because you may want to get a better sense of what’s happening, here’s a version of the clock that keeps time based on seconds:
And some GIFs of it:
Thanks Golan Levin for the template for keeping track of real milliseconds.