Task #13: The wheels on the pattern go round and round

For lucky #13, we’re going to focus on a particular function. This came up in a discussion on synchronizing patterns between multiple PBs, but the key piece is so important to understanding a vital part of PB, it’s worth deep diving.

Time() keeps on slipping slipping slipping into the future…

What does time() do?

You can then use wave() or triangle () to alter this behavior/pattern if need be.

If we’ve talked about the PB as a two stroke engine (pre-render then render, over and over), then time() is the transmission to the gears. Remember, you decide the interval, do you want a long slow buildup from 0 to 1?.. If you give it a value of 1, the “gear” takes 65 seconds to spin around one turn. Want it faster, lower the interval, want it slower, raise it.

gear_a = time(1)
gear_b = time(2)

So gear b will take ~130 seconds to turn once, going from 0 to 1 over that timeframe, and then resetting to 0.

Gear_c = time(.5)
This will take about 32.5 seconds to do the same range.

Your task, this week:
Draw multiple LINE lengths. (Each length will go across the matrix, 0 being almost no line, 1 being the entire row/column)
Make them each connected to time()
Add in sliders to adjust, maybe allow using wave or triangle, and see if you can visually see the difference of each.

(Matrixless folks, you can also do this task, just make them all in a line…)

Bonus: discover new ways to use time() and share.

@jeff I recall you wrote up a good walk through on this recently… Link?

Hi! Which concept? I had the recent write up on how I do speed sliders with time().

Is the idea of the challenge to make a line drawing function that you pass lengths into? And are we mostly trying for horizontal and vertical? I know we’re always generous with interpretation which is the fun part, but just checking to see if I understand the intent.

Actually it was a video “write-up” that I felt was really good on how time works

Yes, I intentionally left it vague:

For those getting starting, they can do simple rows or columns that change size (get longer) based on time() [Think of a spectrometer display but instead of sound, the bars cycle based on timing… So one bar grows/shrinks at the one minute rate, another at 3 minutes, another at 10 seconds, and so on]

If you want to write a generic line drawing function, have at it. Always useful! That’s way harder than I envisioned to also be “time based” (I can picture a way to do it, of course, but definitely more advanced… But that’s the fun, as you said.)

Just came across this, by the author of Soulmate. It’s a great blog post. Code is C but the technique is great.

https://www.soulmatelights.com/blog/1-leds-lines-and-linear-algebra

The distance function line drawing is super cool, but the classic still works: Bresenham algorithm in Javascript - Stack Overflow

1 Like

Yes that’s a good basic one too… But it yields only the bare minimum of making a line between two given points. The distance one gives you a much fuzzier way to make more than single pixel lines, so you could antialias (lower brightness) and so on

But yes, that’s probably what I would have written as a “do the minimum to draw a line”

Indeed, the distance one is fuzzier, but Bresenham gives sharper antialiased lines, including thick lines … found some more code:
http://members.chello.at/~easyfilter/bresenham.html

1 Like

oh great, more stuff to port into a library! :grinning:

FWIW that Bresenham AA line code is broken. :roll_eyes:

Fortunately the Xiaolin Wu algorithm works perfectly, see below!

// This pattern creates a rectangular framebuffer
// and does its own integer coordinate mapping
// and draws a few lines using Xiaolin Wu's antialiased line algorithm.

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

// Duplicate PixelBlaze 'Mapper' functionality without normalizing

var width = 8
var height = 32

var coordmap = array(pixelCount)

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

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

// ... Increment a pixel, with 'alpha' multiplication here to keep WuLine sane

function PixelAdd(x,y,a,r,g,b) {
  x = floor(x)
  y = floor(y)
  if (x >= 0 && x < width && y >= 0 && y < height) {
    Pr[x][y] = clamp(Pr[x][y] + r*a, 0, 1)
    Pg[x][y] = clamp(Pg[x][y] + g*a, 0, 1)
    Pb[x][y] = clamp(Pb[x][y] + b*a, 0, 1)
  }
}

// Xiaolin Wu's antialiased line algorithm
// Adapted from https://gist.github.com/polyamide/3f33cb4dc69e22fbf8b66cee39b78d60
// to replace 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) {
    PixelAdd(yPx1, xPx1, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(yPx1 + 1, xPx1, frac(yEnd) * xGap, r, g, b )
  } else {
    PixelAdd(xPx1, yPx1, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(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) {
    PixelAdd(yPx2, xPx2, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(yPx2 + 1, xPx2, frac(yEnd) * xGap, r, g, b )
  } else {
    PixelAdd(xPx2, yPx2, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(xPx2, yPx2 + 1, frac(yEnd) * xGap, r, g, b )
  }

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

// Clear (or fade if blur enabled) framebuffer
// and render a few bouncy lines

export function beforeRender(delta) {
  for (i=0; i<width; i++) {
    for (j=0; j<height; j++) {
      Pr[i][j] = blur ? Pr[i][j]/2 : 0
      Pg[i][j] = blur ? Pg[i][j]/2 : 0
      Pb[i][j] = blur ? Pb[i][j]/2 : 0
    }
  }

  t1 = time(.03)
  t2 = time(.05)
  t3 = time(.07)
  t4 = time(.11)
  t5 = time(.13)
  
  wm1 = width-1
  hm1 = height-1

  WuLine(
    wm1 * wave(t1),
    hm1 * triangle(t2),
    wm1 * wave(t3),
    hm1 * wave(t4),
    triangle(t5), wave(t3)/2, square(t1,0.5)
  )

  WuLine(
    wm1 * (1-wave(t3)),
    hm1 * wave(t4),
    wm1 * wave(t2),
    hm1 * triangle(t5),
    0, wave(t1), 1-wave(t1)
  )

  WuLine(
    wm1 * wave(t4),
    hm1 * triangle(t1),
    wm1 * wave(t5),
    hm1 * wave(t2),
    0.75,0.5,-2 // this line subtracts blue
  )
}

// Draw from the framebuffer

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

  r = Pr[x][y]
  g = Pg[x][y]
  b = Pb[x][y]
  
  rgb(r*r,g*g,b*b)
}
2 Likes

… just for the record …

Code for HSV version
// This pattern creates a rectangular framebuffer
// and does its own integer coordinate mapping
// and draws a few lines 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 8x32 LED matrix
  coords = array(2)
  coords[0] = x
  coords[1] = y
  coordmap[index] = coords
}

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

// 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 PixelAdd(x,y,a,h,s,v) {
  //x = floor(x)
  //y = floor(y)
  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)
  }
}

// Xiaolin Wu's antialiased line algorithm
// Adapted from https://gist.github.com/polyamide/3f33cb4dc69e22fbf8b66cee39b78d60
// to replace 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) {
    PixelAdd(yPx1, xPx1, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(yPx1 + 1, xPx1, frac(yEnd) * xGap, r, g, b )
  } else {
    PixelAdd(xPx1, yPx1, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(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) {
    PixelAdd(yPx2, xPx2, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(yPx2 + 1, xPx2, frac(yEnd) * xGap, r, g, b )
  } else {
    PixelAdd(xPx2, yPx2, 1 - frac(yEnd) * xGap, r, g, b )
    PixelAdd(xPx2, yPx2 + 1, frac(yEnd) * xGap, r, g, b )
  }

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

// Clear (or fade if blur enabled) framebuffer
// and render a few bouncy lines

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

  t1 = time(.03)
  t2 = time(.05)
  t3 = time(.07)
  t4 = time(.11)
  t5 = time(.13)
  
  wm1 = width-1
  hm1 = height-1

  WuLine(
    wm1 * wave(t1),
    hm1 * triangle(t2),
    wm1 * wave(t3),
    hm1 * wave(t4),
    wave(t1),1,1
    // triangle(t5), wave(t3)/2+0.5, square(t1,0.5)/2 + 0.5
  )

  WuLine(
    wm1 * (1-wave(t3)),
    hm1 * wave(t4),
    wm1 * wave(t2),
    hm1 * triangle(t5),
    1/3, 1, 1
    // 0, wave(t1), 1-wave(t1)/2
  )

  WuLine(
    wm1 * wave(t4),
    hm1 * triangle(t1),
    wm1 * wave(t5),
    hm1 * wave(t2),
    2/3, 1, 1
    // 0.75,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