Multimap multi-pattern - it's alive!

As discussed here

Teaser photo


Video was too big, even at 9megs

I haven’t fleshed out all of the bits and pieces that would make this 100% fully functional to my vision yet.
But it’s working. The above is a quick snapshot of 5 different “submaps” plus a default background, running 6 different patterns (most of which are just different variations of pulse, with different color, wave, and timing)

Layered, multiple maps, with potential to renormalize or not (including to renumber pixel index and even pixelcount), fed by multiple patterns, with each map having it’s own section and pattern, and because it’s layered, you can set arbitrary map boundaries, and the top one is the visible one over the bottom one. So for example, the center spot is one map in front and there is a larger circle behind it.

It’s quite simple in code, but I want my demo to flesh out the options better as documentation/example code. I haven’t taken advantage of the renormalize or reindex yet, for example, and I want it way more impressive than just some pulses.

This is reason #27 for making all patterns be mapped, but then again, if I can “fake” the index and pixelcount for any given map/pattern, might not matter.

3 Likes

Ok, this demo of the concept is WAY overkill. But it demos most of the potential ways to use this. Has KITT, Spirals, SlowShifts, pulses, and more. I should enter this as my ‘art on the wall’ but I actually have better taste than this.

/*
  Multiple Maps, multiple patterns - by ScruffyNerf   beta v0.9
  inspired by various code bits by zranger, Jeff Vyduna and Ben Hencke 
  Spiral Code Pattern by ChrisNZ
  
  While single strips or 3d maps could be used, that's beyond this demo, but the principle is the same.
  
  Requirements: Matrix with a 2D map
  This allows you to split a matrix of any size into multiple sub maps, by formula (to carve the range up), 
  and can run different patterns on each. (You could even change patterns, over time, not implemented here)
  Consolidate any code needed from beforerender() into a single function (because it's not index/map based),
  but you can run entirely different render() codes for each pattern
  Remapping x/y values can either be renormalized (the new map is now 0..1) 
  OR retain the partial map (so perhaps x=0.5->1,y=0.5->1)
*/

// These are the map functions it will consider.
// Order matters, it will consider only the FIRST return of true
// This allows overlapping maps, with the first (0) being "on top", 
// and potentially this also allows a background/default map (matching none of the submaps listed)

//  these functions take index,x,y and modify a set of globals
//     isInMap is set to true, if the pixel is in this submap
//     NewIndex is the "new" index value, if any
//     NewPixelCount is the pixelcount of the newmap, if any
//     NewX is the new value of x for that pixel, might be renormalized, might not, still in range of 0..1
//     NewY is the new value of y for that pixel, might be renormalized, might not, still in range of 0..1
var isInMap = false;
var NewIndex = 0;
var NewPixelCount = 0;
var NewX = 0;
var NewY = 0;
var matched = false;
var centerx;
var centery;


var totalMaps = 5 // maximum number of maps
var mapCount = 5 // active number of maps
var maps = array(totalMaps)

maps[0] = inmap0
maps[1] = inmap1
maps[2] = inmap2
maps[3] = inmap3
maps[4] = inmap4

var patternCount = 5 // total number of patterns
var patterns = array(patternCount)

patterns[0] = blinkWRGB
patterns[1] = pulseBlue
patterns[2] = pulseGreen
patterns[3] = spirals
patterns[4] = slowcolorshift

export var tBlink // used for pattern timing
export var tRotate // used for pattern timing

function inmap0(index, x, y) {
    centerx = x - 0.5;
    centery = y - 0.5;
    if (centerx * centerx + centery * centery < 0.02) { // is this in the very center 'circle'?
        isInMap = true;
        NewX = centerx;
        NewY = centery;
    }
}

function inmap1(index, x, y) {
    if (x + y > .25 && x - y > .25 && atan2(y, x) < tBlink * 1.8) {
        // Left corner, slowly growing outward...
        isInMap = true;
    }
}

function inmap2(index, x, y) {
    centerx = x - 0.5;
    centery = y - 0.5;
    if (centerx * centerx + centery * centery < 0.075 && ((PI + atan2(centerx, centery)) / PI2 > tRotate)) {
        // is this in the bigger center circle, as we count down?  This makes it into a disappearing band
        isInMap = true;
        NewX = centerx;
        NewY = centery;
    }
}

function inmap3(index, x, y) {
    if (x > .6) {
        if (y > .6) { // this should match the lower right corner... for a spiral
            isInMap = true;
            NewX = renormalize(x, .6, 1)
            NewY = renormalize(y, .6, 1)
        }
    }
}

function inmap4(index, x, y) {
    if (x < .14) { // is this in the very top few lines?, but let's remove the topmost line...
        if (x > .03) {
            isInMap = true;
            NewPixelCount = 16;
            NewIndex = index - 16;
        }
    }
}

function blinkWRGB() {
    step = floor(16 * tBlink) // 0123456789.....16
    h = floor(step % 8 / 2) / 3 // RRGGBBrrRRGGBBrr
    v = step < 6 || step < 13 && (step - 7) % 2 == 1 // 1111110010101000
    hsv(h, step > 6, v) // WWWWWW__R_G_B___
}

function pulseBlue() {
    hsv(.6, 1, wave(tBlink * 1.5))
}

function pulseGreen() {
    hsv(.3, 1, wave(tBlink))
}

// spiral controls
export var twistSpeed = .002
export var rotateSpeed = .001
var startingColor = .5
var colorSpeed = .1
var twist, rotation, colorShift, arms
// How quickly the spiral should rotate back and forth
export function sliderTwistSpeed(v) {
    twistSpeed = v = 0 ? 0 : .015 / v
}
// How quickly the entire pattern should rotate
export function sliderRotationSpeed(v) {
    rotateSpeed = v = 0 ? 0 : .005 / v
}
// How many arms of symmetry the pattern should have (1-3)
export function sliderArms(v) {
    arms = 1 + floor(v * 2.999)
}

function spirals(index, x0, y0) {
    twist = wave(time(twistSpeed)) * 2 - 1
    rotation = time(rotateSpeed)
    colorShift = time(colorSpeed)
    x = (NewX - .5) * 2
    y = (NewY - .5) * 2
    dist = sqrt(x * x + y * y)
    angle = (atan2(y, x) + PI) / PI / 2
    angle += dist * twist / 2
    h = angle * arms - rotation + 10
    h = h - floor(h)
    v = (1.07 - dist) * (h < .5 ? h * h * h : h)
    h = (h + startingColor) / 2 + colorShift
    hsv(h, 1, v)
}

/*
  Slow color shift pattern
*/

function slowcolorshift() {
    t1 = time(.15) * PI2
    t2 = time(.1)
    h = (t2 + 1 + sin(NewIndex / 2 + 5 * sin(t1)) / 5) + NewIndex / (NewPixelCount * 4)
    v = wave((NewIndex / 2 + 5 * sin(t1)) / PI2)
    v = v * v * v * v
    hsv(h, 1, v)
}

function kitt() {
    tailPct = .6
    speed = .05
    pct1 = NewY / 2 - time(speed)
    pct2 = -NewY / 2 - time(speed)
    v = max(0, (tailPct - 1 + triangle(pct1) * square(pct1, .5)) / tailPct) +
        max(0, (tailPct - 1 + triangle(pct2) * square(pct2, .5)) / tailPct)
    hsv(0, 1, v * v * v)
}

function renormalize(value, rangestart, rangeend) {
    return ((value - rangestart) / (rangeend - rangestart))
}

export function beforeRender(delta) {
    tBlink = time(twistSpeed)
    tRotate = time(rotateSpeed)
}

export function render2D(index, x, y) {
    matched = false;
    NewIndex = index;
    NewPixelCount = pixelCount;
    for (var p = 0; p < mapCount; p++) {
        isInMap = false;
        maps[p](index, x, y);
        if (isInMap) {
            patterns[p]();
            matched = true;
            p = mapCount;
        }
    }
    if (matched == false) {
        NewY = y;
        kitt();
    }
}

export function render(index) {
    render2D(index, index / pixelCount, 0)
}

Running on a 16x16 panel, I’m getting 33FPS, not bad for running 6 different patterns, overlapping some…

I stripped out a few Spiral controls, and borrowed 2 of the 3 sliders left to use for timing on other patterns.

Note the spiral is overlapped by the green and blue, but not the red KITT, it’s also renormalized, and thinks it’s running on a smaller area… Blue and Green show other patterns underneath if they aren’t lit, and Green is behind Blue.

it’s hella noisy, but it’s a good demo of the ability to run multiple patterns AND multiple maps, with very little rewriting of the originals. I did use globals to make code cleaner, and not be passing X and Ys all over, so most of the code rewriting was for that. (I used the fancy new arrayless KITT too)

3 Likes

I realize this definitely needs some documentation. I could see someone saying “multimap? What’s that?” for example.

I’m defining a map as “a set of points”.

You’d still need to build a single mapping in PB, whether by explicit points, math, or whatever, and let it get normalized into 0…1 on all axes. (Axies? Axis?)

Then you define a submap, which in most of my example is either static math (all points where X is less than some number, for example), OR math involving variables, which can be changing, so for example, maps that shift in time. You could also use a set of values, like a array, but I didn’t feel like adding one, as it’s just a hardcoding of matching math values.

I’ll document this more, do some clean up, and post it as an example. I need to populate my GitHub with this stuff anyway. Plus I’ll stick not quite so horrifically busy example into the pattern library.

I did come up with a neat idea to use this, using @ChrisNZ 's spirals… Imagine multiple spirals, coming and going, and sliding around. So instead of one small spiral in the corner like the above, a handful of spirals changing sizes and origins.

1 Like

From looking at the code, it almost feels like painting layers with masks.

1 Like

That’s an excellent analogy.

Awesome proof-of-concept! I particularly like the idea of layering and having multiple maps. It’s a great toolbox for wall or room-sized installations with odd shapes, multiple panels and whatever else one might want to build.

(Also, I think this satisfies the “wall art” project requirement – I could see a big version hanging a modern art museum. Yeah, it’s a little over-the-top, but you can’t help reacting to it, and that’s pretty much what art is for!)

This is brilliant. Some day when I grow up I may catch up with y’all. But I doubt it… not it this life time anyway!.Thank you so much for taking the lead and providing great inspiration!

1 Like

To clarify, I think this is brilliant because it speaks to great potential, layering and juxtaposing patterns… that fuels creativity and imagination.

And that’s a very good thing.

2 Likes

Ok, so two months later and I haven’t found the time to make this more “friendly” yet. Now on my shorter Todo list, so hopefully next week.

1 Like

Also posted to pattern library. The version above uses more features, but is way more complex. This is a cleaned up, basic and simple version, for new folks to learn from.

/*
  Multiple SubMaps with multiple patterns v1.0 - by ScruffyNerf
  inspired by various code bits by zranger, Jeff Vyduna and Ben Hencke  

  Very Simple demo version, a truly minimal example (2 ranges, 2 patterns)
  There are lot of ways to do what this pattern does without using multiple maps, it's for building on.

  Requirements: Matrix with a 2D map
*/

//  these functions can take index,x,y and modify a set of globals to use instead
//     isInMap is set to true, if the pixel is in this submap
//     NewIndex is the "new" index value, if any [NOT USED IN EXAMPLE]
//     NewPixelCount is the pixelcount of the new map, if any [NOT USED IN EXAMPLE]
//     NewX is the new value of x for that pixel, might be renormalized, might not, still in range of 0..1
//     NewY is the new value of y for that pixel, might be renormalized, might not, still in range of 0..1
var isInMap
var NewIndex = 0;
var NewPixelCount = 0;
var NewX,NewY

// These are the map functions it will consider.
// Order matters, it will consider only the FIRST return of true
// This allows overlapping maps, with the first (0) being "on top", 
// and potentially this also allows a background/default map (matching none of the submaps listed)
var mapCount = 2 // number of maps+patterns
var maps = array(mapCount)
maps[0] = inmap0
maps[1] = inmap1
// maps[2] = your map functions go here

function inmap0(index, x, y) {
  centerx = x - 0.5;
  centery = y - 0.5;
	// circle from center of matrix
  if (centerx * centerx + centery * centery < 0.04) { 
    isInMap = true;
    NewX = centerx;
    NewY = centery;
  }
}

function inmap1(index, x, y) {
  if (x < .5 && y < .5) {
    isInMap = true;
  }
}

// if you are learning how this works, I recommend adding a new map first, and experimenting...
// then add a new pattern  (You'll want to just copy a pattern for the new map.  Map # matches Pattern # for simplicity )

var patterns = array(mapCount)
patterns[0] = pulseBlue
patterns[1] = pulseGreen
// patterns[2] = your pattern function(s) goes here

function pulseBlue(index,x,y){
  hsv(.6, 1, wave(time(.02)))
}

function pulseGreen(index,x,y) {
  hsv(.3, 1, wave(time(x*y+.03)))
}

export function render2D(index, x, y) {
  matched = false;
  NewPixelCount = pixelCount;
  NewIndex = index;
	NewX = x;
	NewY = y;
  for (var p = 0; p < mapCount; p++) {
    isInMap = false;
    maps[p](index, x, y);
    if (isInMap) {
      patterns[p](NewIndex,NewX,NewY);
      matched = true; // to avoid turning light off
      p = mapCount;  // stop the for loop
      }
  }
  if (matched == false) {
		// no match, no light
		hsv(0,0,0)
  }
}

// just so it'll render in 1D
export function render(index) {
    render2D(index, index / pixelCount, 0)
}
2 Likes

How can i display different patterns on different sections of my map…ie split the y into several defined segments and have patterns play only on those segments… with a pattern that plays over all ???

It’s all math, so decide where the Y breaks are, maybe Y < .25, or abs(Y-.5)<.1 (ie between .4 and .6), etc.

The examples try to make it clear… Play with it.
As for a background pattern, instead of the HSV(0,0,0), call a pattern there.

Post your code, working or not!

1 Like

where can i find out hat these symbols mean for example…< and abs ect… i know their math functions but i dont know what alot of them mean and im sure this would help :relaxed:

It’s all basically JavaScript. So any good tutorial on JavaScript will help you.

https://www.w3schools.com/js/js_comparisons.asp

1 Like

well this seems to work, 5 patterns running in different zones of the Y. im not sure how to implement more complex patterns or the audio reactivity… but this is good progress. also the last pattern doesnt show up…im wandering how to allow the last pattern thru in the dark moments of the previous patterns.

/*
  Multiple SubMaps with multiple patterns v1.0 - by ScruffyNerf
  inspired by various code bits by zranger, Jeff Vyduna and Ben Hencke  

  Very Simple demo version, a truly minimal example (2 ranges, 2 patterns)
  There are lot of ways to do what this pattern does without using multiple maps, it's for building on.

  Requirements: Matrix with a 2D map
*/

//  these functions can take index,x,y and modify a set of globals to use instead
//     isInMap is set to true, if the pixel is in this submap
//     NewIndex is the "new" index value, if any [NOT USED IN EXAMPLE]
//     NewPixelCount is the pixelcount of the new map, if any [NOT USED IN EXAMPLE]
//     NewX is the new value of x for that pixel, might be renormalized, might not, still in range of 0..1
//     NewY is the new value of y for that pixel, might be renormalized, might not, still in range of 0..1
var isInMap
var NewIndex = 0;
var NewPixelCount = 0;
var NewX,NewY
var t1
// These are the map functions it will consider.
// Order matters, it will consider only the FIRST return of true
// This allows overlapping maps, with the first (0) being "on top", 
// and potentially this also allows a background/default map (matching none of the submaps listed)
var mapCount = 5 // number of maps+patterns
var maps = array(mapCount)
maps[0] = inmap0
maps[1] = inmap1
maps[2] = inmap2
maps[3] = inmap3
maps[4] = inmap4

export function beforeRender(delta) {
  t1 = time((.5) / 65.536) // From 0…1 every 4 seconds

}
// maps[2] = your map functions go here

function inmap0(index, x, y) {
  if (x < 1 && y > .99) {
    isInMap = true;
  }
}

function inmap1(index, x, y) {
  if (x < 1 && y > .88 ) {
    isInMap = true;
  }
}
function inmap2(index, x, y) {
  if (x < 1 && y > .8) {
    isInMap = true;
  }
}
function inmap3(index, x, y) {
  if (x < 1 && y > .12) {
    isInMap = true;
  }
}
function inmap4(index, x, y) {
  if (x < 1 && y > .00) {
    isInMap = true;
  }
}

// if you are learning how this works, I recommend adding a new map first, and experimenting...
// then add a new pattern  (You'll want to just copy a pattern for the new map.  Map # matches Pattern # for simplicity )

var patterns = array(mapCount)
patterns[0] = pulseBlue
patterns[1] = pulseGreen
patterns[2] = pulseRed
patterns[3] = pulseviolet
patterns[4] = pulsecf
// patterns[2] = your pattern function(s) goes here

function pulseBlue(index,x,y){
  hsv(.6, 1, wave(time(.02)))
}

function pulseGreen(index,x,y) {
  hsv(.3, 1, triangle(time(.04)))
}
function pulseRed(index,x,y) {
  hsv(.01, 1, wave(time(.008)))
}
function pulseviolet(index,x,y) {
  hsv(.8, 1, wave(time(.0008)))
}
function pulsecf(index,x,y) {
  hsv(.2, 1, wave(time(.001)))
}

export function render2D(index, x, y) {
  matched = false;
  NewPixelCount = pixelCount;
  NewIndex = index;
	NewX = x;
	NewY = y;
  for (var p = 0; p < mapCount; p++) {
    isInMap = false;
    maps[p](index, x, y);
    if (isInMap) {
      patterns[p](NewIndex,NewX,NewY);
      matched = true; // to avoid turning light off
      p = mapCount;  // stop the for loop
      }
  }
  if (matched == false) {
		// no match, no light
	



  v = clamp((triangle((y/0.5) - t1 + 0.5) -.5) * .5, 0, 1)
  
  hsv((t1 / 0.3)* 0.2, 1, v * v)
}
}

// just so it'll render in 1D
export function render(index) {
    render2D(index, index / pixelCount, 0)
}

So the X part isn’t needed (it’s always true, as you’ve written it…), And the Y part is always going to match that last one, since it’ll always be greater than zero. (I don’t believe map value go to an actual 0 or 1, just between those values)

If you want the “none of the above” option to work, you need to change things so that at times, none of those tests are true.

For example, make the X value check check to see if it’s .5 or less… Then half of that section will be “background”. Make another test check for X is greater than .5, and now you’ve got the background on the other side.

To make it run behind the “dark moments” is more complex:

You’d need to NOT match when the light wouldn’t be on as well… For example
wave(time(.0008) is used to decide the brightness of the violet section…
But if you add into inmap3() a check to see if wave(time(.0008) is currently greater than .25, so it fails, meaning the brightness would be between 0 (off) and .25 (pretty dim still) then it wouldn’t match that map, then…

However, inmap4 would match as true then…
Because you need to add a check to inmap4 for y to not be greater than .12 (so it won’t match inmap3’s area.

Hope this makes sense… Keep playing with it!

mmmm i think it makes sense but wouldnt this just turn off the map…? rather than allow another pattern to be visable thru the dark areas… so if i have a triangle wave running the black would effectively become see thru…a blending option

So the basic example isn’t meant to have multiple things on the same pixel, it’s set so that first to say “Mine!” gets the pixel.

My suggestion is to make the map check logic say “oh, even though the pixel is in my map, it’s not going to be lit, so NOT mine this time…”

You could rewrite this to have multiple maps share a pixel and blend colors/brightness…
But then you’d have to rewrite all of the patterns to not write directly (HSV) and instead return values as to what they’d like the pixel to be, merge these together into one result and then finally light the pixel. Very doable, but not done.

If we had a way (nudge nudge to @wizard) to read the current pixel’s info (and then merge new color data, perhaps), or some other method, more of the current way the pattern is set could be left intact. Or we could use a canvas array…

Added You could cheat it with 3 globals H,S,V and then set those in each pattern, merging existing values, and finally at the end, only then light the pixel via hsv(H,S,V). I might implement this.

But…

I wrote this to minimize changing pattern code.
Basically you can (as my more complex demo shows) do pretty fancy patterns all with a single pattern render function for each. Which could be a direct copy from an existing pattern you like.

I’ll mull over the best way to make this handle overlapping patterns, and probably write something up.

Right now, my suggestion is the best way approach this: make your logic smarter about when the map “gives up” the pixel to something else.

Keep in mind: this is an advanced technique, and you really need to understand how your pattern(s) work, and the mapping is thrown on top of all of that. My “kitchen sink” original example uses some map logic that change over time, which leads to a look like patterns are overlapping, but in reality the map itself is shifting so for those pixels, which map says “Mine!” Is really what changes.

1 Like

so im trying to figure out if i can basically point to the v in the hsv so that when the value falls below a certain point that the map match becomes false… i thought maybe i could do it in the inmap section…using something like:
function inmap4(index, x, y) {
if (x < 1 && y > .00) {
isInMap = true;
}
if (x < 1 && y > .00) {
isInMap = true;
}
else if (v <0.04){
isInMap = false;
}
but this comes up with the error that v is undefined…mmm not sure how or where to define it… or if this is even the right approach, command…i kinda feel like im fishing in the dark…and sooooooo much to learn