Detailed Beginner Tutorial for Making Wearables with PixelBlaze

I ran a workshop this weekend for my Burning Man camp, that I called Making Wearable LEDs, the “I’ve never done this before and it’s under month until Burning Man” Way and had a ton of fun doing it. If that describes you or any of your friends, I hope this tutorial is helpful! I’d love any feedback, comments, suggestions; I’ll keep iterating on it. For now, the world has comment access to the presentation.

Note that this is VERY MUCH not the only way, or even the best way, to make wearables – it’s just one path which is meant to be really straightforward for beginners.


I also linked this in the appendix of the post, but if you want to modify any existing Patterns to use the new Palette functionality, start by pasting this code at the top of the Pattern:

Primary code for palettes and an automated palette switcher
//next whole section is for palettes.

/* 
  Palettes via http://soliton.vm.bytemark.co.uk/pub/cpt-city/; some picked from
  ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb
  Patterns with decimals converted for FastLED with gammas (2.6, 2.2, 2.5)
  Code simplification by ZacharyRD (Zachary Reiss-Davis), Palette Blending Design by zranger1
  Patterns squished into single lines using double-tabs where line breaks would be in expanded spacing. 
  This is a cosmetic choice to make them fit into a single page that can be skipped over.
  The palettes are also indented to just be a logical group; there's no functions involved. 
*/
      // blue purple teal pop of yellow, balanced.
      var inferno = [ 0.0, 0/255, 0/255, 4/255, 0.1, 22/255, 11/255, 57/255, 0.2, 66/255, 10/255, 104/255, 0.3, 106/255, 23/255, 110/255, 0.4, 147/255, 38/255, 103/255, 0.5, 188/255, 55/255, 84/255, 0.6, 221/255, 81/255, 58/255, 0.7, 243/255, 120/255, 25/255, 0.8, 252/255, 165/255, 10/255, 0.9, 246/255, 215/255, 70/255, 1.0, 252/255, 255/255, 164/255, ]
      //yellow-orange-red-purple-navy
      //http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_04.png.index.html
      var bhw1_04_gp = [0, 229,227,  1,   15, 227,101,  3,    142,  40,  1, 80,   198,  17,  1, 79,   255,   0,  0, 45]
      arrayMutate(bhw1_04_gp,(v, i ,a) => v / 255);
      // blue-purple-red
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/red/tn/Analogous_1.png.index.html
      var Sunset_Real = [0.0, 0.471, 0.0, 0.0,    0.086, 0.702, 0.086, 0.0,   0.2, 1.0, 0.408, 0.0,   0.333, 0.655, 0.086, 0.071,   0.529, 0.392, 0.0, 0.404,   0.776, 0.063, 0.0, 0.51,    1.0, 0.0, 0.0, 0.627,];
      // Battery Saver: black-blue-purple-pink-white Top pick.
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Blue_Magenta_White.png.index.html
      var Analogous_1 = [0.0, 0.012, 0.0, 1.0,    0.247, 0.09, 0.0, 1.0,    0.498, 0.263, 0.0, 1.0,   0.749, 0.557, 0.0, 0.176,   1.0, 1.0, 0.0, 0.0,];
      // this is a really good one. Orange Pink Green. Should be garish but isn't. 
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/icecream/tn/rainbowsherbet.png.index.html
      var rainbowsherbet = [0.0, 1.0, 0.129, 0.016,   0.169, 1.0, 0.267, 0.098,   0.337, 1.0, 0.027, 0.098,   0.498, 1.0, 0.322, 0.404,   0.667, 1.0, 1.0, 0.949,   0.82, 0.165, 1.0, 0.086,    1.0, 0.341, 1.0, 0.255,];
      // really good blending, purples blues and pinks. Mild but good.
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr65_hult.png.index.html
      var BlacK_Blue_Magenta_White = [0.0, 0.0, 0.0, 0.0,   0.165, 0.0, 0.0, 0.176,   0.329, 0.0, 0.0, 1.0,   0.498, 0.165, 0.0, 1.0,   0.667, 1.0, 0.0, 1.0,   0.831, 1.0, 0.216, 1.0,   1.0, 1.0, 1.0, 1.0,];
      // Battery Saver: black magenta red yellow.
      //better than just black magenta red.
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Red_Magenta_Yellow.png.index.html
      var gr65_hult = [0.0, 0.969, 0.69, 0.969,   0.188, 1.0, 0.533, 1.0,   0.349, 0.863, 0.114, 0.886,   0.627, 0.027, 0.322, 0.698,   0.847, 0.004, 0.486, 0.427,   1.0, 0.004, 0.486, 0.427,];
      // yellow to greens to blues. Very little red. 
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/gmt/tn/GMT_drywet.png.index.html
      var GMT_drywet = [0.0, 0.184, 0.118, 0.008,   0.165, 0.835, 0.576, 0.094,   0.329, 0.404, 0.859, 0.204,   0.498, 0.012, 0.859, 0.812,   0.667, 0.004, 0.188, 0.839,   0.831, 0.004, 0.004, 0.435,   1.0, 0.004, 0.027, 0.129,];
      // Battery Saver: an excellent fire look, but too much black in it for many patterns. Black - red - orange - yellow - white.
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/lava.png.index.html
      var lava = [0.0, 0.0, 0.0, 0.0,   0.18, 0.071, 0.0, 0.0,    0.376, 0.443, 0.0, 0.0,   0.424, 0.557, 0.012, 0.004,   0.467, 0.686, 0.067, 0.004,   0.573, 0.835, 0.173, 0.008,   0.682, 1.0, 0.322, 0.016,   0.737, 1.0, 0.451, 0.016,   0.792, 1.0, 0.612, 0.016,   0.855, 1.0, 0.796, 0.016,   0.918, 1.0, 1.0, 0.016,   0.957, 1.0, 1.0, 0.278,   1.0, 1.0, 1.0, 1.0,];
      // reds to oranges to yellows to purple blue. No black in it. 
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Real.png.index.html
      var BlacK_Red_Magenta_Yellow = [0.0, 0.0, 0.0, 0.0,   0.165, 0.165, 0.0, 0.0,   0.329, 1.0, 0.0, 0.0,   0.498, 1.0, 0.0, 0.176,   0.667, 1.0, 0.0, 1.0,   0.831, 1.0, 0.216, 0.176,   1.0, 1.0, 1.0, 0.0,];
      // as described, blue cyan yellow -- slightly blue biased. 
      // http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/Blue_Cyan_Yellow.png.index.html
      var Blue_Cyan_Yellow = [0.0, 0.0, 0.0, 1.0,   0.247, 0.0, 0.216, 1.0,   0.498, 0.0, 1.0, 1.0,   0.749, 0.165, 1.0, 0.176,   1.0, 1.0, 1.0, 0.0,];
var palettes = [
  inferno,
  bhw1_04_gp, 
  Sunset_Real, 
  Analogous_1, 
  rainbowsherbet, 
  BlacK_Blue_Magenta_White, 
  gr65_hult, 
  GMT_drywet, 
  lava, 
  BlacK_Red_Magenta_Yellow, ]

// control variables for palette switch timing (these are in seconds)
var PALETTE_HOLD_TIME = 15
var PALETTE_TRANSITION_TIME = 2;

// internal variables used by the palette manager.
// Usually not necessary to change these.
export var currentIndex = 0;
var nextIndex = (currentIndex + 1) % palettes.length;

// primarily useful for testing, go to the next palette in the main array. Skips the blend step. 
export function triggerIncrementPalette(){
  currentIndex = (currentIndex + 1) % palettes.length;
  runTime = 0;
}

// arrays to hold palette rgb interpolation results
var pixel1 = array(3);
var pixel2 = array(3);

// array to hold calculated blended palette
var PALETTE_SIZE = 16;
var currentPalette = array(4 * PALETTE_SIZE)

// palette timing related variables
var inTransition = 0;
var blendValue = 0;
runTime = 0

// Startup initialization for palette manager
setPalette(currentPalette);
buildBlendedPalette(palettes[currentIndex],palettes[nextIndex],blendValue)  

// user space version of Pixelblaze's paint function. Stores
// interpolated rgb color in rgbArray
function paint2(v, rgbArray, pal) {
  var k,u,l;
  var rows = pal.length / 4;

  // find the top bounding palette row
  for (i = 0; i < rows;i++) {
    k = pal[i * 4];
    if (k >= v) break;
  }

  // fast path for special cases
  if ((i == 0) || (i >= rows) || (k == v)) {
    i = 4 * min(rows - 1, i);
    rgbArray[0] = pal[i+1];
    rgbArray[1] = pal[i+2];
    rgbArray[2] = pal[i+3];    
  }
  else {
    i = 4 * (i-1);
    l = pal[i]   // lower bound    
    u = pal[i+4]; // upper bound

    pct = 1 -(u - v) / (u-l);
    
    rgbArray[0] = mix(pal[i+1],pal[i+5],pct);
    rgbArray[1] = mix(pal[i+2],pal[i+6],pct);
    rgbArray[2] = mix(pal[i+3],pal[i+7],pct);    
  }
}

// utility function:
// interpolate colors within and between two palettes
// and set the LEDs directly with the result.  To be
// used in render() functions
function paletteMix(pal1, pal2, colorPct,palettePct) {
  paint2(colorPct,pixel1,pal1);
  paint2(colorPct,pixel2,pal2);  
  
  rgb(mix(pixel1[0],pixel2[0],palettePct),
      mix(pixel1[1],pixel2[1],palettePct),
      mix(pixel1[2],pixel2[2],palettePct)
   )
}

// construct a new palette in the currentPalette array by blending 
// between pal1 and pal2 in proportion specified by blend
function buildBlendedPalette(pal1, pal2, blend) {
  var entry = 0;
  
  for (var i = 0; i < PALETTE_SIZE;i++) {
    var v = i / (PALETTE_SIZE - 1);
    
    paint2(v,pixel1,pal1);
    paint2(v,pixel2,pal2);  
    
    // build new palette at currrent blend level
    currentPalette[entry++] = v;
    currentPalette[entry++] = mix(pixel1[0],pixel2[0],blend)
    currentPalette[entry++] = mix(pixel1[1],pixel2[1],blend)
    currentPalette[entry++] = mix(pixel1[2],pixel2[2],blend)    
  }
}

Then go down to “BeforeRender”

And paste this into the start of the "BeforeRender" function
//INSERT THIS PALETTE BLOCK WITHIN BEFORE RENDER AT THE END. 
  //  here till end of beforerender is for palette blending. 
  runTime = (runTime + delta / 1000) % 3600;

  // Palette Manager - handle palette switching and blending with a 
  // tiny state machine  
  if (inTransition) {
    if (runTime >= PALETTE_TRANSITION_TIME) {
      // at the end of a palette transition, switch to the 
      // next set of palettes and reset everything for the
      // normal hold period.
      runTime = 0;
      inTransition = 0
      blendValue = 0
      currentIndex = (currentIndex + 1) % palettes.length
      nextIndex = (nextIndex + 1) % palettes.length   

    }
    else {
      // evaluate blend level during transition
      blendValue = runTime / PALETTE_TRANSITION_TIME
    }
    
    // blended palette is only recalculated during transition times. The rest of 
    // the time, we run with the current palette at full speed.
    buildBlendedPalette(palettes[currentIndex],palettes[nextIndex],blendValue)          
  }
  else if (runTime >= PALETTE_HOLD_TIME) {
    // when hold period ends, switch to palette transition
    runTime = 0
    inTransition = 1
  }
  

Finally, replace the “Hue” call with paint(h,v) in your Render function(s),

I also posted this on the LEDs are Awesome Facebook group, with less code etc.

It builds off my previous post, Beginners Guide to Making a LED Festival Coat , as well.

3 Likes

BTW – @wizard and @zranger1 I’m taking the new palette functionality and the code @zranger1 mainly wrote for me in How to modify existing patterns to use Palette functions in v3.30 as an anchor for my “plug-and-play palette functionality” that I added to a lot of palettes for my camp and provided above. I think it’s a minor improvement over the Pattern library “Fast Palette Blending”, although it’s extremely similar, of course.

(@zranger1 if I’m going to beg a favor and someone like you to help me with code, at least I’m generalizing it and making it useful to everyone!)

2 Likes

Love this! It reminds me that I should share my learnings and failures in wearables, mainly around how to attach leds to clothing in ways that work well and don’t work well.

1 Like

@hex337 – I’d love to hear more about said learnings and failures, especially if you don’t mind me taking your notes and adding them to my Slides! As I said, I’m NOT an expert here – and I know there’s people in this group who do better work than I do, and most good tutorials are built on explaining all the ways we have messed up in the past.

@ZacharyRD, and other palette code users, I just found and fixed a sneaky off-by-one bug in the palette manager. You’ll probably want to fix this in your code too. It affects the last (highest) entry in the interpolated palette, and can cause pixels to be incorrectly rendered as black.

To fix the problem, in the function buildBlendedPalette(pal1, pal2, blend), change the line that reads:
var v = i / PALETTE_SIZE;
to:
var v = i / (PALETTE_SIZE - 1);

1 Like

That… explains some weird behavior I was seeing and couldn’t troubleshoot properly.

Thanks a lot — super helpful to know and fix.

The testing and updating process here will be “interesting”, because I think I optimized around this bug by accident when choosing which palettes to include in my stock set I put on 5 patterns / 15 PixelBlaze, and I’m going to test them all again to make sure they still all fit the look I’m going for. I’m traveling the next few days but will update my code block above as well after I retest.

Thanks again for all you do for the community — even with the bug the overall system works!