Ver Fonte

Implement 2D Scene ScaleMode (#595)

Pavel Alexandrov há 6 anos atrás
pai
commit
74b792c363
3 ficheiros alterados com 355 adições e 41 exclusões
  1. 29 10
      h2d/RenderContext.hx
  2. 249 31
      h2d/Scene.hx
  3. 77 0
      samples/ScaleMode2D.hx

+ 29 - 10
h2d/RenderContext.hx

@@ -93,7 +93,7 @@ class RenderContext extends h3d.impl.RenderContext {
 		// todo : we might prefer to auto-detect this by running a test and capturing its output
 		baseShader.pixelAlign = #if flash true #else false #end;
 		baseShader.halfPixelInverse.set(0.5 / engine.width, 0.5 / engine.height);
-		baseShader.viewport.set( -scene.width * 0.5, -scene.height * 0.5, 2 / scene.width, -2 * baseFlipY / scene.height);
+		baseShader.viewport.set( -scene.width * 0.5 - scene.offsetX, -scene.height * 0.5 - scene.offsetY, 2 / scene.width * scene.ratioX, -2 * baseFlipY / scene.height * scene.ratioY);
 		baseShader.filterMatrixA.set(1, 0, 0);
 		baseShader.filterMatrixB.set(0, 1, 0);
 		baseShaderList.next = null;
@@ -190,14 +190,33 @@ class RenderContext extends h3d.impl.RenderContext {
 
 		if( restore ) {
 			var tinf = targetsStack[targetsStackIndex - 1];
-			var t = tinf == null ? null : tinf.t;
-			var startX = tinf == null ? 0 : tinf.x;
-			var startY = tinf == null ? 0 : tinf.y;
-			var width = tinf == null ? scene.width : tinf.w;
-			var height = tinf == null ? scene.height : tinf.h;
+			var t : h3d.mat.Texture;
+			var startX : Int, startY : Int, width : Int, height : Int;
+			var ratioX : Float, ratioY : Float, offsetX : Float, offsetY : Float;
+			if ( tinf == null ) {
+				t = null;
+				startX = 0;
+				startY = 0;
+				width = scene.width;
+				height = scene.height;
+				ratioX = scene.ratioX;
+				ratioY = scene.ratioY;
+				offsetX = scene.offsetX;
+				offsetY = scene.offsetY;
+			} else {
+				t = tinf.t;
+				startX = tinf.x;
+				startY = tinf.y;
+				width = tinf.w;
+				height = tinf.h;
+				ratioX = 1;
+				ratioY = 1;
+				offsetX = 0;
+				offsetY = 0;
+			}
 			initShaders(baseShaderList);
 			baseShader.halfPixelInverse.set(0.5 / (t == null ? engine.width : t.width), 0.5 / (t == null ? engine.height : t.height));
-			baseShader.viewport.set( -width * 0.5 - startX, -height * 0.5 - startY, 2 / width, -2 * (t == null ? baseFlipY : targetFlipY) / height);
+			baseShader.viewport.set( -width * 0.5 - startX - offsetX, -height * 0.5 - startY - offsetY, 2 / width * ratioX, -2 * (t == null ? baseFlipY : targetFlipY) / height * ratioY);
 			curX = startX;
 			curY = startY;
 			curWidth = width;
@@ -213,8 +232,8 @@ class RenderContext extends h3d.impl.RenderContext {
 		renderY = y;
 		renderW = w;
 		renderH = h;
-		var scaleX = engine.width / scene.width;
-		var scaleY = engine.height / scene.height;
+		var scaleX = engine.width * scene.ratioX / scene.width;
+		var scaleY = engine.height * scene.ratioY / scene.height;
 		if( inFilter != null ) {
 			var fa = baseShader.filterMatrixA;
 			var fb = baseShader.filterMatrixB;
@@ -228,7 +247,7 @@ class RenderContext extends h3d.impl.RenderContext {
 			w = rx2 - rx1;
 			h = ry2 - ry1;
 		}
-		engine.setRenderZone(Std.int((x - curX) * scaleX + 1e-10), Std.int((y - curY) * scaleY + 1e-10), Std.int(w * scaleX + 1e-10), Std.int(h * scaleY + 1e-10));
+		engine.setRenderZone(Std.int((x - curX + scene.viewportX) * scaleX + 1e-10), Std.int((y - curY + scene.viewportY) * scaleY + 1e-10), Std.int(w * scaleX + 1e-10), Std.int(h * scaleY + 1e-10));
 	}
 
 	public inline function clearRenderZone() {

+ 249 - 31
h2d/Scene.hx

@@ -1,21 +1,117 @@
 package h2d;
 import hxd.Math;
 
+/**
+	Viewport alignment when scaling mode supports it.
+**/
+enum ScaleModeAlign {
+	/** Anchor Scene viewport horizontally to left side of the window. When passed to verticalAlign it will be treated as Center. **/
+	Left;
+	/** Anchor Scene viewport horizontally to right side of the window. When passed to verticalAlign it will be treated as Center. **/
+	Right;
+	/** Anchor to the center of window. **/
+	Center;
+	/** Anchor Scene viewport vertically to the top of a window. When passed to horizontalAlign it will be treated as Center. **/
+	Top;
+	/** Anchor Scene viewport vertically to the bottom of a window. When passed to horizontalAlign it will be treated as Center. **/
+	Bottom;
+}
+
+/**
+	Scaling mode of the 2D Scene.  
+	See `ScaleMode2D` sample for showcase.
+**/
+enum ScaleMode {
+
+	/**
+		Matches scene size to window size. `width` and `height` of Scene will match window size. Default scaling mode.
+	**/
+	Resize;
+
+	/**
+		Sets constant Scene size and stretches it to cover entire window. This behavior is same as old `setFixedSize` method.
+	**/
+	Stretch(width : Int, height : Int);
+
+	/**
+		Sets constant scene size and upscales it with preserving aspect-ratio to fit the window.  
+		If `integerScale` is `true` - scaling will be performed  with only integer increments (1x, 2x, 3x, ...). Default: `false`  
+		`horizontalAlign` controls viewport anchoring horizontally. Accepted values are `Left`, `Center` and `Right`. Default: `Center`  
+		`verticalAlign` controls viewport anchoring vertically. Accepted values are `Top`, `Center` and `Bottom`. Default: `Center`  
+		With `800x600` window, `LetterBox(320, 260)` will result in center-aligned Scene of size `320x260` upscaled to fit into screen.
+	**/
+	LetterBox(width : Int, height : Int, ?integerScale : Bool, ?horizontalAlign : ScaleModeAlign, ?verticalAlign : ScaleModeAlign);
+
+	/**
+		Sets constant Scene size, scale and alignment. Does not perform any adaptation to the screen apart from alignment.
+		`horizontalAlign` controls viewport anchoring horizontally. Accepted values are `Left`, `Center` and `Right`. Default: `Center`  
+		`verticalAlign` controls viewport anchoring vertically. Accepted values are `Top`, `Center` and `Bottom`. Default: `Center`  
+		With `800x600` window, `Fixed(200, 150, 2, Left, Center)` will result in Scene size of `200x150`, and visually upscaled to `400x300`, and aligned to middle-left of the window.
+	**/
+	Fixed(width : Int, height: Int, zoom : Float, ?horizontalAlign : ScaleModeAlign, ?verticalAlign : ScaleModeAlign);
+
+	/**
+		Upscales/downscales Scene according to `level` and matches Scene size to `ceil(window size / level)`.  
+		With `800x600` window, `Zoom(2)` will result in `400x300` Scene size upscaled to fill entire window.
+	**/
+	Zoom(level : Float);
+
+	/**
+		Ensures that Scene size will be of minimum specified size.  
+		Automatically calculates zoom level based on provided size according to `min(window width / min width, window height / min height)`, then applies same scaling as `Zoom(level)`.
+		Behavior is similiar to LetterBox, however instead of letterboxing effect, Scene size will change to cover the letterboxed parts.  
+		`minWidth` or `minHeight` can be set to `0` in order to force scaling adjustment account only for either horizontal of vertical window size.  
+		If `integerScale` is `true` - scaling will be performed  with only integer increments (1x, 2x, 3x, ...). Default: `false`  
+		With `800x600` window, `AutoZoom(320, 260, false)` will result in Scene size of `347x260`. `AutoZoom(320, 260, true)` will result in size of `400x300`.
+	**/
+	AutoZoom(minWidth : Int, minHeight : Int, ?integerScaling : Bool);
+}
+
 /**
 	h2d.Scene is the root class for a 2D scene. All root objects are added to it before being drawn on screen.
 **/
 class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.InteractiveScene {
 
 	/**
-		The current width (in pixels) of the scene. Can change if the screen gets resized.
+		The current width (in pixels) of the scene. Can change if the screen gets resized or `scaleMode` changes.
 	**/
 	public var width(default,null) : Int;
 
 	/**
-		The current height (in pixels) of the scene. Can change if the screen gets resized.
+		The current height (in pixels) of the scene. Can change if the screen gets resized or `scaleMode` changes.
 	**/
 	public var height(default, null) : Int;
 
+	/**
+		Horizontal viewport offset relative to top-left corner of the window. Can change if the screen gets resized or `scaleMode` changes.  
+		Offset is in internal Scene resolution pixels.
+	**/
+	public var viewportX(default, null) : Float;
+	/**
+		Vertical viewport offset relative to top-left corner of the window. Can change if the screen gets resized or `scaleMode` changes.  
+		Offset is in internal Scene resolution pixels.
+	**/
+	public var viewportY(default, null) : Float;
+	/**
+		Physical vertical viewport offset relative to the center of the window. Assigned if the screen gets resized or `scaleMode` changes.  
+		Offset is in internal Scene resolution pixels.
+	**/
+	public var offsetX : Float;
+	/**
+		Physical horizontal viewport offset relative to the center of the window. Assigned if the screen gets resized or `scaleMode` changes.  
+		Offset is in internal Scene resolution pixels.
+	**/
+	public var offsetY : Float;
+
+	/**
+		Horizontal ratio of the window used by the Scene (including scaling). Can change if the screen gets resized or `scaleMode` changes.
+	**/
+	public var ratioX(default, null) : Float;
+	/**
+		Vertical ratio of the window used by the Scene (including scaling). Can change if the screen gets resized or `scaleMode` changes.
+	**/
+	public var ratioY(default, null) : Float;
+
 	/**
 		The current mouse X coordinates (in pixel) relative to the scene.
 	**/
@@ -30,7 +126,16 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 		The zoom factor of the scene, allows to set a fixed x2, x4 etc. zoom for pixel art
 		When setting a zoom > 0, the scene resize will be automaticaly managed.
 	**/
-	public var zoom(default, set) : Int = 0;
+	@:deprecated("zoom is deprecated, use scaleMode = Zoom(v) instead")
+	public var zoom(get, set) : Int;
+
+	/**
+		Scene scaling mode. ( default : Fill )
+		Important thing to keep in mind - Scene does not clip rendering to it's scaled size and
+		graphics can render outside of it. However `drawTile` does check for those bounds and 
+		will clip out tiles that are outside of the scene bounds.
+	**/
+	public var scaleMode(default, set) : ScaleMode = Resize;
 
 	/**
 		Set the default value for `h2d.Drawable.smooth` (default: false)
@@ -42,7 +147,6 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	**/
 	public var renderer(get, set) : RenderContext;
 
-	var fixedSize : Bool;
 	var interactive : Array<Interactive>;
 	var eventListeners : Array< hxd.Event -> Void >;
 	var ctx : RenderContext;
@@ -60,6 +164,12 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 		ctx = new RenderContext(this);
 		width = e.width;
 		height = e.height;
+		offsetX = 0;
+		offsetY = 0;
+		ratioX = 1;
+		ratioY = 1;
+		viewportX = 0;
+		viewportY = 0;
 		interactive = new Array();
 		eventListeners = new Array();
 		shapePoint = new h2d.col.Point();
@@ -75,17 +185,22 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 		this.events = events;
 	}
 
+	function get_zoom() : Int {
+		return switch ( scaleMode ) {
+			case Zoom(level): Std.int(level);
+			default: 0;
+		}
+	}
+
 	function set_zoom(v:Int) {
-		var e = h3d.Engine.getCurrent();
-		var twidth = Math.ceil(window.width / v);
-		var theight = Math.ceil(window.height / v);
-		var totalWidth = twidth * v;
-		var totalHeight = theight * v;
-		// increase back buffer size if necessary
-		if( totalWidth != e.width || totalHeight != e.height )
-			e.resize(totalWidth, totalHeight);
-		setFixedSize(twidth, theight);
-		return zoom = v;
+		scaleMode = Zoom(v);
+		return v;
+	}
+
+	function set_scaleMode( v : ScaleMode ) {
+		scaleMode = v;
+		checkResize();
+		return v;
 	}
 
 	function get_renderer() return ctx;
@@ -94,32 +209,109 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	/**
 		Set the fixed size for the scene, will prevent automatic scene resizing when screen size changes.
 	**/
+	@:deprecated("setFixedSize is deprecated, use scaleMode = Strech(w, h) instead")
 	public function setFixedSize( w : Int, h : Int ) {
-		width = w;
-		height = h;
-		fixedSize = true;
-		posChanged = true;
+		scaleMode = Stretch(w, h);
 	}
 
 	@:dox(hide) @:noCompletion
 	public function checkResize() {
-		if( fixedSize && zoom == 0 ) return;
 		var engine = h3d.Engine.getCurrent();
-		var scale = zoom == 0 ? 1 : zoom;
-		if( width * scale != engine.width || height * scale != engine.height ) {
-			width = engine.width;
-			height = engine.height;
-			posChanged = true;
-			if( zoom != 0 ) this.zoom = zoom;
+
+		inline function setSceneSize( w : Int, h : Int ) {
+			if ( w != this.width || h != this.height ) {
+				width = w;
+				height = h;
+				posChanged = true;
+			}
+		}
+
+		inline function calcRatio( scale : Float ) {
+			ratioX = (width * scale) / engine.width;
+			ratioY = (height * scale) / engine.height;
+		}
+
+		inline function calcViewport( horizontal : ScaleModeAlign, vertical : ScaleModeAlign, zoom : Float ) {
+			if ( horizontal == null ) horizontal = Center;
+			switch ( horizontal ) {
+				case Left:
+					offsetX = (engine.width - width * zoom) / (2 * zoom);
+					viewportX = 0;
+				case Right:
+					offsetX = -((engine.width - width * zoom) / (2 * zoom));
+					viewportX = (engine.width - width * zoom) / zoom;
+				default:
+					offsetX = 0;
+					viewportX = (engine.width - width * zoom) / (2 * zoom);
+			}
+
+			if ( vertical == null ) vertical = Center;
+			switch ( vertical ) {
+				case Top:
+					offsetY = (engine.height - height * zoom) / (2 * zoom);
+					viewportY = 0;
+				case Bottom:
+					offsetY = -((engine.height - height * zoom) / (2 * zoom));
+					viewportY = (engine.height - height * zoom) / zoom;
+				default:
+					offsetY = 0;
+					viewportY = (engine.height - height * zoom) / (2 * zoom);
+			}
+		}
+
+		inline function zeroViewport() {
+			offsetX = 0;
+			offsetY = 0;
+			viewportX = 0;
+			viewportY = 0;
+		}
+
+		switch ( scaleMode ) {
+			case Resize:
+				setSceneSize(engine.width, engine.height);
+				ratioX = 1;
+				ratioY = 1;
+				zeroViewport();
+			case Stretch(_width, _height):
+				setSceneSize(_width, _height);
+				ratioX = 1;
+				ratioY = 1;
+				zeroViewport();
+			case LetterBox(_width, _height, integerScale, horizontalAlign, verticalAlign):
+				setSceneSize(_width, _height);
+				var zoom = Math.min(engine.width / _width, engine.height / _height);
+				if ( integerScale ) {
+					zoom = Std.int(zoom);
+					if (zoom == 0) zoom = 1;
+				}
+				calcRatio(zoom);
+				calcViewport(horizontalAlign, verticalAlign, zoom);
+			case Fixed(_width, _height, zoom, horizontalAlign, verticalAlign):
+				setSceneSize(_width, _height);
+				calcRatio(zoom);
+				calcViewport(horizontalAlign, verticalAlign, zoom);
+			case Zoom(level):
+				setSceneSize(Math.ceil(engine.width / level), Math.ceil(engine.height / level));
+				calcRatio(level);
+				zeroViewport();
+			case AutoZoom(minWidth, minHeight, integerScaling):
+				var zoom = Math.min(engine.width / minWidth, engine.height / minHeight);
+				if ( integerScaling ) {
+					zoom = Std.int(zoom);
+					if ( zoom == 0 ) zoom = 1;
+				}
+				setSceneSize(Math.ceil(engine.width / zoom), Math.ceil(engine.height / zoom));
+				calcRatio(zoom);
+				zeroViewport();
 		}
 	}
 
 	inline function screenXToLocal(mx:Float) {
-		return mx * width / (window.width * scaleX) - x;
+		return mx * width / (window.width * ratioX * scaleX) - x - viewportX;
 	}
 
 	inline function screenYToLocal(my:Float) {
-		return my * height / (window.height * scaleY) - y;
+		return my * height / (window.height * ratioY * scaleY) - y - viewportY;
 	}
 
 	function get_mouseX() {
@@ -497,10 +689,22 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	override function sync( ctx : RenderContext ) {
 		if( !allocated )
 			onAdd();
-		checkResize();
 		super.sync(ctx);
 	}
 
+	override function onAdd()
+	{
+		checkResize();
+		super.onAdd();
+		window.addResizeEvent(checkResize);
+	}
+
+	override function onRemove()
+	{
+		super.onRemove();
+		window.removeResizeEvent(checkResize);
+	}
+
 	/**
 		Capture the scene into a texture and render the resulting Bitmap
 	**/
@@ -515,14 +719,28 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 
 		var tex = target.getTexture();
 		engine.pushTarget(tex);
-		var ow = width, oh = height, of = fixedSize;
-		setFixedSize(tex.width, tex.height);
+		var ow = width, oh = height, ox = offsetX, oy = offsetY;
+		var ovx = viewportX, ovy = viewportY, orx = ratioX, ory = ratioY;
+		width = tex.width;
+		height = tex.height;
+		ratioX = 1;
+		ratioY = 1;
+		offsetX = 0;
+		offsetY = 0;
+		viewportX = 0;
+		viewportY = 0;
+		posChanged = true;
 		render(engine);
 		engine.popTarget();
 
 		width = ow;
 		height = oh;
-		fixedSize = of;
+		ratioX = orx;
+		ratioY = ory;
+		offsetX = ox;
+		offsetY = oy;
+		viewportX = ovx;
+		viewportY = ovy;
 		posChanged = true;
 		engine.setRenderZone();
 		engine.end();

+ 77 - 0
samples/ScaleMode2D.hx

@@ -0,0 +1,77 @@
+import hxd.Key;
+import h2d.Scene;
+
+class ScaleMode2D extends SampleApp {
+
+	override function init()
+	{
+
+		var bg = new h2d.Bitmap(h2d.Tile.fromColor(0x333333), s2d);
+		var minBg = new h2d.Bitmap(h2d.Tile.fromColor(0x222222), s2d);
+
+		super.init();
+
+		var mode:Int = 0;
+		// Width and height used in Stretch, LetterBox, Fixed and AutoZoom
+		var width:Int = 320;
+		var height:Int = 240;
+		// Zoom used in Fixed and Zoom
+		var zoom:Float = 1;
+		// Integer Scale used in LetterBox and AutoZoom
+		var intScale:Bool = false;
+		// Vertical and Horizontal Align used in LetterBox and Fixed.
+		var halign:ScaleModeAlign = Center;
+		var valign:ScaleModeAlign = Center;
+
+		var sceneInfo:h2d.Text;
+
+		function setMode()
+		{
+			switch ( mode ) {
+				case 0:
+					s2d.scaleMode = Resize;
+				case 1:
+					s2d.scaleMode = Stretch(width, height);
+				case 2:
+					s2d.scaleMode = LetterBox(width, height, intScale, halign, valign);
+				case 3:
+					s2d.scaleMode = Fixed(width, height, zoom, valign, halign);
+				case 4:
+					s2d.scaleMode = Zoom(zoom);
+				case 5:
+					s2d.scaleMode = AutoZoom(width, height, intScale);
+			}
+			minBg.scaleX = width;
+			minBg.scaleY = height;
+			bg.scaleX = s2d.width;
+			bg.scaleY = s2d.height;
+			sceneInfo.text = "Scene size: " + s2d.width + "x" + s2d.height;
+		}
+
+		addText("Press R to set ScaleMode to Resize");
+		addChoice("ScaleMode", ScaleMode.getConstructors(), function(idx) { mode = idx; }, 0);
+		addSlider("width", function() { return width; }, function(v) { width = Std.int(v); }, 0, 800);
+		addSlider("height", function() { return height; }, function(v) { height = Std.int(v); }, 0, 600);
+		addSlider("zoom", function() { return zoom; }, function(v) { zoom = v; }, 0.01, 5);
+		addCheck("integerScale", function() { return intScale; }, function(v) { intScale = v; });
+		addChoice("HAlign", ["Left", "Center", "Right"], function(v) { halign = [Left, Center, Right][v]; }, 1 );
+		addChoice("VAlign", ["Top", "Center", "Bottom"], function(v) { valign = [Top, Center, Bottom][v]; }, 1 );
+		addButton("Apply", setMode);
+		sceneInfo = addText("");
+		addText("Light-grey: Actual Scene width and height");
+		addText("Dark-grey: Parameter-specified width and height");
+
+		setMode();
+	}
+
+	override function update(dt:Float)
+	{
+		if (Key.isReleased(Key.R)) s2d.scaleMode = Resize;
+	}
+
+	static function main() {
+		hxd.Res.initEmbed();
+		new ScaleMode2D();
+	}
+
+}