-
+
+
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
@@ -52,22 +70,36 @@
class Simulation {
constructor(canvas,data) {
this.canvas=canvas
- this.W = canvas.width = data.fuel.shape[0]
- this.H = canvas.height = data.fuel.shape[1]
+ this.H = canvas.height = data.fuel.shape[0]
+ this.W = canvas.width = data.fuel.shape[1]
this.clock = 0;
- // this.data = nj.zeros([this.W,this.H,4]) // fire,fuel, ash, alpha
-
data.fuel = data.fuel.reshape([data.fuel.shape[0],data.fuel.shape[1],1])
data.fire = nj.zeros(data.fuel.shape)
data.ash = nj.zeros(data.fuel.shape)
- data.alpha = nj.ones(data.fuel.shape)
- this.data = nj.concatenate([data.fire,data.fuel,data.ash,data.alpha])
+ data.elev = data.elev.reshape([data.elev.shape[0],data.elev.shape[1],1])
+ this.data = nj.concatenate([data.fire,data.fuel,data.ash,data.elev])
+
+ var coords = this.ignite()
+ this.display();
+ }
+
+ /* set an initial fire */
+ ignite(e){
+ // TODO make click to ignore work with zooming
+ var canvas = this.canvas
// initial point away from walls
- // we will make a nice bright cross
- var x = _.round(d3.randomUniform(20, this.W-20)())
- var y = _.round(d3.randomUniform(20, this.H-20)())
+ // we will make a nice bright cross so it's visible
+ var x,y
+ if (e){
+ // get click location FIXME might not work on all browsers
+ y=_.round(e.layerX/this.canvas.offsetWidth*this.canvas.width)
+ x=_.round(e.layerY/this.canvas.offsetHeight*this.canvas.height)
+ } else {
+ x = _.round(d3.randomUniform(20, this.W-20)())
+ y = _.round(d3.randomUniform(20, this.H-20)())
+ }
console.log('fire',x,y)
this.data.set(x,y,0,1)
this.data.set(x-1,y-1,0,0.3)
@@ -75,130 +107,199 @@ class Simulation {
this.data.set(x-1,y+1,0,0.3)
this.data.set(x+1,y+1,0,0.3)
this.data.set(x,y,1,0.3)
-
- this.display();
+ this.display()
+ return [x,y]
}
+
+ /* advance model by one tick */
tick(){
// PARAMS TODO move them
var fuelMultipler = 5 // how many turns it burns for
var fireMultiplier = 1
+ var fireGrowth = 1.5
+ var transmissionChance = 0.3
+
+ // distance to diagonal tiles as a ratio to tile size np.sqrt(1**2+1**2)
+ var diagDist = 1.42
// tick the environment
this.clock++;
- if (this.clock>100) return 0;
+ var t0 = new Date().getTime()
+ document.getElementById('tick').innerText=this.clock
+ if (this.clock>1000) return 0;
console.log('tick',this.clock)
+ //
+ // we are modifying the data in place, so freeze a copy of the old data
+ var oldData = this.data.clone()
- // I could do nearby fires as a convoluton, but lets wait to include elevation diff
- var all_fires = this.data.slice(null,null,[0,1]).clone()
- // var filter = nj.array([0.5,1,0.5,1,0,1,0.5,1,0.5]).reshape(3,3,1)
- // var nearby_fires = nj.convolve(all_fires,filter)
- // console.log('max',nearby_fires.max())
-
- var new_fires = []
-
+ /**
+ * Equations: transmission_probability from a nearby tile:
+ * $ t = I * Rt$ where I is fire intensity and Rt is the slope term
+ * $ Rt = exp(0.069 theta) $ theta is the slope angle from -90 to 90 degrees
+ * this reflects that its hard for fire to spread downhill
+ * $ theta = atan(dh/w) $ where dh is the difference in height and w is width
+ * $ theta ~= dh/w $ using the small tan approximation
+ *
+ * giving the total equation
+ * $ t = I * exp(0.069 *dh/w) $
+ *
+ * (from Noble et al 1980, DOI: 10.1111/j.1442-9993.1980.tb01243.x)
+ */
var fires=0
for (var x = 1; x < this.W-1; x++) {
for (var y = 1; y < this.H-1; y++) {
// can we do this as a convolution?
- var fire = this.data.get(x,y,0)*fireMultiplier
- var fuel = this.data.get(x,y,1)*fuelMultipler
- var ash = this.data.get(x,y,2)
+ var fire = oldData.get(x,y,0)*fireMultiplier
+ var fuel = oldData.get(x,y,1)*fuelMultipler
+ var ash = oldData.get(x,y,2)
+ var transmissionProbability = 0
if (fuel==0) continue
ash += fire // fire from last turn causes ash to build up
fuel = _.clamp(fuel-fire,0,fuelMultipler) // and fuel to decrease
if (fire>0){
- fire*=2 // exponentially grow within pixel
+ fire*=fireGrowth // exponentially grow within pixel
} else {
// each neighbouring tile might light it
- //
- // TODO it spreads right because new updates effects the next cell, fix this
- // TODO include slope or elevation differences
- var fires_adajacent = [
- this.data.get(x-1,y,0),
- this.data.get(x,y+1,0),
- this.data.get(x+1,y,0),
- this.data.get(x,y-1,0),
- ]
- var fires_diagonal = [
- this.data.get(x-1,y-1,0),
- this.data.get(x+1,y+1,0),
- this.data.get(x+1,y-1,0),
- this.data.get(x-1,y+1,0),
- ]
- var nearby_fires = (_.sum(fires_adajacent)+_.sum(fires_diagonal)/2)/8
- if (nearby_fires==0) continue
+ // intensity of fires in nearby cells
+ var fires_nearby = oldData.slice([x-1,x+2],[y-1,y+2],[0,1]).reshape(3,3)
+ // account for diagonal and zero the middle
+ if (fires_nearby.sum()==0) continue
+ var width_inv = nj.array([
+ [1/diagDist,1, 1/diagDist ],
+ [1, 1e-7, 1 ],
+ [diagDist, 1, 1/diagDist ]
+ ])
+ var intensity = nj.multiply(fires_nearby,width_inv)
+
+
+ // Slope spreading term //
+ // get difference in height ( height is in pixel width units)
+ var height = oldData.slice([x-1,x+2],[y-1,y+2],[3,4]).reshape(3,3)
+ var h0 = oldData.get(x,y,3)
+ var dHeight = nj.subtract(height,h0)
+ var Rt = nj.exp(nj.multiply(dHeight,width_inv).multiply(0.069))
+
+ transmissionProbability = nj.multiply(intensity,Rt)
+ transmissionProbability = transmissionProbability.mean()
// neighbouring tiles have a chance of lighting our tile
- if (nearby_fires0){
- fires+=fire
- // console.debug('fire',x,y,fire,fuel,ash)
- }
}
}
- // set all the new fires
- for (var x = 1; x < this.W-1; x++) {
- for (var y = 1; y < this.H-1; y++) {
- this.data.set(x,y,0,all_fires.get(x,y,0))
- }
- }
-
- console.log('tick',this.clock,'change',fires)
- return fires
+ var t = new Date().getTime()-t0
+ console.log('tick',this.clock,'time',t)
+ return t
}
+
+ /** push data onto canvases */
display(){
- var H = this.canvas.height;
- var W = this.canvas.width;
- var ctx = this.canvas.getContext('2d');
+ var H = this.W;
+ var W = this.H;
+
+ // first show fire
+
+ // Fill in the fire layer
+ var canvas = document.getElementById('fire')
+ var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0,0,H,W)
- console.assert(this.data.selection.data.length==imageData.data.length)
-
- // for (var x = 1; x < this.W-1; x++) {
- // for (var y = 1; y < this.H-1; y++) {
- // // mix the data into colors
- // var cell = this.data.slice([0,1],[0,1],null).reshape(1,4)
- // var filter = nj.array([
- // [1,0,0,0],
- // [0,1,0,0],
- // [0,0,1,0],
- // [0,0,0,1] // r,g,b,a
- // ]).reshape(4,4)
- // var colors = nj.dot(cell,filter)
- //
- // // now set imageData
- // for (var i = 0; i < 4; i++) {
- // imageData.data[i + this.W *(x + 4*z)]=colors.selection.data[i]*255.0
- // }
- // }
- // }
-
- // set the data with fire,fuel,ash as red,green,blue
- for (var i = 0; i < imageData.data.length; i++) {
- imageData.data[i]=this.data.selection.data[i]*255.0
+ var color_scale = d3.interpolateLab('yellow','red')
+ var fire = this.data.slice(null,null,[0,1])
+ for (var x = 0; x < imageData.width; x++) {
+ for (var y = 0; y < imageData.height; y++) {
+ var d = fire.get(x,y,0)*2
+ var c = new d3.color(color_scale(d))
+ var i = (x*H+y)*4
+ imageData.data[i+0]=c.r
+ imageData.data[i+1]=c.g
+ imageData.data[i+2]=c.b
+ imageData.data[i+3]=_.clamp(d*255*2,0,255)
+ }
}
ctx.putImageData(imageData, 0, 0);
+ console.assert(this.data.selection.data.length==imageData.data.length)
+
+ // Fill in the fire layer
+ var canvas = document.getElementById('fuel')
+ var ctx = canvas.getContext('2d');
+ var imageData = ctx.getImageData(0,0,H,W)
+ var color_scale = d3.interpolateLab('white','green')
+ var fuel = this.data.slice(null,null,[1,2])
+ for (var x = 0; x < imageData.width; x++) {
+ for (var y = 0; y < imageData.height; y++) {
+ var d = fuel.get(x,y,0)*2
+ var c = new d3.color(color_scale(d))
+ var i = (x*H+y)*4
+ imageData.data[i+0]=c.r
+ imageData.data[i+1]=c.g
+ imageData.data[i+2]=c.b
+ imageData.data[i+3]=_.clamp(d*255*2,0,255)
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ console.assert(this.data.selection.data.length==imageData.data.length)
+
+
+ // Fill in the ash layer
+ var canvas = document.getElementById('ash')
+ var ctx = canvas.getContext('2d');
+ var imageData = ctx.getImageData(0,0,H,W)
+ var color_scale = d3.interpolateLab('white','grey')
+ var ash = this.data.slice(null,null,[2,3])
+ for (var x = 0; x < imageData.width; x++) {
+ for (var y = 0; y < imageData.height; y++) {
+ var d = ash.get(x,y,0)*2
+ var c = new d3.color(color_scale(d))
+ var i = (x*H+y)*4
+ imageData.data[i+0]=c.r
+ imageData.data[i+1]=c.g
+ imageData.data[i+2]=c.b
+ imageData.data[i+3]=_.clamp(d*255*2,0,255)
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ console.assert(this.data.selection.data.length==imageData.data.length)
}
- start(n=10000){
+ /** this returns stats **/
+ stats(){
+ var fire = this.data.slice(null,null,[0,1])
+ var burning_cells = 0
+ for (var x = 1; x < this.W-1; x++) {
+ for (var y = 1; y < this.H-1; y++) {
+ if (fire.get(x,y,0)!=0) burning_cells++
+ }
+ }
+ return {
+ fire: _.round(fire.sum(),2),
+ fuel: _.round(this.data.slice(null,null,[1,2]).sum(),2),
+ ash: _.round(this.data.slice(null,null,[2,3]).sum(),2),
+ burning_cells:burning_cells
+ }
+ }
+ start(n=5000){
this.stop()
this.loop = setInterval(()=>{
this.tick()
@@ -212,11 +313,18 @@ class Simulation {
+
+