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
}