浏览代码

Add `shape` support to `h2d.Interactive` (#463)

Pavel Alexandrov 6 年之前
父节点
当前提交
586d5b53a4
共有 11 个文件被更改,包括 308 次插入4 次删除
  1. 16 1
      h2d/Interactive.hx
  2. 14 0
      h2d/Scene.hx
  3. 5 1
      h2d/col/Circle.hx
  4. 7 0
      h2d/col/Collider.hx
  5. 73 0
      h2d/col/PixelsCollider.hx
  6. 4 0
      h2d/col/Polygon.hx
  7. 21 0
      h2d/col/PolygonCollider.hx
  8. 4 0
      h2d/col/Polygons.hx
  9. 5 1
      h2d/col/RoundRect.hx
  10. 8 1
      h2d/col/Triangle.hx
  11. 151 0
      samples/Interactive2D.hx

+ 16 - 1
h2d/Interactive.hx

@@ -20,10 +20,25 @@ class Interactive extends Drawable implements hxd.SceneEvents.Interactive {
 	var mouseDownButton : Int = -1;
 	var parentMask : Mask;
 
-	public function new(width, height, ?parent) {
+	/**
+		Detailed shape collider for Interactive.
+		Keep in mind that shape parts that are out of [0, 0, width, height] bounds will never interact with the mouse.
+	**/
+	public var shape : h2d.col.Collider;
+	/**
+		Detailed shape X offset from Interactive.
+	**/
+	public var shapeX : Float = 0;
+	/**
+		Detailed shape Y offset from Interactive.
+	**/
+	public var shapeY : Float = 0;
+
+	public function new(width, height, ?parent, ?shape) {
 		super(parent);
 		this.width = width;
 		this.height = height;
+		this.shape = shape;
 		cursor = Button;
 	}
 

+ 14 - 0
h2d/Scene.hx

@@ -49,6 +49,7 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	var window : hxd.Window;
 	@:allow(h2d.Interactive)
 	var events : hxd.SceneEvents;
+	var shapePoint : h2d.col.Point;
 
 	/**
 		Create a new scene. A default 2D scene is already available in `hxd.App.s2d`
@@ -61,6 +62,7 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 		height = e.height;
 		interactive = new Array();
 		eventListeners = new Array();
+		shapePoint = new h2d.col.Point();
 		window = hxd.Window.getInstance();
 		posChanged = true;
 	}
@@ -153,6 +155,7 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	public function getInteractive( x : Float, y : Float ) : Interactive {
 		var rx = x * matA + y * matB + absX;
 		var ry = x * matC + y * matD + absY;
+		var pt = shapePoint;
 		for( i in interactive ) {
 
 			var dx = rx - i.absX;
@@ -192,6 +195,11 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 			}
 			if( !visible ) continue;
 
+			if (i.shape != null) {
+				pt.set((kx / max) * i.width + i.shapeX, (ky / max) * i.height + i.shapeY);
+				if ( !i.shape.contains(pt) ) continue;
+			}
+
 			return i;
 		}
 		return null;
@@ -239,6 +247,7 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 		var rx = event.relX;
 		var ry = event.relY;
 		var index = last == null ? 0 : interactive.indexOf(cast last) + 1;
+		var pt = shapePoint;
 		for( idx in index...interactive.length ) {
 			var i = interactive[idx];
 			if( i == null ) break;
@@ -280,6 +289,11 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 			}
 			if( !visible ) continue;
 
+			if (i.shape != null) {
+				pt.set((kx / max) * i.width + i.shapeX, (ky / max) * i.height + i.shapeY);
+				if ( !i.shape.contains(pt) ) continue;
+			}
+
 			event.relX = (kx / max) * i.width;
 			event.relY = (ky / max) * i.height;
 			i.handleEvent(event);

+ 5 - 1
h2d/col/Circle.hx

@@ -1,7 +1,7 @@
 package h2d.col;
 import hxd.Math;
 
-class Circle {
+class Circle implements Collider {
 
 	public var x : Float;
 	public var y : Float;
@@ -68,4 +68,8 @@ class Circle {
 		return '{${Math.fmt(x)},${Math.fmt(y)},${Math.fmt(ray)}}';
 	}
 
+	public function contains( p : Point ) : Bool {
+		return distanceSq(p) == 0;
+	}
+
 }

+ 7 - 0
h2d/col/Collider.hx

@@ -0,0 +1,7 @@
+package h2d.col;
+
+interface Collider /* extends hxd.impl.Serializable.StructSerializable */ {
+
+	public function contains( p : Point ) : Bool;
+
+}

+ 73 - 0
h2d/col/PixelsCollider.hx

@@ -0,0 +1,73 @@
+package h2d.col;
+
+/**
+	A Pixels collider. Checks for pixel color value under point to be above the cutoff value.
+	Note that it checks as `channel > cutoff`, not `channel >= cutoff`, hence value of 255 will always be considered below cutoff.
+**/
+class PixelsCollider implements Collider {
+
+	public var pixels : hxd.Pixels;
+
+	/**
+		The red channel cutoff value in range of -1...255 (default : 255)
+	**/
+	public var redCutoff : Int;
+	/**
+		The green channel cutoff value in range of -1...255 (default : 255)
+	**/
+	public var greenCutoff : Int;
+	/**
+		The blue channel cutoff value in range of -1...255 (default : 255)
+	**/
+	public var blueCutoff : Int;
+
+	/**
+		The alpha channel cutoff value in range of -1...255 (default : 127)
+	**/
+	public var alphaCutoff : Int;
+
+	/**
+		If true, will collide if any channel is above cutoff. Otherwise will collide only if all channels above their cutoff values. (default : true)
+	**/
+	public var collideOnAny : Bool;
+
+	/**
+		Horizontal stretch of pixels to check for collision. (default : 1)
+	**/
+	public var scaleX : Float = 1;
+	/**
+		Vertical stretch of pixels to check for collision. (default : 1)
+	**/
+	public var scaleY : Float = 1;
+
+	/**
+		Create new BitmapCollider with specified bitmap, channel cutoff values and check mode.
+	**/
+	public function new(pixels: hxd.Pixels, alphaCutoff:Int = 127, redCutoff:Int = 255, greenCutoff = 255, blueCutoff = 255, collideOnAny = true) {
+		this.pixels = pixels;
+		this.alphaCutoff = alphaCutoff;
+		this.redCutoff = redCutoff;
+		this.greenCutoff = greenCutoff;
+		this.blueCutoff = blueCutoff;
+		this.collideOnAny = collideOnAny;
+	}
+
+	public function contains( p : Point ) {
+		var ix : Int = Math.floor(p.x / scaleX);
+		var iy : Int = Math.floor(p.y / scaleY);
+		if ( pixels == null || ix < 0 || iy < 0 || ix >= pixels.width || iy >= pixels.height ) return false;
+		var pixel = pixels.getPixel(ix, iy);
+		if ( collideOnAny ) {
+			return (pixel >>> 24       ) > alphaCutoff ||
+			       (pixel >>> 16 & 0xff) > blueCutoff ||
+			       (pixel >>> 8  & 0xff) > greenCutoff ||
+			       (pixel        & 0xff) > redCutoff;
+		} else {
+			return (pixel >>> 24       ) > alphaCutoff &&
+			       (pixel >>> 16 & 0xff) > blueCutoff &&
+			       (pixel >>> 8  & 0xff) > greenCutoff &&
+			       (pixel        & 0xff) > redCutoff;
+		}
+	}
+
+}

+ 4 - 0
h2d/col/Polygon.hx

@@ -48,6 +48,10 @@ abstract Polygon(Array<Point>) from Array<Point> to Array<Point> {
 		return b;
 	}
 
+	public function getCollider(isConvex : Bool = false) {
+		return new PolygonCollider([this], isConvex);
+	}
+
 	inline function xSort(a : Point, b : Point) {
 		if(a.x == b.x)
 			return a.y < b.y ? -1 : 1;

+ 21 - 0
h2d/col/PolygonCollider.hx

@@ -0,0 +1,21 @@
+package h2d.col;
+
+class PolygonCollider implements Collider {
+
+	public var polygons : Polygons;
+	public var isConvex : Bool;
+
+	/**
+		Create new PolygonCollider with specified Polygons and flag to check as convex or concave.
+	**/
+	public function new( polygons:Polygons, isConvex : Bool = false ) {
+		this.polygons = polygons;
+		this.isConvex = isConvex;
+	}
+
+	public function contains( p : Point ) {
+		if (polygons == null) return false;
+		return polygons.contains(p, isConvex);
+	}
+
+}

+ 4 - 0
h2d/col/Polygons.hx

@@ -28,6 +28,10 @@ abstract Polygons(Array<Polygon>) from Array<Polygon> to Array<Polygon> {
 		return b;
 	}
 
+	public function getCollider(isConvex : Bool = false) {
+		return new PolygonCollider(this, isConvex);
+	}
+
 	public function contains( p : Point, isConvex = false ) {
 		for( pl in polygons )
 			if( pl.contains(p, isConvex) )

+ 5 - 1
h2d/col/RoundRect.hx

@@ -1,6 +1,6 @@
 package h2d.col;
 
-class RoundRect {
+class RoundRect implements Collider {
 
 	public var x : Float;
 	public var y : Float;
@@ -73,4 +73,8 @@ class RoundRect {
 		return new Point(px, py);
 	}
 
+	public function contains( p : Point ) {
+		return inside(p);
+	}
+
 }

+ 8 - 1
h2d/col/Triangle.hx

@@ -1,6 +1,6 @@
 package h2d.col;
 
-class Triangle {
+class Triangle implements Collider {
 
 	static inline var UNDEF = 1.1315e-17;
 
@@ -44,4 +44,11 @@ class Triangle {
 		return new h3d.col.Point(1 - s - t, s, t);
 	}
 
+	public function contains( p : Point ) {
+		var area = getInvArea() * 0.5;
+		var s = area * (a.y * c.x - a.x * c.y + (c.y - a.y) * p.x + (a.x - c.x) * p.y);
+		var t = area * (a.x * b.y - a.y * b.x + (a.y - b.y) * p.x + (b.x - a.x) * p.y);
+		return s >= 0 && t >= 0 && s + t < 1;
+	}
+
 }

+ 151 - 0
samples/Interactive2D.hx

@@ -0,0 +1,151 @@
+import h2d.col.RoundRect;
+import h2d.col.Circle;
+import h2d.col.Triangle;
+import h2d.col.Point;
+import h2d.col.Polygon;
+import h2d.col.PolygonCollider;
+
+class Interactive2D extends SampleApp {
+
+	var hover : Bool;
+	var shouldRotate : Bool;
+	var interactive : h2d.Interactive;
+	var graphics : h2d.Graphics;
+	var polygonShape : PolygonCollider;
+	var triangleShape : Triangle;
+	var circleShape : Circle;
+	var rectShape : RoundRect;
+
+	static inline var rectY : Int = 32;
+	static inline var rectWidth : Int = 64; // *2
+	static inline var rectHeight : Int = 32;
+
+	public function new() {
+		super();
+	}
+
+
+	override private function init()
+	{
+		super.init();
+
+		var poly : Polygon = new Polygon([
+			new Point(64, 16), 
+			new Point(96, 0), 
+			new Point(127, 0), 
+			new Point(127, 32), 
+			new Point(111, 63), 
+			new Point(127, 95), 
+			new Point(127, 127), 
+			new Point(96, 127), 
+			new Point(64, 111), 
+			new Point(32, 127), 
+			new Point(1, 127), 
+			new Point(1, 95), 
+			new Point(17, 63), 
+			new Point(1, 32), 
+			new Point(1, 0), 
+			new Point(32, 0), 
+			new Point(64, 16), 
+		]);
+		// Polygon collider can be used both for single polygon or for multiple polygons at once.
+		polygonShape = poly.getCollider();
+		triangleShape = new Triangle(new Point(64, 0), new Point(128, 128), new Point(0, 128));
+		circleShape = new Circle(64, 64, 64);
+		rectShape = new RoundRect(rectWidth, rectY+rectHeight, rectWidth*2, rectHeight*2, 0);
+
+		interactive = new h2d.Interactive(128, 128, s2d);
+		graphics = new h2d.Graphics(interactive);
+
+		interactive.onOver = function( e : hxd.Event ) {
+			hover = true;
+			redrawGraphics();
+		}
+		interactive.onOut = function( e : hxd.Event ) {
+			hover = false;
+			redrawGraphics();
+		}
+		
+		addCheck("isEllipse", function() return interactive.isEllipse, function(v) interactive.isEllipse = v);
+		addCheck("Rotate", function() return shouldRotate, function(v) {
+			shouldRotate = v;
+			if (!v) setRotation(0);
+		});
+		addChoice("Shape", ["Polygon", "Triangle", "Circle", "RoundRect", "None"], setShape, 0);
+		setShape(0);
+	}
+
+	function setRotation( v : Float ) {
+		interactive.rotation = v;
+		var cos = Math.cos(v);
+		var sin = Math.sin(v);
+		var hw = -interactive.width * .5;
+		var hh = -interactive.height * .5;
+		interactive.x = s2d.width  * .5 + (cos * hw - sin * hh);
+		interactive.y = s2d.height * .5 + (sin * hw + cos * hh);
+	}
+
+	function redrawGraphics() {
+		graphics.clear();
+		graphics.beginFill(hover ? 0xff0000 : 0xf8931f);
+		if (interactive.shape == polygonShape) {
+			for (polygon in polygonShape.polygons) {
+				var it = polygon.iterator();
+				var pt = it.next();
+				graphics.moveTo(pt.x, pt.y);
+
+				while (it.hasNext()) {
+					pt = it.next();
+					graphics.lineTo(pt.x, pt.y);
+				}
+			}
+		} else if (interactive.shape == triangleShape) {
+			graphics.moveTo(triangleShape.a.x, triangleShape.a.y);
+			graphics.lineTo(triangleShape.b.x, triangleShape.b.y);
+			graphics.lineTo(triangleShape.c.x, triangleShape.c.y);
+		} else if (interactive.shape == circleShape) {
+			graphics.drawCircle(circleShape.x, circleShape.y, circleShape.ray);
+		} else if (interactive.shape == rectShape) {
+				var size = rectWidth - rectHeight;
+				var k = 10;
+				for( i in 0...k+1 ) {
+					var a = Math.PI * i / k - Math.PI / 2;
+					graphics.lineTo(size + rectWidth + rectHeight * Math.cos(a), rectY + rectHeight + rectHeight * Math.sin(a));
+				}
+				for( i in 0...k+1 ) {
+					var a = Math.PI * i / k + Math.PI / 2;
+					graphics.lineTo(-size + rectWidth + rectHeight * Math.cos(a), rectY + rectHeight + rectHeight * Math.sin(a));
+				}
+		} else {
+			graphics.drawRect(0, 0, interactive.width, interactive.height);
+		}
+
+		graphics.endFill();
+	}
+
+	function setShape( index : Int ) {
+
+		switch (index) {
+			case 0: interactive.shape = polygonShape;
+			case 1: interactive.shape = triangleShape;
+			case 2: interactive.shape = circleShape;
+			case 3: interactive.shape = rectShape;
+			case 4: interactive.shape = null;
+		}
+		
+		redrawGraphics();
+		setRotation(interactive.rotation);
+	}
+
+	override private function update(dt:Float)
+	{
+		super.update(dt);
+		if (shouldRotate) setRotation(interactive.rotation + dt * .1);
+	}
+
+	static function main() {
+		hxd.Res.initEmbed();
+		new Interactive2D();
+	}
+
+}