Flow My LED Tears

This is still alpha code, because it needs better slider tweaking of values/ranges and some potential speed increase (only doing 11.5 FPS on 16x16)…

But it’s damn good, I think. It’s like I mixed Pacifica with a lava lamp, and I’m likely going to put this into a big 4foot paper lamp eventually.

It’s based on a Perlin3D noise field, turned into a flowfield, inspired by Dan Shiffman
But we don’t have objects, so the particle code was rewritten.
But we don’t have high res or a canvas, so the display is different.
But yeah, it started life there…

Feedback is welcome. I’ll tweak it when it’s not 2am, and I’m fresh on it. So not posted in the pattern library yet, cause I’m sure I’ll adjust this more.

Very Organic looking, but it all depends on the many sliders, so play with it.
250 max particles right now (which for a 256 pixel display is plenty)
Speed is up to you, but I find slow speeds work well.
Initial Color will reset the color as you slide. You can turn off the color shift to zero, so it’ll stay that color. A pretty sea green/blue or… Just realized I added averaging to hue, so getting the reds is actually quite hard right now. That’s a bug. But the yellow orange is nice too. In fact, it reminds me of a fire, changing little bits, glowing like coals, I found myself staring at it, and drifting away… I call it a success, and worse, it’ll never be the same ever… The number of random bits involved, that fire will never be seen again. This doesn’t feel “random” though.

Noise settings are hard to visualize here, but basically the further right, the more chaotic and faster.
Fades are fussy, Likely best at near 1. Brightness Fade is clear, the Sat Fade adds aging/grayness…

Code
// Flowfield in LEDs aka Flow My LED Tears
// loosely inspired by Dan Shiffman's CodingTrain version of a Perlin Flowfield
// Written by Scruffynerf 8/2021  v1.0beta

// canvas setup
var canvasHue = array(pixelCount)
var canvasSat = array(pixelCount)
var canvasVal = array(pixelCount)
var width = sqrt(pixelCount)
var height = sqrt(pixelCount)  

// particle setup
var maxnumberofParticles = 250
var particlesPosX = array(maxnumberofParticles)
var particlesPosY = array(maxnumberofParticles)
var particlesVelX = array(maxnumberofParticles)
var particlesVelY = array(maxnumberofParticles)
var particlesAccX = array(maxnumberofParticles)
var particlesAccY = array(maxnumberofParticles)
var particlesHue  = array(maxnumberofParticles)

var flowfield = array(pixelCount)

//misc variables
var time

// sliders
export var numberofParticles = 250
export function sliderParticles(v){
  numberofParticles = floor(250 * v)
}

export var maxspeed = .005
export function sliderMaxSpeed(v){
  maxspeed = v/20
}

export var pspeed = 0.01
export function sliderSpeed(v){
  pspeed = .04 * v
}

export var huecolor = .66
export function sliderInitColor(v){
  huecolor = v
  particlesHue.mutate(recolor)
}

function recolor(v,i,a){
  return huecolor + .001 * i
}

export var cspeed = 0.01
export function sliderColorSpeed(v){
cspeed = .004 * v
}

export var noisescale = 1
export function sliderNoiseScale(v){
  noisescale = 0.0001+(v*10)
}

export var nspeed = 5000
export function sliderNoiseSpeed(v){
  nspeed = 5000 * v
}

export var vfadespeed = .95
export function sliderFade(v){
  vfadespeed = v
}

export var sfadespeed = .95
export function sliderSatFade(v){
  sfadespeed = v
}

// init all particles - on start
particlesHue.forEach(particleInit)

// before each frame, do updates
export function beforeRender(delta) {
  time += delta/nspeed
  particlesPosX.forEach(followParticle)
  particlesPosX.forEach(updateParticle)
  particlesPosX.forEach(showParticle)
  canvasVal.mutate(valfade)
  canvasSat.mutate(satfade)
}

function valfade(v,i,a){
  return v*(vfadespeed-0.001)
}

function satfade(v,i,a){
  return v*(sfadespeed)
}

// during each frame, render canvas
export function render2D(index, x, y) {
  // update the flowfield
  x = floor(x*width)
  pixel = x+(floor(y*height)*height)
  flowfield[pixel] = noise(x*noisescale, y*noisescale,time)* PI2 * 3
  
  //draw the pixel from the canvas
  if (pixel >=0 && pixel < pixelCount) { 
    hsv(canvasHue[pixel],canvasSat[pixel],canvasVal[pixel])
  }
}

function drawPixel(x, y, hue, sat, val) {
  if (x < 0 || x > 1 || y < 0 || y > 1 ){ return }
  x = floor(x*width)
  cindex = x+(floor(y*height)*height)
  if (cindex > pixelCount - 1) return
  if (cindex < 0 ) return
  canvasHue[cindex] = hue
  canvasSat[cindex] = sat
  canvasVal[cindex] += val
}

// particle code
function particleInit(v,i,a){
  particlesPosX[i] = random(1)
  particlesPosY[i] = random(1)
  particlesVelX[i] = 0
  particlesVelY[i] = 0
  particlesAccX[i] = 0
  particlesAccY[i] = 0
  particlesHue[i] = huecolor
  huecolor = huecolor + .0004
}

function followParticle(v,i,a){
  x = floor(particlesPosX[i]*width)
  cindex = x+(floor(particlesPosY[i]*height)*height)
  if (cindex > pixelCount - 1) cindex = pixelCount -1
  if (cindex < 0) cindex = 0;
  angle = flowfield[cindex]
  particlesAccX[i] = cos(angle) * pspeed
  particlesAccY[i] = sin(angle) * pspeed
}

function updateParticle(v,i,a) {
  particlesVelX[i] += particlesAccX[i]
  particlesVelY[i] += particlesAccY[i]
  particlesVelX[i] = clamp(particlesVelX[i],-maxspeed,maxspeed)
  particlesVelY[i] = clamp(particlesVelY[i],-maxspeed,maxspeed)
  particlesAccX[i] = 0
  particlesAccY[i] = 0
  particlesPosX[i] += particlesVelX[i]
  particlesPosY[i] += particlesVelY[i]
  if (particlesPosX[i] >= 1) {
    particlesPosX[i] += -1 ;
  }
  if (particlesPosY[i] >= 1) {
    particlesPosY[i] += -1;
  }
  if (particlesPosX[i] <= 0) {
    particlesPosX[i] += .99;
  }
  if (particlesPosY[i] <= 0) {
    particlesPosY[i] += .99;
  }
}

function showParticle(v,i,a){
  if (i > numberofParticles) return
  // add to canvas here
  drawPixel(particlesPosX[i], particlesPosY[i], particlesHue[i], 1, .1)
  // increase the color
  particlesHue[i] = (particlesHue[i] + cspeed)%1;
}


// Perlin 3D noise code is mostly below
//
// Written by Thom Chiovoloni, dedicated into the public domain 
// per http://creativecommons.org/publicdomain/zero/1.0
// https://github.com/thomcc/quick-noise.js
//
// adapted by ScruffyNerf into Pixelblaze's JSish language
// you could replace the use of random() with a different RNG if you wish

var arraySize = 256
var permSize = arraySize*2
var gradSize = permSize
var arr = array(arraySize)
var perm = array(permSize)
var grad1 = array(gradSize)
var grad2 = array(gradSize)
var grad3 = array(gradSize)
var gradBasis = array(36)
var gradIndex = 0
var gradIdx = 0

function buildTable() {
		arr.mutate(arrayFill)
		arr.mutate(arrayShuffle)
		arr.mutate(arrayShuffle) // let's mix it up again, just to be sure, optional
		perm.mutate(permClone)
		grad(1,1,0); grad(-1,1,0); grad(1,-1,0); grad(-1,-1,0)
    grad(1,0,1); grad(-1,0,1); grad(1,0,-1); grad(-1,0,-1)
    grad(0,1,1); grad(0,-1,1); grad(0,1,-1); grad(0,-1,-1)
    perm.forEach(gradPop)
}

function arrayFill(v,i,a){
  return i	  
}

function arrayShuffle(v,i,a){
  r = floor(random(a.length))
  t = arr[r]
  arr[r] = v
  return t
}

function permClone(v,i,a){
  if (i > arraySize -1) {
    return arr[i-arraySize]
  } else {
    return arr[i]
  }
}

function grad(x, y, z) {
  gradBasis[gradIndex] = x
  gradIndex++
  gradBasis[gradIndex] = y
  gradIndex++
  gradBasis[gradIndex] = z
  gradIndex++
}

function gradPop(v,i,a){
  g = v%12 * 3
  grad1[gradIdx] = gradBasis[g]
  grad2[gradIdx] = gradBasis[g+1]
  grad3[gradIdx] = gradBasis[g+2]
  gradIdx++
}

function fade(t) {
  return t * t * t * (t * (t * 6 - 15) + 10)
}

buildTable()

function noise(x, y, z, xWrap, yWrap, zWrap) {
	// x, y, z are numbers.
	// xWrap, yWrap, and zWrap are integer powers of two between 0 and 256.
	// (0 and 256 are equivalent). If these aren't provided, they default to 0.
	
  xMask = ((xWrap-1) & 255) >> 0
	yMask = ((yWrap-1) & 255) >> 0
	zMask = ((zWrap-1) & 255) >> 0

	px = floor(x)
  py = floor(y)
	pz = floor(z)

	x0 = (px+0) & xMask
	x1 = (px+1) & xMask

	y0 = (py+0) & yMask
	y1 = (py+1) & yMask

	z0 = (pz+0) & zMask
	z1 = (pz+1) & zMask

	x -= px
	y -= py
	z -= pz

	u = fade(x)
	v = fade(y)
	w = fade(z)

	r0 = perm[x0]
	r1 = perm[x1]

	r00 = perm[r0+y0]
	r01 = perm[r0+y1]
	r10 = perm[r1+y0]
	r11 = perm[r1+y1]

	h000 = perm[r00+z0]
	h001 = perm[r00+z1]
	h010 = perm[r01+z0]
	h011 = perm[r01+z1]
	h100 = perm[r10+z0]
	h101 = perm[r10+z1]
	h110 = perm[r11+z0]
	h111 = perm[r11+z1]

	n000 = grad1[h000]*(x+0) + grad2[h000]*(y+0) + grad3[h000]*(z+0)
	n001 = grad1[h001]*(x+0) + grad2[h001]*(y+0) + grad3[h001]*(z-1)
	n010 = grad1[h010]*(x+0) + grad2[h010]*(y-1) + grad3[h010]*(z+0)
	n011 = grad1[h011]*(x+0) + grad2[h011]*(y-1) + grad3[h011]*(z-1)
	n100 = grad1[h100]*(x-1) + grad2[h100]*(y+0) + grad3[h100]*(z+0)
	n101 = grad1[h101]*(x-1) + grad2[h101]*(y+0) + grad3[h101]*(z-1)
	n110 = grad1[h110]*(x-1) + grad2[h110]*(y-1) + grad3[h110]*(z+0)
	n111 = grad1[h111]*(x-1) + grad2[h111]*(y-1) + grad3[h111]*(z-1)

	n00 = n000 + (n001-n000) * w
	n01 = n010 + (n011-n010) * w
	n10 = n100 + (n101-n100) * w
	n11 = n110 + (n111-n110) * w

	n0 = n00 + (n01-n00) * v
	n1 = n10 + (n11-n10) * v

	return n0 + (n1-n0) * u
}

Now a PB v2 version, which forced me to deal with space limitation… and made me re-eval so much… and in the process, hugely sped it up. I no longer calc the flowfield at all until we need to know the value, I removed the acceleration tracking, it wasn’t needed either. I can only do 105 particles (memory limits), but DAMN, it’s faster too. 18fps on a v2? Wow. Can’t wait to try it with a v3 again.

This lacks the Saturation slider right now, though.

v1.2beta, runs on a v2 PB (and v3!)
// Flowfield in LEDs aka Flow My LED Tears
// loosely inspired by Dan Shiffman's CodingTrain version of a Perlin Flowfield
// Written by Scruffynerf 8/2021  v1.2beta

// canvas setup
var canvasHue = array(pixelCount)
//var canvasSat = array(pixelCount)
var canvasVal = array(pixelCount)
var width = sqrt(pixelCount)
var height = sqrt(pixelCount)  

// particle setup
var maxnumberofParticles = 105
var particlesPosX = array(maxnumberofParticles)
var particlesPosY = array(maxnumberofParticles)
var particlesVelX = array(maxnumberofParticles)
var particlesVelY = array(maxnumberofParticles)
var particlesHue  = array(maxnumberofParticles)

//misc variables
var time

// sliders
export var numberofParticles = maxnumberofParticles
export function sliderParticles(v){
  numberofParticles = floor(maxnumberofParticles * v)
}

export var maxspeed = .005
export function sliderMaxSpeed(v){
  maxspeed = v/20
}

export var pspeed = 0.01
export function sliderSpeed(v){
  pspeed = .04 * v
}

export var huecolor = .66
export function sliderInitColor(v){
  huecolor = v
  particlesHue.mutate(recolor)
}

function recolor(v,i,a){
  return huecolor + .001 * i
}

export var cspeed = 0.01
export function sliderColorSpeed(v){
cspeed = .004 * v
}

export var noisescale = 1
export function sliderNoiseScale(v){
  noisescale = 0.0001+(v*10)
}

export var nspeed = 5000
export function sliderNoiseSpeed(v){
  nspeed = 5000 * v
}

export var vfadespeed = .95
export function sliderFade(v){
  vfadespeed = v
}

//export var sfadespeed = .95
//export function sliderSatFade(v){
//  sfadespeed = v
//}

// init all particles - on start
particlesHue.forEach(particleInit)

// before each frame, do updates
export function beforeRender(delta) {
  time += delta/nspeed
  particlesPosX.forEach(updateParticle)
  particlesPosX.forEach(showParticle)
  canvasVal.mutate(valfade)
  //canvasSat.mutate(satfade)
}

function valfade(v,i,a){
  return v*(vfadespeed-0.001)
}

//function satfade(v,i,a){
//  return v*(sfadespeed)
//}

// during each frame, render canvas
export function render2D(index, x, y) {
  // update the flowfield
  x = floor(x*width)
  pixel = x+(floor(y*height)*height)
  //flowfield[pixel] = noise(x*noisescale, y*noisescale,time)* PI2 * 3
  
  //draw the pixel from the canvas
  if (pixel >=0 && pixel < pixelCount) { 
    //hsv(canvasHue[pixel],canvasSat[pixel],canvasVal[pixel])
    hsv(canvasHue[pixel],1,canvasVal[pixel])
  }
}

function drawPixel(x, y, hue, sat, val) {
  if (x < 0 || x > 1 || y < 0 || y > 1 ){ return }
  x = floor(x*width)
  cindex = x+(floor(y*height)*height)
  if (cindex > pixelCount - 1) return
  if (cindex < 0 ) return
  canvasHue[cindex] = (canvasHue[cindex] + hue) / 2
  //canvasSat[cindex] = sat
  canvasVal[cindex] += val
}

// particle code
function particleInit(v,i,a){
  particlesPosX[i] = random(1)
  particlesPosY[i] = random(1)
  particlesVelX[i] = 0
  particlesVelY[i] = 0
  particlesHue[i] = huecolor
  huecolor = huecolor + .0004
}

function updateParticle(v,i,a) {
  angle = noise(particlesPosX[i]*noisescale, particlesPosY[i]*noisescale,time)* PI2 * 3
  particlesVelX[i] += cos(angle) * pspeed
  particlesVelY[i] += sin(angle) * pspeed
  particlesVelX[i] = clamp(particlesVelX[i],-maxspeed,maxspeed)
  particlesVelY[i] = clamp(particlesVelY[i],-maxspeed,maxspeed)
  particlesPosX[i] += particlesVelX[i]
  particlesPosY[i] += particlesVelY[i]
  if (particlesPosX[i] >= 1) {
    particlesPosX[i] += -1 ;
  }
  if (particlesPosY[i] >= 1) {
    particlesPosY[i] += -1;
  }
  if (particlesPosX[i] <= 0) {
    particlesPosX[i] += .99;
  }
  if (particlesPosY[i] <= 0) {
    particlesPosY[i] += .99;
  }
}

function showParticle(v,i,a){
  if (i > numberofParticles) return
  // add to canvas here
  drawPixel(particlesPosX[i], particlesPosY[i], particlesHue[i], 1, .1)
  // increase the color
  particlesHue[i] = (particlesHue[i] + cspeed)%1;
}

// Perlin 3D noise (now with even more reduced array usage!)
//
// Written by Thom Chiovoloni, dedicated into the public domain 
// per http://creativecommons.org/publicdomain/zero/1.0
// https://github.com/thomcc/quick-noise.js
//
// adapted by ScruffyNerf into Pixelblaze's JSish language
// you could replace the use of random() with a different RNG if you wish

var arraySize = 256
var permSize = arraySize
var gradSize = permSize
var perm = array(permSize)
var grad1 = array(gradSize)
var grad2 = array(gradSize)
var grad3 = array(gradSize)
var gradBasis = array(36)
var gradIndex = 0
var gradIdx = 0


function buildTable() {
		perm.mutate(arrayFill)
		perm.mutate(arrayShuffle)
		perm.mutate(arrayShuffle) // for good measure, shuffle twice
		grad(1,1,0); grad(-1,1,0); grad(1,-1,0); grad(-1,-1,0)
    grad(1,0,1); grad(-1,0,1); grad(1,0,-1); grad(-1,0,-1)
    grad(0,1,1); grad(0,-1,1); grad(0,1,-1); grad(0,-1,-1)
    perm.forEach(gradPop)
}

function arrayFill(v,i,a){
  return i	  
}

function arrayShuffle(v,i,a){
  r = floor(random(a.length))
  t = a[r]
  a[r] = v
  return t
}

function grad(x, y, z) {
  gradBasis[gradIndex] = x
  gradIndex++
  gradBasis[gradIndex] = y
  gradIndex++
  gradBasis[gradIndex] = z
  gradIndex++
}

function gradPop(v,i,a){
  g = v%12 * 3
  grad1[gradIdx] = gradBasis[g]
  grad2[gradIdx] = gradBasis[g+1]
  grad3[gradIdx] = gradBasis[g+2]
  gradIdx++
}

function fade(t) {
  return t * t * t * (t * (t * 6 - 15) + 10)
}

buildTable()

function noise(x, y, z, xWrap, yWrap, zWrap) {
	// x, y, z are numbers.
	// xWrap, yWrap, and zWrap are integer powers of two between 0 and 256.
	// (0 and 256 are equivalent). If these aren't provided, they default to 0.
	
  xMask = ((xWrap-1) & 255) >> 0
	yMask = ((yWrap-1) & 255) >> 0
	zMask = ((zWrap-1) & 255) >> 0

	px = floor(x)
  py = floor(y)
	pz = floor(z)

	x0 = (px+0) & xMask
	x1 = (px+1) & xMask

	y0 = (py+0) & yMask
	y1 = (py+1) & yMask

	z0 = (pz+0) & zMask
	z1 = (pz+1) & zMask

	x -= px
	y -= py
	z -= pz

	u = fade(x)
	v = fade(y)
	w = fade(z)

	r0 = perm[x0%256]
	r1 = perm[x1%256]

	r00 = perm[(r0+y0)%256]
	r01 = perm[(r0+y1)%256]
	r10 = perm[(r1+y0)%256]
	r11 = perm[(r1+y1)%256]

	h000 = perm[(r00+z0)%256]%256
	h001 = perm[(r00+z1)%256]%256
	h010 = perm[(r01+z0)%256]%256
	h011 = perm[(r01+z1)%256]%256
	h100 = perm[(r10+z0)%256]%256
	h101 = perm[(r10+z1)%256]%256
	h110 = perm[(r11+z0)%256]%256
	h111 = perm[(r11+z1)%256]%256

	n000 = grad1[h000]*(x+0) + grad2[h000]*(y+0) + grad3[h000]*(z+0)
	n001 = grad1[h001]*(x+0) + grad2[h001]*(y+0) + grad3[h001]*(z-1)
	n010 = grad1[h010]*(x+0) + grad2[h010]*(y-1) + grad3[h010]*(z+0)
	n011 = grad1[h011]*(x+0) + grad2[h011]*(y-1) + grad3[h011]*(z-1)
	n100 = grad1[h100]*(x-1) + grad2[h100]*(y+0) + grad3[h100]*(z+0)
	n101 = grad1[h101]*(x-1) + grad2[h101]*(y+0) + grad3[h101]*(z-1)
	n110 = grad1[h110]*(x-1) + grad2[h110]*(y-1) + grad3[h110]*(z+0)
	n111 = grad1[h111]*(x-1) + grad2[h111]*(y-1) + grad3[h111]*(z-1)

	n00 = n000 + (n001-n000) * w
	n01 = n010 + (n011-n010) * w
	n10 = n100 + (n101-n100) * w
	n11 = n110 + (n111-n110) * w

	n0 = n00 + (n01-n00) * v
	n1 = n10 + (n11-n10) * v

	return n0 + (n1-n0) * u
}
2 Likes

@wizard, is there any way right now to detect v2 vs v3 in code? I could change the particle max if there is… Otherwise, it’ll have to be limited to the lower number, and hand changed if you want more particles.

@zranger1 came up with a detection method here:

2 Likes

Hmm, yeah there isn’t a great alternative when it comes to allocating different amounts of memory.

The new pb2/3 version looks great! Runs at 25 fps on my pb3 w/ max particles. I’ll be playing with this for a while.

1 Like