Making Fire I

I saw a really cool lamp on YouTube which is not for sale, so I thought I'd try to make something like it using the Raspberry Pi.
Since I'm a programmer and not an electrical engineer I thought I'd start with the fun part first - programmatic Fire using a custom particle generator.
My Pi will be running node.js which mainly uses JavaScript, so the easiest way to get started was to pick a JavaScript library available on node (paper.js) for my simulated RGB display. I had to be careful here and separate my particle emission logic from my display logic so that modifying it for the physical display later will be easier.
This project consists of two main parts so far, the particle system and the display loop. The display loop simply reads the state of the particle system and renders it out. The particle system maintains its own time on all the objects and makes sure the kinematics for the particles are updated on a per-tick basis. Doing it this way will make it a whole lot easier to adapt to an RGB LED display later (since I basically need to compute density per LED, rather than pixels.)
Since this is part one and more or less hacked together, here's a PoC that a JavaScript particle generator might work. Please ignore the magic numbers.
Here's the result of the first session's testing:
Here's the code if you're interested!
Main HTML File:
<!DOCTYPE html> <html> <head> <!-- Load the Paper.js library --> <script type="text/javascript" src="js/paper-full.js"></script> <!-- Load external PaperScript and associate it with myCanvas --> <script type="text/paperscript" src="js/drawlayer.js" canvas="myCanvas"> </script> <!-- Define inlined JavaScript --> <script type="text/javascript"> // Global collections var emberCollection = []; var windCollection = []; // Magic numbers var maxPosition = 5; // depends on how many particles there are... var magicAccel = -0.001; var magicFramerate = 50; var magicZones = [0.17, 0.15, 0.10, 0.05]; var maxEmbers = 80; var firewidth = 0.6; var firespread = 0.2; var firebaseheight = 0.7; var embersPerTick = 5; var ticks = 0; // Only executed our code once the DOM is ready. window.onload = function() { class Wind { constructor(position_y, acceleration_x) { this.position = position_y; // 0 - 1, height of window this.acceleration = acceleration_x; } } windCollection.push(new Wind(0.15, 0)); // Fire comes from the bottom and rises // wind changes acceleration left or right class Ember { constructor(x, y) { this.startposition = new paper.Point(x, y); this.startvelocity = new paper.Point(0, 0); this.acceleration = new paper.Point(0, magicAccel); // acceleration numbers are arbitrary, units of (screen height) this.time = 0; this.symbol = null; this.scale = 1; this.debugcolor = "green"//f4bc42 this.decayresistence = 0.2;// add less scaling this.scaleboost = 0.0;// add less scaling } get position() { return new paper.Point(this.startposition.x + this.time*this.startvelocity.x + 0.5 * this.time*this.time*this.acceleration.x, (this.startposition.y + this.time*this.startvelocity.y + 0.5 * this.time*this.time*this.acceleration.y ) / maxPosition); } applyNewAcceleration(newAcceleration) { this.startvelocity = new paper.Point(this.startvelocity.x + this.acceleration.x*this.time, this.startvelocity.y + this.acceleration.y*this.time); this.startposition = this.position; this.acceleration.x += newAcceleration.x; this.acceleration.y += newAcceleration.y; this.time = 0; } recycle(into) { console.log("Max "+this.position) this.startposition = into.startposition; this.startvelocity = into.startvelocity; this.acceleration = into.acceleration; this.time = 0; this.scale = 1; if (this.symbol != null) this.symbol.remove(); this.symbol = null; this.debugcolor = "red"; } } var ttl = 0; function tickFn(){ ticks++; // spawn new embers up to 100 then overwrite them var newEmberCount = Math.random() * embersPerTick ; var ct = emberCollection.length; for (var i = 0; i < newEmberCount; i++){ ttl++; var smallrand =Math.random()*firespread; // Recycle the old ones var newEmber = new Ember(firewidth - smallrand, firebaseheight+Math.random() * (firebaseheight/2)); // want it near center, so 0.5 +- 0.1 // Sam doesn't like the start velocity //newEmber.startvelocity = new paper.Point(0.0025 - Math.random()*0.005, 0.0025-Math.random()*0.005); newEmber.acceleration.x += (smallrand - 0.1)/1000; // Should trend back towards center... var newIndex = ct+i; if (newIndex >= maxEmbers){ // recycle the symbol from the old object // since we want a fixed number of max objects emberCollection[ttl % maxEmbers].recycle(newEmber); } else{ emberCollection[newIndex] = newEmber; } } for (var j = 0; j < emberCollection.length; j++){ var oldPos = emberCollection[j].position; emberCollection[j].time++; var newPos = emberCollection[j].position; if (newPos.y > magicZones[0]){ emberCollection[j].debugcolor = "firebrick"; } else if (newPos.y > magicZones[1]){ emberCollection[j].debugcolor = "red"; } else if (newPos.y > magicZones[2]) { emberCollection[j].debugcolor = "orange"; } else if (newPos.y > magicZones[3]) { emberCollection[j].debugcolor = "yellow"; } else { emberCollection[j].debugcolor = "grey"; } } } // Could have periodic flares where the scale increases for 1 frame // idea - crosswinds which change and add x acceleration, have to // sum the accel and reset time? or time traveled per acceleration? // each time accel changes, snapshot current velocity, add new // acceleration, reset time to 0? window.setInterval(function() { tickFn(); }, magicFramerate); } </script> </head> <body> <canvas id="myCanvas" style="width:100%; height: 100%; background:black;" resize> </canvas> </body> </html>
Draw Loop:
/** * Created by Nick on 12/14/2016. */ var paperEmbers = emberCollection; // Link back to globals... this is sloppy var lastTick =0; // The onFrame function is called up to 60 times a second: function onFrame(event) { if (lastTick == ticks) return; // Don't redraw the same "frame" lastTick = ticks; for (var i =0; i < paperEmbers.length; i++){ if (paperEmbers[i].symbol == null){ paperEmbers[i].symbol = new Path.Circle({ center: paperEmbers[i].startposition * view.size, radius: view.size.width*0.025, fillColor: '#f4bc42' }); } // translate the points from the model into points in the view paperEmbers[i].symbol.position = paperEmbers[i].position* view.size * (new Point(1,maxPosition)); // Set the fill color based on the stored property paperEmbers[i].symbol.fillColor = paperEmbers[i].debugcolor; // problem - scale is cumulative // solution - always reverse the previous action. This creates rounding // errors but since embers are short lived // it doesn't matter if (Math.abs(paperEmbers[i].symbol.position.y) < view.size.height) { var oldScale = paperEmbers[i].scale; paperEmbers[i].scale = Math.abs(paperEmbers[i].symbol.position.y) / view.size.height + paperEmbers[i].scaleboost; paperEmbers[i].symbol.scale(1/oldScale * paperEmbers[i].scale); // reverse the old scale and apply a new one } } }