LEDaliClock matrix pattern

@jeff 's scrolling text marquee 2D (in the pattern library) does a nice job. (It’s bitmapped)

Found some simple linear font code in github I may modify… scalable, and done right, could be lots of useful. Absolutely requires a framebuffer approach, so working on my best version of that (minimizing arrays if possible)

You’ve managed to inspire me, despite the many PB todos (and in progresses) on my plate :slight_smile:

1 Like

Here’s my first crack at an analog clock. All 3 hands (white, short hour hand; mint green minute hand; red second hand) sweep at ms level. The blue spinner is plain ms, but also includes brightened marks at the standard 12 positions (easier to see in person of course).

Analog LED clock

I’ll share the code once I have optimized, but the implementation was straightforward: Since my PB isn’t time-synced, the positions of the hands tell me it took about 1h15m from the time I turned it on to the time I made the video!

2 Likes

Nifty… We had some clock discussion a while back, especially about how to handle the seconds and so on. And Ben posted a nice clock+ himself.

GitHub - romeovs/creep: a pretty sweet 4px wide pixel font. ← nice 4px wide font :relaxed:

3x5 font… (Technically 3x6, uses descendants)
https://robey.lag.net/2010/01/23/tiny-monospace-font.html

That would allow you to do 4 letters (3pixels plus pixel between) in 16 pixels wide… but only 2 rows for 16x16… So 8 letters. Obviously much better if scrolling… Or using 32x8 matrix (8 letters in a row!)

Adapting Jeff’s scrolling code for this, which uses 8bit font compacted into 32 bit storage, would make sense, and be really efficient… You’d get 8-10 characters packed depending on how you packed it. (8 is easy, 10 would be annoyingly awkward). Or store it per character, it would be one 32 bit value per character: 3x6 is 18, and we have 32 bits… Heck, the few descendants can be tweaked to give you a pure 3X5 or just 15 bits… So store it as a half a 32 value… Wow… Just 13 bytes for 26 letters? The first 32 are redundant, so the entire font (all 96 chars!) would fit in just 48 32-bit values? That’s tiny.

Cleaned up and optimized. In particular the neat thing here is:
function WuLine(pfn, x0, y0, x1, y1, h, s, v) {

… where pfn is a function, and I use two different blending functions in the clock!

// This pattern creates a rectangular framebuffer
// and does its own integer coordinate mapping
// and draws an analog clock
// with sweeping hands
// using Xiaolin Wu's antialiased line algorithm.

var blur = 0
export function sliderMotion_Blur(v) {
  blur = v * 0.95
}

// export var hour, minute, second, hx, hy, mx, my, sx, sy

// Duplicate PixelBlaze 'Mapper' functionality without normalizing

var width = 16
var height = width
var mid = width/2 - 0.5
var midm1 = mid - 1
var midm2 = mid - 2
var hlen = midm2 * 2/3

var coordmap = array(pixelCount)

for (index=0; index<pixelCount; index++) {
  x = floor(index / height)
  y = index % height
  y = x % 2 == 1 ? height - 1 - y : y // I have a zigzag LED matrix
  coords = array(2)
  coords[0] = x
  coords[1] = y
  coordmap[index] = coords
}

// HSV to RGB using global variables

var h, s, v // filled by HSVtoRGB

function HSVtoRGB(h, s, v) {
    var i, f, p, q, t;
    i = floor(h * 6); 
    f = h * 6 - i;
    p = v * (1 - s); 
    q = v * (1 - f * s); 
    t = v * (1 - (1 - f) * s); 
    im6 = i % 6 
    if (im6 == 0) {
      r = v; g = t; b = p;
    } else { if (im6 == 1) {
      r = q; g = v; b = p;
    } else { if (im6 == 2) {
      r = p; g = v; b = t;
    } else { if (im6 == 3) {
      r = p; g = q; b = v;
    } else { if (im6 == 4) {
      r = t; g = p; b = v;
    } else {if (im6 == 5) {
      r = v; g = p; b = q;
    }}}}}}
}

// RGB framebuffer
// ... Storage 

var Pr = array(width); for (i=0; i<width; i++) Pr[i] = array(height)
var Pg = array(width); for (i=0; i<width; i++) Pg[i] = array(height)
var Pb = array(width); for (i=0; i<width; i++) Pb[i] = array(height)

// ... Render a pixel
//  - Only used by WuLine, so we do 'alpha' here to keep WuLine readable
//  - Uses max() so overlapping line pixels don't make bright spots

function PixelMax(x,y,a,h,s,v) {
  if (x >= 0 && x < width && y >= 0 && y < height) {
    HSVtoRGB(h,s,v*a*a*a)
    Pr[x][y] = max(Pr[x][y], r)
    Pg[x][y] = max(Pg[x][y], g)
    Pb[x][y] = max(Pb[x][y], b)
  }
}

// just attenuate the existing pixel data by a
function PixelMultiply(x,y,a,h,s,v) {
  if (x >= 0 && x < width && y >= 0 && y < height) {
    Pr[x][y] = clamp(Pr[x][y] * a, 0, 1)
    Pg[x][y] = clamp(Pg[x][y] * a, 0, 1)
    Pb[x][y] = clamp(Pb[x][y] * a, 0, 1)
  }
}


// Xiaolin Wu's antialiased line algorithm
// Adapted from https://gist.github.com/polyamide/3f33cb4dc69e22fbf8b66cee39b78d60
// (replaced utility functions with PixelBlaze built-ins)

function WuLine(pfn, x0, y0, x1, y1, h, s, v) {
  if (x0 == x1 && y0 == y1) return

  steep = abs(y1 - y0) > abs(x1 - x0);

  if (steep) {
    tmp = y0; y0 = x0; x0 = tmp;
    tmp = y1; y1 = x1; x1 = tmp;
  }

  if (x0 > x1) {
    tmp = x0; x0 = x1; x1 = tmp;
    tmp = y0; y0 = y1; y1 = tmp;
  }

  dx = x1 - x0;
  dy = y1 - y0;
  gradient = dy / dx;

  xEnd = round(x0);
  yEnd = y0 + gradient * (xEnd - x0);
  xGap = 1 - frac(x0 + 0.5);
  xPx1 = xEnd;
  yPx1 = trunc(yEnd);

  if (steep) {
    pfn(yPx1, xPx1, 1 - frac(yEnd) * xGap, h, s, v )
    pfn(yPx1 + 1, xPx1, frac(yEnd) * xGap, h, s, v )
  } else {
    pfn(xPx1, yPx1, 1 - frac(yEnd) * xGap, h, s, v )
    pfn(xPx1, yPx1 + 1, frac(yEnd) * xGap, h, s, v )
  }

  intery = yEnd + gradient;

  xEnd = round(x1);
  yEnd = y1 + gradient * (xEnd - x1);
  xGap = frac(x1 + 0.5);

  xPx2 = xEnd;
  yPx2 = trunc(yEnd);

  if (steep) {
    pfn(yPx2, xPx2, 1 - frac(yEnd) * xGap, h, s, v )
    pfn(yPx2 + 1, xPx2, frac(yEnd) * xGap, h, s, v )
  } else {
    pfn(xPx2, yPx2, 1 - frac(yEnd) * xGap, h, s, v )
    pfn(xPx2, yPx2 + 1, frac(yEnd) * xGap, h, s, v )
  }

  if (steep) {
    for (x = xPx1 + 1; x <= xPx2 - 1; x++) {
      pfn(trunc(intery), x, 1 - frac(intery), h, s, v )
      pfn(trunc(intery) + 1, x, frac(intery), h, s, v )
      intery = intery + gradient;
    }
  } else {
    for (x = xPx1 + 1; x <= xPx2 - 1; x++) {
      pfn(x, trunc(intery), 1 - frac(intery), h, s, v )
      pfn(x, trunc(intery) + 1, frac(intery), h, s, v )
      intery = intery + gradient
    }
  }
}

// Render-related variables persistent between frames
export var lastsecond = -1
export var ms = 0

// Clear (or fade if blur enabled) framebuffer
// and render the current time

export function beforeRender(delta) {
  for (i=0; i<width; i++) {
    for (j=0; j<height; j++) {
      Pr[i][j] = blur * Pr[i][j]
      Pg[i][j] = blur * Pg[i][j]
      Pb[i][j] = blur * Pb[i][j]
    }
  }
  
  // get the time
  second = clockSecond()

  if (second == lastsecond) {
    ms = clamp(ms + delta, 0, 1000)
  } else {
    ms = 0
    lastsecond = second
  }
  
  second = (second + ms/1000) % 60
  minute = (clockMinute() + second/60) % 60
  hour = (clockHour() + minute/60) % 12
  
  hangle = PI * (hour/6)
  mangle = PI * (minute/30)
  sangle = PI * (second/30)
  uangle = PI * (ms/500)
  
  hx = clamp(sin(hangle), -1,1) * hlen
  hy = clamp(-cos(hangle),-1,1) * hlen
  mx = clamp(sin(mangle), -1,1) * midm2
  my = clamp(-cos(mangle),-1,1) * midm2
  sx = clamp(sin(sangle), -1,1) * midm1
  sy = clamp(-cos(sangle),-1,1) * midm1
  su = sin(uangle)
  cu = -cos(uangle)
  ux = clamp(su, -1,1) * mid
  uy = clamp(cu,-1,1) * mid
  vx = clamp(su, -1,1) * midm1
  vy = clamp(cu,-1,1) * midm1

  WuLine(PixelMax, mid, mid, mid + hx, mid + hy, 0, 0, 1)
  WuLine(PixelMax, mid, mid, mid + mx, mid + my, 1/3, 0.9, 1)
  WuLine(PixelMax, mid, mid, mid + sx, mid + sy, 0, 0.9, 1)
  WuLine(PixelMax, mid + vx, mid + vy, mid + ux, mid + uy, 2/3, 2/3, 1/4)
  
  for (h = 0; h < 12; h++) {
    ha = PI * (h/6)
    sh = sin(ha)
    ch = -cos(ha)
    x1 = mid + clamp(sh, -1,1) * midm1
    y1 = mid + clamp(ch,-1,1) * midm1
    x2 = mid + clamp(sh, -1,1) * mid
    y2 = mid + clamp(ch,-1,1) * mid
    
    WuLine(PixelMultiply, x1, y1, x2, y2, 1, 1, 1)
  }
}

// Draw from the framebuffer

export function render(index) {
  coords = coordmap[index]
  x = coords[0]
  y = coords[1]

  rgb(Pr[x][y], Pg[x][y], Pb[x][y])
}
1 Like