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.