diff --git a/flixel/graphics/tile/FlxDrawQuadsItem.hx b/flixel/graphics/tile/FlxDrawQuadsItem.hx index bd73f8b346..ec5088b5cd 100644 --- a/flixel/graphics/tile/FlxDrawQuadsItem.hx +++ b/flixel/graphics/tile/FlxDrawQuadsItem.hx @@ -2,44 +2,88 @@ package flixel.graphics.tile; import flixel.FlxCamera; import flixel.graphics.frames.FlxFrame; -import flixel.graphics.tile.FlxDrawBaseItem.FlxDrawItemType; -import flixel.system.FlxAssets.FlxShader; +import flixel.graphics.tile.FlxDrawBaseItem; +import flixel.system.FlxAssets; +import flixel.system.FlxBuffer; import flixel.math.FlxMatrix; +import flixel.math.FlxRect; import openfl.geom.ColorTransform; import openfl.display.ShaderParameter; import openfl.Vector; +typedef QuadRectRaw = { x:Float, y:Float, width:Float, height:Float }; +@:forward +abstract QuadRect(QuadRectRaw) from QuadRectRaw to QuadRectRaw +{ + @:from + public static inline function fromFlxRect(rect:FlxRect):QuadRect + { + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; + } + + @:from + public static inline function fromRect(rect:openfl.geom.Rectangle):QuadRect + { + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; + } + + public inline function toFlxRect(rect:FlxRect):FlxRect + { + return rect.set(this.x, this.y, this.width, this.height); + } +} + +typedef QuadTransformRaw = { a:Float, b:Float, c:Float, d:Float, tx:Float, ty:Float }; +@:forward +abstract QuadTransform(QuadTransformRaw) from QuadTransformRaw to QuadTransformRaw +{ + @:from + public static inline function fromMatrix(matrix:FlxMatrix):QuadTransform + { + return { a: matrix.a, b: matrix.b, c: matrix.c, d: matrix.d, tx: matrix.tx, ty: matrix.ty }; + } + + public inline function toMatrix(matrix:FlxMatrix):FlxMatrix + { + matrix.setTo(this.a, this.b, this.c, this.d, this.tx, this.ty); + return matrix; + } +} + +typedef QuadColorMult = { r:Float, g:Float, b:Float, a:Float }; +typedef QuadColorOffset = { r:Float, g:Float, b:Float, a:Float }; + class FlxDrawQuadsItem extends FlxDrawBaseItem { static inline var VERTICES_PER_QUAD = #if (openfl >= "8.5.0") 4 #else 6 #end; public var shader:FlxShader; - var rects:Vector; - var transforms:Vector; + var rects:FlxBuffer; + var transforms:FlxBuffer; var alphas:Array; - var colorMultipliers:Array; - var colorOffsets:Array; + var colorMultipliers:FlxBufferArray; + var colorOffsets:FlxBufferArray; public function new() { super(); type = FlxDrawItemType.TILES; - rects = new Vector(); - transforms = new Vector(); + rects = new FlxBuffer(); + transforms = new FlxBuffer(); alphas = []; } override public function reset():Void { super.reset(); - rects.length = 0; - transforms.length = 0; - alphas.splice(0, alphas.length); + rects.resize(0); + transforms.resize(0); + alphas.resize(0); if (colorMultipliers != null) - colorMultipliers.splice(0, colorMultipliers.length); + colorMultipliers.resize(0); if (colorOffsets != null) - colorOffsets.splice(0, colorOffsets.length); + colorOffsets.resize(0); } override public function dispose():Void @@ -54,18 +98,8 @@ class FlxDrawQuadsItem extends FlxDrawBaseItem override public function addQuad(frame:FlxFrame, matrix:FlxMatrix, ?transform:ColorTransform):Void { - var rect = frame.frame; - rects.push(rect.x); - rects.push(rect.y); - rects.push(rect.width); - rects.push(rect.height); - - transforms.push(matrix.a); - transforms.push(matrix.b); - transforms.push(matrix.c); - transforms.push(matrix.d); - transforms.push(matrix.tx); - transforms.push(matrix.ty); + rects.push(frame.frame); + transforms.push(matrix); var alphaMultiplier = transform != null ? transform.alphaMultiplier : 1.0; for (i in 0...VERTICES_PER_QUAD) @@ -83,28 +117,14 @@ class FlxDrawQuadsItem extends FlxDrawBaseItem { if (transform != null) { - colorMultipliers.push(transform.redMultiplier); - colorMultipliers.push(transform.greenMultiplier); - colorMultipliers.push(transform.blueMultiplier); - - colorOffsets.push(transform.redOffset); - colorOffsets.push(transform.greenOffset); - colorOffsets.push(transform.blueOffset); - colorOffsets.push(transform.alphaOffset); + colorMultipliers.push(transform.redMultiplier, transform.greenMultiplier, transform.blueMultiplier, 1); + colorOffsets.push(transform.redOffset, transform.greenOffset, transform.blueOffset, transform.alphaOffset); } else { - colorMultipliers.push(1); - colorMultipliers.push(1); - colorMultipliers.push(1); - - colorOffsets.push(0); - colorOffsets.push(0); - colorOffsets.push(0); - colorOffsets.push(0); + colorMultipliers.push(1, 1, 1, 1); + colorOffsets.push(0, 0, 0, 0); } - - colorMultipliers.push(1); } } } diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 31387c58ed..a21c297040 100644 --- a/flixel/math/FlxRect.hx +++ b/flixel/math/FlxRect.hx @@ -433,39 +433,45 @@ class FlxRect implements IFlxPooled rect.putWeak(); return result; } - + /** * Returns the area of intersection with specified rectangle. * If the rectangles do not intersect, this method returns an empty rectangle. * - * @param rect Rectangle to check intersection against. - * @return The area of intersection of two rectangles. + * @param rect Rectangle to check intersection against + * @param result The resulting instance, if `null`, a new one is created + * @return The area of intersection of two rectangles */ public function intersection(rect:FlxRect, ?result:FlxRect):FlxRect { if (result == null) result = FlxRect.get(); - - var x0:Float = x < rect.x ? rect.x : x; - var x1:Float = right > rect.right ? rect.right : right; - if (x1 <= x0) - { - rect.putWeak(); - return result; - } - - var y0:Float = y < rect.y ? rect.y : y; - var y1:Float = bottom > rect.bottom ? rect.bottom : bottom; - if (y1 <= y0) - { - rect.putWeak(); - return result; - } - + + final x0:Float = x < rect.x ? rect.x : x; + final x1:Float = right > rect.right ? rect.right : right; + final y0:Float = y < rect.y ? rect.y : y; + final y1:Float = bottom > rect.bottom ? rect.bottom : bottom; rect.putWeak(); + + if (x1 <= x0 || y1 <= y0) + return result.set(0, 0, 0, 0); + return result.set(x0, y0, x1 - x0, y1 - y0); } + /** + * Resizes `this` instance so that it fits within the intersection of the this and + * the target rect. If there is no overlap between them, The result is an empty rect. + * + * @param rect Rectangle to check intersection against + * @return This rect, useful for chaining + * @since 5.9.0 + */ + public function clipTo(rect:FlxRect):FlxRect + { + return rect.intersection(this, this); + } + /** * The middle point of this rect * diff --git a/flixel/system/FlxBuffer.hx b/flixel/system/FlxBuffer.hx new file mode 100644 index 0000000000..1f5442eb89 --- /dev/null +++ b/flixel/system/FlxBuffer.hx @@ -0,0 +1,478 @@ +package flixel.system; + +import haxe.macro.TypeTools; +#if macro +import haxe.macro.Type; +import haxe.macro.Expr; +import haxe.macro.Context; + +using haxe.macro.Tools; + +class BufferMacro +{ + public static function build(isVector:Bool, includeGetters = true):ComplexType + { + final local = Context.getLocalType(); + switch local { + // Extract the type parameter + case TInst(local, [type]): + return buildBuffer(getFields(type, includeGetters), type, isVector); + default: + throw "Expected TInst"; + } + } + + static function getFields(type:Type, includeGetters:Bool):Array + { + // Follow it to get the underlying anonymous structure + switch type.follow() + { + case TAbstract(abType, []): + final fields = getFields(abType.get().type, includeGetters).copy(); + // add abstract fields too + if (includeGetters && (abType.get().impl != null || abType.get().impl.get() != null)) + { + final statics = abType.get().impl.get().statics.get(); + for (field in statics) + { + if (field.kind.match(FVar(AccCall, _))) + { + fields.push(field); + } + } + } + return fields; + case TAnonymous(type): + return type.get().fields; + case TInst(found, _): + throw 'Expected type parameter to be an anonymous structure, got ${found.get().name}'; + case TEnum(found, _): + throw 'Expected type parameter to be an anonymous structure, got ${found.get().name}'; + case found: + throw 'Expected type parameter to be an anonymous structure, got ${found.getName()}'; + } + } + + static function buildBuffer(fields:Array, type:Type, isVector:Bool) + { + // Sort fields by pos to ensure order is maintained (weird that this is needed) + fields.sort((a, b)->a.pos.getInfos().max - b.pos.getInfos().max); + + // Distinguish getters from actual fields + final getters:Array = fields.copy(); + var i = fields.length; + while (i-- > 0) + { + // TODO: Prevent double adds for getters over typedef fields? + final field = fields[i]; + if (field.kind.match(FVar(AccCall, _))) + { + fields.remove(field); + } + } + + // Generate unique name for each type + final arrayType = fields[0].type; + final arrayTypeName = getTypeName(arrayType); + final prefix = (isVector ? "" : "Array_") + arrayTypeName + "_" + getTypeIdentifier(type); + final name = "Buffer_" + prefix; + final iterName = "BufferIterator_" + prefix; + final kvIterName = "BufferKeyValueIterator_" + prefix; + final complexType = type.toComplexType(); + + // Check whether the generated type already exists + try + { + Context.getType(name); + Context.getType(iterName); + Context.getType(kvIterName); + + // Return a `ComplexType` for the generated type + return TPath({pack: [], name: name}); + } + catch (e) {} // The generated type doesn't exist yet + + final length = fields.length; + if (length < 2) + throw "Just use an array"; + + // Make sure all fields use the same type + for (i in 1...length) + { + if (arrayType.toString() != fields[i].type.toString()) + { + throw 'Cannot build buffer for type: { ${fields.map((f)->f.type.toString()).join(", ")} }'; + } + } + + // An easy way to instantiate the type from an index + final objectDecl:Expr = + { + pos: Context.currentPos(), + expr: EObjectDecl([ + for (i => field in fields) + { + { + field: field.name, + expr: macro this[iReal + $v{i}], + } + } + ]) + } + + final pushEachFunc:Field = + { + doc:"Creates an item with the given values and adds it at the end of this Buffer and returns the new length of this Array.\n\n" + + "This operation modifies this Array in place.\n\n" + + "this.length increases by 1.", + pos: Context.currentPos(), + name: "push", + access: [APublic, AOverload, AExtern, AInline], + kind:FFun + ({ + args: fields.map(function (f):FunctionArg return { type: f.type.toComplexType(), name: f.name }), + expr:macro { + $b{[for (field in fields) + { + macro this.push($i{field.name}); + }]} + return length; + } + }) + } + + final bufferCt = TPath({pack: [], name: name}); + // Get the iterator complex types (which are actually created later) + final iterCt = { name: iterName, pack: [] }; + final kvIterCt = { name: kvIterName, pack: [] }; + final itemTypeName = getTypeName(type); + final arrayCT = arrayType.toComplexType(); + + // define the buffer + final def = macro class $name + { + static inline var FIELDS:Int = $v{length}; + + /** The number of items in this buffer */ + public var length(get, never):Int; + inline function get_length() return Std.int(this.length / FIELDS); + + public inline function new () + { + $e{ isVector + ? macro this = new openfl.Vector() + : macro this = [] + } + } + + /** Fetches the item at the desired index */ + @:arrayAccess + public inline function get(i:Int):$complexType + { + final iReal = i * FIELDS; + return $e{objectDecl}; + } + + /** Fetches the item at the desired index */ + @:arrayAccess + public inline function set(pos:Int, item:$complexType):$complexType + { + $b{[ + for (i=>field in fields) + { + final name = field.name; + macro this[pos * FIELDS + $v{i}] = item.$name; + } + ]} + return item; + } + + /** + * Adds the item at the end of this Buffer and returns the new length of this Array + * + * This operation modifies this Array in place + * + * `this.length` increases by `1` + */ + public overload extern inline function push(item:$complexType):Int + { + $b{[ + for (field in fields) + { + final name = field.name; + macro this.push(item.$name); + } + ]} + return length; + } + + /** + * Creates an item with the given values and adds it at the end of this Buffer and returns the new length of this Array + * + * This operation modifies this Array in place + * + * `this.length` increases by `1` + */ + // public overload extern inline function push($a{???}):Int + // { + // $b{[fields.map((f)->macro this.push($i{f.name}))]} + // return length; + // } + + /** Removes and returns the first item */ + public inline function shift():$complexType + { + final iReal = 0; + final item = $e{objectDecl}; + $b{[ + for (field in fields) + { + macro this.shift(); + } + ]} + return item; + } + + /** Removes and returns the last item */ + public inline function pop():$complexType + { + final iReal = this.length - FIELDS; + final item = $e{objectDecl}; + $b{[ + for (field in fields) + { + macro this.pop(); + } + ]} + return item; + } + + /** + * Inserts the element x at the position pos. + * + * This operation modifies this Array in place. + * + * The offset is calculated like so: + * + * If pos exceeds this.length, the offset is this.length. + * If pos is negative, the offset is calculated from the end of this Array, i.e. this.length + pos. If this yields a negative value, the offset is 0. + * Otherwise, the offset is pos. + * If the resulting offset does not exceed this.length, all elements from and including that offset to the end of this Array are moved one index ahead. + */ + public inline function insert(pos:Int, item:$complexType) + { + $b{[ + for (i=>field in fields) + { + final name = field.name; + if (isVector) + { + macro this.insertAt(pos * FIELDS + $v{i}, item.$name); + } + else + { + macro this.insert(pos * FIELDS + $v{i}, item.$name); + } + } + ]} + } + + /** + * Set the length of the Array. + * If len is shorter than the array's current size, the last length - len elements will + * be removed. If len is longer, the Array will be extended + * + * **Note:** Since FlxBuffers are actually Arrays of some primitive, often `Float`, it + * will likely add all zeros + * + * @param length The desired length + */ + public inline function resize(length:Int) + { + $e{isVector + ? macro this.length = length * FIELDS + : macro this.resize(length * FIELDS) + } + } + + /** + * Creates a shallow copy of the range of this Buffer, starting at and including pos, + * up to but not including end. + * + * This operation does not modify this Buffer. + * + * The elements are not copied and retain their identity. + * + * If end is omitted or exceeds this.length, it defaults to the end of this Buffer. + * + * If pos or end are negative, their offsets are calculated from the end of this + * Buffer by this.length + pos and this.length + end respectively. If this yields + * a negative value, 0 is used instead. + * + * If pos exceeds this.length or if end is less than or equals pos, the result is []. + */ + public inline function slice(pos:Int, ?end:Int):$bufferCt + { + return this.slice(pos * FIELDS, (end != null ? end : length) * FIELDS); + } + + /** Returns an iterator of the buffer's items */ + public inline function iterator() + { + return new $iterCt(this); + } + + /** Returns an iterator of the buffer's indices and items */ + public inline function keyValueIterator() + { + return new $kvIterCt(this); + } + }; + + // Generate unique doc, but with static example + def.doc = 'An `${isVector ? "openfl.Vector" : "Array"}<$arrayTypeName>` disguised as an `Array<$itemTypeName>`.' + + "\nOften used in under-the-hood Flixel systems, like rendering," + + "\nwhere creating actual instances of objects every frame would balloon memory." + + "\n" + + "\n## Example" + + "\nIn the following example, see how it behaves quite similar to an Array" + + "\n```haxe" + + "\n var buffer = new FlxBuffer<{final x:Float; final y:Float;}>();" + + "\n for (i in 0...100)" + + "\n buffer.push({ x: i % 10, y: Std.int(i / 10) });" + + "\n " + + "\n buffer.shift();" + + "\n buffer.pop();" + + "\n " + + "\n for (i=>item in buffer)" + + "\n {" + + "\n trace('$i: $item');" + + "\n }" + + "\n```" + + "\n" + + "\n## Caveats" + + "\n- Can only be modified via `push`, `pop`, `shift` and `resize`. Missing notable" + + "\nfeatures like `insert` and setting via array access operator, these can be" + + "\nimplemented but are low priority" + + "\n- Editing items retrieved from the buffer will not edit the corresponding indicies," + + "\nfor that reason it is recommended to use final vars" + + "\n- all retrieved items must be handled via inline functions to avoid ever actually" + + "\ninstantiating an anonymous structure. This includes `Std.string(item)`"; + + // Add our overloaded push methods from before + def.fields.push(pushEachFunc); + + for (i => field in getters) + { + final fieldName = field.name; + final funcName = "get" + fieldName.charAt(0).toUpperCase() + fieldName.substr(1); + // Create the field in another class (for easy reification) then move it over + def.fields.push((macro class TempClass + { + /** Helper for `get(item).$name` */ + public inline function $funcName(index:Int) + { + return get(index).$fieldName; + } + }).fields[0]); + } + + // `macro class` gives a TDClass, so that needs to be replaced + // Determine our buffer's base + final listType = (isVector ? macro:openfl.Vector<$arrayCT> : macro:Array<$arrayCT>); + def.kind = TDAbstract(listType, [listType], [listType]); + Context.defineType(def); + + // Make our iterator + final iterDef = macro class $iterName + { + var list:$bufferCt; + var length:Int; + var i:Int; + + inline public function new(list:$bufferCt) + { + this.list = list; + this.length = list.length; + i = 0; + } + + inline public function hasNext() + { + return i < length; + } + + inline public function next() + { + return list[i++]; + } + } + + // `macro class` gives a TDClass, so that needs to be replaced + // iterDef.kind = TDClass(null, [], false, false, false); + Context.defineType(iterDef); + + // Make our key-value iterator + final kvIterDef = macro class $kvIterName + { + var list:$bufferCt; + var length:Int; + var i:Int; + + inline public function new(list:$bufferCt) + { + this.list = list; + this.length = list.length; + i = 0; + } + + inline public function hasNext() + { + return i < length; + } + + inline public function next() + { + final key = i++; + return { key:key, value:list.get(key) }; + } + } + + // `macro class` gives a TDClass, so that needs to be replaced + // iterDef.kind = TDClass(null, [], false, false, false); + Context.defineType(kvIterDef); + + // Return a `ComplexType` for the generated type + return bufferCt; + } + + static function getTypeIdentifier(type:Type) + { + return switch(type) + { + case TAnonymous(type): type.get().fields.map((f)->'${f.name}').join("_"); + default: getTypeName(type); + } + } + + static function getTypeName(type:Type) + { + return switch(type) + { + case TAbstract(type, []): type.get().name; + case TAnonymous(type): + '{ ${type.get().fields.map((f)->'${f.name}: ${getTypeName(f.type)}').join(", ")} }'; + case TInst(type, []): type.get().name; + case TType(type, []): type.get().name; + default: type.getName(); + } + } +} +#else + +@:genericBuild(flixel.system.FlxBuffer.BufferMacro.build(true)) +class FlxBuffer {} + +@:genericBuild(flixel.system.FlxBuffer.BufferMacro.build(false)) +class FlxBufferArray {} +#end \ No newline at end of file diff --git a/flixel/text/FlxBitmapText.hx b/flixel/text/FlxBitmapText.hx index b4b8aa86f1..bc9ac97171 100644 --- a/flixel/text/FlxBitmapText.hx +++ b/flixel/text/FlxBitmapText.hx @@ -4,15 +4,21 @@ import openfl.display.BitmapData; import flixel.FlxBasic; import flixel.FlxG; import flixel.FlxSprite; +import openfl.geom.ColorTransform; import flixel.graphics.frames.FlxBitmapFont; import flixel.graphics.frames.FlxFrame; +#if FLX_RENDER_TRIANGLE +import flixel.graphics.tile.FlxDrawTrianglesItem; +#else +import flixel.graphics.tile.FlxDrawQuadsItem; +#end import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.text.FlxText.FlxTextAlign; import flixel.text.FlxText.FlxTextBorderStyle; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; -import openfl.geom.ColorTransform; +import flixel.system.FlxBuffer; using flixel.util.FlxColorTransformUtil; @@ -191,9 +197,10 @@ class FlxBitmapText extends FlxSprite var pendingTextBitmapChange:Bool = true; var pendingPixelsChange:Bool = true; - var textData:Array; - var textDrawData:Array; - var borderDrawData:Array; + // var textData:Array; + var textData:FlxBuffer; + var textDrawData:FlxBuffer; + var borderDrawData:FlxBuffer; /** * Helper bitmap buffer for text pixels but without any color transformations @@ -227,10 +234,9 @@ class FlxBitmapText extends FlxSprite } else { - textData = []; - - textDrawData = []; - borderDrawData = []; + textData = new FlxBuffer(); + textDrawData = new FlxBuffer(); + borderDrawData = new FlxBuffer(); } this.text = text; @@ -318,79 +324,26 @@ class FlxBitmapText extends FlxSprite else { checkPendingChanges(true); - - var textLength:Int = Std.int(textDrawData.length / 3); - var borderLength:Int = Std.int(borderDrawData.length / 3); - - var dataPos:Int; - - var cr:Float = color.redFloat; - var cg:Float = color.greenFloat; - var cb:Float = color.blueFloat; - - var borderRed:Float = borderColor.redFloat * cr; - var borderGreen:Float = borderColor.greenFloat * cg; - var borderBlue:Float = borderColor.blueFloat * cb; - var bAlpha:Float = borderColor.alphaFloat * alpha; - - var textRed:Float = cr; - var textGreen:Float = cg; - var textBlue:Float = cb; - var tAlpha:Float = alpha; - - if (useTextColor) - { - textRed *= textColor.redFloat; - textGreen *= textColor.greenFloat; - textBlue *= textColor.blueFloat; - tAlpha *= textColor.alphaFloat; - } - - var bgRed:Float = cr; - var bgGreen:Float = cg; - var bgBlue:Float = cb; - var bgAlpha:Float = alpha; - - if (background) - { - bgRed *= backgroundColor.redFloat; - bgGreen *= backgroundColor.greenFloat; - bgBlue *= backgroundColor.blueFloat; - bgAlpha *= backgroundColor.alphaFloat; - } - - var drawItem; - var currFrame:FlxFrame = null; - var currTileX:Float = 0; - var currTileY:Float = 0; - var sx:Float = scale.x * _facingHorizontalMult; - var sy:Float = scale.y * _facingVerticalMult; - - var ox:Float = origin.x; - var oy:Float = origin.y; - - if (_facingHorizontalMult != 1) - { - ox = frameWidth - ox; - } - if (_facingVerticalMult != 1) + + final full = 0xFF*0xFF; + function multiplyColors(a:FlxColor, b:FlxColor) { - oy = frameHeight - oy; + return FlxColor.fromRGBFloat(a.red * b.red / full, a.green * b.green / full, a.blue * b.blue / full, a.alpha * b.alpha / full); } - - var clippedFrameRect; - + + final colorFullAlpha = color.rgb | 0xFF000000; + final borderColorMult = multiplyColors(borderColor, colorFullAlpha); + final bgColorMult = multiplyColors(backgroundColor, colorFullAlpha); + final textColorMult = useTextColor ? multiplyColors(textColor, colorFullAlpha) : colorFullAlpha; + + final clippedFrameRect = FlxRect.get(); if (clipRect != null) - { - clippedFrameRect = clipRect.intersection(FlxRect.weak(0, 0, frameWidth, frameHeight)); - - if (clippedFrameRect.isEmpty) - return; - } + clipRect.intersection(FlxRect.weak(0, 0, frameWidth, frameHeight), clippedFrameRect) else - { - clippedFrameRect = FlxRect.get(0, 0, frameWidth, frameHeight); - } + clippedFrameRect.set(0, 0, frameWidth, frameHeight); + + if (clippedFrameRect.isEmpty) + return; final cameras = getCamerasLegacy(); for (camera in cameras) @@ -411,91 +364,42 @@ class FlxBitmapText extends FlxSprite if (background) { + final offsetX = _facingHorizontalMult != 1 ? frameWidth - origin.x : origin.x; + final offsetY = _facingVerticalMult != 1 ? frameHeight - origin.y : origin.y; // backround tile transformations - currFrame = FlxG.bitmap.whitePixel; + final currFrame = FlxG.bitmap.whitePixel; _matrix.identity(); _matrix.scale(0.1 * clippedFrameRect.width, 0.1 * clippedFrameRect.height); - _matrix.translate(clippedFrameRect.x - ox, clippedFrameRect.y - oy); - _matrix.scale(sx, sy); + _matrix.translate(clippedFrameRect.x - offsetX, clippedFrameRect.y - offsetY); + _matrix.scale(scale.x * _facingHorizontalMult, scale.y * _facingVerticalMult); if (angle != 0) { _matrix.rotateWithTrig(_cosAngle, _sinAngle); } - _matrix.translate(_point.x + ox, _point.y + oy); - _colorParams.setMultipliers(bgRed, bgGreen, bgBlue, bgAlpha); + _matrix.translate(_point.x + offsetX, _point.y + offsetY); + _colorParams.setMultipliersFromColor(bgColorMult); camera.drawPixels(currFrame, null, _matrix, _colorParams, blend, antialiasing); } - var hasColorOffsets:Bool = (colorTransform != null && colorTransform.hasRGBAOffsets()); - - drawItem = camera.startQuadBatch(font.parent, true, hasColorOffsets, blend, antialiasing, shader); - - for (j in 0...borderLength) - { - dataPos = j * 3; - - currFrame = font.getCharFrame(Std.int(borderDrawData[dataPos])); - - currTileX = borderDrawData[dataPos + 1]; - currTileY = borderDrawData[dataPos + 2]; - - if (clipRect != null) - { - clippedFrameRect.copyFrom(clipRect).offset(-currTileX, -currTileY); - currFrame = currFrame.clipTo(clippedFrameRect); - } - - currFrame.prepareMatrix(_matrix); - _matrix.translate(currTileX - ox, currTileY - oy); - _matrix.scale(sx, sy); - if (angle != 0) - { - _matrix.rotateWithTrig(_cosAngle, _sinAngle); - } - - _matrix.translate(_point.x + ox, _point.y + oy); - _colorParams.setMultipliers(borderRed, borderGreen, borderBlue, bAlpha); - drawItem.addQuad(currFrame, _matrix, _colorParams); - } - - for (j in 0...textLength) - { - dataPos = j * 3; - - currFrame = font.getCharFrame(Std.int(textDrawData[dataPos])); - - currTileX = textDrawData[dataPos + 1]; - currTileY = textDrawData[dataPos + 2]; - - if (clipRect != null) - { - clippedFrameRect.copyFrom(clipRect).offset(-currTileX, -currTileY); - currFrame = currFrame.clipTo(clippedFrameRect); - } - - currFrame.prepareMatrix(_matrix); - _matrix.translate(currTileX - ox, currTileY - oy); - _matrix.scale(sx, sy); - if (angle != 0) - { - _matrix.rotateWithTrig(_cosAngle, _sinAngle); - } - - _matrix.translate(_point.x + ox, _point.y + oy); - _colorParams.setMultipliers(textRed, textGreen, textBlue, tAlpha); - drawItem.addQuad(currFrame, _matrix, _colorParams); - } + final hasColorOffsets:Bool = (colorTransform != null && colorTransform.hasRGBAOffsets()); + final drawItem = camera.startQuadBatch(font.parent, true, hasColorOffsets, blend, antialiasing, shader); + + // draw the text border + batchDrawData(drawItem, borderDrawData, borderColorMult); + // draw the actual text + batchDrawData(drawItem, textDrawData, textColorMult); + #if FLX_DEBUG FlxBasic.visibleCount++; #end } - + // dispose clipRect helpers clippedFrameRect.put(); - + #if FLX_DEBUG if (FlxG.debugger.drawDebug) { @@ -504,6 +408,56 @@ class FlxBitmapText extends FlxSprite #end } } + + #if FLX_RENDER_TRIANGLE + function batchDrawData(drawItem:FlxDrawTrianglesItem, list:FlxBuffer, color:FlxColor) + #else + function batchDrawData(drawItem:FlxDrawQuadsItem, list:FlxBuffer, color:FlxColor) + #end + { + _colorParams.setMultipliersFromColor(color); + + final scaleX = scale.x * _facingHorizontalMult; + final scaleY = scale.y * _facingVerticalMult; + + final offsetX = _facingHorizontalMult != 1 ? frameWidth - origin.x : origin.x; + final offsetY = _facingVerticalMult != 1 ? frameHeight - origin.y : origin.y; + + for (item in list) + { + final currFrame = getClippedCharFrame(item); + currFrame.prepareMatrix(_matrix); + _matrix.translate(item.x - offsetX, item.y - offsetY); + _matrix.scale(scaleX, scaleY); + if (angle != 0) + { + _matrix.rotateWithTrig(_cosAngle, _sinAngle); + } + + _matrix.translate(_point.x + offsetX, _point.y + offsetY); + // TODO: cull offscreen chars + drawItem.addQuad(currFrame, _matrix, _colorParams); + } + } + + /** + * Gets the character frame and clips it according to the clipRect + */ + inline function getClippedCharFrame(item:TileDrawItem) + { + final currFrame = font.getCharFrame(item.charCode); + + return if (clipRect == null) + currFrame; + else + { + final clippedFrameRect = FlxRect.get().copyFrom(clipRect); + clippedFrameRect.offset(-item.x, -item.y); + final result = currFrame.clipTo(clippedFrameRect); + clippedFrameRect.put(); + result; + } + } override function set_clipRect(Rect:FlxRect):FlxRect { @@ -1088,25 +1042,20 @@ class FlxBitmapText extends FlxSprite } else if (FlxG.renderTile) { - textData.splice(0, textData.length); + textData.resize(0); } _fieldWidth = frameWidth; - var numLines:Int = _lines.length; - var line:UnicodeString; - var lineWidth:Int; - - var ox:Int, oy:Int; - + final numLines:Int = _lines.length; for (i in 0...numLines) { - line = _lines[i]; - lineWidth = _linesWidth[i]; + final line:UnicodeString = _lines[i]; + final lineWidth = _linesWidth[i]; // LEFT - ox = font.minOffsetX; - oy = i * (font.lineHeight + lineSpacing) + padding; + var ox = font.minOffsetX; + var oy = i * (font.lineHeight + lineSpacing) + padding; if (alignment == FlxTextAlign.CENTER) { @@ -1151,17 +1100,14 @@ class FlxBitmapText extends FlxSprite function blitLine(line:UnicodeString, startX:Int, startY:Int):Void { - var data:Array = []; + final data = new FlxBuffer(); addLineData(line, startX, startY, data); while (data.length > 0) { - final charCode = Std.int(data.shift()); - final x = data.shift(); - final y = data.shift(); - - final charFrame = font.getCharFrame(charCode); - _flashPoint.setTo(x, y); + final item = data.shift(); + final charFrame = font.getCharFrame(item.charCode); + _flashPoint.setTo(item.x, item.y); charFrame.paint(textBitmap, _flashPoint, true); } } @@ -1174,10 +1120,8 @@ class FlxBitmapText extends FlxSprite addLineData(line, startX, startY, textData); } - function addLineData(line:UnicodeString, startX:Int, startY:Int, data:Array) + function addLineData(line:UnicodeString, startX:Int, startY:Int, data:FlxBuffer) { - var pos:Int = data.length; - var curX:Float = startX; var curY:Int = startY; @@ -1202,9 +1146,7 @@ class FlxBitmapText extends FlxSprite final hasFrame = font.charExists(charCode); if (hasFrame && !isSpace) { - data[pos++] = charCode; - data[pos++] = curX; - data[pos++] = curY; + data.push(charCode, curX, curY); } if (hasFrame || isSpace) @@ -1278,8 +1220,8 @@ class FlxBitmapText extends FlxSprite } else { - textDrawData.splice(0, textDrawData.length); - borderDrawData.splice(0, borderDrawData.length); + textDrawData.resize(0); + borderDrawData.resize(0); } // use local var to avoid get_width and recursion @@ -1441,32 +1383,24 @@ class FlxBitmapText extends FlxSprite if (!FlxG.renderTile) return; - var data:Array = isFront ? textDrawData : borderDrawData; + final data = isFront ? textDrawData : borderDrawData; - var pos:Int = data.length; - var textPos:Int; - var textLen:Int = Std.int(textData.length / 3); - var rect = FlxRect.get(); - var frameVisible; + final rect = FlxRect.get(); + final textLen:Int = textData.length; for (i in 0...textLen) { - textPos = 3 * i; - - frameVisible = true; - + final textPos = i; + final item = textData.get(textPos); + if (clipRect != null) { - rect.copyFrom(clipRect).offset(-textData[textPos + 1] - posX, -textData[textPos + 2] - posY); - frameVisible = font.getCharFrame(Std.int(textData[textPos])).clipTo(rect).type != FlxFrameType.EMPTY; - } - - if (frameVisible) - { - data[pos++] = textData[textPos]; - data[pos++] = textData[textPos + 1] + posX; - data[pos++] = textData[textPos + 2] + posY; + rect.copyFrom(clipRect).offset(-item.x - posX, -item.y - posY); + if (font.getCharFrame(item.charCode).clipTo(rect).type == FlxFrameType.EMPTY) + continue; } + + data.push(item.charCodeRaw, item.x + posX, item.y + posY); } rect.put(); @@ -1805,6 +1739,19 @@ enum WordSplitConditions WIDTH(minPixels:Int); } +@:noCompletion +private typedef TileDrawItemRaw = { final charCodeRaw:Float; final x:Float; final y:Float; }; +@:forward +/** + * Internal type used for batch rendering characters, used by FlxBuffer + * **Warning:** all methods must be inlined + */ +abstract TileDrawItem(TileDrawItemRaw) from TileDrawItemRaw +{ + public var charCode(get, never):Int; + inline function get_charCode() return Std.int(this.charCodeRaw); +} + /* * TODO - enum WordSplitMethod: determines how words look when split, ex: * * whether split words start on a new line diff --git a/flixel/util/FlxColorTransformUtil.hx b/flixel/util/FlxColorTransformUtil.hx index c13f7b4e01..f66d3811e0 100644 --- a/flixel/util/FlxColorTransformUtil.hx +++ b/flixel/util/FlxColorTransformUtil.hx @@ -13,6 +13,15 @@ class FlxColorTransformUtil return transform; } + + /** + * Helper for transform.setMultipliers(color.redFloat, color.greenFloat, color.blueFloat, color.alphaFloat) + * @since 5.9.0 + */ + public static inline function setMultipliersFromColor(transform:ColorTransform, color:FlxColor):ColorTransform + { + return setMultipliers(transform, color.redFloat, color.greenFloat, color.blueFloat, color.alphaFloat); + } public static function setOffsets(transform:ColorTransform, red:Float, green:Float, blue:Float, alpha:Float):ColorTransform { @@ -23,7 +32,16 @@ class FlxColorTransformUtil return transform; } - + + /** + * Helper for transform.setOffsets(color.redFloat, color.greenFloat, color.blueFloat, color.alphaFloat) + * @since 5.9.0 + */ + public static inline function setOffsetsFromColor(transform:ColorTransform, color:FlxColor):ColorTransform + { + return setOffsets(transform, color.redFloat, color.greenFloat, color.blueFloat, color.alphaFloat); + } + /** * Returns whether red, green, or blue multipliers are set to anything other than 1. */ diff --git a/tests/unit/src/FlxAssert.hx b/tests/unit/src/FlxAssert.hx index 1c501493ac..b083700050 100644 --- a/tests/unit/src/FlxAssert.hx +++ b/tests/unit/src/FlxAssert.hx @@ -124,4 +124,13 @@ class FlxAssert else Assert.fail('Value [$actual] is not within [$margin] of [( x:$expectedX | y:$expectedY )]', info); } + + public static function allEqual(expected:T, results:Array, ?msg:String, ?info:PosInfos) + { + for (i=>actual in results) + { + final message = msg != null ? msg : 'Value $i [$actual] was not equal to expected value [$expected]'; + Assert.areEqual(expected, actual, msg, info); + } + } } diff --git a/tests/unit/src/flixel/math/FlxRectTest.hx b/tests/unit/src/flixel/math/FlxRectTest.hx index b2faa081ad..2f501a65de 100644 --- a/tests/unit/src/flixel/math/FlxRectTest.hx +++ b/tests/unit/src/flixel/math/FlxRectTest.hx @@ -94,4 +94,60 @@ class FlxRectTest extends FlxTest pivot.put(); expected.put(); } + + @Test + function testIntersection() + { + rect1.set(0, 0, 100, 100); + rect2.set(50, 50, 100, 100); + + final expected = FlxRect.get(50, 50, 50, 50); + final result = FlxRect.get(); + rect1.intersection(rect2, result); + FlxAssert.rectsNear(expected, result, 0.0001); + + expected.put(); + result.put(); + } + + @Test + function testIntersectionEmpty() + { + rect1.set(0, 0, 100, 100); + rect2.set(200, 200, 100, 100); + + final expected = FlxRect.get(0, 0, 0, 0); + final result = FlxRect.get(1000, 1000, 1000, 1000); + rect1.intersection(rect2, result); + FlxAssert.rectsNear(expected, result, 0.0001); + + expected.put(); + result.put(); + } + + @Test + function testClipTo() + { + rect1.set(0, 0, 100, 100); + rect2.set(50, 50, 100, 100); + + final expected = FlxRect.get(50, 50, 50, 50); + rect1.clipTo(rect2); + FlxAssert.rectsNear(expected, rect1, 0.0001); + + expected.put(); + } + + @Test + function testClipToEmpty() + { + rect1.set(0, 0, 100, 100); + rect2.set(200, 200, 100, 100); + + final expected = FlxRect.get(0, 0, 0, 0); + rect1.clipTo(rect2); + FlxAssert.rectsNear(expected, rect1, 0.0001); + + expected.put(); + } } diff --git a/tests/unit/src/flixel/system/FlxBufferTest.hx b/tests/unit/src/flixel/system/FlxBufferTest.hx new file mode 100644 index 0000000000..eb54bba66c --- /dev/null +++ b/tests/unit/src/flixel/system/FlxBufferTest.hx @@ -0,0 +1,206 @@ +package flixel.system; + +import flixel.system.FlxBuffer; +import massive.munit.Assert; + +class FlxBufferTest +{ + @Test + function testVector() + { + final bufferStr = new FlxBuffer<{ x:String, y:String }>(); + for (i in 0...100) + bufferStr.push({ x: Std.string(i % 10), y: Std.string(Std.int(i / 10)) }); + + bufferStr.shift(); + bufferStr.pop(); + + // make sure it compiles + for (i=>item in bufferStr) + { + item; + i; + } + + final bufferTD = new FlxBuffer<{ x:Float, y:Float }>(); + final bufferTD2 = new FlxBuffer(); + Assert.areEqual(1, bufferTD.push(5, 10)); + Assert.areEqual(5, bufferTD.getX(0)); + Assert.areEqual(10, bufferTD.getY(0)); + + final buffer = new FlxBuffer(); + for (i in 0...100) + buffer.push({ x: i % 10, y: Std.int(i / 10) }); + + Assert.areEqual(5, buffer.getX(15)); + Assert.areEqual(1, buffer.getY(15)); + Assert.areEqual(6, buffer.getSum(15)); + + buffer.set(0, { x: 1000, y: 1000 }); + FlxAssert.allEqual(2000.0, [buffer.getSum(0), buffer.get(0).sum]); + buffer.insert(1, { x: 500, y: 500 }); + FlxAssert.allEqual(1000.0, [buffer.getSum(1), buffer.get(1).sum]); + + // make sure it compiles + for (i=>item in buffer) + { + item; + i; + } + + Assert.areEqual(2000, buffer.shift().sum); + Assert.areEqual(18, buffer.pop().sum); + } + + @Test + function testArray() + { + final bufferStr = new FlxBufferArray<{ x:String, y:String }>(); + for (i in 0...100) + bufferStr.push({ x: Std.string(i % 10), y: Std.string(Std.int(i / 10)) }); + + bufferStr.shift(); + bufferStr.pop(); + + // make sure it compiles + for (i=>item in bufferStr) + { + item; + i; + } + + final bufferTD = new FlxBufferArray<{ x:Float, y:Float }>(); + final bufferTD2 = new FlxBufferArray(); + Assert.areEqual(1, bufferTD.push(5, 10)); + Assert.areEqual(5, bufferTD.getX(0)); + Assert.areEqual(10, bufferTD.getY(0)); + + final buffer = new FlxBufferArray(); + for (i in 0...100) + buffer.push({ x: i % 10, y: Std.int(i / 10) }); + + Assert.areEqual(5, buffer.getX(15)); + Assert.areEqual(1, buffer.getY(15)); + Assert.areEqual(6, buffer.getSum(15)); + + buffer.set(0, { x: 1000, y: 1000 }); + FlxAssert.allEqual(2000.0, [buffer.getSum(0), buffer.get(0).sum]); + buffer.insert(1, { x: 500, y: 500 }); + FlxAssert.allEqual(1000.0, [buffer.getSum(1), buffer.get(1).sum]); + + // make sure it compiles + for (i=>item in buffer) + { + item; + i; + } + + Assert.areEqual(2000, buffer.shift().sum); + Assert.areEqual(18, buffer.pop().sum); + } + + @Test + function testArgOrder() + { + final bVector = new FlxBuffer<{ d:Int, c:Int, b:Int, a:Int }>(); + final bArray = new FlxBufferArray<{ d:Int, c:Int, b:Int, a:Int }>(); + + bVector.push(0, 10, 20, 30); + bArray.push(0, 10, 20, 30); + FlxAssert.allEqual( 0.0, [ bVector.getD(0), bVector.get(0).d, bArray.getD(0), bArray.get(0).d]); + FlxAssert.allEqual(10.0, [ bVector.getC(0), bVector.get(0).c, bArray.getC(0), bArray.get(0).c]); + FlxAssert.allEqual(20.0, [ bVector.getB(0), bVector.get(0).b, bArray.getB(0), bArray.get(0).b]); + FlxAssert.allEqual(30.0, [ bVector.getA(0), bVector.get(0).a, bArray.getA(0), bArray.get(0).a]); + + bVector.push({ d:5, c:15, b:25, a:35 }); + bArray.push({ d:5, c:15, b:25, a:35 }); + FlxAssert.allEqual( 5.0, [ bVector.getD(1), bVector.get(1).d, bArray.getD(1), bArray.get(1).d]); + FlxAssert.allEqual(15.0, [ bVector.getC(1), bVector.get(1).c, bArray.getC(1), bArray.get(1).c]); + FlxAssert.allEqual(25.0, [ bVector.getB(1), bVector.get(1).b, bArray.getB(1), bArray.get(1).b]); + FlxAssert.allEqual(35.0, [ bVector.getA(1), bVector.get(1).a, bArray.getA(1), bArray.get(1).a]); + + bVector.insert(1, { d:7, c:17, b:27, a:37 }); + bArray.insert(1, { d:7, c:17, b:27, a:37 }); + FlxAssert.allEqual( 7.0, [ bVector.getD(1), bVector.get(1).d, bArray.getD(1), bArray.get(1).d]); + FlxAssert.allEqual(17.0, [ bVector.getC(1), bVector.get(1).c, bArray.getC(1), bArray.get(1).c]); + FlxAssert.allEqual(27.0, [ bVector.getB(1), bVector.get(1).b, bArray.getB(1), bArray.get(1).b]); + FlxAssert.allEqual(37.0, [ bVector.getA(1), bVector.get(1).a, bArray.getA(1), bArray.get(1).a]); + + bVector.set(2, { d:8, c:18, b:28, a:38 }); + bArray.set(2, { d:8, c:18, b:28, a:38 }); + FlxAssert.allEqual( 8.0, [ bVector.getD(2), bVector.get(2).d, bArray.getD(2), bArray.get(2).d]); + FlxAssert.allEqual(18.0, [ bVector.getC(2), bVector.get(2).c, bArray.getC(2), bArray.get(2).c]); + FlxAssert.allEqual(28.0, [ bVector.getB(2), bVector.get(2).b, bArray.getB(2), bArray.get(2).b]); + FlxAssert.allEqual(38.0, [ bVector.getA(2), bVector.get(2).a, bArray.getA(2), bArray.get(2).a]); + } + + @Test + function testSlice() + { + final bVector = new FlxBuffer(); + final bArray = new FlxBufferArray(); + final array = new Array(); + for (i in 0...100) + { + final item:XYAbs = { x: i % 10, y: Std.int(i / 10) }; + bVector.push(item); + bArray.push(item); + array.push(item); + } + + FlxAssert.allEqual(100, + [ bVector.length, bVector.slice(0).length + , bArray.length, bArray.slice(0).length + , array.length, array.slice(0).length + ]); + FlxAssert.allEqual(900.0, + [ getTotalSum(bVector), getTotalSum(bVector.slice(0)) + , getTotalSum( bArray), getTotalSum( bArray.slice(0)) + , getTotalSum( array), getTotalSum( array.slice(0)) + ]); + FlxAssert.allEqual(575.0, + [ getTotalSum(bVector.slice(50)) + , getTotalSum( bArray.slice(50)) + , getTotalSum( array.slice(50)) + ]); + FlxAssert.allEqual(450.0, + [ getTotalSum(bVector.slice(25, 75)), getTotalSum(bVector.slice(25, -25)) + , getTotalSum( bArray.slice(25, 75)), getTotalSum( bArray.slice(25, -25)) + , getTotalSum( array.slice(25, 75)), getTotalSum( array.slice(25, -25)) + ]); + } + + public overload extern inline function getTotalSum(buffer:FlxBuffer) + { + return getTotalSum((cast buffer.iterator():Iterator)); + } + + public overload extern inline function getTotalSum(buffer:FlxBufferArray) + { + return getTotalSum((cast buffer.iterator():Iterator)); + } + + public overload extern inline function getTotalSum(array:Array) + { + return getTotalSum(array.iterator()); + } + + public overload extern inline function getTotalSum(iter:Iterator) + { + // return Lambda.fold(buffer, (item, total)->total + item.sum, 0); + var total = 0.0; + for (item in iter) + total += item.sum; + return total; + } +} + +typedef XY = { x:Float, y:Float } +@:forward +abstract XYAbs(XY) from XY +{ + public var sum(get, never):Float; + inline function get_sum() { return this.x + this.y; } + + public function toString() { return '( ${this.x} | ${this.y} )'; } +} \ No newline at end of file