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.