Help: Understanding the basics of the canvas and mapping

Hi All,

Newbie here.

I get the idea that the patterns (2D) drawn in a canvas are scaled to the coordinates of the pixelMap.

Being used to framebuffers I am bit struggling with the x, y variables in the render2D function.

  1. x, y variables in the render function, are they the coordinates of the canvas (where the math functions are drawn) which is stated by Wizard as ranging from 0 to 1 , or are they the x,y values from the pixel map ?

  2. canvas to pixel map scaling , how it works ?

Lets says I need to draw a circle on a 4x4 matrix of led , for the sake of understanding lets say we need to lit two middle pixels each in first and last row and first & last pixels of middle two rows, how the translation happens from the canvas coordinates x,y i.e. ranging 0 to 1 to the Pixel MAP ?

0 1 1 0
1 0 0 1
1 0 0 1
0 1 1 0

Example Map : [x,y] pixel map provided for a zig-zag 4x4 led matrix as

[[0,0],[0,1],[0,2],[0,3], [1,0],[1,1],[1,2],[1,3], [2,0],[2,1],[2,2],[2,3], [3,0],[3,1],[3,2],[3,3]]

I can partially get it in my head but don’t want to speculate than hearing from the experts here. Thanks in advance.

The pixel map gets scaled/normalized so all points fit within a unit square, so the render2D() call will only ever receive x and y values ranging from 0 to 1. The Pixelblaze does this by dividing all the x values in your map by the maximum x value in your map, and similarly for y. So for the example map you provide, this will be converted to approximately(!) the following:

[
  [0,    0], [0,    0.333], [0,    0.667], [0,    0.999],
  [0.333,0], [0.333,0.333], [0.333,0.667], [0.333,0.999], 
  [0.667,0], [0.667,0.333], [0.667,0.667], [0.667,0.999], 
  [0.999,0], [0.999,0.333], [0.999,0.667], [0.999,0.999]
]

(With the latest firmware you can also now get the Pixelblaze to retain the aspect ratio of your map. It wouldn’t make any difference in the above example, but if you had a map with a maximum x value of 10 but the maximum y value was only 5, then the scaled map would have a maximum x value of either 0.999 or 0.499 depending on this setting. The maximum x value would be 0.999 regardless since it is the larger dimension of the two).

If you want to convert your 4x4 map back to framebuffer like x and y values then you can do something like this:

// At the top of your pattern
width = 4
height = pixelCount / width
// Alternatively, for a square grid:
width = sqrt(pixelCount)
height = width

export render2D(index, x, y) {
  x = x * width
  y = y * height
  ...
}

That said, I would recommend avoiding the above when possible because you can often get much nicer results if instead of framebuffers you invert your thinking and consider a render2D() call to be taking a point sample from a mathematical expression of your desired pattern. That’s pretty abstract, so as something more concrete let’s look at your circle example using this approach:

function circle(x, y, r) {
  return hypot(x, y) - r
}

circleThreshold = 0.1
circleRadius = 0.45

export function render2D(index, x, y) {
  // The circle center is [0.5, 0.5]
  dx = x - 0.5
  dy = y - 0.5

  // Figure out how far away this point is from our circle
  dist = abs(circle(dx, dy, circleRadius))

  // If this point is close enough to the circle, render it
  if (dist < circleThreshold) {
    // The brightness of the pixel depends on how close to the circle this point is
    v = 1 - dist / circleThreshold
    hsv(0, 0, v)
  } else {
    rgb(0,0,0)
  }
}

NOTE: The above code was written off the top of my head and is completely untested as I don’t have a Pixelblaze handy right now. Apologies if it doesn’t compile or the maths is messed up, but hopefully you get the general idea and can fix any bugs. Please let me know and I’ll correct them for future readers!

A 4x4 grid is probably too small for the above code to look very good, and maybe the threshold and radius values need tweaking, but with a bigger matrix the benefits will become very obvious compared to a Bresenham’s circle algorithm type approach. You get antialiasing / subpixel rendering out of the box, and you can change the center / radius / thickness using floating point values that will animate in a very smooth way compared to an integer framebuffer approach. It also means pixels can be arbitrarily located in 2D (or 3D) space rather than constrained to a grid.

I’d suggest looking at some of the more recent patterns in the pattern library for deeper understanding and inspiration of how to write patterns in this way. It takes a bit of getting used to, but is very powerful compared to framebuffers.

1 Like

@ChrisNZ Thanks very much for the prompt response.

Your code works well and guides the thinking process in terms of math functions and parameters to draw the animations. I can quickly modify it to get a nice bloom effect , offset the centre and radius of the circle.

I have gone through other patterns but wanted to clarify & confirm on the basic approach.

  1. Is it fair to say that we are not drawing on any other canvas but the scaled map itself is the canvas and we just iterate for every pixel for every frame and use the math/logic to lit or not the current pixel ?

  2. still wondering if I need to add or subtract the previous frame can it be achieved without a frame buffer ? for example if I have to show the clock with two hands which will not change/animate for a while and merge that with a background animation like blooming how do I approach ?

With respect to the great circle example that you have provided , I have challenged myself an exercise to rewrite the circle with general circle equation for concentric circles (may be there is a better logic/thought process again)

x=centreX + radius * math.cos(theta) , y= centreY + *radius * math.sin(theta) ,

and also possibly will try the mid point approach . But I am sure I may nudge you & others again for help. Thanks kindly.

I’m glad (and amazed…) to hear the code worked as-is! :joy: It sounds like you understand correctly and are on the right track.

  1. Yes that’s correct, the Pixelblaze doesn’t have a buffer (even internally), it just calls render2D() in the order it needs to output the pixels.
  2. If you do need access to data from a previous frame then you’ll need to create your own array to act as a buffer for the previous frame and manage it yourself. Take a look at e.g. the “Doom Fire (v2.0) 2D” effect on (currently) page 2 of the pattern library for an example of this sort of thing. Note that with the generally low number of LEDs combined with performance of the Pixelblaze means that it is often simpler just to re-render everything, i.e. in your example recalculate/redraw the clock each frame, even if it hasn’t changed.

Looks like you’ll get the concentric circles figured out very quickly. One nice thing you’ll find is that it’s often a lot easier to work with a unit map than a frame buffer for these sorts of effects because you can use floating point (it’s actually fixed point under the hood) and don’t have to scale according to the size of your frame.

You might want to also look at the transformation functions. These allow you to transform the map’s coordinates before any calls to render2D() are made. For example:

export function beforeRender() {
  // Reset the map to the unit square
  resetTransform()
  // Center the coordinates on [0,0] so we no longer need to subtract 0.5 from x and y
  // in the render2D() function of my circle example above
  translate(-0.5, -0.5)

  // wiggle all the coordinates around the orgin over time
  dx = wave(time(0.05)) / 3.0
  dy = wave(time(0.09)) / 3.0
  translate(dx, dy)
}

Once again, the above code is untested sorry but hopefully gives you the general idea!

2 Likes

For some uses you will would need to store the previous frame (like if the current frame’s pixels depend on their previous values), but if you just want to underlay your pattern with a non-blank background then you can overlay one pattern with another by replacing the “clear unused” part of the topmost pattern. Using @ChrisNZ’s example:

export function render2D(index, x, y) {

  // The circle center is [0.5, 0.5]
  dx = x - 0.5
  dy = y - 0.5

  // Figure out how far away this point is from our circle
  dist = abs(circle(dx, dy, circleRadius))

  // If this point is close enough to the circle, render it
  if (dist < circleThreshold) {
    // The brightness of the pixel depends on how close to the circle this point is
    v = 1 - dist / circleThreshold
    hsv(0, 0, v)
  } else {
    // 
    // BEGIN CHANGE
    // 
    // Instead of setting unused pixels to zero...
    // rgb(0,0,0)
    // ...calculate a different pattern as a background layer.
    hsv(time(0.1)+index/pixelCount, 1, 1);  
    // 
    // END CHANGE
    //
  }
}
1 Like

Generally if you can, prefer it without a canvas / frame buffer. Pixelblaze maps support any arbitrary coordinates, they don’t have to live on a regular grid, and using the mathy approach will mean your pattern works on anything.

You can implement a canvas / buffer of course, and many patterns do. In that case, typically you scale up the 0-1 values to the width of your internal canvas and map e.g. y to rows and x to columns. Here’s an example:
2D canvas example.epe (7.9 KB)

If you don’t need a proper 2D canvas / buffer, but only need a previous pixel’s value, you can do that with less work with an array using the pixel index only without any x, y info and works on irregular maps. too. Handy for fades, and that sort of thing. This pattern is an example:

Lissajous curve tracer.epe (12.4 KB)

1 Like

@pixie Good Day…!! Thanks for the inputs and the code sample. I get that thought process.

@wizard Thanks for pointing to the examples. I will look into them. I agree that the math form is more robust. And the thought process of moving away from framebuffers to mathy is a great brain exercise, brushing up the algebra equations & basics parallelly.