Using frequency data to turn on LEDs

I did that change a few days ago, that’s correct(I also think you made a typo, should be v + v2, not v1 + v2).

I’ve made a few changes to the code and thought maybe I broke something, reverted to a previous version(yours) and still have the same issue, LEDs won’t turn on if grow speed > 0.96. BUT, I found the following: it won’t work for LEDs in the setActivePattern group, but it will work for the ones in the setInactivePattern.

An additional observation: While using redGrowFromCenter, when a sensor becomes inactive the LEDs will turn on and after about 3 seconds will momentarily fade out/in.

Does that help?

I think it’s pretty likely that we’ve just got bits and pieces out of sync. If you’ll post your code, or message it to me, I’ll go over it, add some more documentation so it’ll be easier to work with in the future, and send it back to you!

Here’s a cleaned-up code, I removed all my changes so it is identical to what you provided about 10 posts ago, with the addition of redGrowFromCenter. It has the same effect as the one with my changes.

export var frequencyData = array(32)


// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96 * 2

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 12

var fades = array(NUM_SENSORS)
var activePattern  = array(NUM_SENSORS)
var inactivePattern  = array(NUM_SENSORS)
var fadeAmount

// variables to hold final hue, saturation and brightness
// these should be set by individual pattern functions
var hue,sat,bri

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setActivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    activePattern[start + i] = pFn
  }
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setInactivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    inactivePattern[start + i] = pFn
  }
}

//////////////////////////////////////////////////////////////
// a few pattern functions as examples.  You can do anything
// you want in these, as long as you eventually set the 
// hue, sat and bri values.

// Displays a rainbow pattern over the pixels associated with
// a single sensor
function rainbowPatternByGroup(index,x,y,z) {
  hue = t1 + (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1)
  sat = 1
  bri = 1
}

// RED
function solidRed(index,x,y,z) {
  hue = 0;
  sat = 1;
  bri = 1;
}

// BLUE
function pulsingBlue(index,x,y,z) {
  hue = 0.6667;
  sat = 1;
  bri = 1 - (0.925 * wave(t1));
}

// Rainbow pattern over all pixels
function rainbowAllPixels(index,x,y,z) {
  hue = t1 + index/pixelCount
  sat = 0.8
  bri = 0.2
}

// On activation, "grow" the lighted region of the segment outward
// from the center.
function redGrowFromCenter(index,x,y,z) { 
  
  // calculate position in the sensor's group of pixels
  var pos = (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1) 
  sensor = floor(index / PIXELS_PER_SENSOR)  
  
  hue = 0
  sat = 1

  // on activation, grow from center at speed set by growSpeed
  // higher values are faster, lower are slower.
  // (you could put this on a slider or make it global.  It's
  // just here to keep the code short)
  var growSpeed = 2
  bri = 1-abs(pos - 0.5) - growSpeed * (1 - (3.5 - fades[sensor]))
}

// set patterns for active and inactive sensors
// - you can change these in beforeRender() at any point
// - you can run as many of these as there are sensors
// - you can have as many in your pattern as you want.
setInactivePattern(pulsingBlue,0,NUM_SENSORS)

setActivePattern(rainbowPatternByGroup,0,96)
setActivePattern(solidRed,96,96)

// timer value, set in beforeRender() and used by several pattern functions
var t1

export function beforeRender(delta) {
  t1 = time(0.04)
  
  fadeAmount = delta / 1000 // fade out over 1 second
  fades.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed
  
  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16) //+ 6
    bit = sensor % 16
    intValue = frequencyData[element] << 16
    if (intValue & (1<< bit)) {
      b = element
      fades[sensor] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
    }
  }
}

export function render(index) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  
  if (sensor >= 96) {
    // do something with pixels not attached to a sensor
    // in this case just leave them off
    return;
  }

  // get data for the second set of sensors  
  var sensor2 = sensor + 96

  // get current brightness for each sensor bank
  v = fades[sensor]
  v2 = fades[sensor2]
  
  // see if a sensor in either bank has been activated
  if ((v + v2) > 0) {
  
    // get current activation state for each sensor bank
    // (this will be 1 if on, 0 if off)
    on = v > 0
    on2 = v2 > 0
    
    // pick the color of the most recently activated sensor
    if (v >= v2) {
      activePattern[sensor](index) 
    }
    else {
      activePattern[sensor2](index) 
    }
    bri *= v + v2
  }
  
  // if no sensor has been activated, use the inactive pattern
  else {
    inactivePattern[sensor](index)
  }

  hsv(hue,sat,bri)
}

I kind of suspect that this is because my fake test sensors are one-shot. When triggered, they give a very brief “on” pulse, and then go back to off until triggered again.

I’m guessing the real ones stay triggered as long as the user keeps touching them? If this is the case, the current code would do the wrong thing.

While adjusting, I’ll fix it to works for both fade in and fade out no matter what the user does.

Here’s a new experiment. This version has:

  • simple control over fade in, minimum hold, and fade out times, all in seconds.
  • automatic reversible animation – the fade-in animation will play again in reverse during fade-out. (It’s a side effect of the new timer design, but generally looks pretty cool. Of course it’s possible for individual patterns to do something else if you want.)

And lots of other behavior options and things to play with in the code. Give it a try and let me know what you think!

Latest Pattern Code

// DEBUGGING BLOCK - adds a test switch that controls one or more
// sensors. Comment out, or remove for production
export function toggleSetSensors(isOn) {
  setSensorBit(99,isOn)
  setSensorBit(0,isOn)
}
// END DEBUGGING BLOCK


// data from sensors comes in here...
export var frequencyData = array(32) 

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96 * 2

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 12

// Sensor behavior flag:
// If set to true (1), LEDs will light only when their sensor transitions
// from 0 to 1 and will run through their complete animation sequence to fade-out whether or
// not the physical sensor bit is still in the '1' state.
// Otherwise on sensor asctivation, LEDs will run their fade-in animation and remain in the
// fully lit "hold" state until the sensor bit goes back to '0' and then completing
// the fade out.
var ONE_SHOT = 1

// time, in seconds, that it takes to "fade in" on sensor activation
var FADE_IN_TIME = 2

// time it takes to fade out after sensor deactivation, once 
// minimum hold time expires
var FADE_OUT_TIME = 2

// minimum time the pixel block will be lit following sensor
// activation.  Can be zero.  If using one-shot mode,
// should be set to at least FADE_IN_TIME seconds.
var MIN_HOLD_TIME = 2

// variables to manage switch state and timing
var previousState = array(NUM_SENSORS)
var activationTime = array(NUM_SENSORS)
var virtualSensors = array(NUM_SENSORS)

var sensorLevels = array(NUM_SENSORS)

// arrays to hold pattern functions for each sensor
var activePattern  = array(NUM_SENSORS)
var inactivePattern  = array(NUM_SENSORS)

// variables to hold final hue, saturation and brightness
// these should be set by individual pattern functions
var hue,sat,bri

// timers:
// timebase tracks real time in seconds since pattern startup
// t1 is available for use by pattern functions and can be set to any
// desired speed.
var timebase;
var t1

function setSensorFadeLevel(sensor,delta) {
  fadeIn = fadeOut = 1
  
   if (virtualSensors[sensor]) {
     // if the switch is on, run the fade in 'till we reach full brightness        
     sensorLevels[sensor] += delta/FADE_IN_TIME
   }
   else {
     // if it's off, run fade out 'till we reach zero.
     sensorLevels[sensor] -= delta/FADE_OUT_TIME
   }
   sensorLevels[sensor] = clamp(sensorLevels[sensor],0,1)
}

// sets the bit flag for a sensor to the specified (0 or 1) value
function setSensorBit(sensor,newValue) {
    element = floor(sensor / 16)
    bit = sensor % 16
    
    // set or clear bit as necessary
    if (newValue) {
      v = (frequencyData[element] << 16) | (1 << bit)
    }
    else {
      v = (frequencyData[element] << 16) & ~(1 << bit)
    }  
    
    // save value
    frequencyData[element] = v >> 16
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setActivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    activePattern[start + i] = pFn
  }
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setInactivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    inactivePattern[start + i] = pFn
  }
}

// sets default value for all sensor patterns, active and inactivePattern
// should be called during pattern initialization.
function initializeSensorPatterns(defaultPattern){
  p = defaultPattern
  activePattern.mutate(v => { return p})
  inactivePattern.mutate(v => { return p})  
}

//////////////////////////////////////////////////////////////
// a few pattern functions as examples.  You can do anything
// you want in these, as long as you eventually set the 
// hue, sat and bri values.

// Displays a rainbow pattern over the pixels associated with
// a single sensor
function rainbowByGroup(index,sensor,x,y,z) {
  hue = t1 + (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1)
  sat = 1
  bri = 1
}

// Rainbow pattern over all pixels
function rainbowAllPixels(index,sensor,x,y,z) {
  hue = t1 + index/pixelCount
  sat = 0.8
  bri = 0.2
}

// BLACK (the default for everything that isn't set by the user)
function solidBlack(index,sensor,x,y,z) {
  hue = 0;
  sat = 0;
  bri = 0;
}

// RED
function solidRed(index,sensor,x,y,z) {
  hue = 0;
  sat = 1;
  bri = 1;
}

// BLUE
function pulsingBlue(index,sensor,x,y,z) {
  hue = 0.6667;
  sat = 1;
  bri = 1 - (0.925 * wave(t1));
}

// On activation, "grow" the lighted region of the segment outward
// from the edges to the center.  Fades from the center to the edges
// on deactivation.
function redGrowFromEdges(index,sensor,x,y,z) { 
  // calculate position in the sensor's group of pixels
  var pos = (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR-1) 

  hue = 0
  sat = 1

  // On activation, light pixels depending on their distance
  // from the segment center 
  bri = abs(pos - 0.5) - (.5-sensorLevels[sensor])
}


///////////////////////////////////////////////////////////
// Initialization 

// this must be called during startup
doGlobalInitialization();

// set patterns for active and inactive sensors
// - you can change these in beforeRender() at any point
// - you can run as many of these as there are sensors
// - you can have as many in your pattern as you want.
setInactivePattern(pulsingBlue,0,NUM_SENSORS)

setActivePattern(redGrowFromEdges,0,96)
setActivePattern(rainbowByGroup,96,96)

export function beforeRender(delta) {
  // time in seconds, wraps about every 4 hours of continuous 
  // run time.
  timebase = (timebase + delta/1000) % 14400
  
  t1 = time(0.04)
  
  // check each sensor to see if it has changed state, and
  // run fadein/fadeout timing routines as needed.
  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16)
    bit = sensor % 16
    state = (frequencyData[element] << 16) & (1 << bit)
    
  // when switch changes to triggered state, start the minimum hold timer
  // (can also act as a debouncer)    
    if (state != previousState[sensor]) {
      if (state) {
        activationTime[sensor] = timebase
        virtualSensors[sensor] = 1
      }
      previousState[sensor] = state
    }
    // if a sensor is on, or was activated less than MIN_HOLD_TIME ago, activate its
    // corresponding virtual sensor, which will be used for lighting calculations
    // if MIN_HOLD_TIME > 0, the virtual sensor also serves as a debouncing mechanism. 
    // Short taps on the sensor will result in it being "on" for at least the MIN_HOLD_TIME
    // In One-Shot mode, MIN_HOLD_TIME should be set to at least FADE_IN_TIME seconds.
    virtualSensors[sensor] = (state && !ONE_SHOT) || ((timebase - activationTime[sensor]) <= MIN_HOLD_TIME)
    
    setSensorFadeLevel(sensor,delta)
  }
}

export function render(index) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  
  if (sensor >= 96) {
    // do something with pixels not attached to a sensor
    // in this case just leave them off
    return;
  }

  // get data for the second set of sensors  
  var sensor2 = sensor + 96
  
  // get current activation state for each sensor bank
  // for this purpose, the sensor is considered active if its
  // switch is active OR if it is doing its final fade-out
  on = virtualSensors[sensor] || (sensorLevels[sensor] > 0)
  on2 = virtualSensors[sensor2] || (sensorLevels[sensor2] > 0)  

  bri = 0
  v = 0
  
  // see if a sensor in either bank has been activated
  if (on || on2) {
    
    // get sensor activation times
    t = (on) ? activationTime[sensor] : 14400
    t2 = (on2) ? activationTime[sensor2] : 14400    
    
    // pick the color and brightness of the most recently activated sensor
    if (t < t2) {
      activePattern[sensor](index,sensor) 
      v = sensorLevels[sensor]      
    }
    else {
      activePattern[sensor2](index,sensor2) 
      v = sensorLevels[sensor2]      
    }
    bri *= v
  }
  
  // if no sensor has been activated, of if the active pattern has
  // faded below a threshold brightness, use the inactive pattern
  if (bri < 0.08) {
    inactivePattern[sensor](index,sensor)
  }

  hsv(hue,sat,bri)
}

function doGlobalInitialization() {
  // set all patterns functions to a default black
  activePattern.mutate(v => solidBlack)
  inactivePattern.mutate(v => solidBlack)
  
  // this makes sure everything's inactive at startup
  activationTime.mutate(v => -100)  
  
  // convert fade times to ms for easier calculation
  FADE_OUT_TIME *= 1000; 
  FADE_IN_TIME *= 1000;  
}

You’re a genius, now it’s got all the bells and whistles for me to play:)

This iteration exposed the following problem for me, which originates from my inability to understand how to code for pixelblaze of course…

Due to the high number of leds(1152), my framerate was about 28-29 with the previous version, now it dropped to 21-22. Not a very big issue as I’m not doing any fancy animations that look particularly wrong, but…

Being me and being sloppy, I started doing some of the animations on the ESP32 side(because as I said it is easier there for me). So in one mode for example, when an input is triggered, it will trigger all inputs of the same row in succession(left and right), then turn them off and keep on only the originating input. I timed it to 40ms intervals in order to not miss a frame on the pixelblaze side(I’m sending the data via serial if you remember) and it worked. I might not have any hue control over the individual inputs when in this mode, but it was good enough.
Then I went and did an expanding circle originating from the active input, again it looked ok on PB, but with the new code some parts of it don’t refresh, the inputs of some rows might not turn on, leading me to believe(I might be wrong in this) that PB doesn’t always read the serial every 40ms and might be a bit slower when low on resources.

In any case, I know that this approach is wrong and very limiting as I have 0 control over the individual LEDs. I started experimenting with render2D which I was dreading as I cannot really get it. I’m using your example from another thread:

speed = 3;
thickness = 0.075

// move coordinate origin to center of display
translate(-0.5,-0.5) 

// create sawtooth waveform with period determined by speed, and
// amplitude range [0,0.5] to match adjusted coordinates
export function beforeRender(delta) {
  t1 = 0.5 * time(.01 * speed)
}

// draw smoothed circle 
export function render2D(index,x,y) {
  h = t1
  s = 1
  v = 1.0 - abs(hypot(x,y) - t1) / thickness
  
  hsv(h, s, v)
}

But I don’t know how I can set the origin to be either a ‘sensor’ or say pixel 6 of a sensor as it is almost in the middle, instead of x,y coordinates, I don’t even know how to trigger the animation only once per activation.

Btw, how can I define a half circle in the mapper? My layout is 96 half circles (with some gap between them zig zaging to the top. I’m planning to use the online tool to do the mapping but thought it would be probably a simple thing to define but after turning the forum upside down, I couldn’t find any relevant examples.

On mapping half circles: It’d look something like this (mostly borrowed from Pixelblaze’s ring example – to get a half circle instead of a full ring, I just changed (PI * 2) to PI.

function (pixelCount) {
  var map = [];
  for (circ = 0; circ < 96; circ++) {
    for (i = 0; i < 12; i++) {
      i = circ % 2 == 1 ? 12 - 1 - i : i //zigzag
      c = i / 12 * Math.PI
      map.push([Math.cos(c), Math.sin(c)+circ * 0.4])
    }
  }
  return map
}

On frame rate & timing: With the “New Pattern” pattern running and 1152 pixels, I get about 28 fps. Not surprising that it slows down a bit when the pattern is doing some compensation.

Almost certainly, the Pixelblaze is using the last sensor frame it received before starting a frame. Anything that comes in while it’s rendering probably gets discarded.

So you’re doing the right thing by slowing down the send frame rate. (There’s a lower limit beyond which the Pixelblaze decides that the sensor board has gone away or malfunctioned, but I think that’s way down below 10fps.)

To prevent possible timing mishaps, you might want to keep sending frames from the ESP at a higher rate, but only reading your sensors and changing the data in the frames at about 20hz (or slower.)

30ms turned out to be the sweet spot, but any animation happening on the ESP side will eventually miss steps when transmitted to PB due to the timing constraints.

Playing around with the mapPixels function, I can have an expanding circle start from any coordinate on the matrix based on pixel index:

speed = 25;
thickness = 0.08;
maxRadius = 1.5;

coordinates = array(pixelCount * 2);

function saveCoordinates(index, x, y) {
  coordinates[index * 2] = x;      
  coordinates[index * 2 + 1] = y; 
}

mapPixels(saveCoordinates);

var t2 = 0;

export function inputNumberPixel(v) {
  pixel = v;
  t2 = 0;   
}

export function beforeRender(delta) {
  t2 += delta * speed * 0.0001;
}


export function render2D(index, x, y) {
  var originX = coordinates[pixel * 2];
  var originY = coordinates[pixel * 2 + 1];

  var distance = hypot(x - originX, y - originY);

  var bri = 1.0 - abs(distance - t2) / thickness;

  bri = clamp(bri, 0, 1);

  var hue = t2;

  var sat = 1;

  hsv(hue, sat, bri);
}

I tried many variations so I could incorporate it in your code but can’t get any meaningful result.

I’m replacing pixel with (sensor*12 +5) to get the 6th pixel of every sensor as the origin point.

Then, if I make a new function for the circle:

function circles(index,sensor,x,y,z) {
  
 var sensor = floor(index / PIXELS_PER_SENSOR)
  
  var originX = coordinates[(sensor*12 + 5) * 2];
  var originY = coordinates[(sensor*12 + 5) * 2 + 1];

  var distance = hypot(x - originX, y - originY);

  hue = t2;
  sat = 1;
  
  bri = 1.0 - abs(distance - t2) / thickness;
  bri = clamp(bri, 0, 1);
   
}

And reset T2 on sensor state change to true:

if (state != previousState[sensor]) {
      if (state) {
        t2 = 0;
        activationTime[sensor] = timebase
        virtualSensors[sensor] = 1
      }
      previousState[sensor] = state
     
    }

I just get that sensor to turn on, no circle.

If I do it in a more lame way, by adding a variable that will be true when the circles function is selected and then move everything in render2D like so:

export function render2D(index,x,y) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  
  if (circle) {
      var originX = coordinates[(sensor*12 + 5) * 2];
  var originY = coordinates[(sensor*12 + 5) * 2 + 1];

  var distance = hypot(x - originX, y - originY);

  hue = t2;
  sat = 1;
  
  bri = 1.0 - abs(distance - t2) / thickness;
  bri = clamp(bri, 0, 1);
  } else {
  
  
  if (sensor >= 96) {
    // do something with pixels not attached to a sensor
    // in this case just leave them off
    return;
  }

  // get data for the second set of sensors  
  var sensor2 = sensor + 96
  
  // get current activation state for each sensor bank
  // for this purpose, the sensor is considered active if its
  // switch is active OR if it is doing its final fade-out
  on = virtualSensors[sensor] || (sensorLevels[sensor] > 0)
  on2 = virtualSensors[sensor2] || (sensorLevels[sensor2] > 0)  

  bri = 0
  v = 0
  
  // see if a sensor in either bank has been activated
  if (on || on2) {
    
    // get sensor activation times
    t = (on) ? activationTime[sensor] : 14400
    t2 = (on2) ? activationTime[sensor2] : 14400    
    
    // pick the color and brightness of the most recently activated sensor
    if (t < t2) {
      activePattern[sensor](index,sensor) 
      v = sensorLevels[sensor]      
    }
    else {
      activePattern[sensor2](index,sensor2) 
      v = sensorLevels[sensor2]      
    }
    bri *= v
  }
  
  // if no sensor has been activated, of if the active pattern has
  // faded below a threshold brightness, use the inactive pattern
  if (bri < 0.08) {
    inactivePattern[sensor](index,sensor)
  }
  
  }

  hsv(hue,sat,bri)
}

I just get a short flash of all pixels when a sensor is active.

Is there something obvious that I’m missing?

Here’s a version that uses your circle drawing code to draw an animated circle at the pixel specified by the “Pixel” control.

It works by drawing the circle during background drawing process in render2D – if the current pixel is part of the circle, it will be drawn that way. Otherwise the normal “inactive” animation for that sensor will be drawn.

Really, very little changed here – mostly just switched from render() to render2D(), and added a circle function and its supporting variables. Hope this helps!

Latest Pattern Code
// DEBUGGING BLOCK - adds a test switch that controls one or more
// sensors. Comment out, or remove for production
export function toggleSetSensors(isOn) {
  setSensorBit(99,isOn)
  setSensorBit(0,isOn)
}

var originPixel;
export function inputNumberPixel(v) {
  originPixel = v;
}

// END DEBUGGING BLOCK


// data from sensors comes in here...
export var frequencyData = array(32) 

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96 * 2

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 12

// Sensor behavior flag:
// If set to true (1), LEDs will light only when their sensor transitions
// from 0 to 1 and will run through their complete animation sequence to fade-out whether or
// not the physical sensor bit is still in the '1' state.
// Otherwise on sensor asctivation, LEDs will run their fade-in animation and remain in the
// fully lit "hold" state until the sensor bit goes back to '0' and then completing
// the fade out.
var ONE_SHOT = 1

// time, in seconds, that it takes to "fade in" on sensor activation
var FADE_IN_TIME = 2

// time it takes to fade out after sensor deactivation, once 
// minimum hold time expires
var FADE_OUT_TIME = 2

// minimum time the pixel block will be lit following sensor
// activation.  Can be zero.  If using one-shot mode,
// should be set to at least FADE_IN_TIME seconds.
var MIN_HOLD_TIME = 2

// variables to manage switch state and timing
var previousState = array(NUM_SENSORS)
var activationTime = array(NUM_SENSORS)
var virtualSensors = array(NUM_SENSORS)

var sensorLevels = array(NUM_SENSORS)

// arrays to hold pattern functions for each sensor
var activePattern  = array(NUM_SENSORS)
var inactivePattern  = array(NUM_SENSORS)

coordinates = array(pixelCount * 2);

function saveCoordinates(index, x, y,z) {
  coordinates[index * 2] = x;      
  coordinates[index * 2 + 1] = y; 
}
mapPixels(saveCoordinates);

// variables to hold final hue, saturation and brightness
// these should be set by individual pattern functions
var hue,sat,bri

// timers:
// timebase tracks real time in seconds since pattern startup
// t1 is available for use by pattern functions and can be set to any
// desired speed.
var timebase;
var t1,t2;

function setSensorFadeLevel(sensor,delta) {
  fadeIn = fadeOut = 1
  
   if (virtualSensors[sensor]) {
     // if the switch is on, run the fade in 'till we reach full brightness        
     sensorLevels[sensor] += delta/FADE_IN_TIME
   }
   else {
     // if it's off, run fade out 'till we reach zero.
     sensorLevels[sensor] -= delta/FADE_OUT_TIME
   }
   sensorLevels[sensor] = clamp(sensorLevels[sensor],0,1)
}

// sets the bit flag for a sensor to the specified (0 or 1) value
function setSensorBit(sensor,newValue) {
    element = floor(sensor / 16)
    bit = sensor % 16
    
    // set or clear bit as necessary
    if (newValue) {
      v = (frequencyData[element] << 16) | (1 << bit)
    }
    else {
      v = (frequencyData[element] << 16) & ~(1 << bit)
    }  
    
    // save value
    frequencyData[element] = v >> 16
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setActivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    activePattern[start + i] = pFn
  }
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setInactivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    inactivePattern[start + i] = pFn
  }
}

// sets default value for all sensor patterns, active and inactivePattern
// should be called during pattern initialization.
function initializeSensorPatterns(defaultPattern){
  p = defaultPattern
  activePattern.mutate(v => { return p})
  inactivePattern.mutate(v => { return p})  
}

//////////////////////////////////////////////////////////////
// a few pattern functions as examples.  You can do anything
// you want in these, as long as you eventually set the 
// hue, sat and bri values.

// Displays a rainbow pattern over the pixels associated with
// a single sensor
function rainbowByGroup(index,sensor,x,y,z) {
  hue = t1 + (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1)
  sat = 1
  bri = 1
}

// Rainbow pattern over all pixels
function rainbowAllPixels(index,sensor,x,y,z) {

  hue = t1 + index/pixelCount
  sat = 0.8
  bri = 0.2
}


// BLACK (the default for everything that isn't set by the user)
function solidBlack(index,sensor,x,y,z) {
  hue = 0;
  sat = 0;
  bri = 0;
}

// RED
function solidRed(index,sensor,x,y,z) {
  hue = 0;
  sat = 1;
  bri = 1;
}

// BLUE
function pulsingBlue(index,sensor,x,y,z) {
  hue = 0.6667;
  sat = 1;
  bri = 1 - (0.925 * wave(t1));
}

// On activation, "grow" the lighted region of the segment outward
// from the edges to the center.  Fades from the center to the edges
// on deactivation.
function redGrowFromEdges(index,sensor,x,y,z) { 
  // calculate position in the sensor's group of pixels
  var pos = (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR-1) 

  hue = 0
  sat = 1

  // On activation, light pixels depending on their distance
  // from the segment center 
  bri = abs(pos - 0.5) - (.5-sensorLevels[sensor])
}


// draw an expanding animated circle with its center at the specified
// pixel
function circleAtIndex(origin,x,y) {
  var ox = coordinates[origin * 2];
  var oy = coordinates[origin * 2 + 1];

  var distance = hypot(x - ox, y - oy);

  bri = 1.0 - abs(distance - t2) / 0.1;
  bri = clamp(bri, 0, 1);

  hue = t2;
  sat = 1;
}


///////////////////////////////////////////////////////////
// Initialization 

// this must be called during startup
doGlobalInitialization();

// set patterns for active and inactive sensors
// - you can change these in beforeRender() at any point
// - you can run as many of these as there are sensors
// - you can have as many in your pattern as you want.
setInactivePattern(pulsingBlue,0,NUM_SENSORS)

setActivePattern(redGrowFromEdges,0,96)
setActivePattern(rainbowAllPixels,96,96)

export function beforeRender(delta) {
  // time in seconds, wraps about every 4 hours of continuous 
  // run time.
  timebase = (timebase + delta/1000) % 14400
  
  t1 = time(0.04)
  t2 = time(0.05);  
  
  // check each sensor to see if it has changed state, and
  // run fadein/fadeout timing routines as needed.
  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16)
    bit = sensor % 16
    state = (frequencyData[element] << 16) & (1 << bit)
    
  // when switch changes to triggered state, start the minimum hold timer
  // (can also act as a debouncer)    
    if (state != previousState[sensor]) {
      if (state) {
        activationTime[sensor] = timebase
        virtualSensors[sensor] = 1
      }
      previousState[sensor] = state
    }
    // if a sensor is on, or was activated less than MIN_HOLD_TIME ago, activate its
    // corresponding virtual sensor, which will be used for lighting calculations
    // if MIN_HOLD_TIME > 0, the virtual sensor also serves as a debouncing mechanism. 
    // Short taps on the sensor will result in it being "on" for at least the MIN_HOLD_TIME
    // In One-Shot mode, MIN_HOLD_TIME should be set to at least FADE_IN_TIME seconds.
    virtualSensors[sensor] = (state && !ONE_SHOT) || ((timebase - activationTime[sensor]) <= MIN_HOLD_TIME)
    
    setSensorFadeLevel(sensor,delta)
  }
}

export function render2D(index,x,y) {
  var sensor = floor(index / PIXELS_PER_SENSOR)

  if (sensor >= 96) {
    // do something with pixels not attached to a sensor
    // in this case just leave them off
    return;
  }

  // get data for the second set of sensors  
  var sensor2 = sensor + 96
  
  // get current activation state for each sensor bank
  // for this purpose, the sensor is considered active if its
  // switch is active OR if it is doing its final fade-out
  on = virtualSensors[sensor] || (sensorLevels[sensor] > 0)
  on2 = virtualSensors[sensor2] || (sensorLevels[sensor2] > 0)  

  bri = 0
  v = 0
  
  // see if a sensor in either bank has been activated
  if (on || on2) {
    
    // get sensor activation times
    t = (on) ? activationTime[sensor] : 14400
    t2 = (on2) ? activationTime[sensor2] : 14400    
    
    // pick the color and brightness of the most recently activated sensor
    if (t < t2) {
      activePattern[sensor](index,sensor,x,y) 
      v = sensorLevels[sensor]      
    }
    else {
      activePattern[sensor2](index,sensor2,x,y) 
      v = sensorLevels[sensor2]      
    }
    bri *= v
  }
  
  // if no sensor has been activated, of if the active pattern has
  // faded below a threshold brightness, use the inactive pattern
  if (bri < 0.08) {
    // see if this pixel is part of an active circle
    circleAtIndex(originPixel,x,y);
    // if not, draw the background pattern
    if (bri < 0.08) {
      inactivePattern[sensor](index,sensor,x,y)
    }
  }

  hsv(hue,sat,bri)
}

function doGlobalInitialization() {
  // set all patterns functions to a default black
  activePattern.mutate(v => solidBlack)
  inactivePattern.mutate(v => solidBlack)
  
  // this makes sure everything's inactive at startup
  activationTime.mutate(v => -100)  
  
  // convert fade times to ms for easier calculation
  FADE_OUT_TIME *= 1000; 
  FADE_IN_TIME *= 1000;  
}
2 Likes

I’m getting a very strange behavior:

With all patterns(active or inactive) set to solidBlack, there is a constant animation of a circle that starts from the bottom right(0,0 on my map) and expands.

Screen Shot 2024-06-24 at 11.12.40 PM

If I use circleAtIndex on an active pattern the animation keeps going starting at 0,0. If I activate a sensor in any row(bar for the upper 2 rows), the circle is half-drawn, but anyway doesn’t originate from the active sensor.

Screen Shot 2024-06-24 at 11.24.10 PM

Say that we solve this, the other thing I realized today while playing with the circle pattern I posted above was that it is impossible to draw 2 circles at the same time(if for example 2 sensors became active together or the second before the first had passed the edges of the matrix.

It’s pretty much doing what it’s supposed to right now. This was just a demo of how to draw a circle at a given pixel – if you use the webUI “Pixel” control to set the starting pixel index, that’s where the circle will start. It’s completely independent of the sensors at the moment. circleAtIndex() is meant to be called only the once, from ‘render2D()’, where it currently lives.

Give me a day or two, and I can hook it up to the sensors (should be fairly simple – if your active pattern function sets the variable ‘originPixel’ to the center of its sensor segment, that’s where the circle will start.)

On multiple circles: how many do you think you’ll want going at once? There’s going to be a practical limit imposed by the pixelblaze’s speed. I’m thinking 3 or 4-ish maybe, but need to play with it to find out.

Edit: Oh, can you post your mapping function for me to use for testing? Thanks!

Ah, my bad for not explaining it better!

Here’s my map:

function(pixelCount) {
  var map = [];
  var horizontalSpacing = 3.5;
  var verticalSpacing = 3;    

  var totalCircles = 8;  
  var totalRows = 12;   

  for (row = totalRows - 1; row >= 0; row--) { 
    for (circ = totalCircles - 1; circ >= 0; circ--) { 
      var zigzagIndex = (totalRows - 1 - row) % 2 === 0 ? circ : totalCircles - 1 - circ;
      for (i = 0; i < 12; i++) {
        var angle = -i / 12 * Math.PI; 
        map.push([
          Math.cos(angle) * 1 + zigzagIndex * horizontalSpacing, 
          Math.sin(angle) * 1 + row * verticalSpacing     
        ]);
      }
    }
  }
  return map;
}

I’ll try to keep the animation fast so that 3-4 are plenty, I’m doing this with all other animations, 1152 pixels are stretching it and the more I put in the pattern the lower the fps gets-I’m down to 14-19 at best:)

An irrelevant question:

If I want to translate an object on the y axis, start from -1.5, end at 0.5, how do I do it?
I tried the following(with many variations):

var travel = -1.5
export function beforeRender() {

  theta = PI2 * time(0.061);
  resetTransform();
  clamp(travel, -1.5, 0.5)
  translate( -0.5, travel * time(0.08));  
    rotate(theta);
}

I’m sure you can see straight away it doesn’t work:) I understand that clamp is the wrong function, what would be the equivalent?

Thanks for all the time you’ve spent on this so far:)

Glad to help! And thanks for the map, it really clarified things.

Below is update that will draw one-shot expanding circles centered on a sensor’s segment when it is activated. Up to 3 circles can be active at a time, and they are assigned to the most recently pressed 3 sensors.

On translation: You’ve just got the order slightly wrong. clamp() is applied after a value has been calculated to limit the range. What you want to do map time()'s [0,1] range output into the range [-1.5,0.5]

Here’s a function that’ll do the job:

// maps a value in the range [0,1] to an arbitrary range
// [a,b] 
function map(v,a,b) {
  return a + (b - a) * v;
}

// to use...
travel = map(time(0.08), -1.5, 0.5)

Edit: If you tried the code before seeing this edit, be sure to
export var frequencyData = array(32)
or you will get no data from the sensors. I removed the export statement for easier debugging and forgot to put it back before posting this. Fixed now, but… doh!

Update: 3 Circles

// DEBUGGING BLOCK - adds a test switch that controls one or more
// sensors. Comment out, or remove for production
export function toggleSensor1(isOn) {
  setSensorBit(52,isOn)
}

export function toggleSensor2(isOn) {
  setSensorBit(10,isOn)  
}

export function toggleSensor3(isOn) {
  setSensorBit(90,isOn)  
}

// END DEBUGGING BLOCK

// variables for expanding circles drawn at active
// sensors
var MAX_CIRCLES = 3
var circleSpeed = 1700
var circleTrigger = 0;
var circleOrigin = array(MAX_CIRCLES)
var circlePos = array(MAX_CIRCLES)
circleOrigin.mutate(v=>{return -1})

// data from sensors comes in here...
export var frequencyData = array(32) 

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96 * 2

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 12

// Sensor behavior flag:
// If set to true (1), LEDs will light only when their sensor transitions
// from 0 to 1 and will run through their complete animation sequence to fade-out whether or
// not the physical sensor bit is still in the '1' state.
// Otherwise on sensor asctivation, LEDs will run their fade-in animation and remain in the
// fully lit "hold" state until the sensor bit goes back to '0' and then completing
// the fade out.
var ONE_SHOT = 1

// time, in seconds, that it takes to "fade in" on sensor activation
var FADE_IN_TIME = 2

// time it takes to fade out after sensor deactivation, once 
// minimum hold time expires
var FADE_OUT_TIME = 2

// minimum time the pixel block will be lit following sensor
// activation.  Can be zero.  If using one-shot mode,
// should be set to at least FADE_IN_TIME seconds.
var MIN_HOLD_TIME = 2

// variables to manage switch state and timing
var previousState = array(NUM_SENSORS)
var activationTime = array(NUM_SENSORS)
var virtualSensors = array(NUM_SENSORS)

var sensorLevels = array(NUM_SENSORS)

// arrays to hold pattern functions for each sensor
var activePattern  = array(NUM_SENSORS)
var inactivePattern  = array(NUM_SENSORS)

// saved x,y coordinates of all pixels
coordinates = array(pixelCount * 2);

function saveCoordinates(index, x, y,z) {
  coordinates[index * 2] = x;      
  coordinates[index * 2 + 1] = y; 
}
mapPixels(saveCoordinates);

// variables to hold final hue, saturation and brightness
// these should be set by individual pattern functions
var hue,sat,bri

// timers:
// timebase tracks real time in seconds since pattern startup
// t1 is available for use by pattern functions and can be set to any
// desired speed.
var timebase;
var t1,t2;

function setSensorFadeLevel(sensor,delta) {

   if (virtualSensors[sensor]) {
     // if the switch is on, run the fade in 'till we reach full brightness        
     sensorLevels[sensor] += delta/FADE_IN_TIME
   }
   else {
     // if it's off, run fade out 'till we reach zero.
     sensorLevels[sensor] -= delta/FADE_OUT_TIME
   }
   sensorLevels[sensor] = clamp(sensorLevels[sensor],0,1)
}

// sets the bit flag for a sensor to the specified (0 or 1) value
function setSensorBit(sensor,newValue) {
    element = floor(sensor / 16)
    bit = sensor % 16
    
    // set or clear bit as necessary
    if (newValue) {
      v = (frequencyData[element] << 16) | (1 << bit)
    }
    else {
      v = (frequencyData[element] << 16) & ~(1 << bit)
    }  
    
    // save value
    frequencyData[element] = v >> 16
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setActivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    activePattern[start + i] = pFn
  }
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setInactivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    inactivePattern[start + i] = pFn
  }
}

// sets default value for all sensor patterns, active and inactivePattern
// should be called during pattern initialization.
function initializeSensorPatterns(defaultPattern){
  p = defaultPattern
  activePattern.mutate(v => { return p})
  inactivePattern.mutate(v => { return p})  
}

//////////////////////////////////////////////////////////////
// a few pattern functions as examples.  You can do anything
// you want in these, as long as you eventually set the 
// hue, sat and bri values.

// Displays a rainbow pattern over the pixels associated with
// a single sensor
function rainbowByGroup(index,sensor,x,y,z) {
  hue = t1 + (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1)
  sat = 1
  bri = 1
}

// Rainbow pattern over all pixels
function rainbowAllPixels(index,sensor,x,y,z) {

  hue = t1 + index/pixelCount
  sat = 0.8
  bri = 0.2
}


// BLACK (the default for everything that isn't set by the user)
function solidBlack(index,sensor,x,y,z) {
  hue = 0;
  sat = 0;
  bri = 0;
}

// RED
function solidRed(index,sensor,x,y,z) {
  hue = 0;
  sat = 1;
  bri = 1;
}

// BLUE
function pulsingBlue(index,sensor,x,y,z) {
  hue = 0.6667;
  sat = 1;
  bri = 1 - (0.925 * wave(t1));
}

// On activation, "grow" the lighted region of the segment outward
// from the edges to the center.  Fades from the center to the edges
// on deactivation.
function redGrowFromEdges(index,sensor,x,y,z) { 
  // calculate position in the sensor's group of pixels
  var pos = (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR-1)

  hue = 0
  sat = 1

  // On activation, light pixels depending on their distance
  // from the segment center 
  bri = abs(pos - 0.5) - (.5-sensorLevels[sensor])
}

// Add a call to this function from any "Active" pattern, and
// it will draw a circle with origin at the sensor's segment center.
function triggerCircle(sensor) {
  circleOrigin[circleTrigger] = (1+sensor) * PIXELS_PER_SENSOR - (PIXELS_PER_SENSOR / 2)
}

// draw expanding animated circles with its center at the specified
// pixels
function circleAtIndex(x,y,delta) {

  for (var i = 0; i < MAX_CIRCLES; i++) {
    if (bri >= 0.08) break;    
    var p = circleOrigin[i]
    if (p < 0) continue
    
    var ox = coordinates[p * 2];
    var oy = coordinates[p * 2 + 1];

    var d = hypot(x - ox, y - oy); 

    bri = 1.0 - abs(d - circlePos[i]) / 0.1;
    bri = clamp(bri, 0, 1);
    hue = circlePos[i] + i * 0.618;    
  }
    
  sat = 0.9;
}

///////////////////////////////////////////////////////////
// Initialization 

// this must be called during startup
doGlobalInitialization();

// set patterns for active and inactive sensors
// - you can change these in beforeRender() at any point
// - you can run as many of these as there are sensors
// - you can have as many in your pattern as you want.
setInactivePattern(pulsingBlue,0,NUM_SENSORS)

setActivePattern(solidRed,0,96)
setActivePattern(rainbowAllPixels,96,96)

export function beforeRender(delta) {
  // time in seconds, wraps about every 4 hours of continuous 
  // run time.
  timebase = (timebase + delta/1000) % 14400
  
  t1 = time(0.04)
  t2 = time(0.05);  
  
  // move active circles, and reset them once they're off the display
  for (var i = 0; i < MAX_CIRCLES; i++) {
    if (circleOrigin[i] >= 0) {
      circlePos[i] += delta/circleSpeed;
      if (circlePos[i] > 1) {
        circleOrigin[i] = -1;
        circlePos[i] = 0;
      }
    }
  }
  
  // check each sensor to see if it has changed state, and
  // run fadein/fadeout timing routines as needed.
  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16)
    bit = sensor % 16
    state = (frequencyData[element] << 16) & (1 << bit)
    
  // when switch changes to triggered state, start the minimum hold timer
  // (can also act as a debouncer)    
    if (state != previousState[sensor]) {
      if (state) {
        activationTime[sensor] = timebase
        virtualSensors[sensor] = 1
        
        // on activation, trigger a one-shot expanding circle
        // at the center of the sensor's pixels.  Up to MAX_CIRCLES
        // circles can be active at a time.
        triggerCircle(sensor)
        circleTrigger = (circleTrigger + 1) % MAX_CIRCLES 
      }
      previousState[sensor] = state
    }
    // if a sensor is on, or was activated less than MIN_HOLD_TIME ago, activate its
    // corresponding virtual sensor, which will be used for lighting calculations
    // if MIN_HOLD_TIME > 0, the virtual sensor also serves as a debouncing mechanism. 
    // Short taps on the sensor will result in it being "on" for at least the MIN_HOLD_TIME
    // In One-Shot mode, MIN_HOLD_TIME should be set to at least FADE_IN_TIME seconds.
    virtualSensors[sensor] = (state && !ONE_SHOT) || ((timebase - activationTime[sensor]) <= MIN_HOLD_TIME)
    
    setSensorFadeLevel(sensor,delta)
  }
}

export function render2D(index,x,y) {
  var sensor = floor(index / PIXELS_PER_SENSOR)

  // get data for the second set of sensors  
  var sensor2 = sensor + 96
  
  // get current activation state for each sensor bank
  // for this purpose, the sensor is considered active if its
  // switch is active OR if it is doing its final fade-out
  on = virtualSensors[sensor] || (sensorLevels[sensor] > 0)
  on2 = virtualSensors[sensor2] || (sensorLevels[sensor2] > 0)  

  bri = 0
  v = 0

  // see if a sensor in either bank has been activated
  if (on || on2) {
    
    // get sensor activation times
    var t = (on) ? activationTime[sensor] : 14400
    var t2 = (on2) ? activationTime[sensor2] : 14400    
    
    // pick the color and brightness of the most recently activated sensor
    if (t < t2) {
      activePattern[sensor](index,sensor,x,y) 
      v = sensorLevels[sensor]      
    }
    else {
      activePattern[sensor2](index,sensor2,x,y) 
      v = sensorLevels[sensor2]      
    }
    bri *= v
  }
  
  // see if this pixel is part of an active circle
  circleAtIndex(x,y);

  // if no sensor has been activated, of if the active pattern has
  // faded below a threshold brightness, use the inactive pattern
  if (bri < 0.08) {
    inactivePattern[sensor](index,sensor,x,y)
  }

  hsv(hue,sat,bri)
}

function doGlobalInitialization() {
  // set all patterns functions to a default black
  activePattern.mutate(v => solidBlack)
  inactivePattern.mutate(v => solidBlack)
  
  // this makes sure everything's inactive at startup
  activationTime.mutate(v => -100)  
  
  // convert fade times to ms for easier calculation
  FADE_OUT_TIME *= 1000; 
  FADE_IN_TIME *= 1000;  
}

Oh don’t worry, I did it so many times in the past, it’s the first thing I check nowadays:)

Playing with circleAtIndex, I thought I’d try a similar approach to get an expanding row based on sensor trigger. Given the topology of the matrix and the half circles it didn’t make sense to use coordinates, but pixel index, kind of like this which works:

speed = 25;
coordinates = array(pixelCount * 2);
pixelsPerRow = 96; 

var t2 = 0;
var originPixel = 0;
var rowStart = 0;
var animationDuration = 1.2; 
var isAnimating = false;

export function inputNumberPixel(v) {
  originPixel = v;
  rowStart = floor(v / pixelsPerRow) * pixelsPerRow;
  t2 = 0;
  isAnimating = true;
}

export function beforeRender(delta) {
  if (isAnimating) {
    t2 += delta * speed * 0.0001;
    if (t2 > (animationDuration + 1)) {
      isAnimating = false; 
    }
  }
}

function expandingRow(index) {
  var pixelInRowIndex = index % pixelsPerRow;
  var originInRowIndex = originPixel % pixelsPerRow;
  
  var distance = abs(pixelInRowIndex - originInRowIndex) / (pixelsPerRow / 2);
  
  var animationProgress = t2 / animationDuration;
  
  var isExpanding = animationProgress < 0.5;
  var phaseProgress = isExpanding ? animationProgress * 2 : (animationProgress - 0.5) * 2;
  
  var bri;
  if (isExpanding) {
    bri = 1 - (distance / phaseProgress);
  } else {
    bri = (distance / phaseProgress) - 1;
  }
  bri = clamp(bri, 0, 1);
  
  var hue = animationProgress;
  var sat = 1;
  hsv(hue, sat, bri);
}

export function render2D(index, x, y) {

  if (floor(index / pixelsPerRow) == floor(originPixel / pixelsPerRow)) {
    expandingRow(index);
  }
}

Then, by applying a similar approach to circleAtIndex in the code, I would expect to get the row to expand on sensor trigger. Is there something obvious I’m missing?

Forgive the long code, at this stage it is too messy to remove everything else. You’ll recognise plenty of your old patterns in there:)

// variables for expanding circles drawn at active
// sensors
var MAX_CIRCLES = 3
var circleSpeed = 1300
var circleTrigger = 0;
var circleOrigin = array(MAX_CIRCLES)
var circlePos = array(MAX_CIRCLES)
circleOrigin.mutate(v=>{return -1})

// variables for Oasis
var t5,t6,t7,t8;   
export var speed = 0.7;

// variables for expandingRow
var expandingRowSpeed = 25;
var pixelsPerRow = 96; 
var expandingRowT2 = 0;
var expandingRowOriginPixel = 0;
var expandingRowStart = 0;
var expandingRowAnimationDuration = 1.2; 
var isExpandingRowAnimating = false;

// variables for Finished Circles
var t10 = 0;
var expansionCount = 0;  
var maxExpansions = 4;  

// data from sensors comes in here...
export var frequencyData = array(32) 
export var light
export var modes
export var gameState
export var accelerometer

pinMode(25,OUTPUT)
digitalWrite(25, HIGH) //Turn on the wall

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96 * 2

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 12

// Sensor behavior flag:
// If set to true (1), LEDs will light only when their sensor transitions
// from 0 to 1 and will run through their complete animation sequence to fade-out whether or
// not the physical sensor bit is still in the '1' state.
// Otherwise on sensor asctivation, LEDs will run their fade-in animation and remain in the
// fully lit "hold" state until the sensor bit goes back to '0' and then completing
// the fade out.
var ONE_SHOT = 0

// time, in seconds, that it takes to "fade in" on sensor activation
var FADE_IN_TIME = 0.3

// time it takes to fade out after sensor deactivation, once 
// minimum hold time expires
var FADE_OUT_TIME = 0.4

// minimum time the pixel block will be lit following sensor
// activation.  Can be zero.  If using one-shot mode,
// should be set to at least FADE_IN_TIME seconds.
var MIN_HOLD_TIME = 0.2

// variables to manage switch state and timing
var previousState = array(NUM_SENSORS)
var activationTime = array(NUM_SENSORS)
var virtualSensors = array(NUM_SENSORS)

var sensorLevels = array(NUM_SENSORS)

// arrays to hold pattern functions for each sensor
var activePattern  = array(NUM_SENSORS)
var inactivePattern  = array(NUM_SENSORS)

// saved x,y coordinates of all pixels
coordinates = array(pixelCount * 2);

function saveCoordinates(index, x, y,z) {
  coordinates[index * 2] = x;      
  coordinates[index * 2 + 1] = y; 
}
mapPixels(saveCoordinates);

// variables to hold final hue, saturation and brightness
// these should be set by individual pattern functions
var hue,sat,bri

// timers:
// timebase tracks real time in seconds since pattern startup
// t1 is available for use by pattern functions and can be set to any
// desired speed.
var timebase;
var t1

var t3  // Color Bands Timer
var t4  // Color Bands Timer

function setSensorFadeLevel(sensor,delta) {
  fadeIn = fadeOut = 1
  
   if (virtualSensors[sensor]) {
     // if the switch is on, run the fade in 'till we reach full brightness        
     sensorLevels[sensor] += delta/FADE_IN_TIME
   }
   else {
     // if it's off, run fade out 'till we reach zero.
     sensorLevels[sensor] -= delta/FADE_OUT_TIME
   }
   sensorLevels[sensor] = clamp(sensorLevels[sensor],0,1)
}

// sets the bit flag for a sensor to the specified (0 or 1) value
function setSensorBit(sensor,newValue) {
    element = floor(sensor / 16)
    bit = sensor % 16
    
    // set or clear bit as necessary
    if (newValue) {
      v = (frequencyData[element] << 16) | (1 << bit)
    }
    else {
      v = (frequencyData[element] << 16) & ~(1 << bit)
    }  
    
    // save value
    frequencyData[element] = v >> 16
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setActivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    activePattern[start + i] = pFn
  }
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setInactivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    inactivePattern[start + i] = pFn
  }
}

// sets default value for all sensor patterns, active and inactivePattern
// should be called during pattern initialization.
function initializeSensorPatterns(defaultPattern){
  p = defaultPattern
  activePattern.mutate(v => { return p})
  inactivePattern.mutate(v => { return p})  
}

//////////////////////////////////////////////////////////////
// a few pattern functions as examples.  You can do anything
// you want in these, as long as you eventually set the 
// hue, sat and bri values.

// Displays a rainbow pattern over the pixels associated with
// a single sensor
function rainbowByGroup(index,sensor,x,y,z) {
  hue = t1 + (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1)
  sat = 1
  bri = 1
}

// Rainbow pattern over all pixels
function rainbowAllPixels(index,sensor,x,y,z) {
  hue = t1 + index/pixelCount
  sat = 0.8
  bri = 0.2
}

// BLACK (the default for everything that isn't set by the user)
function solidBlack(index,sensor,x,y,z) {
  hue = 0;
  sat = 0;
  bri = 0;
}

// RED
function solidRed(index,sensor,x,y,z) {
  hue = 0;
  sat = 1;
  bri = 1;
}

// BLUE
function pulsingBlue(index,sensor,x,y,z) {
  hue = 0.6667;
  sat = 1;
  bri = 1 - (0.925 * wave(t1));
}

function oasis(index,sensor,x,y,z) {
  
    var x,v;
  x = index/pixelCount;
  v =  (wave((22 * x) - t5) - 0.5) << 1;
  v += (wave((30 * x) + t6) - 0.5) << 1;
  v += (wave((14 * x) + t7) - 0.5) << 1;
  v += (wave((10 * x) - t8) - 0.5) << 1;
  v = v >> 2; 
  
  hue = .34-(0.055 * triangle(x+v));
  sat = 1.4-v;
  bri = max(-1.73,v)+.25;
}

function oasisInactive(index,sensor,x,y,z) {
  
    var x,v;
  x = index/pixelCount;
  v =  (wave((22 * x) - t5) - 0.5) << 1;
  v = v >> 3; 
  
  hue = .34-(0.055 * triangle(x+v));
  sat = 1.4-v;
  bri = (max(-1.73,v)+.25) * .05;
}


// On activation, "grow" the lighted region of the segment outward
// from the edges to the center.  Fades from the center to the edges
// on deactivation.
function rainbowGrowFromEdges(index,sensor,x,y,z) { 
  // calculate position in the sensor's group of pixels
  var pos = (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR-1) 

  hue = t1 + (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1)
  sat = 1

  // On activation, light pixels depending on their distance
  // from the segment center 
  bri = abs(pos - 0.5) - (.5-sensorLevels[sensor])
}


function colorBands(index,sensor,x,y,z) {
  hue = index / (pixelCount /2) // Notice how each hue appears twice
  
  // Create the areas where white is mixed in. Start with a wave.
  sat = wave(-index / 3 + t3)
  
  // A little desaturation goes a long way, so it's typical to start from 1 
  // (saturated) and sharply dip to 0 to make white areas.
  sat = 1 - sat * sat * sat * sat * sat
  
  // Create the slowly moving dark regions
  bri = wave(index / 2 + t4) * wave(index / 5 - t4) + wave(index / 7 + t4)
  
  bri = bri * bri * bri * bri
}

// Add a call to this function from any "Active" pattern, and
// it will draw a circle with origin at the sensor's segment center.
function triggerCircle(sensor) {
  circleOrigin[circleTrigger] = (1+sensor) * PIXELS_PER_SENSOR - (PIXELS_PER_SENSOR / 2)
}

// draw expanding animated circles with its center at the specified
// pixels
function circleAtIndex(x,y,delta) {

  for (var i = 0; i < MAX_CIRCLES; i++) {
    if (bri >= 0.08) break;    
    var p = circleOrigin[i]
    if (p < 0) continue
    
    var ox = coordinates[p * 2];
    var oy = coordinates[p * 2 + 1];

    var d = hypot(x - ox, y - oy); 

    bri = 1.0 - abs(d - circlePos[i]) / 0.1;
    bri = clamp(bri, 0, 1);
    hue = circlePos[i] + i * 0.618;    
  }
    
  sat = 0.9;
}

function hexStar(x, y, r) {
  // rescale to pointy parts of star
  x = abs(x * 1.73205); y = abs(y * 1.73205); 
  
  var dot = 2 * min(-0.5 * x + 0.866025 * y, 0);
  x -= dot * -0.5; y -= dot * 0.866025;
  
  dot = 2 * min(0.866025 * x + -0.5 * y, 0);
  x -= dot * 0.866025; y -= dot * -0.5;
  
  x -= clamp(x, r * 0.57735, r * 1.73205);
  y -= r;
  
  return ((y > 0) - (y < 0)) * hypot(x, y) / 1.73205;
}

// Sensor trigger for Expanding Row
function triggerExpandingRow(sensor) {
  expandingRowOriginPixel = sensor * PIXELS_PER_SENSOR;
  expandingRowStart = floor(expandingRowOriginPixel / pixelsPerRow) * pixelsPerRow;
  expandingRowT2 = 0;
  isExpandingRowAnimating = true;
}

// Expanding Row main
function expandingRow(index, sensor) {
  var pixelInRowIndex = index % pixelsPerRow;
  var originInRowIndex = expandingRowOriginPixel % pixelsPerRow;
  var distance = abs(pixelInRowIndex - originInRowIndex) / (pixelsPerRow / 2);
  var animationProgress = expandingRowT2 / expandingRowAnimationDuration;
  var isExpanding = animationProgress < 0.5;
  var phaseProgress = isExpanding ? animationProgress * 2 : (animationProgress - 0.5) * 2;
  
  var rowBri;
  if (isExpanding) {
    rowBri = 1 - (distance / phaseProgress);
  } else {
    rowBri = (distance / phaseProgress) - 1;
  }
  rowBri = clamp(rowBri, 0, 1) * sensorLevels[sensor];
  var rowHue = animationProgress;
  var rowSat = 1;
  
  hue = rowHue;
  sat = rowSat;
  bri = rowBri;
}

///////////////////////////////////////////////////////////
// Initialization 

// this must be called during startup
doGlobalInitialization();


export function beforeRender(delta) {
   modes = floor(light*10)
  gameState = floor(accelerometer[0]*10)
 
  
 if (gameState == 0) {
   
  resetTransform();
  expansionCount = 0;
  
  t5 = time(speed *.16);
  t6 = time(speed *.1);
  t7 = time(speed *.14);
  t8 = time(speed *.11);
  
  // time in seconds, wraps about every 4 hours of continuous 
  // run time.
  timebase = (timebase + delta/1000) % 14400
  
  t1 = time(0.04)
  t2 += delta * 15 * 0.0001;
  t3 = time(.25) //Color Bands timer 1
  t4 = time(.05) //Color Bands timer 2
  
  // Update expandingRow animation
    if (isExpandingRowAnimating) {
      expandingRowT2 += delta * expandingRowSpeed * 0.0001;
      if (expandingRowT2 > (expandingRowAnimationDuration + 1)) {
        isExpandingRowAnimating = false;
      }
    }
  
  // move active circles, and reset them once they're off the display
  for (var i = 0; i < MAX_CIRCLES; i++) {
    if (circleOrigin[i] >= 0) {
      circlePos[i] += delta/circleSpeed;
      if (circlePos[i] > 1) {
        circleOrigin[i] = -1;
        circlePos[i] = 0;
      }
    }
  }

// check each sensor to see if it has changed state, and
  // run fadein/fadeout timing routines as needed.
  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16)
    bit = sensor % 16
    state = (frequencyData[element] << 16) & (1 << bit)
    
  // when switch changes to triggered state, start the minimum hold timer
  // (can also act as a debouncer)    
    if (state != previousState[sensor]) {
     
      if (state) {
        activationTime[sensor] = timebase
        virtualSensors[sensor] = 1
        
        // on activation, trigger a one-shot expanding circle
        // at the center of the sensor's pixels.  Up to MAX_CIRCLES
        // circles can be active at a time.
         if (modes == 9 && sensor < 96) {
      triggerExpandingRow(sensor);
    } else if (modes == 7) {
      triggerCircle(sensor)
      circleTrigger = (circleTrigger + 1) % MAX_CIRCLES 
    }
       
      }
      previousState[sensor] = state
    }
    // if a sensor is on, or was activated less than MIN_HOLD_TIME ago, activate its
    // corresponding virtual sensor, which will be used for lighting calculations
    // if MIN_HOLD_TIME > 0, the virtual sensor also serves as a debouncing mechanism. 
    // Short taps on the sensor will result in it being "on" for at least the MIN_HOLD_TIME
    // In One-Shot mode, MIN_HOLD_TIME should be set to at least FADE_IN_TIME seconds.
    virtualSensors[sensor] = (state && !ONE_SHOT) || ((timebase - activationTime[sensor]) <= MIN_HOLD_TIME)
    
    setSensorFadeLevel(sensor,delta)
  }

    
  if (modes == 0 ) {  //  WEB BUTTONS
setInactivePattern(solidBlack,0,NUM_SENSORS)
setActivePattern(colorBands,0,96)
setActivePattern(solidRed,96,96)
} else if (modes == 2){  // FOREST
setInactivePattern(oasisInactive,0,NUM_SENSORS)
setActivePattern(oasis,0,96)
setActivePattern(solidBlack,96,96)
} else if (modes == 4){  // PENTATONIC 1
setInactivePattern(solidBlack,0,NUM_SENSORS)
setActivePattern(rainbowGrowFromEdges,0,96)
setActivePattern(solidBlack,96,96)
} else if (modes == 7){  // PENTATONIC 2
setInactivePattern(solidBlack,0,NUM_SENSORS)
setActivePattern(rainbowAllPixels,0,96)
setActivePattern(solidBlack,96,96)
} else if (modes == 9){  // PENTATONIC 3
setInactivePattern(solidBlack,0,NUM_SENSORS)
setActivePattern(solidRed,0,96)
setActivePattern(solidBlack,96,96)
}

} else if (gameState == 1){ // Intro - Star
   expansionCount = 0;
   
   theta = PI2 * time(0.1);
   resetTransform();
   translate(-0.5,-0.5);  
   rotate(-theta);  
  
} else if (gameState == 2){ // Finished - Circle
    if (expansionCount < maxExpansions) {
    t10 += delta * 10 * 0.0001;
    if (t10 > 1) {  
      t10 = 0;  
      expansionCount++;  
    }
  }
}
}


export function render2D(index,x,y) {

if (gameState == 0){
  
  var sensor = floor(index / PIXELS_PER_SENSOR)
  
  // get data for the second set of sensors  
  var sensor2 = sensor + 96
  
  // get current activation state for each sensor bank
  // for this purpose, the sensor is considered active if its
  // switch is active OR if it is doing its final fade-out
  on = virtualSensors[sensor] || (sensorLevels[sensor] > 0)
  on2 = virtualSensors[sensor2] || (sensorLevels[sensor2] > 0)  

  bri = 0
  v = 0
  
  // see if a sensor in either bank has been activated
  if (on || on2) {
      t = (on) ? activationTime[sensor] : 14400
      t2 = (on2) ? activationTime[sensor2] : 14400    
      
      if (t < t2) {
        activePattern[sensor](index,sensor) 
        v = sensorLevels[sensor]      
      } else {
        activePattern[sensor2](index,sensor2) 
        v = sensorLevels[sensor2]      
      }
      bri *= v
    }
    
    if (modes == 7) {
      circleAtIndex(x,y);
    } else if (modes == 9 && isExpandingRowAnimating) {
      if (floor(index / pixelsPerRow) == floor(expandingRowOriginPixel / pixelsPerRow)) {
        expandingRow(index);
        return; // Skip the rest of the rendering for this pixel
      }
    }
  
    // if no sensor has been activated, of if the active pattern has
  // faded below a threshold brightness, use the inactive pattern
  if (bri < 0.08) {
    inactivePattern[sensor](index,sensor)
  }
  hsv(hue,sat,bri)
  
} else if (gameState == 1) {
    var hexV = 0;
  var d = hexStar(x, y, .3);
  //var h, s;

  if (d <= 0) {
    hexV = 1 - d;
    hexV = hexV * hexV;
    s = 1.5 - abs(d) / .3;
    h = d + time(-0.05);      
    hsv(h, s, hexV);
  } else {
    hsv(0, 0, 0); // Set pixels outside the star to black
  } 
} else if (gameState == 2) {
    if (expansionCount < maxExpansions) {
  distance = hypot(x - 0.5, y - 0.5)
    h =  distance*1.8 ; 
    v = 1.0 - abs(distance - t10) / 0.14;
    v = clamp(v, 0, 0.2); 
    s = 0.9;
    hsv(h, s, v);
  } 
  }

}


function doGlobalInitialization() {
  // set all patterns functions to a default black
  activePattern.mutate(v => solidBlack)
  inactivePattern.mutate(v => solidBlack)
  
  // this makes sure everything's inactive at startup
  activationTime.mutate(v => -100)  
  
  // convert fade times to ms for easier calculation
  FADE_OUT_TIME *= 1000; 
  FADE_IN_TIME *= 1000;  
}

Looking really good! Just some tiny things to get the expanding row going.

First, in render2D(), where it says:

      if (floor(index / pixelsPerRow) == floor(expandingRowOriginPixel / pixelsPerRow)) {
        expandingRow(index);
        return; 
      }

change it to:

        expandingRow(index);
        hsv(hue,sat,bri)        
        return; 

so that the colors set by ExpandingRow() will be displayed. (Otherwise, render2D() returns without setting the pixel color, which means it’ll be black!)

Then, in ExpandingRow() itself, comment out or remove the line that says:

  rowBri = clamp(rowBri, 0, 1) * sensorLevels[sensor];

This fixes a couple of things. First, in render2D(), you aren’t passing it a sensor value, which means you’ll be always be looking at sensorLevels[0], which is probably not activated, so will have a value of 0, making everything black.

If you really want to tie it to the current brightness level of the sensor that was activated, you can do that by using:

  rowBri = clamp(rowBri, 0, 1) * sensorLevels[expandingRowOriginPixel / PIXELS_PER_SENSOR];

but in my testing, the expanding row animation goes fast enough that the extra calculation doesn’t make a visible difference.

I was avoiding this(as I am in other animations I’m doing in the same pattern) because it affects the sensor pixels themselves and I have not figured out how you are doing it exactly in circleAtIndex. At the moment, the LEDs of the active sensor will stay off until the expanding row animation is finished, then turn on. How do I untie them from it?

The other thing that’s driving me crazy, every trigger causes the entire row to flash prior to the animation. I thought it was the timing of the flag in beforeRender but it’s not. Did you see the same effect?

Another question, I’m trying to set FADE_IN_TIME depending on the mode I’m in(calculated in beforeRender), but the value it takes when declared can’t be changed later, either in beforeRender or within the setSensorFadeLevel function. If for example I do this in before Render it has no effect:

if (modes == 4) {
    FADE_IN_TIME = 1.2
  } else {
    FADE_IN_TIME = .3
  }

Jeeeesus, I’m stupid some times…

  // convert fade times to ms for easier calculation
  FADE_OUT_TIME *= 1000; 
  FADE_IN_TIME *= 1000;  

Hi!,
Wow, this is getting complicated, and looks like it’ll be fun to play with! Nice catch on the seconds vs. millis. I’d totally forgotten about that.

On the flashing, I think it was always there, I just wasn’t seeing it in the preview. It’s caused by a divide-by-zero in ExpandingRow(), here:

    rowBri = 1 - (distance / phaseProgress);

On Pixelblaze, anything divided by zero is zero. So when phaseProgress is zero, brightness gets set to 1, regardless of distance.

Here’s a take on ExpandingRow() that avoids that kind of division.

Expanding Row Function
// Expanding Row main
function expandingRow(index, sensor) {
  
  // figure out where we are in the row, and find the center pixel
  var pixelInRowIndex = index % pixelsPerRow;
  var originPixel = pixelsPerRow / 2; 
  
  // calculate distance from center, and scale to range 0..1 for easier calculation
  var distance = 2 * abs(pixelInRowIndex - originPixel) / pixelsPerRow;
  
  // use the animation timer position to create a smooth fade in/fade out curve
  // (the -0.25 offset puts the wave's phase in the right place for this)
  var animationProgress = expandingRowT2 / expandingRowAnimationDuration;
  var phaseProgress = wave(-0.25 + animationProgress)

  // calculate brightness
  var  rowBri = phaseProgress * (1 - distance)

  hue = animationProgress
  sat = 1
  bri = rowBri;
}

To temporarily disable the active sensor LEDs coming on when the sensor is triggered, you can add the following two lines to your triggerExpandingRow() function.

  // these two lines disable activePattern graphics when a sensor
  // is activated.
  activationTime[sensor] = -1
  virtualSensors[sensor] = 0

Or you can just setActivePattern() to all black for that sensor in triggerExpandingRow() and change it back when the animation is complete.

Now the row where the active sensor is, always lights up from the center to the edges regardless of the sensor’s position.

Maybe I explained it wrong, I’m trying to mimic the expanding circle behavior but just on the row where the active sensor is, so the sequence would be: sensor turns on, a wave turns on the leds left and right until it gets outside the matrix, with the sensor leds staying on as long as the sensor is active(they get their value from setActivePattern and not expandingRow).

I was doing the animation on the Arduino side, but due to the timing constrains of the serial communication I had to bring it to PB…

Try the code below. It does a slow not-subtle-at-all expansion so you can really see what’s going on, and the sensor’s pixels stay lit with the active pattern as long as it’s activated. Just had to change the origin pixel to get the wave to start from the right place.

Also, with this code, you can get rid of the hsv(hue,sat,bri) and return statements right below the call to ExpandingRow() if you want, and both active and inactive patterns will be shown.

function triggerExpandingRow(sensor) {
  // set origin pixel to centermost of sensor's pixels
  expandingRowOriginPixel = (sensor * PIXELS_PER_SENSOR) + PIXELS_PER_SENSOR / 2;
  expandingRowStart = sensor * pixelsPerRow;
  expandingRowT2 = 0;
  isExpandingRowAnimating = true;
}

// Expanding Row main
function expandingRow(index, sensor) {
  
  // figure out where we are in the row, and find the center pixel
  var pixelInRowIndex = index % pixelsPerRow;
  var originPixel = (expandingRowOriginPixel % pixelsPerRow)

  // calculate distance from target pixel, and scale so we'll fill the row no matter
  // where the pixel is on it.
  var distance = abs(pixelInRowIndex - originPixel) / pixelsPerRow;
  
  var animationProgress = expandingRowT2 / expandingRowAnimationDuration;
  var isExpanding = animationProgress < 0.5;
  var phaseProgress = frac(2 * animationProgress);

  // calculate brightness
  rowBri = isExpanding ? phaseProgress > distance : phaseProgress < distance

  hue = animationProgress
  sat = 1
  // this makes sure the sensor's pixels stay lit if activated
  bri = max(bri,rowBri);
}