HaxeFlixel/flixel

View on GitHub
flixel/graphics/frames/FlxTileFrames.hx

Summary

Maintainability
Test Coverage
package flixel.graphics.frames;

import openfl.display.BitmapData;
import openfl.geom.Point;
import flixel.graphics.FlxGraphic;
import flixel.graphics.frames.FlxFramesCollection.FlxFrameCollectionType;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.util.FlxBitmapDataUtil;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;

/**
 * Spritesheet frame collection. It is used for tilemaps and animated sprites.
 */
class FlxTileFrames extends FlxFramesCollection
{
    /**
     * Atlas frame from which this frame collection had been generated.
     * Could be `null` if this collection generated from rectangle.
     */
    var atlasFrame:FlxFrame;

    /**
     * Image region of the image from which this frame collection had been generated.
     */
    var region:FlxRect;

    /**
     * The size of frame in this spritesheet.
     */
    public var tileSize:FlxPoint;

    /**
     * Offsets between frames in this spritesheet.
     */
    var tileSpacing:FlxPoint;

    public var numRows:Int = 0;

    public var numCols:Int = 0;

    function new(parent:FlxGraphic, ?border:FlxPoint)
    {
        super(parent, FlxFrameCollectionType.TILES, border);
    }

    /**
     * Gets frame by its "position" in spritesheet.
     */
    public inline function getByTilePosition(column:Int, row:Int):FlxFrame
    {
        return frames[row * numCols + column];
    }

    /**
     * Gets source `BitmapData`, generates new `BitmapData` with spaces between frames
     * (if there is no such `BitmapData` in the cache already) and creates `FlxTileFrames` collection.
     *
     * @param   source        The source of graphic for frame collection.
     * @param   tileSize      The size of tiles in spritesheet.
     * @param   tileSpacing   Desired offsets between frames in spritesheet
     *                        (this method takes spritesheet bitmap without offsets between frames and adds them).
     * @param   tileBorder    Border to add around tiles (helps to avoid "tearing" problem).
     * @param   region        Region of image to generate spritesheet from. Default value is `null`, which means that
     *                        the whole image will be used for spritesheet generation.
     * @return   Newly created spritesheet.
     */
    public static function fromBitmapAddSpacesAndBorders(source:FlxGraphicAsset, tileSize:FlxPoint, ?tileSpacing:FlxPoint, ?tileBorder:FlxPoint,
            ?region:FlxRect):FlxTileFrames
    {
        var graphic:FlxGraphic = FlxG.bitmap.add(source, false);
        if (graphic == null)
            return null;

        var key:String = FlxG.bitmap.getKeyWithSpacesAndBorders(graphic.key, tileSize, tileSpacing, tileBorder, region);
        var result:FlxGraphic = FlxG.bitmap.get(key);
        if (result == null)
        {
            var bitmap:BitmapData = FlxBitmapDataUtil.addSpacesAndBorders(graphic.bitmap, tileSize, tileSpacing, tileBorder, region);
            result = FlxG.bitmap.add(bitmap, false, key);
        }

        var borderX:Int = 0;
        var borderY:Int = 0;

        if (tileBorder != null)
        {
            borderX = Std.int(tileBorder.x);
            borderY = Std.int(tileBorder.y);
        }

        var tileFrames:FlxTileFrames = FlxTileFrames.fromGraphic(result, FlxPoint.get().addPoint(tileSize).add(2 * borderX, 2 * borderY), null, tileSpacing);

        if (tileBorder == null)
            return tileFrames;

        return tileFrames.addBorder(tileBorder);
    }

    /**
     * Gets `FlxFrame` object, generates new `BitmapData` with spaces between tiles in the frame
     * (if there is no such `BitmapData` in the cache already) and creates a `FlxTileFrames` collection.
     *
     * @param   frame         Frame to generate tiles from.
     * @param   tileSize      the size of tiles in spritesheet.
     * @param   tileSpacing   desired offsets between frames in spritesheet.
     *                        (this method takes spritesheet bitmap without offsets between frames and adds them).
     * @param   tileBorder    Border to add around tiles (helps to avoid "tearing" problem).
     * @return  Newly created spritesheet.
     */
    public static function fromFrameAddSpacesAndBorders(frame:FlxFrame, tileSize:FlxPoint, ?tileSpacing:FlxPoint, ?tileBorder:FlxPoint):FlxTileFrames
    {
        var bitmap:BitmapData = frame.paint();
        return FlxTileFrames.fromBitmapAddSpacesAndBorders(bitmap, tileSize, tileSpacing, tileBorder);
    }

    /**
     * Generates spritesheet frame collection from provided frame. Can be useful for spritesheets packed into atlases.
     * It can generate spritesheets from rotated and cropped frames also,
     * which is important for devices with limited memory.
     *
     * @param   frame         Frame, containing spritesheet image
     * @param   tileSize      The size of tiles in spritesheet
     * @param   tileSpacing   Offsets between frames in spritesheet.
     *                        Default value is `null`, which means no offsets between tiles.
     * @return  Newly created spritesheet frame collection.
     */
    public static function fromFrame(frame:FlxFrame, tileSize:FlxPoint, ?tileSpacing:FlxPoint):FlxTileFrames
    {
        var graphic:FlxGraphic = frame.parent;
        // find TileFrames object, if there is one already
        var tileFrames:FlxTileFrames = FlxTileFrames.findFrame(graphic, tileSize, null, frame, tileSpacing);
        if (tileFrames != null)
            return tileFrames;

        // or create it, if there is no such object
        tileSpacing = (tileSpacing != null) ? tileSpacing : FlxPoint.get(0, 0);

        tileFrames = new FlxTileFrames(graphic);
        tileFrames.atlasFrame = frame;
        tileFrames.region = frame.frame;
        tileFrames.tileSize = tileSize;
        tileFrames.tileSpacing = tileSpacing;

        tileSpacing.floor();
        tileSize.floor();

        var spacedWidth:Float = tileSize.x + tileSpacing.x;
        var spacedHeight:Float = tileSize.y + tileSpacing.y;

        var numRows:Int = (tileSize.y == 0) ? 1 : Std.int((frame.sourceSize.y + tileSpacing.y) / spacedHeight);
        var numCols:Int = (tileSize.x == 0) ? 1 : Std.int((frame.sourceSize.x + tileSpacing.x) / spacedWidth);

        var helperRect:FlxRect = FlxRect.get(0, 0, tileSize.x, tileSize.y);

        for (j in 0...numRows)
        {
            for (i in 0...numCols)
            {
                helperRect.x = spacedWidth * i;
                helperRect.y = spacedHeight * j;
                tileFrames.pushFrame(frame.subFrameTo(helperRect));
            }
        }

        helperRect = FlxDestroyUtil.put(helperRect);

        tileFrames.numCols = numCols;
        tileFrames.numRows = numRows;
        return tileFrames;
    }

    /**
     * Just generates tile frames collection from specified array of frames.
     *
     * @param   Frames   `Array` of frames to generate tile frames from.
     *                   They all should have the same source size and parent graphic.
     *                   If not then `null` will be returned.
     * @return  Generated collection of frames.
     */
    public static function fromFrames(Frames:Array<FlxFrame>):FlxTileFrames
    {
        var firstFrame:FlxFrame = Frames[0];
        var graphic:FlxGraphic = firstFrame.parent;

        for (frame in Frames)
        {
            if (frame.parent != firstFrame.parent || !frame.sourceSize.equals(firstFrame.sourceSize))
            {
                // frames doesn't have the same size and parent graphic
                return null;
            }
        }

        var tileFrames:FlxTileFrames = new FlxTileFrames(graphic);

        tileFrames.region = null;
        tileFrames.atlasFrame = null;
        tileFrames.tileSize = FlxPoint.get().copyFrom(firstFrame.sourceSize);
        tileFrames.tileSpacing = FlxPoint.get(0, 0);
        tileFrames.numCols = Frames.length;
        tileFrames.numRows = 1;

        for (frame in Frames)
        {
            tileFrames.frames.push(frame);

            if (frame.name != null)
                tileFrames.framesByName.set(frame.name, frame);
        }

        return tileFrames;
    }

    /**
     * Creates new a `FlxTileFrames` collection from atlas frames which begin with
     * a common name (e.g. `"tiles-"`) and differ in indices (e.g. `"001"`, `"002"`, etc.).
     * This method is similar to `FlxAnimationController`'s `addByPrefix()`.
     *
     * @param    Frames   Collection of atlas frames to generate tiles from.
     * @param    Prefix   Common beginning of image names in atlas (e.g. `"tiles-"`).
     * @return   Generated tile frames collection.
     */
    public static function fromAtlasByPrefix(Frames:FlxAtlasFrames, Prefix:String):FlxTileFrames
    {
        var framesToAdd = new Array<FlxFrame>();

        for (frame in Frames.frames)
        {
            if (StringTools.startsWith(frame.name, Prefix))
                framesToAdd.push(frame);
        }

        if (framesToAdd.length > 0)
        {
            var name:String = framesToAdd[0].name;
            var postIndex:Int = name.indexOf(".", Prefix.length);
            var suffix:String = name.substring(postIndex == -1 ? name.length : postIndex, name.length);

            FlxFrame.sortFrames(framesToAdd, Prefix, suffix);
            return FlxTileFrames.fromFrames(framesToAdd);
        }

        return null;
    }

    /**
     * Generates spritesheet frame collection from provided region of image.
     *
     * @param   graphic       Source graphic for spritesheet.
     * @param   tileSize      The size of tiles in spritesheet.
     * @param   region        Region of image to use for spritesheet generation. Default value is `null`,
     *                        which means that the whole image will be used for it.
     * @param   tileSpacing   Offsets between frames in spritesheet.
     *                        Default value is `null`, which means no offsets between tiles.
     * @return  Newly created spritesheet frame collection.
     */
    public static function fromGraphic(graphic:FlxGraphic, tileSize:FlxPoint, ?region:FlxRect, ?tileSpacing:FlxPoint):FlxTileFrames
    {
        // find TileFrames object, if there is one already
        var tileFrames:FlxTileFrames = FlxTileFrames.findFrame(graphic, tileSize, region, null, tileSpacing);
        if (tileFrames != null)
            return tileFrames;

        // or create it, if there is no such object
        if (region == null)
        {
            region = FlxRect.get(0, 0, graphic.width, graphic.height);
        }
        else
        {
            if (region.width == 0)
                region.width = graphic.width - region.x;

            if (region.height == 0)
                region.height = graphic.height - region.y;
        }

        tileSpacing = (tileSpacing != null) ? tileSpacing : FlxPoint.get(0, 0);

        tileFrames = new FlxTileFrames(graphic);
        tileFrames.region = region;
        tileFrames.atlasFrame = null;
        tileFrames.tileSize = tileSize;
        tileFrames.tileSpacing = tileSpacing;

        region.floor();
        tileSpacing.floor();
        tileSize.floor();

        var spacedWidth:Float = tileSize.x + tileSpacing.x;
        var spacedHeight:Float = tileSize.y + tileSpacing.y;

        var numRows:Int = (tileSize.y == 0) ? 1 : Std.int((region.height + tileSpacing.y) / spacedHeight);
        var numCols:Int = (tileSize.x == 0) ? 1 : Std.int((region.width + tileSpacing.x) / spacedWidth);

        var tileRect:FlxRect;

        for (j in 0...numRows)
        {
            for (i in 0...numCols)
            {
                tileRect = FlxRect.get(region.x + i * spacedWidth, region.y + j * spacedHeight, tileSize.x, tileSize.y);
                tileFrames.addSpriteSheetFrame(tileRect);
            }
        }

        tileFrames.numCols = numCols;
        tileFrames.numRows = numRows;
        return tileFrames;
    }

    /**
     * Generates a spritesheet frame collection from the provided image region.
     *
     * @param   source        Source graphic for the spritesheet.
     * @param   tileSize      The size of tiles in spritesheet.
     * @param   region        Region of image to use for spritesheet generation. Default value is `null`,
     *                        which means that whole image will be used for it.
     * @param   tileSpacing   Offsets between frames in spritesheet.
     *                        Default value is `null`, which means no offsets between tiles.
     * @return  Newly created spritesheet frame collection.
     */
    public static function fromRectangle(source:FlxGraphicAsset, tileSize:FlxPoint, ?region:FlxRect, ?tileSpacing:FlxPoint):FlxTileFrames
    {
        var graphic:FlxGraphic = FlxG.bitmap.add(source, false);
        if (graphic == null)
            return null;
        return fromGraphic(graphic, tileSize, region, tileSpacing);
    }

    /**
     * This method takes array of tileset bitmaps and the size of
     * tiles in them and then combine them in one big tileset.
     * The order of bitmaps in the array is important.
     *
     * ```haxe
     * var combinedFrames = FlxTileFrames.combineTileSets(bitmaps, FlxPoint.get(16, 16));
     * tilemap.loadMapFromCSV(mapData, combinedFrames);
     * ```
     *
     * or
     *
     * ```haxe
     * sprite.frames = combinedFrames;
     * ```
     *
     * @param   bitmaps    tilesets
     * @param   tileSize   The size of tiles (tilesets should have tiles of the same size).
     * @return  Atlas frames collection, which you can load in tilemaps or sprites:
     */
    public static function combineTileSets(bitmaps:Array<BitmapData>, tileSize:FlxPoint, ?spacing:FlxPoint, ?border:FlxPoint):FlxTileFrames
    {
        var framesCollections:Array<FlxTileFrames> = [];

        for (bitmap in bitmaps)
            framesCollections.push(FlxTileFrames.fromRectangle(bitmap, tileSize));

        return combineTileFrames(framesCollections, spacing, border);
    }

    /**
     * This method takes array of tile frames collections and then combine them in one big tileset.
     * The order of bitmaps in array is important.
     *
     *
     * ```haxe
     * var combinedFrames = FlxTileFrames.combineTileFrames(tileframes);
     * tilemap.loadMapFromCSV(mapData, combinedFrames);
     * ```
     *
     * or
     *
     * ```haxe
     * sprite.frames = combinedFrames;
     * ```
     *
     * @param   tileframes   Tile frames collection to combine tiles from.
     * @return  Atlas frames collection, which you can load in tilemaps or sprites:
     */
    public static function combineTileFrames(tileframes:Array<FlxTileFrames>, ?spacing:FlxPoint, ?border:FlxPoint):FlxTileFrames
    {
        // we need to calculate the size of result bitmap first
        var totalArea:Int = 0;
        var rows:Int = 0;
        var cols:Int = 0;

        var tileWidth:Int = Std.int(tileframes[0].frames[0].sourceSize.x);
        var tileHeight:Int = Std.int(tileframes[0].frames[0].sourceSize.y);

        var spaceX:Int = 0;
        var spaceY:Int = 0;

        if (spacing != null)
        {
            spaceX = Std.int(spacing.x);
            spaceY = Std.int(spacing.y);
        }

        var borderX:Int = 0;
        var borderY:Int = 0;

        if (border != null)
        {
            borderX = Std.int(border.x);
            borderY = Std.int(border.y);
        }

        for (collection in tileframes)
        {
            cols = collection.numCols;
            rows = collection.numRows;
            totalArea += Std.int(cols * (tileWidth + 2 * borderX) * rows * (tileHeight + 2 * borderY));
        }

        var side:Float = Math.sqrt(totalArea);
        cols = Std.int(side / (tileWidth + 2 * borderX));
        rows = Math.ceil(totalArea / (cols * (tileWidth + 2 * borderX) * (tileHeight + 2 * borderY)));
        var width:Int = Std.int(cols * (tileWidth + 2 * borderX)) + (cols - 1) * spaceX;
        var height:Int = Std.int(rows * (tileHeight + 2 * borderY)) + (rows - 1) * spaceY;

        // now we'll create result atlas and will blit every tile on it.
        var combined:BitmapData = new BitmapData(width, height, true, FlxColor.TRANSPARENT);
        var graphic:FlxGraphic = FlxG.bitmap.add(combined);
        var result:FlxTileFrames = new FlxTileFrames(graphic);
        var destPoint:Point = new Point(borderX, borderY);

        result.region = FlxRect.get(0, 0, width, height);
        result.atlasFrame = null;
        result.tileSize = FlxPoint.get(tileWidth, tileHeight);
        result.tileSpacing = FlxPoint.get(spaceX, spaceY);
        result.numCols = cols;
        result.numRows = rows;
        // paint frames on result canvas with spaces between frames
        for (collection in tileframes)
        {
            for (frame in collection.frames)
            {
                frame.paint(combined, destPoint, true);

                result.addAtlasFrame(FlxRect.get(destPoint.x, destPoint.y, tileWidth, tileHeight), FlxPoint.get(tileWidth, tileHeight), FlxPoint.get(0, 0));
                destPoint.x += tileWidth + 2 * borderX + spaceX;

                if (destPoint.x >= combined.width)
                {
                    destPoint.x = borderX;
                    destPoint.y += tileHeight + 2 * borderY + spaceY;
                }
            }
        }
        // and copy pixels around frames
        FlxBitmapDataUtil.copyBorderPixels(combined, tileWidth, tileHeight, spaceX, spaceY, borderX, borderY, cols, rows);
        return result;
    }

    /**
     * Searches `FlxTileFrames` object for a specified `FlxGraphic` object
     * which has the same parameters (frame size, frame spacings, region of image, etc.).
     *
     * @param   graphic       `FlxGraphic` object to search `FlxTileFrames` for.
     * @param   tileSize      The size of tiles in TileFrames.
     * @param   region        The region of source image used for spritesheet generation.
     * @param   atlasFrame    Optional `FlxFrame` object used for spritesheet generation.
     * @param   tileSpacing   Spaces between tiles in spritesheet.
     * @return  `FlxTileFrames` object which corresponds to specified arguments.
     *          Could be null if there is no such `FlxTileFrames`.
     */
    public static function findFrame(graphic:FlxGraphic, tileSize:FlxPoint, ?region:FlxRect, ?atlasFrame:FlxFrame, ?tileSpacing:FlxPoint,
            ?border:FlxPoint):FlxTileFrames
    {
        var tileFrames:Array<FlxTileFrames> = cast graphic.getFramesCollections(FlxFrameCollectionType.TILES);

        for (sheet in tileFrames)
        {
            if (sheet.equals(tileSize, region, null, tileSpacing, border))
                return sheet;
        }

        return null;
    }

    /**
     * TileFrames comparison method. For internal use.
     */
    public function equals(tileSize:FlxPoint, ?region:FlxRect, ?atlasFrame:FlxFrame, ?tileSpacing:FlxPoint, ?border:FlxPoint):Bool
    {
        if (this.region == null && this.atlasFrame == null)
        {
            return false;
        }

        if (atlasFrame != null)
        {
            region = atlasFrame.frame;
        }

        if (region == null)
            region = FlxRect.weak(0, 0, parent.width, parent.height);

        if (tileSpacing == null)
            tileSpacing = FlxPoint.weak();

        if (border == null)
            border = FlxPoint.weak();

        return this.atlasFrame == atlasFrame
            && this.region.equals(region)
            && this.tileSize.equals(tileSize)
            && this.tileSpacing.equals(tileSpacing)
            && this.border.equals(border);
    }

    override public function addBorder(border:FlxPoint):FlxTileFrames
    {
        var resultBorder:FlxPoint = FlxPoint.get().addPoint(this.border).addPoint(border);
        var resultSize:FlxPoint = FlxPoint.get().copyFrom(tileSize).subtract(2 * border.x, 2 * border.y);
        var tileFrames:FlxTileFrames = FlxTileFrames.findFrame(parent, resultSize, region, atlasFrame, tileSpacing, resultBorder);
        if (tileFrames != null)
        {
            resultSize = FlxDestroyUtil.put(resultSize);
            return tileFrames;
        }

        tileFrames = new FlxTileFrames(parent, resultBorder);
        tileFrames.region = FlxRect.get().copyFrom(region);
        tileFrames.atlasFrame = atlasFrame;
        tileFrames.tileSize = resultSize;
        tileFrames.tileSpacing = FlxPoint.get().copyFrom(tileSpacing);

        for (frame in frames)
        {
            tileFrames.pushFrame(frame.setBorderTo(border));
        }

        return tileFrames;
    }

    override public function destroy():Void
    {
        super.destroy();
        atlasFrame = null;
        region = FlxDestroyUtil.put(region);
        tileSize = FlxDestroyUtil.put(tileSize);
        tileSpacing = FlxDestroyUtil.put(tileSpacing);
    }
}