2024-01-16 21:49:15 +00:00
|
|
|
package funkin.graphics;
|
|
|
|
|
|
|
|
import flixel.FlxSprite;
|
|
|
|
import flixel.util.FlxColor;
|
|
|
|
import flixel.graphics.FlxGraphic;
|
2024-03-21 04:38:52 +00:00
|
|
|
import flixel.tweens.FlxTween;
|
2024-03-19 19:56:00 +00:00
|
|
|
import openfl.display3D.textures.TextureBase;
|
|
|
|
import funkin.graphics.framebuffer.FixedBitmapData;
|
|
|
|
import openfl.display.BitmapData;
|
2024-06-20 03:47:11 +00:00
|
|
|
import flixel.math.FlxRect;
|
|
|
|
import flixel.math.FlxPoint;
|
|
|
|
import flixel.graphics.frames.FlxFrame;
|
|
|
|
import flixel.FlxCamera;
|
2024-01-16 21:49:15 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An FlxSprite with additional functionality.
|
2024-02-22 23:55:24 +00:00
|
|
|
* - A more efficient method for creating solid color sprites.
|
|
|
|
* - TODO: Better cache handling for textures.
|
2024-01-16 21:49:15 +00:00
|
|
|
*/
|
|
|
|
class FunkinSprite extends FlxSprite
|
|
|
|
{
|
2024-02-23 04:37:52 +00:00
|
|
|
/**
|
|
|
|
* An internal list of all the textures cached with `cacheTexture`.
|
|
|
|
* This excludes any temporary textures like those from `FlxText` or `makeSolidColor`.
|
|
|
|
*/
|
|
|
|
static var currentCachedTextures:Map<String, FlxGraphic> = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An internal list of textures that were cached in the previous state.
|
|
|
|
* We don't know whether we want to keep them cached or not.
|
|
|
|
*/
|
|
|
|
static var previousCachedTextures:Map<String, FlxGraphic> = [];
|
|
|
|
|
2024-01-16 21:49:15 +00:00
|
|
|
/**
|
|
|
|
* @param x Starting X position
|
|
|
|
* @param y Starting Y position
|
|
|
|
*/
|
|
|
|
public function new(?x:Float = 0, ?y:Float = 0)
|
|
|
|
{
|
|
|
|
super(x, y);
|
|
|
|
}
|
|
|
|
|
2024-02-22 23:55:24 +00:00
|
|
|
/**
|
|
|
|
* Create a new FunkinSprite with a static texture.
|
|
|
|
* @param x The starting X position.
|
|
|
|
* @param y The starting Y position.
|
|
|
|
* @param key The key of the texture to load.
|
|
|
|
* @return The new FunkinSprite.
|
|
|
|
*/
|
|
|
|
public static function create(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
|
|
|
|
{
|
2024-03-19 19:56:00 +00:00
|
|
|
var sprite:FunkinSprite = new FunkinSprite(x, y);
|
2024-02-22 23:55:24 +00:00
|
|
|
sprite.loadTexture(key);
|
|
|
|
return sprite;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new FunkinSprite with a Sparrow atlas animated texture.
|
|
|
|
* @param x The starting X position.
|
|
|
|
* @param y The starting Y position.
|
|
|
|
* @param key The key of the texture to load.
|
|
|
|
* @return The new FunkinSprite.
|
|
|
|
*/
|
|
|
|
public static function createSparrow(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
|
|
|
|
{
|
2024-03-19 19:56:00 +00:00
|
|
|
var sprite:FunkinSprite = new FunkinSprite(x, y);
|
2024-02-22 23:55:24 +00:00
|
|
|
sprite.loadSparrow(key);
|
|
|
|
return sprite;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new FunkinSprite with a Packer atlas animated texture.
|
|
|
|
* @param x The starting X position.
|
|
|
|
* @param y The starting Y position.
|
|
|
|
* @param key The key of the texture to load.
|
|
|
|
* @return The new FunkinSprite.
|
|
|
|
*/
|
|
|
|
public static function createPacker(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
|
|
|
|
{
|
2024-03-19 19:56:00 +00:00
|
|
|
var sprite:FunkinSprite = new FunkinSprite(x, y);
|
2024-02-22 23:55:24 +00:00
|
|
|
sprite.loadPacker(key);
|
|
|
|
return sprite;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load a static image as the sprite's texture.
|
|
|
|
* @param key The key of the texture to load.
|
|
|
|
* @return This sprite, for chaining.
|
|
|
|
*/
|
|
|
|
public function loadTexture(key:String):FunkinSprite
|
|
|
|
{
|
2024-03-13 01:34:50 +00:00
|
|
|
var graphicKey:String = Paths.image(key);
|
|
|
|
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
2024-02-22 23:55:24 +00:00
|
|
|
|
2024-03-13 01:34:50 +00:00
|
|
|
loadGraphic(graphicKey);
|
2024-02-22 23:55:24 +00:00
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2024-03-19 19:56:00 +00:00
|
|
|
/**
|
|
|
|
* Apply an OpenFL `BitmapData` to this sprite.
|
|
|
|
* @param input The OpenFL `BitmapData` to apply
|
|
|
|
* @return This sprite, for chaining
|
|
|
|
*/
|
|
|
|
public function loadBitmapData(input:BitmapData):FunkinSprite
|
|
|
|
{
|
|
|
|
loadGraphic(input);
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply an OpenFL `TextureBase` to this sprite.
|
|
|
|
* @param input The OpenFL `TextureBase` to apply
|
|
|
|
* @return This sprite, for chaining
|
|
|
|
*/
|
|
|
|
public function loadTextureBase(input:TextureBase):FunkinSprite
|
|
|
|
{
|
|
|
|
var inputBitmap:FixedBitmapData = FixedBitmapData.fromTexture(input);
|
|
|
|
|
|
|
|
return loadBitmapData(inputBitmap);
|
|
|
|
}
|
|
|
|
|
2024-02-22 23:55:24 +00:00
|
|
|
/**
|
|
|
|
* Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
|
|
|
|
* @param key The key of the texture to load.
|
|
|
|
* @return This sprite, for chaining.
|
|
|
|
*/
|
|
|
|
public function loadSparrow(key:String):FunkinSprite
|
|
|
|
{
|
2024-03-13 01:34:50 +00:00
|
|
|
var graphicKey:String = Paths.image(key);
|
2024-02-22 23:55:24 +00:00
|
|
|
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
|
|
|
|
|
|
|
this.frames = Paths.getSparrowAtlas(key);
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load an animated texture (Packer atlas spritesheet) as the sprite's texture.
|
|
|
|
* @param key The key of the texture to load.
|
|
|
|
* @return This sprite, for chaining.
|
|
|
|
*/
|
|
|
|
public function loadPacker(key:String):FunkinSprite
|
|
|
|
{
|
2024-03-13 01:34:50 +00:00
|
|
|
var graphicKey:String = Paths.image(key);
|
2024-02-22 23:55:24 +00:00
|
|
|
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
|
|
|
|
|
|
|
this.frames = Paths.getPackerAtlas(key);
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2024-03-19 19:56:00 +00:00
|
|
|
/**
|
|
|
|
* Determine whether the texture with the given key is cached.
|
|
|
|
* @param key The key of the texture to check.
|
|
|
|
* @return Whether the texture is cached.
|
|
|
|
*/
|
2024-02-22 23:55:24 +00:00
|
|
|
public static function isTextureCached(key:String):Bool
|
|
|
|
{
|
|
|
|
return FlxG.bitmap.get(key) != null;
|
|
|
|
}
|
|
|
|
|
2024-03-19 19:56:00 +00:00
|
|
|
/**
|
|
|
|
* Ensure the texture with the given key is cached.
|
|
|
|
* @param key The key of the texture to cache.
|
|
|
|
*/
|
2024-02-22 23:55:24 +00:00
|
|
|
public static function cacheTexture(key:String):Void
|
|
|
|
{
|
2024-02-23 04:37:52 +00:00
|
|
|
// We don't want to cache the same texture twice.
|
|
|
|
if (currentCachedTextures.exists(key)) return;
|
|
|
|
|
|
|
|
if (previousCachedTextures.exists(key))
|
|
|
|
{
|
|
|
|
// Move the graphic from the previous cache to the current cache.
|
|
|
|
var graphic = previousCachedTextures.get(key);
|
|
|
|
previousCachedTextures.remove(key);
|
|
|
|
currentCachedTextures.set(key, graphic);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Else, texture is currently uncached.
|
2024-03-19 19:56:00 +00:00
|
|
|
var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
|
2024-02-22 23:55:24 +00:00
|
|
|
if (graphic == null)
|
|
|
|
{
|
|
|
|
FlxG.log.warn('Failed to cache graphic: $key');
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
trace('Successfully cached graphic: $key');
|
2024-02-23 04:37:52 +00:00
|
|
|
graphic.persist = true;
|
|
|
|
currentCachedTextures.set(key, graphic);
|
2024-02-22 23:55:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function cacheSparrow(key:String):Void
|
|
|
|
{
|
|
|
|
cacheTexture(Paths.image(key));
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function cachePacker(key:String):Void
|
|
|
|
{
|
|
|
|
cacheTexture(Paths.image(key));
|
|
|
|
}
|
|
|
|
|
2024-02-23 04:37:52 +00:00
|
|
|
/**
|
|
|
|
* Call this, then `cacheTexture` to keep the textures we still need, then `purgeCache` to remove the textures that we won't be using anymore.
|
|
|
|
*/
|
|
|
|
public static function preparePurgeCache():Void
|
|
|
|
{
|
|
|
|
previousCachedTextures = currentCachedTextures;
|
|
|
|
currentCachedTextures = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function purgeCache():Void
|
|
|
|
{
|
|
|
|
// Everything that is in previousCachedTextures but not in currentCachedTextures should be destroyed.
|
|
|
|
for (graphicKey in previousCachedTextures.keys())
|
|
|
|
{
|
|
|
|
var graphic = previousCachedTextures.get(graphicKey);
|
2024-05-01 03:59:25 +00:00
|
|
|
if (graphic == null) continue;
|
2024-02-23 04:37:52 +00:00
|
|
|
FlxG.bitmap.remove(graphic);
|
|
|
|
graphic.destroy();
|
|
|
|
previousCachedTextures.remove(graphicKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static function isGraphicCached(graphic:FlxGraphic):Bool
|
|
|
|
{
|
|
|
|
if (graphic == null) return false;
|
|
|
|
var result = FlxG.bitmap.get(graphic.key);
|
|
|
|
if (result == null) return false;
|
|
|
|
if (result != graphic)
|
|
|
|
{
|
|
|
|
FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-01-16 21:49:15 +00:00
|
|
|
/**
|
|
|
|
* Acts similarly to `makeGraphic`, but with improved memory usage,
|
2024-02-22 23:55:24 +00:00
|
|
|
* at the expense of not being able to paint onto the resulting sprite.
|
2024-01-16 21:49:15 +00:00
|
|
|
*
|
|
|
|
* @param width The target width of the sprite.
|
|
|
|
* @param height The target height of the sprite.
|
|
|
|
* @param color The color to fill the sprite with.
|
2024-02-22 23:55:24 +00:00
|
|
|
* @return This sprite, for chaining.
|
2024-01-16 21:49:15 +00:00
|
|
|
*/
|
|
|
|
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
|
|
|
|
{
|
2024-02-22 23:55:24 +00:00
|
|
|
// Create a tiny solid color graphic and scale it up to the desired size.
|
2024-01-16 21:49:15 +00:00
|
|
|
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
|
|
|
|
frames = graphic.imageFrame;
|
2024-02-22 23:55:24 +00:00
|
|
|
scale.set(width / 2.0, height / 2.0);
|
2024-01-16 21:49:15 +00:00
|
|
|
updateHitbox();
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-03-21 04:38:52 +00:00
|
|
|
* Ensure scale is applied when cloning a sprite.R
|
2024-01-16 21:49:15 +00:00
|
|
|
* The default `clone()` method acts kinda weird TBH.
|
|
|
|
* @return A clone of this sprite.
|
|
|
|
*/
|
|
|
|
public override function clone():FunkinSprite
|
|
|
|
{
|
|
|
|
var result = new FunkinSprite(this.x, this.y);
|
|
|
|
result.frames = this.frames;
|
|
|
|
result.scale.set(this.scale.x, this.scale.y);
|
|
|
|
result.updateHitbox();
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
2024-03-21 04:38:52 +00:00
|
|
|
|
2024-06-20 03:47:11 +00:00
|
|
|
@:access(flixel.FlxCamera)
|
|
|
|
override function getBoundingBox(camera:FlxCamera):FlxRect
|
|
|
|
{
|
|
|
|
getScreenPosition(_point, camera);
|
|
|
|
|
|
|
|
_rect.set(_point.x, _point.y, width, height);
|
|
|
|
_rect = camera.transformRect(_rect);
|
|
|
|
|
|
|
|
if (isPixelPerfectRender(camera))
|
|
|
|
{
|
|
|
|
_rect.width = _rect.width / this.scale.x;
|
|
|
|
_rect.height = _rect.height / this.scale.y;
|
|
|
|
_rect.x = _rect.x / this.scale.x;
|
|
|
|
_rect.y = _rect.y / this.scale.y;
|
|
|
|
_rect.floor();
|
|
|
|
_rect.x = _rect.x * this.scale.x;
|
|
|
|
_rect.y = _rect.y * this.scale.y;
|
|
|
|
_rect.width = _rect.width * this.scale.x;
|
|
|
|
_rect.height = _rect.height * this.scale.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
return _rect;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the screen position of this object.
|
|
|
|
*
|
|
|
|
* @param result Optional arg for the returning point
|
|
|
|
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
|
|
|
|
* @return The screen position of this object.
|
|
|
|
*/
|
|
|
|
public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
|
|
|
|
{
|
|
|
|
if (result == null) result = FlxPoint.get();
|
|
|
|
|
|
|
|
if (camera == null) camera = FlxG.camera;
|
|
|
|
|
|
|
|
result.set(x, y);
|
|
|
|
if (pixelPerfectPosition)
|
|
|
|
{
|
|
|
|
_rect.width = _rect.width / this.scale.x;
|
|
|
|
_rect.height = _rect.height / this.scale.y;
|
|
|
|
_rect.x = _rect.x / this.scale.x;
|
|
|
|
_rect.y = _rect.y / this.scale.y;
|
|
|
|
_rect.round();
|
|
|
|
_rect.x = _rect.x * this.scale.x;
|
|
|
|
_rect.y = _rect.y * this.scale.y;
|
|
|
|
_rect.width = _rect.width * this.scale.x;
|
|
|
|
_rect.height = _rect.height * this.scale.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
|
|
|
|
}
|
|
|
|
|
|
|
|
override function drawSimple(camera:FlxCamera):Void
|
|
|
|
{
|
|
|
|
getScreenPosition(_point, camera).subtractPoint(offset);
|
|
|
|
if (isPixelPerfectRender(camera))
|
|
|
|
{
|
|
|
|
_point.x = _point.x / this.scale.x;
|
|
|
|
_point.y = _point.y / this.scale.y;
|
|
|
|
_point.round();
|
|
|
|
|
|
|
|
_point.x = _point.x * this.scale.x;
|
|
|
|
_point.y = _point.y * this.scale.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
_point.copyToFlash(_flashPoint);
|
|
|
|
camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
|
|
|
|
}
|
|
|
|
|
|
|
|
override function drawComplex(camera:FlxCamera):Void
|
|
|
|
{
|
|
|
|
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
|
|
|
|
_matrix.translate(-origin.x, -origin.y);
|
|
|
|
_matrix.scale(scale.x, scale.y);
|
|
|
|
|
|
|
|
if (bakedRotationAngle <= 0)
|
|
|
|
{
|
|
|
|
updateTrig();
|
|
|
|
|
|
|
|
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
|
|
|
|
}
|
|
|
|
|
|
|
|
getScreenPosition(_point, camera).subtractPoint(offset);
|
|
|
|
_point.add(origin.x, origin.y);
|
|
|
|
_matrix.translate(_point.x, _point.y);
|
|
|
|
|
|
|
|
if (isPixelPerfectRender(camera))
|
|
|
|
{
|
|
|
|
_matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x;
|
|
|
|
_matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
|
|
|
|
}
|
|
|
|
|
2024-03-21 04:38:52 +00:00
|
|
|
public override function destroy():Void
|
|
|
|
{
|
|
|
|
frames = null;
|
|
|
|
// Cancel all tweens so they don't continue to run on a destroyed sprite.
|
|
|
|
// This prevents crashes.
|
|
|
|
FlxTween.cancelTweensOf(this);
|
|
|
|
super.destroy();
|
|
|
|
}
|
2024-01-16 21:49:15 +00:00
|
|
|
}
|