Răsfoiți Sursa

FX2D: emitters

Tom SPIRA 5 ani în urmă
părinte
comite
a98b6beb2b

+ 3 - 3
hide/comp/SceneEditor.hx

@@ -406,7 +406,6 @@ class SceneEditor {
 				selectObjects([]);
 			}
 		};
-		resetCamera();
 
 
 		var cam = @:privateAccess view.getDisplayState("Camera");
@@ -430,6 +429,7 @@ class SceneEditor {
 		cameraController2D.loadFromScene();
 
 		scene.onUpdate = update;
+		resetCamera();
 
 		// BUILD scene tree
 
@@ -668,8 +668,8 @@ class SceneEditor {
 		var startDrag = null;
 		var curDrag = null;
 		var dragBtn = -1;
-		var i3d = Std.instance(int, h3d.scene.Interactive);
-		var i2d = Std.instance(int, h2d.Interactive);
+		var i3d = Std.downcast(int, h3d.scene.Interactive);
+		var i2d = Std.downcast(int, h2d.Interactive);
 		int.onClick = function(e) {
 			if(e.button == K.MOUSE_RIGHT) {
 				var pt = new h3d.Vector(e.relX,e.relY,e.relZ);

+ 37 - 52
hide/view/FXEditor.hx

@@ -75,7 +75,7 @@ private class FXSceneEditor extends hide.comp.SceneEditor {
 
 		var menu = [];
 		if (parent.is2D) {
-			for(name in ["Group 2D", "Bitmap", "Anim2D", "Atlas", "Text", "Shaders", "Shader Graph", "Placeholder"]) {
+			for(name in ["Group 2D", "Bitmap", "Anim2D", "Atlas", "Particle2D", "Text", "Shaders", "Shader Graph", "Placeholder"]) {
 				var item = allTypes.find(i -> i.label == name);
 				if(item == null) continue;
 				allTypes.remove(item);
@@ -289,39 +289,7 @@ class FXEditor extends FileView {
 		}
 
 		if (is2D) {
-			var heapsScene = element.find(".heaps-scene");
-
-			heapsScene.on("mousedown", function(e) {
-				lastPan = null;
-			});
-
-			heapsScene.on("mousemove", function(e : js.jquery.Event) {
-				if (e.buttons == 4) { // middle button
-					if (lastPan == null) {
-						lastPan = new h2d.col.Point(e.clientX, e.clientY);
-						return;
-					}
-					@:privateAccess {
-						scene.s2d.children[0].x += e.clientX - lastPan.x;
-						scene.s2d.children[0].y += e.clientY - lastPan.y;
-					}
-
-					lastPan = new h2d.col.Point(e.clientX, e.clientY);
-					updateGrid();
-				}
-			});
-
-			// Zoom control
-			heapsScene.on("wheel", function(e) {
-				@:privateAccess {
-					if (e.originalEvent.deltaY < 0) {
-						scene.s2d.children[0].scale(1.1);
-					} else {
-						scene.s2d.children[0].scale(0.9);
-					}
-					updateGrid();
-				}
-			});
+			sceneEditor.camera2D = true;
 		}
 
 		var scriptElem = element.find(".fx-script");
@@ -574,6 +542,7 @@ class FXEditor extends FileView {
 		var renderProps = data.find(e -> e.to(hrt.prefab.RenderProps));
 		if(renderProps != null)
 			renderProps.applyProps(scene.s3d.renderer);
+		updateGrid();
 	}
 
 	override function onDragDrop(items : Array<String>, isDrop : Bool) {
@@ -1254,6 +1223,7 @@ class FXEditor extends FileView {
 		var obj2dElt = Std.downcast(elt, hrt.prefab.Object2D);
 		var shaderElt = Std.downcast(elt, hrt.prefab.Shader);
 		var emitterElt = Std.downcast(elt, hrt.prefab.fx.Emitter);
+		var particle2dElt = Std.downcast(elt, hrt.prefab.fx2d.Particle2D);
 		var menuItems : Array<hide.comp.ContextMenu.ContextMenuItem> = [];
 
 		inline function hasTrack(pname) {
@@ -1365,20 +1335,20 @@ class FXEditor extends FileView {
 					menuItems.push(item);
 			}
 		}
+		function addParam(param : hrt.prefab.fx.Emitter.ParamDef, prefix: String) {
+			var label = prefix + (param.disp != null ? param.disp : upperCase(param.name));
+			var item : hide.comp.ContextMenu.ContextMenuItem = switch(param.t) {
+				case PVec(n, _):
+					{
+						label: label,
+						menu: groupedTracks(param.name, xyzwTracks(n)),
+					}
+				default:
+					trackItem(label, [{name: param.name}]);
+			};
+			menuItems.push(item);
+		}
 		if(emitterElt != null) {
-			function addParam(param : hrt.prefab.fx.Emitter.ParamDef, prefix: String) {
-				var label = prefix + (param.disp != null ? param.disp : upperCase(param.name));
-				var item : hide.comp.ContextMenu.ContextMenuItem = switch(param.t) {
-					case PVec(n, _):
-						{
-							label: label,
-							menu: groupedTracks(param.name, xyzwTracks(n)),
-						}
-					default:
-						trackItem(label, [{name: param.name}]);
-				};
-				menuItems.push(item);
-			}
 			for(param in hrt.prefab.fx.Emitter.emitterParams) {
 				if(!param.animate)
 					continue;
@@ -1390,6 +1360,13 @@ class FXEditor extends FileView {
 				addParam(param, "Instance ");
 			}
 		}
+		if (particle2dElt != null) {
+			for(param in hrt.prefab.fx2d.Particle2D.emitter2dParams) {
+				if(!param.animate)
+					continue;
+				addParam(param, "");
+			}
+		}
 		return menuItems;
 	}
 
@@ -1402,10 +1379,12 @@ class FXEditor extends FileView {
 			grid2d.remove();
 			grid2d = null;
 		}
+		
 		if(!showGrid)
 			return;
+
 		if (is2D) {
-			grid2d = new h2d.Graphics(scene.s2d);
+			grid2d = new h2d.Graphics(scene.editor.context.local2d);
 			grid2d.scale(1);
 
 			grid2d.lineStyle(1.0, 12632256, 1.0);
@@ -1418,9 +1397,6 @@ class FXEditor extends FileView {
 			return;
 		}
 
-		if(!showGrid)
-			return;
-
 		grid = new h3d.scene.Graphics(scene.s3d);
 		grid.scale(1);
 		grid.material.mainPass.setPassName("debuggeom");
@@ -1567,5 +1543,14 @@ class FXEditor extends FileView {
 	}
 
 	static var _ = FileTree.registerExtension(FXEditor, ["fx"], { icon : "sitemap", createNew : "FX" });
-	static var _2d = FileTree.registerExtension(FXEditor, ["fx2d"], { icon : "sitemap", createNew : "FX 2D" });
+}
+
+
+class FX2DEditor extends FXEditor {
+
+	override function getDefaultContent() {
+		return haxe.io.Bytes.ofString(ide.toJSON(new hrt.prefab.fx.FX2D().saveData()));
+	}
+
+	static var _2d = FileTree.registerExtension(FX2DEditor, ["fx2d"], { icon : "sitemap", createNew : "FX 2D" });
 }

+ 88 - 83
hide/view/Particles2D.hx

@@ -64,94 +64,99 @@ class Particles2D extends FileView {
 			}
 		}
 	}
+	
+
+	static public function getParamsHTMLform() {
+		return 
+			'<div class="content">
+				<div class="group" name="Display">
+					<dl>
+						<dt>Name</dt><dd><input field="name" onchange="$(this).closest(\'.section\').find(\'>h1 span\').text($(this).val())"/></dd>
+						<dt>Blend Mode</dt><dd><select field="blendMode"/></dd></dd>
+						<dt>Texture</dt><dd><input type="texture" field="texture"/></dd>
+						<dt>Color Gradient</dt><dd><input type="texture" field="colorGradient"/></dd>
+						<dt>Sort Mode</dt><dd><select field="sortMode"/></dd></dd>
+						<dt>Relative</dt><dd><input type="checkbox" field="isRelative"/></dd>
+					</dl>
+				</div>
+
+				<div class="group" name="Emit">
+					<dl>
+						<dt>Mode</dt><dd><select field="emitMode"/></dd>
+						<dt></dt><dd>
+							X <input type="number" style="width:59px" field="dx"/>
+							Y <input type="number" style="width:59px" field="dy"/>
+						</dd>
+						<dt>Count</dt><dd><input type="range" field="nparts" min="0" max="300" step="1"/></dd>
+						<dt>Distance</dt><dd><input type="range" field="emitDist" min="0" max="1000" step="1"/></dd>
+						<dt>Distance Y</dt><dd><input type="range" field="emitDistY" min="0" max="1000" step="1"/></dd>
+						<dt>Angle</dt><dd><input type="range" field="emitAngle" min="-1.571" max="1.571" step="0.0524"/></dd>
+						<dt>Sync</dt><dd><input type="range" field="emitSync" min="0" max="1"/></dd>
+						<dt>Delay</dt><dd><input type="range" field="emitDelay" min="0" max="10"/></dd>
+						<dt>Loop</dt><dd><input type="checkbox" field="emitLoop"/></dd>
+					</dl>
+				</div>
+
+				<div class="group" name="Life">
+					<dl>
+						<dt>Initial</dt><dd><input type="range" field="life" min="0" max="10"/></dd>
+						<dt>Randomness</dt><dd><input type="range" field="lifeRand" min="0" max="1"/></dd>
+						<dt>Fade In</dt><dd><input type="range" field="fadeIn" min="0" max="1"/></dd>
+						<dt>Fade Out</dt><dd><input type="range" field="fadeOut" min="0" max="1"/></dd>
+						<dt>Fade Power</dt><dd><input type="range" field="fadePower" min="0" max="3"/></dd>
+					</dl>
+				</div>
+
+				<div class="group" name="Speed">
+					<dl>
+						<dt>Initial</dt><dd><input type="range" field="speed" min="0" max="1000"/></dd>
+						<dt>Randomness</dt><dd><input type="range" field="speedRand" min="0" max="1"/></dd>
+						<dt>Acceleration</dt><dd><input type="range" field="speedIncr" min="-1" max="1"/></dd>
+						<dt>Gravity</dt><dd><input type="range" field="gravity" min="-500" max="500" step="1"/></dd>
+						<dt>Gravity Angle</dt><dd><input type="range" field="gravityAngle" min="0" max="1" step="0.1"/></dd>
+					</dl>
+				</div>
+
+				<div class="group" name="Size">
+					<dl>
+						<dt>Initial</dt><dd><input type="range" field="size" min="0.01" max="2"/></dd>
+						<dt>Randomness</dt><dd><input type="range" field="sizeRand" min="0" max="1"/></dd>
+						<dt>Growth</dt><dd><input type="range" field="sizeIncr" min="-1" max="1"/></dd>
+						<dt></dt><dd>
+							Grow u <input type="checkbox" field="incrX"/>
+							Grow v <input type="checkbox" field="incrY"/>
+						</dd>
+					</dl>
+				</div>
+
+				<div class="group" name="Rotation">
+					<dl>
+						<dt>Initial</dt><dd><input type="range" field="rotInit" min="0" max="1"/></dd>
+						<dt>Speed</dt><dd><input type="range" field="rotSpeed" min="0" max="20"/></dd>
+						<dt>Randomness</dt><dd><input type="range" field="rotSpeedRand" min="0" max="1"/></dd>
+						<dt>Auto orient</dt><dd><input type="checkbox" field="rotAuto"/></dd>
+					</dl>
+				</div>
+
+				<div class="group" name="Animation">
+					<dl>
+						<dt>Animation Repeat</dt><dd><input type="range" field="animationRepeat" min="0" max="10"/></dd>
+						<dt>Frame Division</dt><dd>
+							X <input type="number" style="width:30px" field="frameDivisionX" min="1" max="16"/>
+							Y <input type="number" style="width:30px" field="frameDivisionY" min="1" max="16"/>
+							# <input type="number" style="width:30px" field="frameCount" min="0" max="32"/>
+						</dd>
+					</dl>
+				</div>
+
+			</div>';
+	}
 
 	function addGroup( g : ParticleGroup ) {
 		var e = new Element('
 			<div class="section">
 				<h1><span>${g.name}</span> &nbsp;<input type="checkbox" field="enable"/></h1>
-				<div class="content">
-
-					<div class="group" name="Display">
-						<dl>
-							<dt>Name</dt><dd><input field="name" onchange="$(this).closest(\'.section\').find(\'>h1 span\').text($(this).val())"/></dd>
-							<dt>Blend Mode</dt><dd><select field="blendMode"/></dd></dd>
-							<dt>Texture</dt><dd><input type="texture" field="texture"/></dd>
-							<dt>Color Gradient</dt><dd><input type="texture" field="colorGradient"/></dd>
-							<dt>Sort Mode</dt><dd><select field="sortMode"/></dd></dd>
-							<dt>Relative</dt><dd><input type="checkbox" field="isRelative"/></dd>
-						</dl>
-					</div>
-
-					<div class="group" name="Emit">
-						<dl>
-							<dt>Mode</dt><dd><select field="emitMode"/></dd>
-							<dt></dt><dd>
-								X <input type="number" style="width:59px" field="dx"/>
-								Y <input type="number" style="width:59px" field="dy"/>
-							</dd>
-							<dt>Count</dt><dd><input type="range" field="nparts" min="0" max="300" step="1"/></dd>
-							<dt>Distance</dt><dd><input type="range" field="emitDist" min="0" max="1000" step="1"/></dd>
-							<dt>Distance Y</dt><dd><input type="range" field="emitDistY" min="0" max="1000" step="1"/></dd>
-							<dt>Angle</dt><dd><input type="range" field="emitAngle" min="-1.571" max="1.571" step="0.0524"/></dd>
-							<dt>Sync</dt><dd><input type="range" field="emitSync" min="0" max="1"/></dd>
-							<dt>Delay</dt><dd><input type="range" field="emitDelay" min="0" max="10"/></dd>
-							<dt>Loop</dt><dd><input type="checkbox" field="emitLoop"/></dd>
-						</dl>
-					</div>
-
-					<div class="group" name="Life">
-						<dl>
-							<dt>Initial</dt><dd><input type="range" field="life" min="0" max="10"/></dd>
-							<dt>Randomness</dt><dd><input type="range" field="lifeRand" min="0" max="1"/></dd>
-							<dt>Fade In</dt><dd><input type="range" field="fadeIn" min="0" max="1"/></dd>
-							<dt>Fade Out</dt><dd><input type="range" field="fadeOut" min="0" max="1"/></dd>
-							<dt>Fade Power</dt><dd><input type="range" field="fadePower" min="0" max="3"/></dd>
-						</dl>
-					</div>
-
-					<div class="group" name="Speed">
-						<dl>
-							<dt>Initial</dt><dd><input type="range" field="speed" min="0" max="1000"/></dd>
-							<dt>Randomness</dt><dd><input type="range" field="speedRand" min="0" max="1"/></dd>
-							<dt>Acceleration</dt><dd><input type="range" field="speedIncr" min="-1" max="1"/></dd>
-							<dt>Gravity</dt><dd><input type="range" field="gravity" min="-500" max="500" step="1"/></dd>
-							<dt>Gravity Angle</dt><dd><input type="range" field="gravityAngle" min="0" max="1" step="0.1"/></dd>
-						</dl>
-					</div>
-
-					<div class="group" name="Size">
-						<dl>
-							<dt>Initial</dt><dd><input type="range" field="size" min="0.01" max="2"/></dd>
-							<dt>Randomness</dt><dd><input type="range" field="sizeRand" min="0" max="1"/></dd>
-							<dt>Growth</dt><dd><input type="range" field="sizeIncr" min="-1" max="1"/></dd>
-							<dt></dt><dd>
-								Grow u <input type="checkbox" field="incrX"/>
-								Grow v <input type="checkbox" field="incrY"/>
-							</dd>
-						</dl>
-					</div>
-
-					<div class="group" name="Rotation">
-						<dl>
-							<dt>Initial</dt><dd><input type="range" field="rotInit" min="0" max="1"/></dd>
-							<dt>Speed</dt><dd><input type="range" field="rotSpeed" min="0" max="20"/></dd>
-							<dt>Randomness</dt><dd><input type="range" field="rotSpeedRand" min="0" max="1"/></dd>
-							<dt>Auto orient</dt><dd><input type="checkbox" field="rotAuto"/></dd>
-						</dl>
-					</div>
-
-					<div class="group" name="Animation">
-						<dl>
-							<dt>Animation Repeat</dt><dd><input type="range" field="animationRepeat" min="0" max="10"/></dd>
-							<dt>Frame Division</dt><dd>
-								X <input type="number" style="width:30px" field="frameDivisionX" min="1" max="16"/>
-								Y <input type="number" style="width:30px" field="frameDivisionY" min="1" max="16"/>
-								# <input type="number" style="width:30px" field="frameCount" min="0" max="32"/>
-							</dd>
-						</dl>
-					</div>
-
-				</div>
+				${getParamsHTMLform()}
 			</div>
 		');
 

+ 13 - 8
hide/view/l3d/Gizmo2D.hx

@@ -35,6 +35,7 @@ class Gizmo2D extends h2d.Object {
 			switch( z ) {
 			case Pan:
 				e.cancel = true;
+				int.cursor = Button;
 			case Scale:
 				int.cursor = cScale;
 			case ScaleX:
@@ -79,22 +80,26 @@ class Gizmo2D extends h2d.Object {
 				inline function scale( m : Float, size : Float ) {
 					return Math.max(0, (m + size * 0.5) / (size * 0.5));
 				}
+				inline function snap(value : Float, step : Float) {
+					if (hxd.Key.isDown(hxd.Key.CTRL))
+						return Math.round(value / step) * step;
+					return value;
+				}
 				var m = { x : 0., y : 0., scaleX : 1., scaleY : 1., rotation : 0. };
 				switch( t ) {
 				case Pan:
-					m.x = dx;
-					m.y = dy;
+					m.x = snap(dx, 10);
+					m.y = snap(dy, 10);
 				case ScaleX:
-					m.scaleX = scale(dx * scaleSign, dragWidth);
+					m.scaleX = snap(scale(dx * scaleSign, dragWidth), 0.1);
 				case ScaleY:
-					m.scaleY = scale(dy * scaleSign, dragHeight);
+					m.scaleY = snap(scale(dy * scaleSign, dragHeight), 0.1);
 				case Scale:
-					m.scaleX = m.scaleY = Math.max(scale(dx,dragWidth), scale(dy, dragHeight));
+					m.scaleX = m.scaleY = Math.max(snap(scale(dx, dragWidth), 0.1), snap(scale(dy, dragHeight), 0.1));
 				case Rotation:
 					var startAng =  Math.atan2(dragStartY - center.y, dragStartX - center.x);
-					m.rotation = hxd.Math.angle(Math.atan2(scene.mouseY - center.y,scene.mouseX - center.x) - startAng);
-					if( hxd.Key.isDown(hxd.Key.CTRL) )
-						m.rotation = Math.round(m.rotation / (Math.PI/8)) * (Math.PI/8);
+					var tmpRotation = hxd.Math.angle(Math.atan2(scene.mouseY - center.y,scene.mouseX - center.x) - startAng);
+					m.rotation = snap(tmpRotation, (Math.PI/8));
 				default:
 				}
 				onMove(m);

+ 29 - 0
hrt/prefab/fx/FX2D.hx

@@ -4,6 +4,7 @@ import hrt.prefab.Prefab as PrefabElement;
 import hrt.prefab.fx.BaseFX.ObjectAnimation;
 import hrt.prefab.fx.BaseFX.ShaderAnimation;
 
+@:allow(hrt.prefab.fx.FX2D)
 class FX2DAnimation extends h2d.Object {
 
 	public var prefab : hrt.prefab.Prefab;
@@ -17,6 +18,7 @@ class FX2DAnimation extends h2d.Object {
 	public var loop : Bool;
 	public var objects: Array<ObjectAnimation> = [];
 	public var shaderAnims : Array<ShaderAnimation> = [];
+	public var emitters : Array<hrt.prefab.fx2d.Particle2D.Particles>;
 
 	var evaluator : Evaluator;
 	var random : hxd.Rand;
@@ -33,6 +35,27 @@ class FX2DAnimation extends h2d.Object {
 		random.init(seed);
 	}
 
+	function init(ctx: Context, def: FX2D) {
+		initEmitters(ctx, def);
+	}
+
+	function initEmitters(ctx: Context, elt: PrefabElement) {
+		var em = Std.downcast(elt, hrt.prefab.fx2d.Particle2D);
+		if(em != null)  {
+			for(emCtx in ctx.shared.getContexts(elt)) {
+				if(emCtx.local2d == null) continue;
+				if(emitters == null) emitters = [];
+				var emobj : hrt.prefab.fx2d.Particle2D.Particles = cast emCtx.local2d;
+				emitters.push(emobj);
+			}
+		}
+		else {
+			for(c in elt.children) {
+				initEmitters(ctx, c);
+			}
+		}
+	}
+
 	public function setTime( time : Float ) {
 
 		this.localTime = time;
@@ -95,6 +118,11 @@ class FX2DAnimation extends h2d.Object {
 		for(anim in shaderAnims) {
 			anim.setTime(time);
 		}
+		if (emitters != null)
+			for(em in emitters) {
+				if(em.visible)
+					em.setTime(time);
+			}
 	}
 }
 
@@ -215,6 +243,7 @@ class FX2D extends BaseFX {
 		else
 			super.make(ctx);
 		#end
+		fxanim.init(ctx, this);
 
 		getObjAnimations(ctx, this, fxanim.objects);
 		BaseFX.getShaderAnims(ctx, this, fxanim.shaderAnims);

+ 20 - 0
hrt/prefab/fx2d/Atlas.hx

@@ -58,6 +58,14 @@ class Atlas extends Object2D {
 		}
 		h2dAnim.pause = !loop;
 		h2dAnim.blendMode = blendMode;
+		
+		#if editor
+			var int = Std.downcast(h2dAnim.getChildAt(0),h2d.Interactive);
+			if( int != null ) {
+				int.width = h2dAnim.getFrame().width;
+				int.height = h2dAnim.getFrame().height;
+			}
+		#end
 	}
 
 	override function makeInstance(ctx:Context):Context {
@@ -70,6 +78,18 @@ class Atlas extends Object2D {
 	}
 
 	#if editor
+
+	override function makeInteractive(ctx:Context):h2d.Interactive {
+		var local2d = ctx.local2d;
+		if(local2d == null)
+			return null;
+		var h2dAnim = cast(local2d, h2d.Anim);
+		var int = new h2d.Interactive(h2dAnim.getFrame().width, h2dAnim.getFrame().height);
+		h2dAnim.addChildAt(int, 0);
+		int.propagateEvents = true;
+		return int;
+	}
+
 	override function edit( ctx : EditContext ) {
 		super.edit(ctx);
 

+ 1 - 1
hrt/prefab/fx2d/Bitmap.hx

@@ -50,7 +50,7 @@ class Bitmap extends Object2D {
 		bmp.tile.setCenterRatio(cRatio[0], cRatio[1]);
 		bmp.blendMode = blendMode;
 		#if editor
-		var int = Std.instance(bmp.getChildAt(0),h2d.Interactive);
+		var int = Std.downcast(bmp.getChildAt(0),h2d.Interactive);
 		if( int != null ) {
 			int.width = bmp.tile.width;
 			int.height = bmp.tile.height;

+ 187 - 0
hrt/prefab/fx2d/Particle2D.hx

@@ -0,0 +1,187 @@
+package hrt.prefab.fx2d;
+
+import hrt.prefab.fx.Evaluator;
+import hrt.prefab.fx.Value;
+import h2d.Particles.ParticleGroup;
+
+class Particles extends h2d.Particles {
+	
+	var evaluator : Evaluator;
+	
+	var lastTime = -1.0;
+	var curTime = 0.0;
+	
+	var random: hxd.Rand;
+	var randomSeed = 0;
+
+	public var catchupSpeed = 4; // Use larger ticks when catching-up to save calculations
+	public var maxCatchupWindow = 0.5; // How many seconds max to simulate when catching up
+
+	// param FX
+	public var enable : Value;
+	public var speed : Value;
+	public var speedIncr : Value;
+	public var gravity : Value;
+	
+	public function new( ?parent ) {
+		super(parent);
+		randomSeed = Std.random(0xFFFFFF);
+		random = new hxd.Rand(randomSeed);
+		evaluator = new Evaluator(random);
+	}
+	
+	function tick( dt : Float, full=true) {
+		var group = this.groups[0];
+
+		var enableValue = evaluator.getFloat(enable, curTime) >= 0.5;
+		if (group.enable != enableValue) // prevent batch.clear() when false
+			group.enable = enableValue;
+
+		group.speed = evaluator.getFloat(speed, curTime);
+		group.speedIncr = evaluator.getFloat(speedIncr, curTime);
+		group.gravity = evaluator.getFloat(gravity, curTime);
+
+		lastTime = curTime;
+		curTime += dt;
+	}
+
+	public function reset() {
+		random.init(randomSeed);
+		curTime = 0.0;
+		lastTime = 0.0;
+	}
+	
+	public function setTime(time: Float) {
+		if(time < lastTime || lastTime < 0) {
+			reset();
+		}
+
+		var catchupTime = time - curTime;
+		#if !editor
+		if(catchupTime > maxCatchupWindow) {
+			curTime = time - maxCatchupWindow;
+			catchupTime = maxCatchupWindow;
+		}
+		#end
+		var catchupTickRate = hxd.Timer.wantedFPS / catchupSpeed;
+		var numTicks = hxd.Math.ceil(catchupTickRate * catchupTime);
+		for(i in 0...numTicks) {
+			tick(catchupTime / numTicks, i == (numTicks - 1));
+		}
+	}
+
+	override function getBounds( ?relativeTo : h2d.Object, ?out : h2d.col.Bounds ) : h2d.col.Bounds {
+		if( out == null ) out = new h2d.col.Bounds() else out.empty();
+		out = super.getBounds(relativeTo, out);
+		out.xMin -= 25*scaleX;
+		out.xMax += 25*scaleX;
+		out.yMin -= 25*scaleY;
+		out.yMax += 25*scaleY;
+		return out;
+	}
+
+}
+
+class Particle2D extends Object2D {
+
+	var paramsParticleGroup : Dynamic;
+
+	override public function load(v:Dynamic) {
+		super.load(v);
+		paramsParticleGroup = v.paramsParticleGroup;
+	}
+
+	override function save() {
+		var o : Dynamic = super.save();
+		o.paramsParticleGroup = paramsParticleGroup;
+		return o;
+	}
+
+	override function updateInstance( ctx: Context, ?propName : String ) {
+		super.updateInstance(ctx, propName);
+
+		var particles2d = (cast ctx.local2d : Particles);
+		
+		particles2d.visible = visible;
+
+		function makeVal(name, def ) : Value {
+			var c = Curve.getCurve(this, name);
+			return c != null ? VCurve(c) : def;
+		}
+
+		function makeVector(name: String, defVal: Float) {
+			var curves = Curve.getCurves(this, name);
+			if(curves == null || curves.length == 0)
+				return null;
+
+			return Curve.getVectorValue(curves, defVal);
+		}
+		
+		particles2d.enable = makeVal("enable", VConst((paramsParticleGroup.enable) ? 1 : 0));
+		particles2d.speed = makeVal("speed", VConst(paramsParticleGroup.speed));
+		particles2d.speedIncr = makeVal("speedIncr", VConst(paramsParticleGroup.speedIncr));
+		particles2d.gravity = makeVal("gravity", VConst(paramsParticleGroup.gravity));
+	}
+
+	override function makeInstance(ctx:Context):Context {
+		ctx = ctx.clone(this);
+		var particle2d = new Particles(ctx.local2d);
+		ctx.local2d = particle2d;
+		ctx.local2d.name = name;
+	
+		var group = new ParticleGroup(particle2d);
+		particle2d.addGroup(group);
+		if (paramsParticleGroup != null)
+			group.load(1, paramsParticleGroup);
+		group.rebuildOnChange = false;
+
+		updateInstance(ctx);
+		return ctx;
+	}
+
+	#if editor
+
+	public static var emitter2dParams : Array<hrt.prefab.fx.Emitter.ParamDef> = [
+		// EMIT PARAMS
+		{ name: "enable", t: PBool, disp: "Enable", def : 1.0, animate: true, groupName : "Emit Params" },
+		{ name: "speed", t: PFloat(), disp: "Initial Speed", def : 1.0, animate: true, groupName : "Emit Params" },
+		{ name: "speedIncr", t: PFloat(), disp: "Acceleration", def : 1.0, animate: true, groupName : "Emit Params" },
+		{ name: "gravity", t: PFloat(), disp: "Gravity", def : 1.0, animate: true, groupName : "Emit Params" }
+	];
+
+	override function makeInteractive(ctx:Context):h2d.Interactive {
+		var local2d = ctx.local2d;
+		if(local2d == null)
+			return null;
+		var particles2d = cast(local2d, h2d.Particles);
+		var int = new h2d.Interactive(50, 50);
+		particles2d.addChildAt(int, 0);
+		int.propagateEvents = true;
+		int.x = int.y = -25;
+		return int;
+	}
+
+	override function edit( ctx : EditContext ) {
+		super.edit(ctx);
+
+		var params = new hide.Element(hide.view.Particles2D.getParamsHTMLform());
+		
+		var particles2d = (cast ctx.getContext(this).local2d : Particles);
+		var group = @:privateAccess particles2d.groups[0];
+		ctx.properties.add(params, group, function (pname) {
+			// if fx2d is running, tick() changes group params and modifies group.save()
+			// if a param has a curve and we changed this param on the right panel, 
+			// the saved value will be the value of the curve at this point.
+			Reflect.setField(paramsParticleGroup, pname, Reflect.field(group.save(), pname));
+		});
+	}
+
+	override function getHideProps() : HideProps {
+		return { icon : "square", name : "Particle2D" };
+	}
+
+	#end
+
+	static var _ = Library.register("particle2D", Particle2D);
+
+}

+ 47 - 10
hrt/prefab/fx2d/Text.hx

@@ -58,9 +58,25 @@ class Text extends Object2D {
 		if (font != null)
 			h2dText.font = font;
 		#if editor
-			if (propName == "text") {
+			if (propName == null || propName == "text") {
 				h2dText.text = text;
 			}
+			var int = Std.downcast(h2dText.getChildAt(0),h2d.Interactive);
+			if( int != null ) {
+				@:privateAccess {
+					h2dText.rebuild();
+					int.width = h2dText.calcWidth;
+					int.height = h2dText.calcHeight;
+					switch (h2dText.textAlign) {
+						case Center:
+							int.x = -int.width/2;
+						case Right:
+							int.x = -int.width;
+						default:
+							int.x = 0;
+					}
+				}
+			}
 		#end
 	}
 
@@ -93,6 +109,19 @@ class Text extends Object2D {
 	}
 
 	#if editor
+
+	override function makeInteractive(ctx:Context):h2d.Interactive {
+		var local2d = ctx.local2d;
+		if(local2d == null)
+			return null;
+		var text = cast(local2d, h2d.Text);
+		@:privateAccess { text.rebuild(); text.updateSize(); }
+		@:privateAccess var int = new h2d.Interactive(text.calcWidth, text.calcHeight);
+		text.addChildAt(int, 0);
+		int.propagateEvents = true;
+		return int;
+	}
+
 	override function edit( ctx : EditContext ) {
 		super.edit(ctx);
 		
@@ -105,27 +134,35 @@ class Text extends Object2D {
 		var leftAlign = new hide.Element('<input type="button" style="width: 50px" value="Left" /> ').appendTo(element);
 		var middleAlign = new hide.Element('<input type="button" style="width: 50px" value="Center" /> ').appendTo(element);
 		var rightAlign = new hide.Element('<input type="button" style="width: 50px" value="Right" /> ').appendTo(element);
-		leftAlign.on("click", function(e) {
-			align = 0;
-			leftAlign.attr("disabled", "true");
+		inline function updateDisabled() {
+			leftAlign.removeAttr("disabled");
 			middleAlign.removeAttr("disabled");
 			rightAlign.removeAttr("disabled");
+			switch (align) {
+				case 1:
+					middleAlign.attr("disabled", "true");
+				case 2:
+					rightAlign.attr("disabled", "true");
+				default:
+					leftAlign.attr("disabled", "true");
+			}
+		}
+		leftAlign.on("click", function(e) {
+			align = 0;
+			updateDisabled();
 			updateInstance(ctx.getContext(this), "align");
 		});
 		middleAlign.on("click", function(e) {
 			align = 1;
-			leftAlign.removeAttr("disabled");
-			middleAlign.attr("disabled", "true");
-			rightAlign.removeAttr("disabled");
+			updateDisabled();
 			updateInstance(ctx.getContext(this), "align");
 		});
 		rightAlign.on("click", function(e) {
 			align = 2;
-			leftAlign.removeAttr("disabled");
-			middleAlign.removeAttr("disabled");
-			rightAlign.attr("disabled", "true");
+			updateDisabled();
 			updateInstance(ctx.getContext(this), "align");
 		});
+		updateDisabled();
 
 		new hide.Element('<dt>Font</dt>').appendTo(gr);
 		var element = new hide.Element('<dd></dd>').appendTo(gr);