diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index e97b906ea4..11ab2ad20e 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -1,6 +1,6 @@ package flixel; -import flixel.FlxBasic.IFlxBasic; +import flixel.FlxBasic; import flixel.animation.FlxAnimationController; import flixel.graphics.FlxGraphic; import flixel.graphics.frames.FlxFrame; @@ -16,6 +16,7 @@ import flixel.util.FlxBitmapDataUtil; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; import flixel.util.FlxDirectionFlags; +import flixel.util.FlxSignal; import openfl.display.BitmapData; import openfl.display.BlendMode; import openfl.geom.ColorTransform; @@ -286,14 +287,16 @@ class FlxSprite extends FlxObject * applied after the frame is clipped. Use `clipToWorldBounds` or `clipToViewBounds` to convert */ public var clipRect(default, set):FlxRect; - var _lastClipRect = FlxRect.get(Math.NaN); + var _lastClipRect:FlxRect; /** * GLSL shader for this sprite. Avoid changing it frequently as this is a costly operation. * @since 4.1.0 */ public var shader:FlxShader; - + + public final onFrameChange = new FlxSignal(); + /** * The actual frame used for sprite rendering */ @@ -406,6 +409,7 @@ class FlxSprite extends FlxObject _halfSize = FlxPoint.get(); _matrix = new FlxMatrix(); _scaledOrigin = new FlxPoint(); + _lastClipRect = FlxRect.get(Math.NaN); } /** @@ -430,6 +434,7 @@ class FlxSprite extends FlxObject _halfSize = FlxDestroyUtil.put(_halfSize); _scaledOrigin = FlxDestroyUtil.put(_scaledOrigin); _lastClipRect = FlxDestroyUtil.put(_lastClipRect); + onFrameChange.removeAll(); framePixels = FlxDestroyUtil.dispose(framePixels); @@ -1855,6 +1860,8 @@ class FlxSprite extends FlxObject if (clipRect != null) _frame.clip(clipRect); + onFrameChange.dispatch(); + return frame; } diff --git a/flixel/graphics/FlxSliceSprite.hx b/flixel/graphics/FlxSliceSprite.hx new file mode 100644 index 0000000000..18967f3fe0 --- /dev/null +++ b/flixel/graphics/FlxSliceSprite.hx @@ -0,0 +1,123 @@ +package flixel.graphics; + +import flixel.graphics.frames.FlxFrame; +import flixel.graphics.frames.slice.FlxSpriteSlicer; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.util.FlxDestroyUtil; + +/** + * A `FlxSprite` with a 9-slice scaling. The `sliceRect` determines how to divide the 9 sections, + * and `displayWidth` and `displayHeight` determine how much the frame is stretched when drawn. + * If `sliceRect` is null, and the curret frame's `slice` is non-null, that slice rect is used + * + * **Note:** All of the slicing functionality is done via `FlxSpriteSlicer`, making it easy to + * add to any other class that extends FlxSprite + * + * @since 6.2.0 + */ +class FlxSliceSprite extends flixel.FlxSprite +{ + /** + * Controls the slicing of this sprite, `null` means no slicing + */ + public var sliceRect(get, set):Null; + inline function get_sliceRect() { return slicer.rect; } + inline function set_sliceRect(value) { return slicer.rect = value; } + + /** + * How large to draw the sliced sprite, relative to the `frameWidth`. + * If the value is `0` or less, the frameWidth is used + */ + public var displayWidth(get, set):Float; + inline function get_displayWidth() { return slicer.displayWidth; } + inline function set_displayWidth(value) + { + frameWidth = Std.int(value); + return slicer.displayWidth = value; + } + + /** + * How large to draw the sliced sprite, relative to the `frameHeight` + * If the value is `0` or less, the frameWidth is used + */ + public var displayHeight(get, set):Float; + inline function get_displayHeight() { return slicer.displayHeight; } + inline function set_displayHeight(value) + { + frameHeight = Std.int(value); + return slicer.displayHeight = value; + } + + /** + * The sprite's 9-slicing data + */ + var slicer(default, null):FlxSpriteSlicer; + + override function initVars():Void + { + super.initVars(); + + slicer = new FlxSpriteSlicer(this); + } + + override function destroy():Void + { + super.destroy(); + + slicer = FlxDestroyUtil.destroy(slicer); + } + + override function updateHitbox() + { + return slicer.hasValidSlicing() ? slicer.updateTargetHitbox() : super.updateHitbox(); + } + + override function getGraphicBounds(?rect:FlxRect):FlxRect + { + return slicer.hasValidSlicing() ? slicer.getTargetGraphicBounds(rect) : super.getGraphicBounds(rect); + } + + override function drawComplex(camera:FlxCamera):Void + { + if (slicer.hasValidSlicing()) + slicer.drawComplex(camera); + else + super.drawComplex(camera); + } + + override function viewToFrameHelper(viewX:Float, viewY:Float, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + result = super.viewToFrameHelper(viewX, viewY, camera, result); + + if (slicer.hasValidSlicing()) + slicer.displayToFrame(result, result); + + return result; + } + + override function worldToFrameSimpleHelper(worldX:Float, worldY:Float, ?result:FlxPoint):FlxPoint + { + result = super.worldToFrameSimpleHelper(worldX, worldY, result); + + if (slicer.hasValidSlicing()) + slicer.displayToFrame(result, result); + + return result; + } + + // override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect + // { + // newRect = super.getScreenBounds(newRect, camera); + + // // if (slicer.hasValidSlicing()) + // // { + // // newRect.x = slicer.displayToFrameX(newRect.x); + // // newRect.y = slicer.displayToFrameX(newRect.y); + // // newRect.width = slicer.displayToFrameX(newRect.width); + // // newRect.height = slicer.displayToFrameX(newRect.height); + // // } + + // return newRect; + // } +} \ No newline at end of file diff --git a/flixel/graphics/atlas/AtlasBase.hx b/flixel/graphics/atlas/AtlasBase.hx index f010411403..f888ae8c2a 100644 --- a/flixel/graphics/atlas/AtlasBase.hx +++ b/flixel/graphics/atlas/AtlasBase.hx @@ -28,7 +28,22 @@ typedef AtlasPos = /** * Rectangle struct use for atlas json parsing, { x:Float, y:Float, w:Float, h:Float } */ -typedef AtlasRect = AtlasPos & AtlasSize; +@:forward +abstract AtlasRect(AtlasPos & AtlasSize) from AtlasPos & AtlasSize +{ + public var l(get, never):Float; inline function get_l() return this.x; + public var r(get, never):Float; inline function get_r() return this.x + this.w; + public var t(get, never):Float; inline function get_t() return this.y; + public var b(get, never):Float; inline function get_b() return this.y + this.h; + + public function toFlxRect(?rect) + { + if (rect == null) + rect = flixel.math.FlxRect.get(); + + return rect.set(this.x, this.y, this.w, this.h); + } +} typedef AtlasFrame = { diff --git a/flixel/graphics/frames/FlxFrame.hx b/flixel/graphics/frames/FlxFrame.hx index 40f2cf87b6..92d932d332 100644 --- a/flixel/graphics/frames/FlxFrame.hx +++ b/flixel/graphics/frames/FlxFrame.hx @@ -1,6 +1,8 @@ package flixel.graphics.frames; import flixel.graphics.FlxGraphic; +import flixel.graphics.frames.slice.FlxFrameSlices; +import flixel.graphics.frames.slice.FlxSliceSection; import flixel.math.FlxMath; import flixel.math.FlxMatrix; import flixel.math.FlxPoint; @@ -145,6 +147,16 @@ class FlxFrame implements IFlxDestroyable /** Internal cache used to draw this frame **/ var blitMatrix:MatrixVector; + /** + * The 9-slice rect of this frame + */ + public var slice:FlxRect; + + /** + * Internal helper for this frame's 9-slicing + */ + var sliceData:FlxFrameSlices; + public function new(parent:FlxGraphic, angle = FlxFrameAngle.ANGLE_0, flipX = false, flipY = false, duration = 0.0) { this.parent = parent; @@ -161,11 +173,42 @@ class FlxFrame implements IFlxDestroyable blitMatrix = new MatrixVector(); if (FlxG.renderTile) tileMatrix = new MatrixVector(); + + sliceData = new FlxFrameSlices(this); } - - @:allow(flixel.graphics.frames.FlxFramesCollection) - @:allow(flixel.graphics.frames.FlxBitmapFont) - function cacheFrameMatrix():Void + + /** + * Updates the internal helpers to prepare this frame for rendering. + * Called automaticaly in `copyTo`. + */ + public function updateCache() + { + if (slice == null && sliceData.rect != null) + sliceData.clear(); + else if (slice != null && (sliceData.rect == null || sliceData.rect.equals(slice))) + sliceData.set(slice); + + // TODO: Defer matrix stuff? + } + + /** + * Fills the target frame sections with this frame's slice data, for rendering + * @param list + */ + public function initSliceSections(frames:FlxSectionList) + { + if (slice != null) + { + for (section=>frame in frames) + sliceData.initSectionFrame(section, frame); + } + } + + /** + * initializes internal data, used for rendering, usually called after creation + * @since 6.1.0 + */ + public function cacheFrameMatrix():Void { blitMatrix.copyFrom(this, true); @@ -768,7 +811,8 @@ class FlxFrame implements IFlxDestroyable clone.angle = angle; clone.frame = FlxDestroyUtil.put(clone.frame); } - + + updateCache(); clone.offset.copyFrom(offset); clone.flipX = flipX; clone.flipY = flipY; @@ -778,6 +822,9 @@ class FlxFrame implements IFlxDestroyable clone.name = name; clone.duration = duration; clone.cacheFrameMatrix(); + clone.slice = slice; + clone.sliceData.copyFrom(sliceData); + return clone; } @@ -789,6 +836,7 @@ class FlxFrame implements IFlxDestroyable offset = FlxDestroyUtil.put(offset); frame = FlxDestroyUtil.put(frame); uv = FlxDestroyUtil.put(uv); + sliceData = FlxDestroyUtil.destroy(sliceData); blitMatrix = null; tileMatrix = null; } diff --git a/flixel/graphics/frames/FlxFramesCollection.hx b/flixel/graphics/frames/FlxFramesCollection.hx index c6f38861f1..26705a37f0 100644 --- a/flixel/graphics/frames/FlxFramesCollection.hx +++ b/flixel/graphics/frames/FlxFramesCollection.hx @@ -10,15 +10,31 @@ import flixel.math.FlxRect; import flixel.util.FlxDestroyUtil; import flixel.util.FlxStringUtil; +class FlxFramesCollection extends FlxTypedFramesCollection +{ + public function new(parent, ?type, ?border) + { + super(parent, type, border); + + if (parent != null) + parent.addFrameCollection(this); + } + + function createFrame():FlxFrame + { + return new FlxFrame(parent); + } +} + /** * Base class for all frame collections. */ -class FlxFramesCollection implements IFlxDestroyable +private abstract class FlxTypedFramesCollection implements IFlxDestroyable { /** * Array with all frames of this collection. */ - public var frames:Array; + public var frames:Array; /** * Number of frames in this collection. @@ -32,12 +48,12 @@ class FlxFramesCollection implements IFlxDestroyable * (give names to your frames). */ @:deprecated("`framesHash` is deprecated, use `getByName()` or `exists()`") - public var framesHash(get, set):Map; + public var framesHash(get, set):Map; /** * Hash of frames, by name, for this frame collection. */ - var framesByName(default, null):Map; + var framesByName(default, null):Map; /** * Graphic object this frames belongs to. @@ -62,19 +78,18 @@ class FlxFramesCollection implements IFlxDestroyable this.type = type; this.border = (border == null) ? FlxPoint.get() : border; frames = []; - framesByName = new Map(); - - if (parent != null) - parent.addFrameCollection(this); + framesByName = new Map(); } + abstract function createFrame():TFrame; + /** * Finds a frame in the collection by its name. * * @param name The name of the frame to find. * @return Frame with specified name (if there is one). */ - public inline function getByName(name:String):FlxFrame + public inline function getByName(name:String):TFrame { return framesByName.get(name); } @@ -96,7 +111,7 @@ class FlxFramesCollection implements IFlxDestroyable * @param index Index of the frame in the frames array. * @return Frame with specified index in this frames collection (if there is one). */ - public inline function getByIndex(index:Int):FlxFrame + public inline function getByIndex(index:Int):TFrame { return frames[index]; } @@ -124,7 +139,7 @@ class FlxFramesCollection implements IFlxDestroyable * @param frame Frame to find. * @return Index of the specified frame. */ - public inline function getFrameIndex(frame:FlxFrame):Int + public inline function getFrameIndex(frame:TFrame):Int { return frames.indexOf(frame); } @@ -145,9 +160,9 @@ class FlxFramesCollection implements IFlxDestroyable * @param size Dimensions of the frame to add. * @return Newly added empty frame. */ - public function addEmptyFrame(size:FlxRect):FlxFrame + public function addEmptyFrame(size:FlxRect):TFrame { - var frame = new FlxFrame(parent); + var frame = createFrame(); frame.type = FlxFrameType.EMPTY; frame.frame = FlxRect.get(); frame.sourceSize.set(size.width, size.height); @@ -156,16 +171,16 @@ class FlxFramesCollection implements IFlxDestroyable } /** - * Adds new regular (not rotated) `FlxFrame` to this frame collection. + * Adds new regular (not rotated) frame to this frame collection. * * @param region Region of image which new frame will display. - * @return Newly created `FlxFrame` object for specified region of image. + * @return Newly created frame for specified region of image. */ - public function addSpriteSheetFrame(region:FlxRect):FlxFrame + public function addSpriteSheetFrame(region:FlxRect):TFrame { // Ensure region not a weak rect region = FlxRect.get().copyFrom(region); - final frame = new FlxFrame(parent); + final frame = createFrame(); frame.frame = checkFrame(region); frame.sourceSize.set(region.width, region.height); frame.offset.set(0, 0); @@ -189,19 +204,27 @@ class FlxFramesCollection implements IFlxDestroyable * @return Newly created and added frame object. */ public function addAtlasFrame(frame:FlxRect, sourceSize:FlxPoint, offset:FlxPoint, ?name:String, angle:FlxFrameAngle = 0, flipX = false, flipY = false, - duration = 0.0):FlxFrame + duration = 0.0):TFrame { if (name != null && exists(name)) + { + sourceSize.put(); + offset.put(); return getByName(name); + } - var texFrame:FlxFrame = new FlxFrame(parent, angle, flipX, flipY, duration); + final texFrame = createFrame(); texFrame.name = name; - texFrame.sourceSize.set(sourceSize.x, sourceSize.y); - texFrame.offset.set(offset.x, offset.y); + texFrame.angle = angle; + texFrame.flipX = flipX; + texFrame.flipY = flipY; + texFrame.duration = duration; + texFrame.sourceSize.copyFrom(sourceSize); + texFrame.offset.copyFrom(offset); texFrame.frame = checkFrame(frame, name); - sourceSize = FlxDestroyUtil.put(sourceSize); - offset = FlxDestroyUtil.put(offset); + sourceSize.put(); + offset.put(); return pushFrame(texFrame); } @@ -214,7 +237,7 @@ class FlxFramesCollection implements IFlxDestroyable */ public function getAllByPrefix(prefix:String) { - final list = new Array(); + final list = new Array(); forEachByPrefix(prefix, (frame)->list.push(frame), false); return list; } @@ -229,7 +252,7 @@ class FlxFramesCollection implements IFlxDestroyable * @param prefix The name prefix to look for. * @since 5.3.0 */ - public inline function forEachByPrefix(prefix:String, func:(FlxFrame)->Void, warnIfEmpty = true, ?warningMsg:String) + public inline function forEachByPrefix(prefix:String, func:(TFrame)->Void, warnIfEmpty = true, ?warningMsg:String) { var warn = warnIfEmpty; for (name => frame in framesByName) @@ -363,7 +386,7 @@ class FlxFramesCollection implements IFlxDestroyable * @param overwriteHash If true, any new frames with matching names will replace old ones. * @return Added frame. */ - public function pushFrame(frameObj:FlxFrame, overwriteHash = false):FlxFrame + public function pushFrame(frameObj:TFrame, overwriteHash = false):TFrame { final name:String = frameObj.name; if (name != null && exists(name) && !overwriteHash) diff --git a/flixel/graphics/frames/slice/FlxFrameSlices.hx b/flixel/graphics/frames/slice/FlxFrameSlices.hx new file mode 100644 index 0000000000..cc1d474f22 --- /dev/null +++ b/flixel/graphics/frames/slice/FlxFrameSlices.hx @@ -0,0 +1,116 @@ +package flixel.graphics.frames.slice; + +import flixel.graphics.frames.FlxFrame; +import flixel.graphics.frames.slice.FlxFrameSlices; +import flixel.graphics.frames.slice.FlxSliceSection; +import flixel.math.FlxRect; +import flixel.util.FlxDestroyUtil; + +/** + * Controls 9-slicing of FlxFrames, used by `FlxSliceSprite` or other + * types that have a `FlxSpriteSlicer` + * @since 6.1.0 + */ +class FlxFrameSlices implements IFlxDestroyable +{ + /** + * The 9-slicing of this frame + */ + public var rect(get, never):Null; + function get_rect() return sections != null ? sections[CC] : null; + + final parent:FlxFrame; + var sections:Null> = null; + + public function new (parent:FlxFrame) + { + this.parent = parent; + } + + public function destroy() + { + clear(); + } + + public function copyFrom(slice:FlxFrameSlices) + { + clear(); + if (slice.sections != null) + sections = [for (rect in slice.sections) FlxRect.getCopy(rect)]; + } + + /** + * Clears any frame data of this frame slice + */ + public function clear() + { + if (sections == null) + return; + + for (rect in sections) + rect.put(); + + sections = null; + } + + /** + * Sets the slicing data of this frame slice + */ + public function set(rect:FlxRect) + { + clear(); + sections = createSourceRects(rect, parent.frame); + } + + /** + * Initializes the given frame to match the desired section of this slice data + * + * @param section The section of the 9 slice + * @param sub The frame to initialize + */ + public function initSectionFrame(section:FlxSliceSection, sub:FlxFrame) + { + parent.subFrameTo(sections[section], sub); + } + + // @:arrayAccess + public inline function getSection(index:FlxSliceSection) + { + return sections[index]; + } + + public inline function iterator() + { + return sections.iterator(); + } + + public inline function keys() + { + return sections.keys(); + } + + public inline function keyValueIterator() + { + return sections.keyValueIterator(); + } + + static function createSourceRects(rect:FlxRect, parentRect:FlxRect) + { + final sections = [for (i in FlxSliceSection) null]; + + final x = [0, rect.x, rect.right, parentRect.width]; + final y = [0, rect.y, rect.bottom, parentRect.height]; + rect.putWeak(); + + for (section in FlxSliceSection) + { + final col = section.column; + final row = section.row; + + final sectionRect = FlxRect.get(); + sectionRect.setBounds(x[col], y[row], x[col + 1], y[row + 1]); + sections[section] = sectionRect; + } + return sections; + } +} \ No newline at end of file diff --git a/flixel/graphics/frames/slice/FlxSliceSection.hx b/flixel/graphics/frames/slice/FlxSliceSection.hx new file mode 100644 index 0000000000..84822d01b6 --- /dev/null +++ b/flixel/graphics/frames/slice/FlxSliceSection.hx @@ -0,0 +1,104 @@ +package flixel.graphics.frames.slice; + +enum abstract FlxSliceSection(Int) from Int to Int +{ + static inline var COLUMNS = 3; + static inline var ROWS = 3; + + var TL; var TC; var TR; + var CL; var CC; var CR; + var BL; var BC; var BR; + + public var column(get, never):Int; + inline function get_column() return this % ROWS; + public var row(get, never):Int; + inline function get_row() return Std.int(this / ROWS); + + static public function createAll():Array + { + return [for (i in iterator()) i]; + } + + static public inline function iterator() + { + return new SectionIterator(); + } + + /** + * [Description] + * @param column + * @param row + * @return FlxSliceSection + */ + static public inline function fromXY(column:Int, row:Int):FlxSliceSection + { + return column + row * ROWS; + } +} + +class SectionIterator +{ + static inline var LENGTH = 9; + var iter:IntIterator; + + public inline function new() { this.iter = 0...LENGTH; } + + public inline function hasNext() { return iter.hasNext(); } + + public inline function next():FlxSliceSection { return iter.next(); } +} + +@:forward(length) +@:forward.variance +abstract FlxSectionList(Array) from Array +{ + @:arrayAccess + public inline function get(section:FlxSliceSection) + { + return this[section]; + } + + @:arrayAccess + public inline function set(section:FlxSliceSection, value:T) + { + return this[section] = value; + } + + public inline function iterator() + { + return this.iterator(); + } + + public inline function keys() + { + return new SectionIterator(); + } + + public inline function keyValueIterator() + { + return new SectionListKeyValueIterator(this); + } +} + +class SectionListKeyValueIterator +{ + final list:FlxSectionList; + var current:Int = 0; + + public inline function new(list) + { + this.list = list; + } + + public inline function hasNext():Bool + { + return current < list.length; + } + + public inline function next() + { + final key:FlxSliceSection = current++; + final value = list[key]; + return { value: value, key: key }; + } +} \ No newline at end of file diff --git a/flixel/graphics/frames/slice/FlxSpriteSlicer.hx b/flixel/graphics/frames/slice/FlxSpriteSlicer.hx new file mode 100644 index 0000000000..58ed839b93 --- /dev/null +++ b/flixel/graphics/frames/slice/FlxSpriteSlicer.hx @@ -0,0 +1,516 @@ +package flixel.graphics.frames.slice; + +import flixel.graphics.frames.FlxFrame; +import flixel.graphics.frames.slice.FlxFrameSlices; +import flixel.graphics.frames.slice.FlxSliceSection; +import flixel.math.FlxMath; +import flixel.math.FlxMatrix; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.util.FlxDestroyUtil; + +/** + * Applies 9-slicing to a FlxSprite, for an example of use this, look at `FlxSliceSprite` + * @since 6.2.0 + */ +class FlxSpriteSlicer implements IFlxDestroyable +{ + final target:FlxSprite; + + var targetFrame(get, never):FlxFrame; + inline function get_targetFrame() @:privateAccess return target._frame; + + var targetFrameWidth(get, never):Int; + inline function get_targetFrameWidth() @:privateAccess return Std.int(target._frame.sourceSize.x); + + var targetFrameHeight(get, never):Int; + inline function get_targetFrameHeight() @:privateAccess return Std.int(target._frame.sourceSize.y); + + /** + * The 9-slicing rect to apply to the target where the top, left, bottom and right + * Determine where to slice the current frame. A value of `null` means "no slicing", + * unless the current frame's `slice` rect is defined, then that rect is used. + */ + public var rect:Null = null; + + /** + * The current, active slicing rect, used internally to detect changes to `rect` + */ + var currentRect = new FlxRect(Math.NaN); + var frameDirty = false; + + /** + * Internal framesĀ used to draw each of the 9 sections + */ + var subframes:FlxSectionList; + + /** + * Internal rects that define where each section will be drawn + */ + var destRects:FlxSectionList; + + /** + * How large to draw the sliced sprite + */ + public var displayWidth:Float = 0.0; + + /** + * How large to draw the sliced sprite + */ + public var displayHeight:Float = 0.0; + + var lastDisplayWidth:Float = 0.0; + var lastDisplayHeight:Float = 0.0; + + public function new (target:FlxSprite) + { + this.target = target; + target.onFrameChange.add(()->frameDirty = true); + subframes = [for (section in FlxSliceSection) new FlxFrame(null)]; + } + + public function destroy() + { + currentRect = FlxDestroyUtil.put(currentRect); + FlxDestroyUtil.putArray(cast destRects); + FlxDestroyUtil.destroyArray(cast subframes); + destRects = null; + subframes = null; + } + + /** + * Whether this slicer is needed given the current slice rect and desired display size + */ + public function hasValidSlicing() + { + return (rect != null || targetFrame.slice != null) && (displayHeight > 0 && displayWidth > 0); + } + + public function setDisplaySize(width:Float, height:Float) + { + displayWidth = width; + displayHeight = height; + } + + public function setScaledDisplaySize(width:Float, height:Float) + { + displayWidth = width / target.scale.x; + displayHeight = height / target.scale.y; + } + + function getValidRect(rect:FlxRect) + { + return clipValidRect(FlxRect.getCopy(rect)); + } + + function clipValidRect(rect:FlxRect) + { + rect.left = FlxMath.bound(rect.left , 0, targetFrameWidth ); + rect.top = FlxMath.bound(rect.top , 0, targetFrameHeight); + rect.right = FlxMath.bound(rect.right , rect.left, targetFrameWidth ); + rect.bottom = FlxMath.bound(rect.bottom, rect.top , targetFrameHeight); + return rect; + } + + /** + * similar to sprite.updateHitbox() but accounts for the slicer's displaySize + */ + public function updateTargetHitbox() + { + final frameWidth = targetFrameWidth; + final frameHeight = targetFrameHeight; + target.width = Math.abs(target.scale.x) * frameWidth; + target.height = Math.abs(target.scale.y) * frameHeight; + target.offset.set(-0.5 * (target.width - frameWidth), -0.5 * (target.height - frameHeight)); + target.centerOrigin(); + } + + /** + * similar to sprite.updategetGraphicBoundsHitbox() but accounts for the slicer's displaySize + */ + public function getTargetGraphicBounds(?rect:FlxRect):FlxRect + { + if (!hasValidSlicing()) + return target.getGraphicBounds(rect); + + if (rect == null) + rect = FlxRect.get(); + + rect.set(target.x, target.y); + if (target.pixelPerfectPosition) + rect.floor(); + + updateCache(); + final sliceOrigin = FlxPoint.get(frameToDisplayXUnsafe(target.origin.x), frameToDisplayYUnsafe(target.origin.y)); + final scaledOrigin = FlxPoint.get(sliceOrigin.x * target.scale.x, sliceOrigin.y * target.scale.y); + rect.x += sliceOrigin.x - target.offset.x - scaledOrigin.x; + rect.y += sliceOrigin.y - target.offset.y - scaledOrigin.y; + final frameWidth = displayWidth > 0 ? displayWidth : targetFrameWidth; + final frameHeight = displayHeight > 0 ? displayHeight : targetFrameHeight; + rect.setSize(frameWidth * target.scale.x, frameHeight * target.scale.y); + + if (target.angle % 360 != 0) + rect.getRotatedBounds(target.angle, scaledOrigin, rect); + scaledOrigin.put(); + sliceOrigin.put(); + + return rect; + } + + /** + * Draws the target according to the slice rect + * @param camera The camera to which to draw + */ + public function drawComplex(camera:FlxCamera) + { + updateCache(); + + for (section=>frame in subframes) + { + if (frame.frame.width > 0 && frame.frame.height > 0) + drawSectionComplex(frame, camera, destRects[section]); + } + } + + /** + * Calls `drawFunc` with every valid section + * @param drawFunc A custom drawing function + */ + public function drawCustom(drawFunc:(FlxFrame, FlxRect)->Void) + { + updateCache(); + + for (section=>frame in subframes) + { + if (frame.frame.width > 0 && frame.frame.height > 0) + drawFunc(frame, destRects[section]); + } + } + + static final globalDrawMatrix = new FlxMatrix(); + @:access(flixel.FlxSprite) + function drawSectionComplex(frame:FlxFrame, camera:FlxCamera, rect:FlxRect):Void + { + final matrix = globalDrawMatrix; + final sliceOrigin = FlxPoint.get(frameToDisplayXUnsafe(target.origin.x), frameToDisplayYUnsafe(target.origin.y)); + frame.prepareMatrix(matrix, FlxFrameAngle.ANGLE_0, target.checkFlipX(), target.checkFlipY()); + if (target.clipRect != null) + matrix.translate(-Math.max(0, target.clipRect.x), -Math.max(0, target.clipRect.y)); + matrix.scale(rect.width / frame.frame.width, rect.height / frame.frame.height); + matrix.translate(-sliceOrigin.x, -sliceOrigin.y); + matrix.translate(rect.x, rect.y); + matrix.scale(target.scale.x, target.scale.y); + + if (target.bakedRotationAngle <= 0) + { + target.updateTrig(); + + if (target.angle != 0) + matrix.rotateWithTrig(target._cosAngle, target._sinAngle); + } + + final point = target.getScreenPosition(camera).subtract(target.offset); + point.add(sliceOrigin.x, sliceOrigin.y); + matrix.translate(point.x, point.y); + sliceOrigin.put(); + point.put(); + + if (target.isPixelPerfectRender(camera)) + { + matrix.tx = Math.floor(matrix.tx); + matrix.ty = Math.floor(matrix.ty); + } + + camera.drawPixels(frame, target.framePixels, matrix, target.colorTransform, target.blend, target.antialiasing, target.shader); + } + + function updateCache() + { + checkSourceRects(); + checkDestRects(); + } + + function checkSourceRects() + { + if (rect != null) + { + if (rectsNotMatch(rect, currentRect) || frameDirty) + updateSourceRects(rect); + } + else if (targetFrame.slice != null) + { + if (rectsNotMatch(targetFrame.slice, currentRect) || frameDirty) + { + if (target.clipRect == null) + updateSourceRectsFromFrame(targetFrame); + else + updateSourceRects(targetFrame.slice); + } + } + else + { + currentRect.set(Math.NaN); + } + } + + function updateSourceRects(rect:FlxRect) + { + currentRect.copyFrom(rect); + frameDirty = false; + + final srcBounds = FlxRect.get(0, 0, targetFrameWidth, targetFrameHeight); + final srcSlice = getValidRect(rect); + if (target.clipRect != null) + { + srcBounds.copyFrom(target.clipRect); + clipValidRect(srcBounds); + clipToSmart(srcSlice, srcBounds); + } + + final x = [srcBounds.x, srcSlice.x, srcSlice.right, srcBounds.right]; + final y = [srcBounds.y, srcSlice.y, srcSlice.bottom, srcBounds.bottom]; + srcBounds.put(); + srcSlice.put(); + final rectHelper = FlxRect.get(); + + for (section in FlxSliceSection) + { + final col = section.column; + final row = section.row; + final sub = subframes[section]; + rectHelper.setBounds(x[col], y[row], x[col + 1], y[row + 1]); + targetFrame.subFrameTo(rectHelper, sub); + sub.offset.copyFrom(targetFrame.offset); + sub.cacheFrameMatrix(); + } + rectHelper.put(); + + updateDestRects(); + } + + function updateSourceRectsFromFrame(frame:FlxFrame) + { + currentRect.copyFrom(frame.slice); + frameDirty = false; + frame.initSliceSections(subframes); + updateDestRects(); + } + + function checkDestRects() + { + if (displayWidth != lastDisplayWidth || displayHeight != lastDisplayHeight) + { + lastDisplayWidth = displayWidth; + lastDisplayHeight = displayHeight; + + if (displayWidth > 0 && displayHeight > 0) + updateDestRects(); + } + } + + function updateDestRects() + { + final srcBounds = FlxRect.get(0, 0, targetFrameWidth, targetFrameHeight); + final srcSlice = getValidRect(currentRect); + + if (target.clipRect != null) + { + srcBounds.copyFrom(target.clipRect); + clipValidRect(srcBounds); + clipToSmart(srcSlice, srcBounds); + } + + final x = + [ frameToDisplayXUnsafe(srcBounds.x) + , frameToDisplayXUnsafe(srcSlice.x) + , frameToDisplayXUnsafe(srcSlice.right) + , frameToDisplayXUnsafe(srcBounds.right) + ]; + final y = + [ frameToDisplayYUnsafe(srcBounds.y) + , frameToDisplayYUnsafe(srcSlice.y) + , frameToDisplayYUnsafe(srcSlice.bottom) + , frameToDisplayYUnsafe(srcBounds.bottom) + ]; + + srcBounds.put(); + srcSlice.put(); + + FlxDestroyUtil.putArray(cast destRects); + destRects = []; + + for (section in FlxSliceSection) + { + final col = section.column; + final row = section.row; + + destRects[section] = FlxRect.get().setBounds(x[col], y[row], x[col + 1], y[row + 1]); + // destRects[section].x += section.column - 1; + // destRects[section].y += section.row - 1; + } + } + + /** + * Transforms the given position from the original frame to where it will be + * drawn, relative to the target's position + * @param x A position on the original frame + */ + public function frameToDisplayX(x:Float) + { + return hasValidSlicing() ? frameToDisplayXUnsafe(x) : x; + } + + /** + * Transforms the given position from the original frame to where it will be + * drawn, relative to the target's position + * @param y A position on the original frame + */ + public function frameToDisplayY(y:Float) + { + return hasValidSlicing() ? frameToDisplayYUnsafe(y) : y; + } + + function frameToDisplayXUnsafe(x:Float) + { + if (x < currentRect.x) + return x; + + if (x > currentRect.right) + return displayWidth - targetFrameWidth + x; + + final middleScaleX = (displayWidth - targetFrameWidth + currentRect.width) / currentRect.width; + return currentRect.x + (x - currentRect.x) * middleScaleX; + } + + function frameToDisplayYUnsafe(y:Float) + { + if (y < currentRect.y) + return y; + + if (y > currentRect.bottom) + return displayHeight - targetFrameHeight + y; + + final middleScaleY = (displayHeight - targetFrameHeight + currentRect.height) / currentRect.height; + return currentRect.y + (y - currentRect.y) * middleScaleY; + } + + overload public inline extern function displayToFrame(display:FlxPoint, ?result:FlxPoint):FlxPoint + { + return displayToFrameHelper(display.x, display.y, result); + } + + overload public inline extern function displayToFrame(displayX:Float, displayY:Float, ?result:FlxPoint):FlxPoint + { + return displayToFrameHelper(displayX, displayY, result); + } + + function displayToFrameHelper(displayX:Float, displayY:Float, ?result:FlxPoint):FlxPoint + { + if (hasValidSlicing()) + { + result.x = displayToFrameXUnsafe(displayX); + result.y = displayToFrameYUnsafe(displayY); + } + else + { + result.x = displayX; + result.y = displayY; + } + + return result; + } + + /** + * Transforms the given position from the stretched display to the original frame + * @param x A position on the stretched display + */ + public function displayToFrameX(x:Float) + { + return hasValidSlicing() ? displayToFrameXUnsafe(x) : x; + } + + /** + * Transforms the given position from the stretched display to the original frame + * @param y A position on the stretched display + */ + public function displayToFrameY(y:Float) + { + return hasValidSlicing() ? displayToFrameYUnsafe(y) : y; + } + + function displayToFrameXUnsafe(x:Float) + { + if (x < rect.x) + return x; + + if (x > displayWidth - (targetFrameWidth - rect.right)) + return x - displayWidth + targetFrameWidth; + + final middleScaleX = (displayWidth - targetFrameWidth + rect.width) / rect.width; + return (x - rect.x) / middleScaleX + rect.x; + } + + function displayToFrameYUnsafe(y:Float) + { + if (y < rect.y) + return y; + + if (y > displayHeight - (targetFrameHeight - rect.bottom)) + return y - displayHeight + targetFrameHeight; + + final middleScaleY = (displayHeight - targetFrameHeight + rect.height) / rect.height; + return (y - rect.y) / middleScaleY + rect.y; + } + + /** + * Similar to clipTo but the result will always be inside b, even if there is no overlap + * @param a The rect to clip + * @param b The rect to which `a` is clipped + */ + function clipToSmart(a:FlxRect, b:FlxRect) + { + if (a.left > b.right) + { + a.x = b.right; + a.width = 0; + } + else if (a.right < b.left) + { + a.x = b.left; + a.width = 0; + } + else + { + a.left = a.left < b.left ? b.left : a.left; + a.right = a.right > b.right ? b.right : a.right; + } + + if (a.top > b.bottom) + { + a.y = b.bottom; + a.height = 0; + } + else if (a.bottom < b.top) + { + a.y = b.top; + a.height = 0; + } + else + { + a.top = a.top < b.y ? b.y : a.y; + a.bottom = a.bottom > b.bottom ? b.bottom : a.bottom; + } + + return a; + } + + static function rectsMatch(a:FlxRect, b:FlxRect) + { + return (a == null && b == null) || (a != null && b != null && a.equals(b)); + } + + static function rectsNotMatch(a:FlxRect, b:FlxRect) + { + return !rectsMatch(a, b); + } +} \ No newline at end of file diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 3dc0d88618..1fadd66834 100644 --- a/flixel/math/FlxRect.hx +++ b/flixel/math/FlxRect.hx @@ -14,14 +14,25 @@ class FlxRect implements IFlxPooled static var _pool:FlxPool = new FlxPool(FlxRect.new.bind(0, 0, 0, 0)); // With the version below, this caused weird CI issues when FLX_NO_POINT_POOL is defined // static var _pool = new FlxPool(FlxRect); - + /** * Recycle or create new FlxRect. * Be sure to put() them back into the pool after you're done with them! */ - public static inline function get(X:Float = 0, Y:Float = 0, Width:Float = 0, Height:Float = 0):FlxRect + public static inline function get(x = 0.0, y = 0.0, width = 0.0, height = 0.0):FlxRect + { + var rect = _pool.get().set(x, y, width, height); + rect._inPool = false; + return rect; + } + + /** + * Recycle or create new FlxRect that matches the given rect. + * Be sure to put() them back into the pool after you're done with them! + */ + public static inline function getCopy(rect:FlxRect):FlxRect { - var rect = _pool.get().set(X, Y, Width, Height); + var rect = _pool.get().copyFrom(rect); rect._inPool = false; return rect; }