Patterns for Stacked Rings

Hey all,

I’ve got this light I’m building to use as a totem to a music festival, and I’d love some help with some patterns for it.

This is the map I’m using for it:

function (pixelCount) {
  var ringSize = 57
  var map = [];
  for (i = 0; i < pixelCount; i++) {
    c = i / pixelCount * Math.PI * 2 * (pixelCount/ringSize)
    map.push([Math.cos(c), Math.sin(c), Math.floor((i)/ringSize)])
  }
  return map
}

It’ll have 5 rings in total when it’s finished.

I’d really love to have some 3D patterns that work well with the shape, but I have no idea what I’m doing programming this thing unfortunately. :confused:

What a neat build!

First here is a math nitpick (free with your forum membership!)

c = i / pixelCount * Math.PI * 2 * (pixelCount/ringSize)

is correct, but pixelCount is redundant since you’re dividing and then multiplying it. Same result but it’s easier to reason about if you write it like this:

c = (i / ringSize) * (Math.PI * 2)

I have some bouncing ball patterns that were written for a 16x16 matrix curled into a cylinder, but mapped in 2D. I’ll try rewriting the patterns for a 3D map like yours and get back to you!

3 Likes

One Pattern I’d really like to use is this:

//  from: https://forum.electromage.com/t/2d-rainbow-patterns/922/3
//
// Radial rainbow for 2D displays
export var speed = 0.5
export var direction = 1

// UI sliders

// higher is faster
export function sliderSpeed(v) {
  speed = 1-v;  
}

// left = inward, right = outward
export function sliderDirection(v) {
  direction = (v < 0.5) ? 1 : -1;  
}

// pythagorean distance from center of display.  Pixelblaze
// provides normalized x,y coords, so center is always going
// to be (0.5,0.5) regardless of real world display dimensions
function getRadius(x, y) {
  x -= 0.2; y -= 0.5;
  return sqrt(x*x + y*y);
}

// generate a timer - a sawtooth wave that we can
// use to animate color -- the direction flag makes
// it positive or negative, depending on the UI
// slider setting
export function beforeRender(delta) {
  t1 = direction * time(0.08 * speed);
}

// use radius and our timer to color every pixel
export function render3D(index, x, y) {
  hsv(t1+getRadius(x, y), 1, 1);
}

But have it radiate from the center ring up and down. I tried changing the getRadius coordinates to use z and x or z and y, but that didn’t work. So I’m not sure how to do it.

The easiest solution is to change your mapping to 2 dimensions. Full 3D is overkill for this, since the radius of the rings is fixed and there are no pixels in the interior volume. Programming in 2D is easier, framerates will be higher, and existing 2D patterns will work with little or no modification.

Try mapping x and y as an angle and a ring number (use the built-in ring mapper as a base) , or as azimuth and elevation angles, or easiest of all, you could just map it as a 57x5 matrix.

With a matrix, you might have to deal occasionally with the “seam” where it wraps around, but it’d be really straightforward to program horizontal and vertical movement effects. (Plus, you’d have all the 2D patterns in the library to play with.)

Wholeheartedly agreed! I looked into porting some of my 2D surface-of-a-cylinder patterns to 3D and there was no way for the code to be sane.

So without further ado, here’s a ball-bouncing-around-a-cylinder pattern. It was made for a 16x16 matrix so there are some hardcoded 16s. Looks like the ball is a gradient from white to some color then to black, with adjustable radii.


var br1 = 1.5/16.0
export function sliderBallRadius1(v) {
    br1 = v * (2.0/16.0)
}

var br2 = 2.0/16.0
export function sliderBallRadius2(v) {
    br2 = v * (3.0/16.0)
}

export function beforeRender(delta) {
  th = time(0.071)
  bx = 1.0 - time(0.013)
  by = triangle(time(0.017)) * (1.0-br2) // ((sin(time(0.037)*PI2) * (1.0-br2)) + 1.0) / 2.0
}

export function render2D(index,x,y) {
  h = (y < 0.5) ? th : by
  s = 1
  v = 0

  dy = abs(y-by)
  if (dy < br2) {
    mx = x
    bx2 = bx
    if ((bx < br2) || (bx > (1.0-br2))) {
      bx2 = ((bx2 + 0.5 ) % 1.0)
      mx = ((mx + 0.5 ) % 1.0)
    }

    dx = abs(mx-bx2)

    if (dx < br2) {
      distance = sqrt(dx*dx + dy*dy)
      if (distance <= br1) {
        v = 1
        s = distance / br1
      } else if (distance < br2) {
        v = 1.0 - ((distance-br1) / (br2-br1))
      }
    }
  }

  hsv(h, s, v*v)
}
1 Like

Thanks! the matrix idea seems good, but ya the “seam” is an issue I don’t know how to deal with

In the case of my bouncing circle, it looks like I just shift the world by 0.5 if it’s near the “seam”:

    if ((bx < br2) || (bx > (1.0-br2))) {
      bx2 = ((bx2 + 0.5 ) % 1.0)
      mx = ((mx + 0.5 ) % 1.0)
    }

It’s worth trying a few different mappings and seeing what you like best. For a great many purposes, you won’t have to deal with coordinate wraparound (the “seam”) at all.

For example, if you map your display to a matrix (setting the aspect ratio in the mapper to “Contain” mode), here’s a very short pattern that will do pretty much what the radial rainbow pattern does, but vertically, starting from the center of your display:

export function beforeRender(delta) {
  t1 = time(.08)
}

export function render2D(index,x,y) {
  h = t1 + 1-abs(y-0.5);
  s = 1
  v = 1
  hsv(h, s, v)
}

And here’s one that shows both horizontal and vertical movement:

var lineWidth = 0.05;

export function beforeRender(delta) {
  t1 = time(.06)
}

export function render2D(index,x,y) {
  h = t1 + x * y
  s = 1
  v = max((1-abs(t1 - x)/lineWidth),(1-abs(t1 - y)/lineWidth));
  hsv(h, s, v)
}
1 Like

The videos I saw looked really good, I wouldn’t ditch the 3d map just yet.

I think you can effectively get the 2d wrapped cylinder effect just by getting atan2 of the x and y, using that as you would x in 2d, and z for y. It’s a very small bit of code to add 3d support like that to an existing 2d only pattern. Plus you could still run true 3d patterns.

//return the angle as a value between 0 and 1.0
//most of Pixelblaze's animation language uses this range
//it also happens to rotate the angle so that 0 is north
function getUnitAngle(x, y) {
  return (PI + atan2(x - .5, y - .5))/PI2 
}

I considered that, but I didn’t want to burn an atan2 for every pixel×frame.

In a 2D pattern in which I want x,y,angle, I store these as 3 coordinates. The 3D preview is a bit funny, but I have the angle in the z parameter.

Could the mapper handle an array of arbitrary length and then call renderND(index, values[])? Then we could have an arbitrary number of precalculated values per pixel.