RGB Clock 2D Pattern

I’ve just uploaded a customizable clock for 2D setups to the pattern site.

I’m putting the code here too, since it relates to several recent discussions and some of the components may be useful to folks making their own clocks.

/*
Draw a clock in 2D using RGB.

The second hand is drawn in red, minutes are green, and hours are blue. 
Sub-seconds can be displayed as white.

There are several controls that can adjust the way the clock is drawn. 
The clock face drawing is broken up into 2 mode groups, one controls 
drawing the hands as they relate to the angle (rotational), and the 
other controls the way they are drawn relative to the center (radially).
*/


//these variables are set by controls
export var sharpness = 14
export var sharpnessVariation = 0
export var sharpnessSpeed = 1
export var distance = 1
export var strength = 1.03
export var secondsBrightness = .1

//time is broken up into variables. These include factional components.
//e.g at 1:59 hours will be about 1.98 instead of 1.0. 
export var secondFraction, seconds, minutes, hours, sharpnessP

//there are 4 clock hand drawing modes and 4 radius modes.
var rmodeFns = array(4) //array of functions that can be used
export var rmode = 1
var cmodeFns = array(4) //array of functions that can be used
export var cmode = 0

//a slider to set which radius mode ot use, from 0-3
export function sliderRadiusMode(v) {
  rmode = floor(.5 + v*3)
}

//a slider to set which hand drawing mode to use, from 0-3
export function sliderHandMode(v) {
  cmode = floor(.5 + v*3)
}

//clock hands can be more defined or smoother
export function sliderSharpness(v) {
  sharpness = 1 + v*v*40
}

//sets the stength/intensity of the hands combined with sharpness
export function sliderStrength(v) {
  strength = 1+v*v
}

//the shaprness level can breathe, varying definition. 
//slide to the left to disable (set to 0)
export function sliderBreathe(v) {
  sharpnessVariation = v*10
}

//The speed of animations, from once a second to once a minute
//this affects breathing speed and animated hand modes
export function sliderSpeed(v) {
  sharpnessSpeed = 1 - v*0.9847412109375
}

//zoom in or out on the clock face
export function sliderDistance(v) {
  distance = 1 + v*5
}

//sub-second hand intensity can be adjusted from completely off to bright white
export function sliderSecondsBrightness(v) {
  secondsBrightness = v*v
}

var lastSeconds = clockSecond() //used to detect when a second threshold is crossed
export function beforeRender(delta) {
  //calculate secondFraction, resetting when a second has elapsed
  if (lastSeconds != clockSecond()) {
    //the second just changed, so subseconds should be zero, however it probably transitioned somewhere between frames
    secondFraction = delta/2000 //assume half a frame of jitter, could be better
    lastSeconds = clockSecond()
  } else {
    secondFraction += delta/1000 //delta is in milliseconds
  }

  //calculate the time as fractional components that will smoothly move
  //around a clockface
  seconds = (lastSeconds + secondFraction)
  //if you prefer your seconds to jump, use this instead
  // seconds = clockSecond()
  minutes = (clockMinute() + seconds/60)
  hours = (clockHour() % 12 + minutes/60)

  t1 = time(sharpnessSpeed)
  sharpnessP = sharpness + ((wave(t1) - .5) * sharpnessVariation)
}


/*
Globals used in animations:
a = the pixel angle on the clock face, in which 0 and 1 meet at the top of the circle.
r = radius, the distance from center
*/
var a, r

/*
Globals used between radial modes and hand drawing modes. 
These are set by the radial mode and used when drawing the hands.

rf = intensity for the given pixel for the sub-seconds hand
rs = intensity for the given pixel for the seconds hand
rm = intensity for the given pixel for the minutes hand
rh = intensity for the given pixel for the hours hand
*/

var rf, rs, rm, rh

/*
Several radial modes are defined.

0 = repeating equidistant gradient arcs are given to each hand.
1 = similar to above, but the arcs are closer together and blend more.
2 = solid arc bands (no radial blending). Combine with hand mode=1 for pixelation
3 = No radial component is used, all hands are drawn as rays to infinity
*/

//equidistant
rmodeFns[0] = () => {
  rf = min(triangle(0.4 + .2 + r * distance) * 1.05, 1)
  rs = min(triangle(0.4 + 0.333 + r * distance) * 1.05, 1)
  rm = min(triangle(0.4 + 0.666 + r * distance) * 1.2, 1)
  rh = min(triangle(0.4 + 0 + r * distance) * 1.2, 1) 
}

//clusterred (intended for distance=1)
rmodeFns[1] = () => {
  rf = min(triangle(r*1.8 * distance) * 1.05, 1)
  rs = min(triangle(r*1.1 * distance) * 1.05, 1)
  rm = min(triangle(r*1.4 * distance) * 1.2, 1)
  rh = min(r < .6 && triangle(r*2.3 * distance) * 1.6, 1) 
  
  rf = abs(r - .1) < .15
  
  rh = clamp(1.22-r*distance,0,1)
}

//bands
rmodeFns[2] = () => {
  rf = abs(r - .1) < .15
  rs = abs(r - .4) < .2
  rm = abs(r - .3) < .15
  rh = abs(r - .1) < .2
}

//rays to infinity
rmodeFns[3] = () => {
  rf = 1
  rs = 1
  rm = 1
  rh = 1
}


/*
Several hand drawing modes are defined.

0 = gradients centered around the clock hand.
1 = a threshold is applied to above making pixels on or off.
2 = gradients that pulse outward.
3 = beams of light shoot from the clock hand outward.
*/

/*
this helper function does most of the work for several hand drawing modes.
Given a 't' fraction from 0-1, and 'rf' a radial filter, draw a gradient with 
strenth and sharpness settings applied.
*/
function angleGradient(t, rf) {
  return pow((strength-triangle(a + t))  * rf, sharpnessP)
}

//gradient
cmodeFns[0] = () => {
  white = min(1, angleGradient(secondFraction, rf)) * secondsBrightness
  red = angleGradient(seconds/60, rs) + white
  green = angleGradient(minutes/60, rm) + white
  blue = angleGradient(hours/12, rh) + white
  rgb(red,green,blue)
}

//threshold
cmodeFns[1] = () => {
  white = (angleGradient(secondFraction, rf) > .5) * secondsBrightness
  red = (angleGradient(seconds/60, rs) > .5) + white
  green = (angleGradient(minutes/60, rm) > .5) + white
  blue = (angleGradient(hours/12, rh) > .5) + white  
  rgb(red,green,blue)
}

//pusle animation
cmodeFns[2] = () => {
  var tf = triangle(a + secondFraction)
  var ts = triangle(a + seconds/60)
  var tm = triangle(a + minutes/60)
  var th = triangle(a + hours/12)
  
  white = min(1,pow(((strength-tf)+triangle(tf - t1)*strength*.2) * rs, sharpnessP)) * secondsBrightness
  red   = pow(((strength-ts)+triangle(ts - t1)*strength*.2) * rs, sharpnessP) + white
  green = pow(((strength-tm)+triangle(tm - t1)*strength*.2) * rm, sharpnessP) + white
  blue  = pow(((strength-th)+triangle(th - t1)*strength*.2) * rh, sharpnessP) + white
  rgb(red,green,blue)
}

//beamshot animation
cmodeFns[3] = () => {
  
  white = (angleGradient(secondFraction, rf) > .5) * secondsBrightness

  var ts = triangle(a + seconds/60)
  var tm = triangle(a + minutes/60)
  var th = triangle(a + hours/12)
  
  red   = pow(((strength-ts)*triangle(triangle(ts - t1))*strength) * rs, sharpnessP)+ white
  green = pow(((strength-tm)*triangle(triangle(tm - t1))*strength) * rm, sharpnessP)+ white
  blue  = pow(((strength-th)*triangle(triangle(th - t1))*strength) * rh, sharpnessP)+ white
  rgb(red,green,blue)
}

var HALF_PI = PI/2
//NOTE: atan2 has a bug in V2.23, when fixed this can be replaced with atan2 directly
function arctan2(y, x) {
  if (x > 0) return atan(y/x)
  if (y > 0) return HALF_PI - atan(x/y)
  if (y < 0) return -HALF_PI - atan(x/y)
  if (x < 0) return PI + atan(y/x)
  return 1.0
}

export function render2D(index, x,y) {
  //center the coordinates and calculate angle (from top) and radius
  x -= 0.5
  y -= 0.5
  a = (PI + arctan2(x, y))/PI2
  r = sqrt(x*x + y*y)
  rmodeFns[rmode]()
  cmodeFns[cmode]()
}

Some example settings:

3 Likes

Nice way you handled the sub-seconds… Checking for the delta, and then calculating it.
How hard will it be to add a ClockDeciSeconds (or milliseconds?) function in V3 code? That would allow removing that entire section.

I’ll have to look at the rest in detail later. But it looks good, and yes, I’m sure this will help inspire some ideas. A 1D clock (pixels in a circle) is good, a 2D clock gives you so many options… I like the choices you offer as sliders.

I’ve been thinking about clocks for more abstract pieces, like the cloud (how can I convey time at a glance on a fluffy cloud) or a nanoleaf “abstract” 2D layout? Lots of answers, including one I like where I emulate a “digital clock” if enough LEDs in the right config are available. (Depends heavily on layout of course). This gives another good method: imagine a cloud where sections conveyed hours/minutes/seconds? And even an abstract blob of pixels on the wall (imagine 10-20 triangles in some config) if there is any decent 2D layout (ie enough to not feel “1D”) could become a clock face with some variation in this.

1 Like

I have tried the pattern but my PixelPlaze v2 with a Electromage 8x8 RGB Panel does only ligth the first few LEDs.

I have no idea what went wrong there, any idea?

Have a 2d map? Should be able to use the matrix example if not.

Try adjusting sliders, does that do anything?

Other 2D samples do work, my pixel map:

function (pixelCount) {
  width = 8
  var map = []
  for (i = 0; i < pixelCount; i++) {
    y = Math.floor(i / width)
    x = i % width
    x = y % 2 == 1 ? width - 1 - x : x //zigzag
    map.push([x, y])
  }
  return map
}

The sliders change the behavior a liddle bit, mostly the flicker pattern/frequency of the first LED

First I suspected some hard coded expectations about the matrix size, but glimpsing thru the source I did not see any…

I have changed the LED Type from WS2812/Neopixel to Buffered WS2812/Neopixel, now it works…

But… why?!

Ah! In unbuffered mode, pixels are streamed out as they are calculated. Some LEDs have a short latch/reset time and if it takes a while to calculate a pixel, they can interpret this as an animation frame. Since this pattern is pretty heavy on the math, it’s not a good fit for the unbuffered driver.

4 Likes

Ah I noticed that, and didn’t know why, thanks!