Simulating mud/slime with Box2D, bitmaps and filters
This is an attempt to simulate something like mud/slime with Box2D for a game concept I am trying do develop.
I won’t go much in detail with the tutorial because at the moment I am still fine-tuning the effect, but I am showing you a step by step process to achieve a slime-looking effect.
First, let’s create some static objects:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package { import flash.display.Sprite; import flash.events.Event; import Box2D.Dynamics.*; import Box2D.Collision.*; import Box2D.Collision.Shapes.*; import Box2D.Common.Math.*; public class Main extends Sprite { private var world:b2World=new b2World(new b2Vec2(0,5),true); private var worldScale:Number=30; public function Main() { debugDraw(); var polygonShape:b2PolygonShape = new b2PolygonShape(); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.restitution=0; fixtureDef.density=1; fixtureDef.friction=0.4; var bodyDef:b2BodyDef= new b2BodyDef(); bodyDef.position.Set(320/worldScale,240/worldScale); var container:b2Body; container=world.CreateBody(bodyDef); polygonShape.SetAsOrientedBox(132/worldScale/2,12/worldScale/2,new b2Vec2(0/worldScale,144/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,200/worldScale/2,new b2Vec2(-60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,276/worldScale/2,new b2Vec2(60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); addEventListener(Event.ENTER_FRAME,update); } private function debugDraw():void { var debugDraw:b2DebugDraw = new b2DebugDraw(); var debugSprite:Sprite = new Sprite(); addChild(debugSprite); debugDraw.SetSprite(debugSprite); debugDraw.SetDrawScale(worldScale); debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit); debugDraw.SetFillAlpha(0.5); world.SetDebugDraw(debugDraw); } private function update(e:Event):void { world.Step(1/30,6,2); world.ClearForces(); world.DrawDebugData(); } } } |
I am just creating some kind of container with a hole in the bottom.
Then, I skin the container with a transparent image and create one little ball at every frame, someone would call them particle. I remove them when they leave the stage to the bottom, and I don’t place more than 200 balls at the same time, although I was able to place about 300 of them without stressing that much my CPU.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
package { import flash.display.Sprite; import flash.events.Event; import Box2D.Dynamics.*; import Box2D.Collision.*; import Box2D.Collision.Shapes.*; import Box2D.Common.Math.*; public class Main extends Sprite { private var world:b2World=new b2World(new b2Vec2(0,5),true); private var worldScale:Number=30; public function Main() { debugDraw(); var backgroundImage:BackgroundImage=new BackgroundImage(); addChild(backgroundImage); var polygonShape:b2PolygonShape = new b2PolygonShape(); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.restitution=0; fixtureDef.density=1; fixtureDef.friction=0.4; var bodyDef:b2BodyDef= new b2BodyDef(); bodyDef.position.Set(320/worldScale,240/worldScale); var container:b2Body; container=world.CreateBody(bodyDef); polygonShape.SetAsOrientedBox(132/worldScale/2,12/worldScale/2,new b2Vec2(0/worldScale,144/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,200/worldScale/2,new b2Vec2(-60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,276/worldScale/2,new b2Vec2(60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); addEventListener(Event.ENTER_FRAME,update); } private function addCircle(pX:Number,pY:Number,h:Number):void { var circleShape:b2CircleShape = new b2CircleShape(h/worldScale); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.density=3; fixtureDef.friction=0.4; fixtureDef.restitution=0; fixtureDef.shape=circleShape; var bodyDef:b2BodyDef = new b2BodyDef(); bodyDef.type=b2Body.b2_dynamicBody; bodyDef.position.Set(pX/worldScale,pY/worldScale); bodyDef.userData=new WaterCircle(); var box:b2Body=world.CreateBody(bodyDef); box.CreateFixture(fixtureDef); } private function debugDraw():void { var debugDraw:b2DebugDraw = new b2DebugDraw(); var debugSprite:Sprite = new Sprite(); addChild(debugSprite); debugDraw.SetSprite(debugSprite); debugDraw.SetDrawScale(worldScale); debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit); debugDraw.SetFillAlpha(0.5); world.SetDebugDraw(debugDraw); } private function update(e:Event):void { var waterParticles:Number=0; world.Step(1/30,6,2); world.ClearForces(); for (var b:b2Body = world.GetBodyList(); b; b = b.GetNext()) { if (b.GetUserData()!=null) { waterParticles++; if (b.GetPosition().y*worldScale>480) { world.DestroyBody(b); } } } if (waterParticles<300) { addCircle(320+1-Math.random()*2,-10,5); } world.DrawDebugData(); } } } |
At this time I have a continuous flux of balls falling down the container.
Next steps consists in rendering balls on a separate DisplayObject using a Bitmap
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
package { import flash.display.Sprite; import flash.events.Event; import flash.display.BitmapData; import flash.display.Bitmap; import flash.geom.Matrix; import Box2D.Dynamics.*; import Box2D.Collision.*; import Box2D.Collision.Shapes.*; import Box2D.Common.Math.*; public class Main extends Sprite { private var world:b2World=new b2World(new b2Vec2(0,5),true); private var worldScale:Number=30; private var waterBitmapData:BitmapData=new BitmapData(640,480,false,0xFF333333); private var waterBitmap:Bitmap=new Bitmap(waterBitmapData); private var waterMatrix:Matrix=new Matrix(); public function Main() { addChild(waterBitmap); var backgroundImage:BackgroundImage=new BackgroundImage(); addChild(backgroundImage); var polygonShape:b2PolygonShape = new b2PolygonShape(); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.restitution=0; fixtureDef.density=1; fixtureDef.friction=0.4; var bodyDef:b2BodyDef= new b2BodyDef(); bodyDef.position.Set(320/worldScale,240/worldScale); var container:b2Body; container=world.CreateBody(bodyDef); polygonShape.SetAsOrientedBox(132/worldScale/2,12/worldScale/2,new b2Vec2(0/worldScale,144/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,200/worldScale/2,new b2Vec2(-60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,276/worldScale/2,new b2Vec2(60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); addEventListener(Event.ENTER_FRAME,update); } private function addCircle(pX:Number,pY:Number,h:Number):void { var circleShape:b2CircleShape = new b2CircleShape(h/worldScale); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.density=3; fixtureDef.friction=0.4; fixtureDef.restitution=0; fixtureDef.shape=circleShape; var bodyDef:b2BodyDef = new b2BodyDef(); bodyDef.type=b2Body.b2_dynamicBody; bodyDef.position.Set(pX/worldScale,pY/worldScale); bodyDef.userData=new WaterCircle(); var box:b2Body=world.CreateBody(bodyDef); box.CreateFixture(fixtureDef); } private function debugDraw():void { var debugDraw:b2DebugDraw = new b2DebugDraw(); var debugSprite:Sprite = new Sprite(); addChild(debugSprite); debugDraw.SetSprite(debugSprite); debugDraw.SetDrawScale(worldScale); debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit); debugDraw.SetFillAlpha(0.5); world.SetDebugDraw(debugDraw); } private function update(e:Event):void { var waterParticles:Number=0; waterBitmapData.fillRect(waterBitmapData.rect,0xFF333333); world.Step(1/30,6,2); world.ClearForces(); for (var b:b2Body = world.GetBodyList(); b; b = b.GetNext()) { if (b.GetUserData()!=null) { waterParticles++; if (b.GetPosition().y*worldScale>480) { world.DestroyBody(b); } else { waterMatrix.tx=b.GetPosition().x*worldScale; waterMatrix.ty=b.GetPosition().y*worldScale; waterBitmapData.draw(b.GetUserData(),waterMatrix); } } } if (waterParticles<300) { addCircle(320+1-Math.random()*2,-10,5); } world.DrawDebugData(); } } } |
Now I can remove the debug draw as all Box2D assets have their own DisplayObject representing them
And now, since I am using a Bitmap, I can add a blur filter to balls
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
package { import flash.display.Sprite; import flash.events.Event; import flash.display.BitmapData; import flash.display.Bitmap; import flash.geom.Matrix; import flash.filters.BlurFilter; import flash.filters.BitmapFilterQuality; import flash.geom.Point; import Box2D.Dynamics.*; import Box2D.Collision.*; import Box2D.Collision.Shapes.*; import Box2D.Common.Math.*; public class Main extends Sprite { private var world:b2World=new b2World(new b2Vec2(0,5),true); private var worldScale:Number=30; private var waterBitmapData:BitmapData=new BitmapData(640,480,false,0xFF333333); private var waterBitmap:Bitmap=new Bitmap(waterBitmapData); private var waterMatrix:Matrix=new Matrix(); private var waterBlur:BlurFilter=new BlurFilter(15,15,flash.filters.BitmapFilterQuality.HIGH); public function Main() { addChild(waterBitmap); var backgroundImage:BackgroundImage=new BackgroundImage(); addChild(backgroundImage); var polygonShape:b2PolygonShape = new b2PolygonShape(); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.restitution=0; fixtureDef.density=1; fixtureDef.friction=0.4; var bodyDef:b2BodyDef= new b2BodyDef(); bodyDef.position.Set(320/worldScale,240/worldScale); var container:b2Body; container=world.CreateBody(bodyDef); polygonShape.SetAsOrientedBox(132/worldScale/2,12/worldScale/2,new b2Vec2(0/worldScale,144/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,200/worldScale/2,new b2Vec2(-60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,276/worldScale/2,new b2Vec2(60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); addEventListener(Event.ENTER_FRAME,update); } private function addCircle(pX:Number,pY:Number,h:Number):void { var circleShape:b2CircleShape = new b2CircleShape(h/worldScale); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.density=3; fixtureDef.friction=0.4; fixtureDef.restitution=0; fixtureDef.shape=circleShape; var bodyDef:b2BodyDef = new b2BodyDef(); bodyDef.type=b2Body.b2_dynamicBody; bodyDef.position.Set(pX/worldScale,pY/worldScale); bodyDef.userData=new WaterCircle(); var box:b2Body=world.CreateBody(bodyDef); box.CreateFixture(fixtureDef); } private function debugDraw():void { var debugDraw:b2DebugDraw = new b2DebugDraw(); var debugSprite:Sprite = new Sprite(); addChild(debugSprite); debugDraw.SetSprite(debugSprite); debugDraw.SetDrawScale(worldScale); debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit); debugDraw.SetFillAlpha(0.5); world.SetDebugDraw(debugDraw); } private function update(e:Event):void { var waterParticles:Number=0; waterBitmapData.fillRect(waterBitmapData.rect,0xFF333333); world.Step(1/30,6,2); world.ClearForces(); for (var b:b2Body = world.GetBodyList(); b; b = b.GetNext()) { if (b.GetUserData()!=null) { waterParticles++; if (b.GetPosition().y*worldScale>480) { world.DestroyBody(b); } else { waterMatrix.tx=b.GetPosition().x*worldScale; waterMatrix.ty=b.GetPosition().y*worldScale; waterBitmapData.draw(b.GetUserData(),waterMatrix); } } } if (waterParticles<300) { addCircle(320+1-Math.random()*2,-10,5); } waterBitmapData.applyFilter(waterBitmapData,waterBitmapData.rect,new Point(0,0),waterBlur); } } } |
At this time I’m getting a “smoke” effect, but this is not what I want
So the final step is to add a threshold to change some colors of my bitmap balls/smoke, play a bit with transparency and add a better background
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
package { import flash.display.Sprite; import flash.events.Event; import flash.display.BitmapData; import flash.display.Bitmap; import flash.geom.Matrix; import flash.filters.BlurFilter; import flash.filters.BitmapFilterQuality; import flash.geom.Point; import Box2D.Dynamics.*; import Box2D.Collision.*; import Box2D.Collision.Shapes.*; import Box2D.Common.Math.*; public class Main extends Sprite { private var world:b2World=new b2World(new b2Vec2(0,5),true); private var worldScale:Number=30; private var waterBitmapData:BitmapData=new BitmapData(640,480,true,0x00000000); private var waterBitmap:Bitmap=new Bitmap(waterBitmapData); private var waterMatrix:Matrix=new Matrix(); private var waterBlur:BlurFilter=new BlurFilter(15,15,flash.filters.BitmapFilterQuality.HIGH); public function Main() { var realBg:RealBg=new RealBg(); addChild(realBg); addChild(waterBitmap); var backgroundImage:BackgroundImage=new BackgroundImage(); addChild(backgroundImage); var polygonShape:b2PolygonShape = new b2PolygonShape(); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.restitution=0; fixtureDef.density=1; fixtureDef.friction=0.4; var bodyDef:b2BodyDef= new b2BodyDef(); bodyDef.position.Set(320/worldScale,240/worldScale); var container:b2Body; container=world.CreateBody(bodyDef); polygonShape.SetAsOrientedBox(132/worldScale/2,12/worldScale/2,new b2Vec2(0/worldScale,144/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,200/worldScale/2,new b2Vec2(-60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); polygonShape.SetAsOrientedBox(12/worldScale/2,276/worldScale/2,new b2Vec2(60/worldScale,0/worldScale),0); fixtureDef.shape=polygonShape; container.CreateFixture(fixtureDef); addEventListener(Event.ENTER_FRAME,update); } private function addCircle(pX:Number,pY:Number,h:Number):void { var circleShape:b2CircleShape = new b2CircleShape(h/worldScale); var fixtureDef:b2FixtureDef = new b2FixtureDef(); fixtureDef.density=3; fixtureDef.friction=0.4; fixtureDef.restitution=0; fixtureDef.shape=circleShape; var bodyDef:b2BodyDef = new b2BodyDef(); bodyDef.type=b2Body.b2_dynamicBody; bodyDef.position.Set(pX/worldScale,pY/worldScale); bodyDef.userData=new WaterCircle(); var box:b2Body=world.CreateBody(bodyDef); box.CreateFixture(fixtureDef); } private function debugDraw():void { var debugDraw:b2DebugDraw = new b2DebugDraw(); var debugSprite:Sprite = new Sprite(); addChild(debugSprite); debugDraw.SetSprite(debugSprite); debugDraw.SetDrawScale(worldScale); debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit); debugDraw.SetFillAlpha(0.5); world.SetDebugDraw(debugDraw); } private function update(e:Event):void { var waterParticles:Number=0; waterBitmapData.fillRect(waterBitmapData.rect,0x00000000); world.Step(1/30,6,2); world.ClearForces(); for (var b:b2Body = world.GetBodyList(); b; b = b.GetNext()) { if (b.GetUserData()!=null) { waterParticles++; if (b.GetPosition().y*worldScale>480) { world.DestroyBody(b); } else { waterMatrix.tx=b.GetPosition().x*worldScale; waterMatrix.ty=b.GetPosition().y*worldScale; waterBitmapData.draw(b.GetUserData(),waterMatrix); } } } if (waterParticles<300) { addCircle(320+1-Math.random()*2,-10,5); } waterBitmapData.applyFilter(waterBitmapData,waterBitmapData.rect,new Point(0,0),waterBlur); waterBitmapData.threshold(waterBitmapData,waterBitmapData.rect,new Point(0,0),">",0X11444444,0x88FF00FF,0xFFFFFFFF, true); } } } |
And that’s it! Not the best slime ever seen, but it’s dynamically generated by Box2D, this means it has a real mass, which is quite awesome.
How would you improve it?
Download the source code of the last example.
They can be easily customized to meet the unique requirements of your project.





(39 votes, average: 4.87 out of 5)








This post has 29 comments
luis
gracias emanuele me fué muy util =)
Vicente Benavent
Really awesome, great effect
balajee
Wow .. That was nice…. work.. congrats…
Francois
Awesome result! Good job!
jarray42
This is absolutly amazing! thank you!
Keep the good work :D
Luis
I’d improve it by changing those colors! ;D But otherwise this effect is awesome.
Chris
Hey Emanuele,
that’s something I’ve been searching for some time ago and came across this site:
http://www.patrickmatte.com/index.php/2009/06/16/simulating-liquid-with-a-physics-engine-in-actionscript-3/
which is actually doing exactly what you did (though with APE and not Box2D), but it’s nice to have an example for AS3 aswell!
Also there was one guy at flashgamelicense (pspmiracle) who did this for his game (Unfreeze me!), but a texture fill instead of a plain color to achieve the effect like in “Where’s my Water”.
Could you go over how to do this? This would give this effect so much more depth.
Thanks! :)
Chris
Emanuele Feronato
Hi Chris,
I can’t find the game, can you point me to a link to see how it’s done?
www.kotubukimedia.com
Hi Emanuele,
Can you please provide a CS4 version of slime.fla. I don’t have a CS5 on my machine. I’m would love to try out this code of yours.
Thanks,
Ai
srinivasarao549
Really awsome boss…
IS there any way to do the fluid simulation with out box2d?
joe
Super awesome
siddhart shekar
hi.. awesome tutorial… but how can u do this in cocos2d? major part being the blur filter which is part of flash… how can it be replicated in cocos2d? thanks… regds
Chris
Very awesome- didn’t know how the last step would have even been done, great eye opener!
siddharth
hey!!… where did my post go…lol… nyway reposting
…AWesome tutorial…. can you show how to do the same in cocos2d… as this uses the flash effects feature… thanks
Simulating mud/slime with Box2D, bitmaps and filters – Emanuele Feronato « eaflash
[...] on http://www.emanueleferonato.com Share this:TwitterFacebookLike this:LikeBe the first to like this [...]
Samuramu
Very nice indeed. The threshold effect is surprisingly effective. Thanks!
Jarray42
Hi Emanuele;
I tried to improve this by reducing the cercle redius:
addCircle(320+1-Math.random()*2,-10,2);
And increasing the number of cercles to 1000 for exemple (or even more); however there is the lag ptoblem again T_T
I tried the grabage collection, but nothing has changed because I don’t want to destroy / remove my particles.
Is there a way to improve the performance of Box2D here ?
Oh yes, I heard something about Stage3D with Starling Framework in flash Player11, it seems it can improve the performance of Box2d by using the GPU.
Thanks :)
Chris
Hey Emanuele,
unfortunalety you can’t play his game atm, since it’s in active bidding.
I’ll link you to his FGL-Page:
http://www.flashgamelicense.com/view_account.php?username=pspmiracle
Let me know if you can redo this effect.
Best regards,
Chris
Husky
Thank you for your sharing! :)
Antoan
I wonder if this is the exact technique used for the green goo in the flash game Oozing Forever :?
Maras
Very nice! It looks very realistic.
Thanks for this tutorial!
tccHtnn
how to implement with cocos2D?
sosese
how to make cocos2d Water Please! :(
SyntheCypher
Adding a bevel filter also helps it look cool here’s my example (made mine before I saw yours).
http://caffeinatednightmare.com/blog/?p=146
GunnyWaffle
This was extremely helpful. However Im trying to do something seemingly impossible with it right now.
I have dynamically colored slime particles (basically box2d balls with bound sprites) and I cant figure out how to apply the threshold and keep all the colors.
Would you know of a way to do this? I’m beginning to think that I have to render each color as a separate bitmap somehow and then blend them together.
siddharth shekar
if any one is looking to recreate this is cocos2sd… you can go here… there is on for v1.0 using box2d and 2.0 using chimpunk..
http://www.cocos2d-iphone.org/forum/topic/32111/page/2?replies=41#post-231632
Cody
Emanuele, could you please help me understand these lines:
for (var b:b2Body = world.GetBodyList(); b; b = b.GetNext()) {
if (b.GetUserData()!=null) {
So I’m pretty sure the for loop scans through the worlds box2d bodies, just not sure how it goes about doing so. Also how does the if statement retrieve ONLY water particles. For instance if I added more bodies to the world, it does not map the water bitmap to it.
Thanks ahead of time!
Jose Samo
Thanks Emmanuele Awesome!! but i got the problem that the bitmap uses a lot of high cpu usage and for mobiles like android, well i didn’t fit me so well. Is there any way to reduce the cpu usage of the bitmap…?
Niko Creatifesprit
Great, thanks & keep up the good work!