LEDaliClock matrix pattern

I saw the “alpha” blending in the code and figured this was easier to do in RGB mode. This probably helps make the junctions of the wulines more smooth.

The morphing is super cool!

I went with RGB because that’s what the existing Wu line implementation used… and if I went with HSV, I’d have to include my own HSVtoRGB code. I’m going to give that a shot because I think it will help resolve the gamma issues. In my previous bouncing lines demo, setting gamma to make the AA smooth ruined the color balance.

That ‘gamma and antialiasing’ link I posted implies that ~sqrt(2) is a good gamma level, but that will vary by LED type and brightness.

This is a SMOP so I’ll post an updated version soon!

It’s called ‘a’ for alpha for historical reasons at this point. It’s really a brightness pre-multiplier, and then PixelMax only updates components that are brighter, which is indeed as you suggest to make the junctions smooth.

The AA is definitely better with HSV = (Digit color, 1, a×a×a) where a is the value from the AA algorithm. Down to 30fps but that’s no surprise!

// This pattern creates a rectangular framebuffer
// and does its own integer coordinate mapping
// and draws a digital clock
// with morphing digits
// using Xiaolin Wu's antialiased line algorithm.

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

// Duplicate PixelBlaze 'Mapper' functionality without normalizing

var width = 32
var height = 8

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 32x8 LED matrix
  coords = array(2)
  coords[0] = x
  coords[1] = y
  coordmap[index] = coords
}

// HSV to RGB using global variables

var r, g, b // 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) {
  //x = floor(x)
  //y = floor(y)
  HSVtoRGB(h,s,v*a*a*a)
  if (x >= 0 && x < width && y >= 0 && y < height) {
    Pr[x][y] = max(Pr[x][y], r)
    Pg[x][y] = max(Pg[x][y], g)
    Pb[x][y] = max(Pb[x][y], b)
  }
}

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

function WuLine(x0, y0, x1, y1, r, g, b) {
  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) {
    PixelMax(yPx1, xPx1, 1 - frac(yEnd) * xGap, r, g, b )
    PixelMax(yPx1 + 1, xPx1, frac(yEnd) * xGap, r, g, b )
  } else {
    PixelMax(xPx1, yPx1, 1 - frac(yEnd) * xGap, r, g, b )
    PixelMax(xPx1, yPx1 + 1, frac(yEnd) * xGap, r, g, b )
  }

  intery = yEnd + gradient;

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

  xPx2 = xEnd;
  yPx2 = trunc(yEnd);

  if (steep) {
    PixelMax(yPx2, xPx2, 1 - frac(yEnd) * xGap, r, g, b )
    PixelMax(yPx2 + 1, xPx2, frac(yEnd) * xGap, r, g, b )
  } else {
    PixelMax(xPx2, yPx2, 1 - frac(yEnd) * xGap, r, g, b )
    PixelMax(xPx2, yPx2 + 1, frac(yEnd) * xGap, r, g, b )
  }

  if (steep) {
    for (x = xPx1 + 1; x <= xPx2 - 1; x++) {
      PixelMax(trunc(intery), x, 1 - frac(intery), r, g, b )
      PixelMax(trunc(intery) + 1, x, frac(intery), r, g, b )
      intery = intery + gradient;
    }
  } else {
    for (x = xPx1 + 1; x <= xPx2 - 1; x++) {
      PixelMax(x, trunc(intery), 1 - frac(intery), r, g, b )
      PixelMax(x, trunc(intery) + 1, frac(intery), r, g, b )
      intery = intery + gradient
    }
  }
}

// Digit data - 5x8 pixels
// - each digit is 7 sequential lines
//   represented by a list of 8 x,y coordinates 

var Digits = array(10)
for (i=0;i<10;i++){
  Digits[i]=array(8)
  for (j=0;j<8;j++){
    Digits[i][j]=array(2)
  }
}

Digits[0][0][0] = 1
Digits[0][0][1] = 0
Digits[0][1][0] = 3
Digits[0][1][1] = 0
Digits[0][2][0] = 4
Digits[0][2][1] = 1
Digits[0][3][0] = 4
Digits[0][3][1] = 6
Digits[0][4][0] = 3
Digits[0][4][1] = 7
Digits[0][5][0] = 1
Digits[0][5][1] = 7
Digits[0][6][0] = 0
Digits[0][6][1] = 6
Digits[0][7][0] = 0
Digits[0][7][1] = 1
Digits[1][0][0] = 0
Digits[1][0][1] = 2
Digits[1][1][0] = 2
Digits[1][1][1] = 0
Digits[1][2][0] = 2
Digits[1][2][1] = 1
Digits[1][3][0] = 2
Digits[1][3][1] = 3
Digits[1][4][0] = 2
Digits[1][4][1] = 5
Digits[1][5][0] = 2
Digits[1][5][1] = 7
Digits[1][6][0] = 0
Digits[1][6][1] = 7
Digits[1][7][0] = 4
Digits[1][7][1] = 7
Digits[2][0][0] = 0
Digits[2][0][1] = 1
Digits[2][1][0] = 1
Digits[2][1][1] = 0
Digits[2][2][0] = 3
Digits[2][2][1] = 0
Digits[2][3][0] = 4
Digits[2][3][1] = 1
Digits[2][4][0] = 4
Digits[2][4][1] = 2
Digits[2][5][0] = 0
Digits[2][5][1] = 6
Digits[2][6][0] = 0
Digits[2][6][1] = 7
Digits[2][7][0] = 4
Digits[2][7][1] = 7
Digits[3][0][0] = 0
Digits[3][0][1] = 1
Digits[3][1][0] = 2
Digits[3][1][1] = 0
Digits[3][2][0] = 4
Digits[3][2][1] = 1
Digits[3][3][0] = 2
Digits[3][3][1] = 3
Digits[3][4][0] = 3
Digits[3][4][1] = 4
Digits[3][5][0] = 4
Digits[3][5][1] = 6
Digits[3][6][0] = 2
Digits[3][6][1] = 7
Digits[3][7][0] = 0
Digits[3][7][1] = 6
Digits[4][0][0] = 3
Digits[4][0][1] = 0
Digits[4][1][0] = 3
Digits[4][1][1] = 3
Digits[4][2][0] = 4
Digits[4][2][1] = 3
Digits[4][3][0] = 3
Digits[4][3][1] = 3
Digits[4][4][0] = 3
Digits[4][4][1] = 7
Digits[4][5][0] = 3
Digits[4][5][1] = 3
Digits[4][6][0] = 0
Digits[4][6][1] = 3
Digits[4][7][0] = 0
Digits[4][7][1] = 0
Digits[5][0][0] = 4
Digits[5][0][1] = 0
Digits[5][1][0] = 0
Digits[5][1][1] = 0
Digits[5][2][0] = 0
Digits[5][2][1] = 4
Digits[5][3][0] = 2
Digits[5][3][1] = 3
Digits[5][4][0] = 4
Digits[5][4][1] = 4
Digits[5][5][0] = 4
Digits[5][5][1] = 6
Digits[5][6][0] = 2
Digits[5][6][1] = 7
Digits[5][7][0] = 0
Digits[5][7][1] = 6
Digits[6][0][0] = 3
Digits[6][0][1] = 0
Digits[6][1][0] = 2
Digits[6][1][1] = 0
Digits[6][2][0] = 0
Digits[6][2][1] = 2
Digits[6][3][0] = 0
Digits[6][3][1] = 6
Digits[6][4][0] = 2
Digits[6][4][1] = 7
Digits[6][5][0] = 4
Digits[6][5][1] = 5
Digits[6][6][0] = 3
Digits[6][6][1] = 3
Digits[6][7][0] = 1
Digits[6][7][1] = 3
Digits[7][0][0] = 0
Digits[7][0][1] = 0
Digits[7][1][0] = 2
Digits[7][1][1] = 0
Digits[7][2][0] = 4
Digits[7][2][1] = 0
Digits[7][3][0] = 4
Digits[7][3][1] = 1
Digits[7][4][0] = 2
Digits[7][4][1] = 3
Digits[7][5][0] = 1
Digits[7][5][1] = 5
Digits[7][6][0] = 1
Digits[7][6][1] = 7
Digits[7][7][0] = 2
Digits[7][7][1] = 7
Digits[8][0][0] = 1
Digits[8][0][1] = 0
Digits[8][1][0] = 3
Digits[8][1][1] = 0
Digits[8][2][0] = 4
Digits[8][2][1] = 1
Digits[8][3][0] = 0
Digits[8][3][1] = 5
Digits[8][4][0] = 1
Digits[8][4][1] = 7
Digits[8][5][0] = 3
Digits[8][5][1] = 7
Digits[8][6][0] = 4
Digits[8][6][1] = 5
Digits[8][7][0] = 0
Digits[8][7][1] = 1
Digits[9][0][0] = 3
Digits[9][0][1] = 3
Digits[9][1][0] = 1
Digits[9][1][1] = 3
Digits[9][2][0] = 0
Digits[9][2][1] = 1
Digits[9][3][0] = 1
Digits[9][3][1] = 0
Digits[9][4][0] = 3
Digits[9][4][1] = 0
Digits[9][5][0] = 4
Digits[9][5][1] = 1
Digits[9][6][0] = 4
Digits[9][6][1] = 3
Digits[9][7][0] = 1
Digits[9][7][1] = 7

// lerp is used on the line ends to morph the digits

function lerp(t,a,b){
  return a*(1-t)+b*t
}

// Draw a Digit
// - starting at x,y 
// - morph each line end between d1,d2 using t (0..1)
// - draw WuLine with r,g,b

function DrawDigit(x,y,dd1,dd2,t,r,g,b) {
  d1 = Digits[dd1]

  if (dd1==dd2) { // skip lerp if digits are the same
    for (L=0;L<7;L++) {
      WuLine(
        x + d1[L][0],
        y + d1[L][1],
        x + d1[L+1][0],
        y + d1[L+1][1],
        r, g, b
      )
    }
  } else {
    d2 = Digits[dd2]
    for (L=0;L<7;L++) {
      WuLine(
        x + lerp(t,d1[L][0],d2[L][0]),
        y + lerp(t,d1[L][1],d2[L][1]),
        x + lerp(t,d1[L+1][0],d2[L+1][0]),
        y + lerp(t,d1[L+1][1],d2[L+1][1]),
        r, g, b
      )
    }
  }
}

// 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]
    }
  }
  
  // Extract digits for each part of current time
  // and next time unit with 'n' prefix
  // (e.g. 'nm2' is second digit of next minute)
  // and collect milliseconds from delta in 'ms'
  hour = clockHour()
  h2 = hour % 10
  h1 = (hour - h2)/10

  nexthour = (hour + 1) % 24
  nh2 = nexthour % 10
  nh1 = (nexthour - nh2)/10
  
  minute = clockMinute()
  m2 = minute % 10
  m1 = (minute - m2)/10
  
  nextminute = (minute + 1) % 60
  nm2 = nextminute % 10
  nm1 = (nextminute - nm2)/10
  
  second = clockSecond()
  s2 = second % 10
  s1 = (second - s2)/10
  
  nextsecond = (second + 1) % 60
  ns2 = nextsecond % 10
  ns1 = (nextsecond - ns2)/10
  
  if (second == lastsecond) {
    ms = clamp(ms + delta, 0, 1000)
  } else {
    ms = 0
    lastsecond = second
  }
  
  // transition to next second digit
  // with wave transition around (0.1 .. 0.9)
  // hint: 1000 / 1428.57142 =~ 0.7
  ts = clamp( wave(clamp(ms/1428.57142-0.1,0,1)-0.25), 0,1)
  tm = second == 59 ? ts : 0
  th = (second == 59 && minute == 59) ? tm : 0
  
  // Rainbow digits because they are right next to each other
  DrawDigit(0,0,h1,nh1,th,0,1,1)
  DrawDigit(5,0,h2,nh2,th,1/6,1,1)
  DrawDigit(11,0,m1,nm1,tm,1/3,1,1)
  DrawDigit(16,0,m2,nm2,tm,1/2,1,1)
  DrawDigit(22,0,s1,ns1,ts,2/3,1,1)
  DrawDigit(27,0,s2,ns2,ts,5/6,1,1)

  // draw pulsing white 'second' dots in remaining 2 columns
  br = (1-ms/1000)
  Pr[10][1] = br
  Pr[10][2] = br
  Pr[21][1] = br
  Pr[21][2] = br
  Pr[10][5] = br
  Pr[10][6] = br
  Pr[21][5] = br
  Pr[21][6] = br
  Pg[10][1] = br
  Pg[10][2] = br
  Pg[21][1] = br
  Pg[21][2] = br
  Pg[10][5] = br
  Pg[10][6] = br
  Pg[21][5] = br
  Pg[21][6] = br
  Pb[10][1] = br
  Pb[10][2] = br
  Pb[21][1] = br
  Pb[21][2] = br
  Pb[10][5] = br
  Pb[10][6] = br
  Pb[21][5] = br
  Pb[21][6] = br
}

// 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])
}

Definitely better antialiasing appearance and motion blur!

In this video I constrained the motion blur to the middle 50% of the second instead of 70%, and had a high motion blur. The AA is a bit hard to see in the video but looks spiffy. The bouncing lines demo is also much improved with this technique. So cool that I will make a video of that next!

AA lines looking much better with HSV mixing:

Thanks for the fruitful discussion @jeff and @Scruffynerf !

These AA lines make me want to make a version of Qix … I wonder how low-res that could be playable. I first played it as Styx which ran at only 160x100: Styx IBM PC CGA Game

1 Like

I did a graph that showed that while the usual gamma code for LEDs wasn’t exactly the same, using either V squared or better yet V cubed, it was pretty close curve wise, and way cheap to calculate.

Yes, your LEDs may be different but without some specific testing to confirm, it’ll likely still be within the expected range, and this would be “good enough”

Yeah, cubed looked about right for me… in my dimly-lit attic. It might be different outside, where the LEDs on full power will give the sun a run for its money!

I have a 32x8 matrix, but I’ve got my 16x16 hooked up right now, and playing with this, and if I set it to 16x16, I get 3 numbers (and blinkers) on half the matrix. I could modify this to add the other 3 numbers under it, but then it’ll be awkward:

12:3
4:56

so I’m thinking a 16x16 layout would be better as
1:3:5
2 4 6

will continue to play with it…

1 Like

The stacked values would work, but then there is only one extra column per row for the blinkers.

Don’t worry — for my next trick, I’m making an analog clock with sweeping second hand for 16x16 matrices. Also, a radar screen!

1 Like

In the process of removing all RGB (among other things)…

Blocky numbers look good…
But dammit, now I want a full 5x8 (really 5x7) font as vector lines, for a Dali-text pattern…

I’m torn between vector and bitmap font though… Both are useful in different ways.

Have to revisit the text scroller pattern.

1 Like

If you keep to straight and 45° lines then it will look just like a bitmap font, except when it’s morphing. I think every useful character could be done like that with maybe 10 lines (try it and see! :stuck_out_tongue: ). As with all such projects, all it requires is a pencil and paper …

… but the lack of support for characters in PB’s pidgin JS will make it harder to use than digits were. Maybe the best approach would be like the mapper: Type a string into the web interface, and it will set a global variable array of asc(character) for lookup, or maybe even store the vector fonts on the web side somewhere and have a string → array of arrays mapper.

However, it’s not impossible to input text directly on the PB. I once had Hokey Spokes which had two buttons - one to step through the alphabet, another to step to the next character. It also would talk to a palm pilot over IR, and the spokes would keep their patterns in sync over IR too.

1 Like

@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