Running multisegments on a matrix's edges

Hello again -

This is primarily for @zranger1 (thank you!). I’m running a 2D pattern with many subpatterns, controlled by a button to switch patterns, on a matrix of (at the moment) 25 x 19 pixels. One subpattern is actually @zranger1’s deft multiSegment pattern in which I’m running 1D patterns on the left and right edges only, with the rest of the pixels black. The left and right edges are straightforward as the pixels are set up like this, where pixels 0-4, 5-14 and 15-19 are configured as three separate strips:

4 5 14 15
3 6 13 16
2 7 12 17
1 8 11 18
0 9 10 19

My question: Is there a way I can set up the top edge/row as a single strip, or put another way, is there a way I can set up any non-consecutive pixels as a single strip?

If this is possible then I am hopeful my ultimate goal of configuring all edges into a single strip would also be possible, but please let me know if you see any issues. To illustrate based on the example above, this hypothetical strip’s pixels would be: 0 1 2 3 4 5 14 15 16 17 18 19 10 9.

Thank you (again) in advance!

Marty

The multisegment pattern is really built for 1D strips. But of course, there are ways around that! If I had to build something fast, in 2D for a reasonable number of pixels (up to 1000 or so), here’s what I’d do:

  • allocate an array with pixelCount elements: segmentData = array(pixelCount)
  • store a segment number for each pixel in the array
  • get rid of all multisegment’s segment calculation logic and, at render time just get the pixel’s segment ID from segmentData[index]
  • to get all the patterns to work properly, you’d still have to keep track of how many pixels were in each segment, and how far into the segment you were. This requires a little per-segment data tracking – I think most of it already exists, but it might have to be modified a bit.

This would let you define segments in any shape, in 2D or 3D. The only downside is you’d have to figure out how to enter or generate the per-pixel segment data you wanted. But making the edges of a matrix into a single segment would be fairly easy this way.

If that won’t do the job, for a true 2D multi pattern solution, @scruffynerf built this amazing tool:

Thank you, I will take a look at @Scruffynerf’s. My goal is to run only 1D patterns, just not on consecutive pixels. I was thinking a solution might look like associating/swapping specific non-consecutive pixels with virtual or otherwise unused consecutive pixels, but haven’t been able to craft one yet.

Thanks.

Marty

Something about this piqued my interest because I’ve had “more flexible mapping” on my to-do list for a while.

I started playing around with a few ideas over the weekend and now I’ve got something ready to share:

preview

Within the limits of available memory, it supports remapping the set of available pixels into any number of regions of arbitrary size and shape, each with its own pattern. The example in the video is running:

  • @jeff’s 1D pattern “KITT” around the outer edges of a 16-by-16 matrix,
  • @jeff’s 2D pattern “Sinusoidal Waves” in the top half of the matrix,
  • @zranger1’s 2D pattern “Radar 2D” in the lower left two-thirds, and
  • @wizard’s 2D “Red-Green” test pattern in the lower right third.

Code and instructions below:

Multiple Patterns in Multiple Regions
////////////////////////////////////////////////////////////////////////////////
//
//  MULTIPLE PATTERNS IN MULTIPLE REGIONS: different patterns running in different user-defined regions.
//
////////////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////////////
//
//  Simple sample patterns.  But you're not limited to these; size permitting, most 
//  freestanding patterns can be used within a custom region. To do so requires copying 
//  the pattern code into this section, renaming the pattern's "beforeRender()" and 
//  "render()" functions to something unique (like "beforeRender_{patternName}" and 
//  "render_{patternName}"), and defining the pixels to be contained within the region.  
//  If the pattern contains additional functions or variables outside the "beforeRender()" 
//  and "render()" functions, they may also need to be renamed to avoid collisions.
//  

//-------------------------------------------------------------------------------------
//  Demo pattern is @jeff's KITT from https://forum.electromage.com/t/kitt-without-arrays/1219
var t_KITT;
export function beforeRender_KITT() { t_KITT = time(.05) }

function pulse(x) {
  var halfsaw = triangle(x - t_KITT) * square(x - t_KITT, .5);
  var tailPct = .4, shift = 1 - tailPct;
  return max(0, (halfsaw - shift) / tailPct)
}

export function render_KITT(index) {
  pct = index / pixelCount / 2; v = pulse(pct) + pulse(-pct); hsv(0, 1, v * v * v)
}


//-------------------------------------------------------------------------------------
//  @jeff's sinusoidal waves from https://forum.electromage.com/t/single-moving-pixel-across-a-2d-map/1324/4
export function beforeRender_SinusoidalWaves(delta) {
  var basePeriod = 1.2 / 65.535
  t1_SW = time(basePeriod); t2_SW = time(basePeriod * 1.05); t3_SW = time(basePeriod * 1.1);
}

// Returns 1 when a & b are proximate, 0 when they are more than `halfwidth`
// apart, and a gamma-corrected brightness for distances within `halfwidth`
function near(a, b, halfwidth) {
  var halfwidthDefault = 0.6; if (halfwidth == 0) halfwidth = halfwidthDefault;
  var v = clamp(1 - abs(a - b) / halfwidth, 0, 1);
  return v * v * v;
}

export function render2D_SinusoidalWaves(index, x, y) {
  x /= 2; y = 1.4 * y - .2;
  r = near(y, wave(x - t1_SW)); g = near(y, wave(x - t2_SW)); b = near(y, wave(x - t3_SW));
  rgb(r, g, b);
}


//-------------------------------------------------------------------------------------
// @zranger1's RADAR 2D
var dishAngle;
export function beforeRender_Radar(delta) {
  dishAngle = time(.03); circleRadius = time(0.015);;
}

export function render2D_Radar(index, x, y) {
 var beamWidth = 0.35;
  x -= 0.5; y -=0.5;             
  v = (PI + atan2(x, y))/PI2;
  d =  0.5-abs(0.5 - abs(dishAngle - v));
  v = (d < beamWidth) ? 1-(d/beamWidth) : 0;
  h = sqrt(x*x + y*y);  
  if (abs(h-circleRadius) < 0.05) { s = 0; v = 0.5 } else { s = 1; }
  hsv(h, s, v*v*v*v);
}


//-------------------------------------------------------------------------------------
//  '2D Red-Green Sweep' converted to Green-Blue
var axis, t1_RG;
export function beforeRender_RedGreenSweep(delta) {
  t1_RG = time(.05);
  axis = t1_RG > .5;
  t1_RG *= 2;
}

export function render2D_RedGreenSweep(index, x, y) {
  h = 1/3 + axis/3;
  v = wave((axis ? y : x) / 2 - t1_RG + .5);
  v = pow(v, 20);
  hsv(h, 1, v);
}


////////////////////////////////////////////////////////////////////////////////
//
//  The regions within this matrix.
//
//  The "beforeRenderer" and "renderer" columns refer to the renamed "beforeRender()" and "render()" 
//  functions of the particular pattern to be run in this region.  If the pattern doesn't have 
//  a render2D() or render3D() function, those fields should be set to zero.
//
//  Regions can contain any number of pixels, in any shape, direction or order.  The The pixel index
//  numbers in the example below refer to a CJMCU 16-by-16 matrix with zigzag addressing:
//    0	 31	 32	 63	 64	 95	 96	127	128	159	160	191	192	223	224	255
//    1	 30	 33	 62	 65	 94	 97	126	129	158	161	190	193	222	225	254
//    2	 29	 34	 61	 66	 93	 98	125	130	157	162	189	194	221	226	253
//    3	 28	 35	 60	 67	 92	 99	124	131	156	163	188	195	220	227	252
//    4	 27	 36	 59	 68	 91	100	123	132	155	164	187	196	219	228	251
//    5	 26	 37	 58	 69	 90	101	122	133	154	165	186	197	218	229	250
//    6	 25	 38	 57	 70	 89	102	121	134	153	166	185	198	217	230	249
//    7	 24	 39	 56	 71	 88	103	120	135	152	167	184	199	216	231	248
//    8	 23	 40	 55	 72	 87	104	119	136	151	168	183	200	215	232	247
//    9	 22	 41	 54	 73	 86	105	118	137	150	169	182	201	214	233	246
//   10	 21	 42	 53	 74	 85	106	117	138	149	170	181	202	213	234	245
//   11	 20	 43	 52	 75	 84	107	116	139	148	171	180	203	212	235	244
//   12	 19	 44	 51	 76	 83	108	115	140	147	172	179	204	211	236	243
//   13	 18	 45	 50	 77	 82	109	114	141	146	173	178	205	210	237	242
//   14	 17	 46	 49	 78	 81	110	113	142	145	174	177	206	209	238	241
//   15	 16	 47	 48	 79	 80	111	112	143	144	175	176	207	208	239	240
//

var regions = [ 
  //  Each region is a 3-element array containing:
  //    - the name of the renamed beforeRender() function,
  //    - the name of the renamed render?D() function,
  //    - a nested array containing the index numbers of all the pixels in the region.
  //
  
  //  A 1D region going around the outer edge of the matrix.
  [ beforeRender_KITT, render_KITT, [ 
        0,  31,  32,  63,  64,  95,  96, 127, 128, 159, 160, 191, 192, 223, 224, // top edge
      255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, // right edge
      240, 239, 208, 207, 176, 175, 144, 143, 112, 111,  80,  79,  48,  47,  16, // bottom edge
       15,  14,  13,  12,  11,  10,   9,   8,   7,   6,   5,   4,   3,   2,   1 // left edge
    ] 
  ],

  //  A 2D region occupying the top half of the matrix (minus the outer edges).
  [ beforeRender_SinusoidalWaves, render2D_SinusoidalWaves, [
       30,  33,  62,  65,  94,  97, 126, 129, 158, 161, 190, 193, 222, 225,
       29,  34,  61,  66,  93,  98, 125, 130, 157, 162, 189, 194, 221, 226,
       28,  35,  60,  67,  92,  99, 124, 131, 156, 163, 188, 195, 220, 227,
       27,  36,  59,  68,  91, 100, 123, 132, 155, 164, 187, 196, 219, 228,
       26,  37,  58,  69,  90, 101, 122, 133, 154, 165, 186, 197, 218, 229,
       25,  38,  57,  70,  89, 102, 121, 134, 153, 166, 185, 198, 217, 230,
    ]
  ],

  //  A 2D region occuping the left two-thirds of the bottom half (minus the outer edges).
  [ beforeRender_Radar, render2D_Radar, [
       24,  39,  56,  71,  88, 103, 120, 135, 
       23,  40,  55,  72,  87, 104, 119, 136, 
       22,  41,  54,  73,  86, 105, 118, 137, 
       21,  42,  53,  74,  85, 106, 117, 138, 
       20,  43,  52,  75,  84, 107, 116, 139, 
       19,  44,  51,  76,  83, 108, 115, 140, 
       18,  45,  50,  77,  82, 109, 114, 141, 
       17,  46,  49,  78,  81, 110, 113, 142, 
    ]
  ],

  //  A 2D region occupying the right third of the bottom half (minus the outer edges).
  [ beforeRender_RedGreenSweep, render2D_RedGreenSweep, [
                                               152, 167, 184, 199, 216, 231,
                                               151, 168, 183, 200, 215, 232,
                                               150, 169, 182, 201, 214, 233,
                                               149, 170, 181, 202, 213, 234,
                                               148, 171, 180, 203, 212, 235,
                                               147, 172, 179, 204, 211, 236,
                                               146, 173, 178, 205, 210, 237,
                                               145, 174, 177, 206, 209, 238
    ]
  ]
];


////////////////////////////////////////////////////////////////////////////////
//
//  There's no need to edit anything below this line.
//

var pixelCountOriginal = pixelCount;
var whichRegion = array(pixelCountOriginal);  //  The region each pixel belongs to.
var whichIndex = array(pixelCountOriginal);   // The relative index of each pixel within its region.
export var mapXYZ;                                   //  The map coordinates for each real pixel.
var numDimensions = 0;

//  HELPER functions.
function allocateXYZ(dimensions) {
  if (numDimensions == 0) {
    numDimensions = dimensions;
    mapXYZ = array(pixelCountOriginal * numDimensions);
  }
}
function storeXYZ(index, x, y, z) {
  if (numDimensions >= 1) mapXYZ[numDimensions*index+0] = x; 
  if (numDimensions >= 2) mapXYZ[numDimensions*index+1] = y; 
  if (numDimensions == 3) mapXYZ[numDimensions*index+2] = z;
}
function rescaleXYZ(realIndexArray) {
  var minima = [1, 1, 1]; var maxima = [0, 0, 0];
  //  Find the highest and lowest values in this region.
  for (var iterIndex = 0; iterIndex < realIndexArray.length; iterIndex++) {
    var thisIndex = realIndexArray[iterIndex];
    for (var iterDimension = 0; iterDimension < numDimensions; iterDimension++) {
      var candidate = mapXYZ[thisIndex * numDimensions + iterDimension];
      if (candidate < minima[iterDimension]) minima[iterDimension] = candidate;
      if (candidate > maxima[iterDimension]) maxima[iterDimension] = candidate;
    }
  }
  //  Now make another pass through and rescale the values to 0..1.  If someone is doing freaky things 
  //  with irregular shapes it might not work, but for simple rectangles this should be fine.
  for (iterIndex = 0; iterIndex < realIndexArray.length; iterIndex++) {
    thisIndex = realIndexArray[iterIndex];
    for (iterDimension = 0; iterDimension < numDimensions; iterDimension++) {
      var span = maxima[iterDimension] - minima[iterDimension];
      var value = mapXYZ[numDimensions*thisIndex+iterDimension];
      value -= minima[iterDimension]; //  offset
      if (span > 0) value /= span;  //  rescale
      mapXYZ[thisIndex * numDimensions + iterDimension] = clamp(value, 0, 1);
    }
  }
  //  For each pixel index in this region, store its region and its relative offset.
  regions.forEach((v, i, a) => { 
    pixelCount = a[i][2].length; 
    for (var iterPixel = 0; iterPixel < pixelCount; iterPixel++) { 
      var whichPixel = a[i][2][iterPixel];
      whichRegion[whichPixel] = i; whichIndex[whichPixel] = iterPixel;
    }
  });
}

//  BEFORERENDER functions.
export var mapMode = -1;  //  Mode selector: 0 = capture coordinates; 1 = rescale coordinates; 2 = draw
export function beforeRender(delta) { 
  saveDelta = delta;
  //  for each client region:
  regions.forEach((v, i, a) => { 
    //  rescale the client map the first time through
    if (mapMode == 1) rescaleXYZ(a[i][2]); 
    // call client beforeRenderer() functions.
    pixelCount = a[i][2].length; if (a[i][0] != 0) a[i][0](saveDelta); 
  });
  //  Move to the next map-capture modes.
  if (mapMode < 2) mapMode++; 
}

//  RENDER functions.
function renderRegions(index) {
  //  Get the appropriate region-specific coordinates for this pixel.
  var offset = index*numDimensions;
  var x = mapXYZ[offset+0];
  var y = 0; if (numDimensions > 1) y = mapXYZ[offset+1];
  var z = 0; if (numDimensions > 2) z = mapXYZ[offset+2];
  //  set the appropriate pixelCount for this region.
  pixelCount = regions[whichRegion[index]][2].length;
  // call client render?D() functions.
  regions[whichRegion[index]][1](whichIndex[index], x, y, z);
}
export function render(index) {
  //  On the first pass, store the map coordinates. On successive passes, call the client renderer.
  if (mapMode == 0) { allocateXYZ(1); storeXYZ(index, index/pixelCount, 0, 0); return; } else renderRegions(index);
}

export function render2D(index, x, y) {
  //  On the first pass, store the map coordinates. On successive passes, call the client renderer.
  if (mapMode == 0) { allocateXYZ(2); storeXYZ(index, x, y, 0); return; } else renderRegions(index);
}

export function render3D(index, x, y, z) {
  //  On the first pass, store the map coordinates. On successive passes, call the client renderer.
  if (mapMode == 0) { allocateXYZ(3); storeXYZ(index, x, y, 0); return; } else renderRegions(index);
}

All the housekeeping needed to multiplex, relocate and rescale each pattern causes about a 66% performance hit because PBscript arrays don’t have a findIndex() or indexOf() function, but the frame rate still seems acceptable for most computationally-light patterns. If you have any ideas for further optimizations, speak up (where’s @scruffynerf when you need him?).

5 Likes

I’m still alive. My health took a bad turn so my vacation from here turned out much longer than planned. Been slowly on the mend and just today was looking at something I’d made and said “I need to make that with LEDs”

Haven’t caught up but you invoked my name, so figured I’d speak up.

5 Likes

@pixie and @scruffynerf! Great to see you both! And @scruffynerf, glad you’re doing a bit better. I’m all too familiar with that “slowly on the mend thing.”

2 Likes

This gives me a lot to work with. Thank you, and hope everyone is feeling better every day.