Skip to content

Instantly share code, notes, and snippets.

@Geokureli
Last active October 29, 2022 00:13
Show Gist options
  • Save Geokureli/d28b094aa26cf305169846becdfc44da to your computer and use it in GitHub Desktop.
Save Geokureli/d28b094aa26cf305169846becdfc44da to your computer and use it in GitHub Desktop.
package flixel.addons.display;
import flixel.FlxBasic;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.graphics.FlxGraphic;
import flixel.graphics.frames.FlxFrame;
import flixel.math.FlxMatrix;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxAssets;
import flixel.util.FlxAxes;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
using flixel.util.FlxColorTransformUtil;
/**
* Used for showing infinitely scrolling backgrounds.
* @author George Kurelic (Original concept by Chevy Ray)
*/
class FlxBackdrop2 extends FlxSprite
{
/**
* The axes to repeat the backdrop, defaults to XY which covers the whole camera.
*/
public var repeatAxes:FlxAxes = XY;
var repeatX(get, never):Bool;
var repeatY(get, never):Bool;
/**
* The gap between repeated tiles, defaults to (0, 0), or no gap.
*/
public var spacing(default, null):FlxPoint = new FlxPoint();
/**
* If true, tiles are pre-rendered to a intermediary bitmap whenever `loadGraphic` is called
* or the following properties are changed: camera size camera zoom, `scale.x`, `scale.y`,
* `spacing.x`, `spacing.y`, `repeatAxes` or `angle`. If these properties change often, it is recommended to
* set `drawBlit` to `false`.
*
* Note: blitting will disable animations and only show the first frame.
*/
public var drawBlit:Bool = FlxG.renderBlit;
/**
* Decides the the size of the blit graphic
*/
public var blitMode:BackdropBlitMode = AUTO;
var _blitGraphic:FlxGraphic = null;
var _prevDrawParams:BackdropDrawParams = {
graphicKey: null,
tilesX: -1,
tilesY: -1,
scaleX: 0.0,
scaleY: 0.0,
spacingX: 0.0,
spacingY: 0.0,
repeatAxes: XY,
angle: 0.0
};
/**
* Creates an instance of the FlxBackdrop class, used to create infinitely scrolling backgrounds.
*
* @param graphic The image you want to use for the backdrop.
* @param repeatAxes If the backdrop should repeat on the X axis.
* @param spacingX Amount of spacing between tiles on the X axis
* @param spacingY Amount of spacing between tiles on the Y axis
*/
public function new(?graphic:FlxGraphicAsset, repeatAxes = XY, spacingX = 0, spacingY = 0)
{
super(0, 0, graphic);
this.repeatAxes = repeatAxes;
this.spacing.set(spacingX, spacingY);
}
override function destroy():Void
{
spacing = FlxDestroyUtil.destroy(spacing);
super.destroy();
}
override function draw()
{
checkEmptyFrame();
if (alpha == 0 || _frame.type == FlxFrameType.EMPTY)
return;
if (scale.x <= 0 || scale.y <= 0)
return;
if (dirty) // rarely
calcFrame(useFramePixels);
if (drawBlit)
{
drawToLargestCamera();
}
for (camera in cameras)
{
if (!camera.visible || !camera.exists || !isOnScreen(camera))
continue;
if (isSimpleRender(camera))
drawSimple(camera);
else
drawComplex(camera);
#if FLX_DEBUG
FlxBasic.visibleCount++;
#end
}
#if FLX_DEBUG
if (FlxG.debugger.drawDebug)
drawDebug();
#end
}
override function isOnScreen(?camera:FlxCamera):Bool
{
if (repeatAxes == XY)
return true;
// if (repeatAxes == NONE)
// return super.isOnScreen(camera);
if (camera == null)
camera = FlxG.camera;
var bounds = getScreenBounds(_rect, camera);
var view = camera.getViewRect();
if (repeatAxes == X)
bounds.x = view.x;
if (repeatAxes == Y)
bounds.y = view.y;
view.put();
return camera.containsRect(bounds);
}
function drawToLargestCamera()
{
var largest:FlxCamera = null;
var largestArea = 0.0;
var view = FlxRect.get();
for (camera in cameras)
{
if (!camera.visible || !camera.exists || !isOnScreen(camera))
continue;
camera.getViewRect(view);
if (view.width * view.height > largestArea)
{
largest = camera;
largestArea = view.width * view.height;
}
}
view.put();
if (largest != null)
regenGraphic(largest);
}
override function isSimpleRenderBlit(?camera:FlxCamera):Bool
{
return (super.isSimpleRenderBlit(camera) || drawBlit) && (camera != null ? isPixelPerfectRender(camera) : pixelPerfectRender);
}
override function drawSimple(camera:FlxCamera):Void
{
var drawDirect = !drawBlit;
final graphic = drawBlit ? _blitGraphic : this.graphic;
final frame = drawBlit ? _blitGraphic.imageFrame.frame : _frame;
// The distance between repeated sprites, in screen space
var tileSize = FlxPoint.get(frame.frame.width, frame.frame.height);
if (drawDirect)
tileSize.addPoint(spacing);
getScreenPosition(_point, camera).subtractPoint(offset);
var tilesX = 1;
var tilesY = 1;
// if (repeatAxes != NONE)
{
var view = camera.getViewRect();
if (repeatX)
{
final left = modMin(_point.x + frameWidth, tileSize.x, view.left) - frameWidth;
final right = modMax(_point.x, tileSize.x, view.right) + tileSize.x;
tilesX = Math.round((right - left) / tileSize.x);
final origTileSizeX = frameWidth + spacing.x;
_point.x = modMin(_point.x + frameWidth, origTileSizeX, view.left) - frameWidth;
}
if (repeatY)
{
final top = modMin(_point.y + frameHeight, tileSize.y, view.top) - frameHeight;
final bottom = modMax(_point.y, tileSize.y, view.bottom) + tileSize.y;
tilesY = Math.round((bottom - top) / tileSize.y);
final origTileSizeY = frameHeight + spacing.y;
_point.y = modMin(_point.y + frameHeight, origTileSizeY, view.top) - frameHeight;
}
}
if (FlxG.renderBlit)
calcFrame(true);
camera.buffer.lock();
for (tileX in 0...tilesX)
{
for (tileY in 0...tilesY)
{
// _point.copyToFlash(_flashPoint);
_flashPoint.setTo(_point.x + tileSize.x * tileX, _point.y + tileSize.y * tileY);
if (isPixelPerfectRender(camera))
{
_flashPoint.x = Math.floor(_flashPoint.x);
_flashPoint.y = Math.floor(_flashPoint.y);
}
final pixels = drawBlit ? _blitGraphic.bitmap : framePixels;
camera.copyPixels(frame, pixels, pixels.rect, _flashPoint, colorTransform, blend, antialiasing);
}
}
camera.buffer.unlock();
}
override function drawComplex(camera:FlxCamera)
{
var drawDirect = !drawBlit;
final graphic = drawBlit ? _blitGraphic : this.graphic;
final frame = drawBlit ? _blitGraphic.imageFrame.frame : _frame;
frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
_matrix.translate(-origin.x, -origin.y);
// The distance between repeated sprites, in screen space
var tileSize = FlxPoint.get(frame.frame.width, frame.frame.height);
if (drawDirect)
{
tileSize.set((frame.frame.width + spacing.x) * scale.x, (frame.frame.height + spacing.y) * scale.y);
_matrix.scale(scale.x, scale.y);
if (bakedRotationAngle <= 0)
{
updateTrig();
if (angle != 0)
_matrix.rotateWithTrig(_cosAngle, _sinAngle);
}
}
var drawItem = null;
if (FlxG.renderTile)
{
var isColored:Bool = (alpha != 1) || (color != 0xffffff);
var hasColorOffsets:Bool = (colorTransform != null && colorTransform.hasRGBAOffsets());
drawItem = camera.startQuadBatch(graphic, isColored, hasColorOffsets, blend, antialiasing, shader);
}
else
{
camera.buffer.lock();
}
getScreenPosition(_point, camera).subtractPoint(offset);
var tilesX = 1;
var tilesY = 1;
// if (repeatAxes != NONE)
{
final view = camera.getViewRect();
final bounds = getScreenBounds(camera);
if (repeatX)
{
final origTileSizeX = (frameWidth + spacing.x) * scale.x;
final left = modMin(bounds.right, origTileSizeX, view.left) - bounds.width;
final right = modMax(bounds.left, origTileSizeX, view.right) + origTileSizeX;
tilesX = Math.round((right - left) / tileSize.x);
_point.x = left + _point.x - bounds.x;
}
if (repeatY)
{
final origTileSizeY = (frameHeight + spacing.y) * scale.y;
final top = modMin(bounds.bottom, origTileSizeY, view.top) - bounds.height;
final bottom = modMax(bounds.top, origTileSizeY, view.bottom) + origTileSizeY;
tilesY = Math.round((bottom - top) / tileSize.y);
_point.y = top + _point.y - bounds.y;
}
view.put();
bounds.put();
}
_point.add(origin.x, origin.y);
final mat = new FlxMatrix();
for (tileX in 0...tilesX)
{
for (tileY in 0...tilesY)
{
mat.copyFrom(_matrix);
mat.translate(_point.x + (tileSize.x * tileX), _point.y + (tileSize.y * tileY));
if (isPixelPerfectRender(camera))
{
mat.tx = Math.floor(mat.tx);
mat.ty = Math.floor(mat.ty);
}
if (FlxG.renderBlit)
{
final pixels = drawBlit ? _blitGraphic.bitmap : framePixels;
camera.drawPixels(frame, pixels, mat, colorTransform, blend, antialiasing, shader);
}
else
{
drawItem.addQuad(frame, mat, colorTransform);
}
}
}
if (FlxG.renderBlit)
camera.buffer.unlock();
}
function getFrameScreenBounds(camera:FlxCamera):FlxRect
{
if (drawBlit)
{
final frame = _blitGraphic.imageFrame.frame.frame;
return FlxRect.get(x, y, frame.width, frame.height);
}
final newRect = FlxRect.get(x, y);
if (pixelPerfectPosition)
newRect.floor();
final scaledOrigin = FlxPoint.weak(origin.x * scale.x, origin.y * scale.y);
newRect.x += -Std.int(camera.scroll.x * scrollFactor.x) - offset.x + origin.x - scaledOrigin.x;
newRect.y += -Std.int(camera.scroll.y * scrollFactor.y) - offset.y + origin.y - scaledOrigin.y;
if (isPixelPerfectRender(camera))
newRect.floor();
newRect.setSize(frameWidth * Math.abs(scale.x), frameHeight * Math.abs(scale.y));
return newRect.getRotatedBounds(angle, scaledOrigin, newRect);
}
function modMin(value:Float, step:Float, min:Float)
{
return value - Math.floor((value - min) / step) * step;
}
function modMax(value:Float, step:Float, max:Float)
{
return value - Math.ceil((value - max) / step) * step;
}
function regenGraphic(camera:FlxCamera)
{
// The distance between repeated sprites, in screen space
var tileSize = FlxPoint.get((frameWidth + spacing.x) * scale.x, (frameHeight + spacing.y) * scale.y);
var view = camera.getViewRect();
var tilesX = 1;
var tilesY = 1;
var repeatX = repeatAxes == X || repeatAxes == XY;
var repeatY = repeatAxes == Y || repeatAxes == XY;
// if (repeatAxes != NONE)
{
inline function min(a:Int, b:Int):Int
return a < b ? a : b;
switch (blitMode)
{
case AUTO | SPLIT(1):
if (repeatX)
tilesX = Math.ceil(view.width / tileSize.x) + 1;
if (repeatY)
tilesY = Math.ceil(view.height / tileSize.y) + 1;
case MAX_TILES(1) | MAX_TILES_XY(1, 1):
case MAX_TILES(max):
if (repeatX)
tilesX = min(max, Math.ceil(view.width / tileSize.x) + 1);
if (repeatY)
tilesY = min(max, Math.ceil(view.height / tileSize.y) + 1);
case MAX_TILES_XY(maxX, maxY):
if (repeatX)
tilesX = min(maxX, Math.ceil(view.width / tileSize.x) + 1);
if (repeatY)
tilesY = min(maxY, Math.ceil(view.height / tileSize.y) + 1);
case SPLIT(portions):
if (repeatX)
tilesX = Math.ceil(view.width / tileSize.x / portions + 1);
if (repeatY)
tilesY = Math.ceil(view.height / tileSize.y / portions + 1);
}
}
view.put();
if (matchPrevDrawParams(tilesX, tilesY))
{
tileSize.put();
return;
}
setDrawParams(tilesX, tilesY);
var graphicSizeX = Math.ceil(tilesX * tileSize.x);
var graphicSizeY = Math.ceil(tilesY * tileSize.y);
if (_blitGraphic == null || (_blitGraphic.width != graphicSizeX || _blitGraphic.height != graphicSizeY))
{
_blitGraphic = FlxG.bitmap.create(graphicSizeX, graphicSizeY, 0x0, true);
}
var pixels = _blitGraphic.bitmap;
pixels.lock();
pixels.fillRect(pixels.rect, FlxColor.TRANSPARENT);
animation.frameIndex = 0;
calcFrame(true);
_matrix.identity();
_matrix.translate(-origin.x, -origin.y);
_matrix.scale(scale.x, scale.y);
if (bakedRotationAngle <= 0)
{
updateTrig();
if (angle != 0)
_matrix.rotateWithTrig(_cosAngle, _sinAngle);
}
_matrix.translate(origin.x, origin.y);
_point.set(_matrix.tx, _matrix.ty);
// draw extra tiles on the edge in case the image protrudes past the tile
// TODO: Use 0 buffer when angle is multiple of 90 with centered origin
final bufferX = repeatX && angle != 0 ? 1 : 0;
final bufferY = repeatY && angle != 0 ? 1 : 0;
for (tileX in -bufferX...tilesX + bufferX)
{
for (tileY in -bufferY...tilesY + bufferY)
{
_matrix.tx = _point.x + tileX * tileSize.x;
_matrix.ty = _point.y + tileY * tileSize.y;
pixels.draw(framePixels, _matrix);
}
}
pixels.unlock();
tileSize.put();
}
inline function get_repeatX()
{
return repeatAxes == X || repeatAxes == XY;
}
inline function get_repeatY()
{
return repeatAxes == Y || repeatAxes == XY;
}
inline function matchPrevDrawParams(tilesX:Int, tilesY:Int)
{
return _prevDrawParams.graphicKey == graphic.key
&& _prevDrawParams.tilesX == tilesX
&& _prevDrawParams.tilesY == tilesY
&& _prevDrawParams.scaleX == scale.x
&& _prevDrawParams.scaleY == scale.y
&& _prevDrawParams.spacingX == spacing.x
&& _prevDrawParams.spacingY == spacing.y
&& _prevDrawParams.repeatAxes == repeatAxes
&& _prevDrawParams.angle == angle;
}
inline function setDrawParams(tilesX:Int, tilesY:Int)
{
_prevDrawParams.graphicKey = graphic.key;
_prevDrawParams.tilesX = tilesX;
_prevDrawParams.tilesY = tilesY;
_prevDrawParams.scaleX = scale.x;
_prevDrawParams.scaleY = scale.y;
_prevDrawParams.spacingX = spacing.x;
_prevDrawParams.spacingY = spacing.y;
_prevDrawParams.repeatAxes = repeatAxes;
_prevDrawParams.angle = angle;
}
}
enum BackdropBlitMode
{
/**
* Not implemented yet.
*/
AUTO;
/**
* Blits a bitmap as big as the specified number of x and y tiles and repeats that.
*/
MAX_TILES_XY(x:Int, y:Int);
/**
* Blits a bitmap as big as the specified number of tiles and repeats that.
*/
MAX_TILES(tiles:Int);
/**
* Blits enough tiles to cover the screen in multiple draws, for example, if the camera is 10x8
* tiles big, SPLIT(2) will draw a blit target 5x4 tiles large and draw it 2x2 times to cover the
* stage.
*/
SPLIT(portions:Int);
}
typedef BackdropDrawParams =
{
graphicKey:String,
tilesX:Int,
tilesY:Int,
scaleX:Float,
scaleY:Float,
spacingX:Float,
spacingY:Float,
repeatAxes:FlxAxes,
angle:Float
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment