Files
Funkin/source/funkin/play/event/SongEventHelper.hx
T

226 lines
6.8 KiB
Haxe

package funkin.play.event;
import flixel.tweens.FlxEase;
import openfl.display.BitmapData;
import flixel.FlxSprite;
class SongEventHelper
{
public static var EASE_CANVAS_SIZE:Int = 200;
public static var easeBitmapMap:Map<String, BitmapData> = new Map<String, BitmapData>();
public static var easeDirList:Array<String> = ["sine", "quad", "cube", "quart", "quint", "expo", "smoothStep", "smootherStep", "elastic", "back", "bounce", "circ"];
public static var easeDirs:Array<String> = ["In", "Out", "InOut"];
public static var easeDotCache:Map<String, Array<FlxSprite>> = new Map<String, Array<FlxSprite>>();
public static function generateEaseGraphsBitmaps():Void
{
for (ease in easeDirList)
for (dir in easeDirs)
{
final func = getEaseFunc(ease, dir);
if (func == null) continue;
final key = ease + dir;
if (!easeBitmapMap.exists(key))
{
final bd = createBitmapFromFunc(func, key);
if (bd != null) easeBitmapMap.set(key, bd);
}
}
var k = "INSTANT";
if (!easeBitmapMap.exists(k))
{
final bd = createBitmapFromFunc(null, k);
if (bd != null) easeBitmapMap.set(k, bd);
}
k = "linear";
if (!easeBitmapMap.exists(k))
{
final bd = createBitmapFromFunc(FlxEase.linear, k);
if (bd != null) easeBitmapMap.set(k, bd);
}
}
static function getEaseFunc(base:String, dir:String):Dynamic
{
var f = Reflect.field(FlxEase, base + dir);
if (f != null) return f;
return FlxEase.linear;
}
public static function getEaseBitmap(key:String):BitmapData
{
if (key == "linearIn" || key == "linearInOut" || key == "linearOut") key = "linear";
return easeBitmapMap.get(key);
}
static function getEaseRange(func:Dynamic, samples:Int):{min:Float, max:Float}
{
if (func == null || samples <= 0) return {min: 0.0, max: 1.0};
var min:Float = 0.0;
var max:Float = 1.0;
var hasValue:Bool = false;
for (i in 0...samples)
{
var t:Float = if (samples > 1) (i / (samples - 1)) else 0.0;
var raw = func(t);
if (Math.isNaN(raw)) continue;
if (!hasValue)
{
min = raw;
max = raw;
hasValue = true;
}
else
{
if (raw < min) min = raw;
if (raw > max) max = raw;
}
}
if (!hasValue) return {min: 0.0, max: 1.0};
return {min: min, max: max};
}
static function getEaseY(raw:Float, size:Int, range:{min:Float, max:Float}):Int
{
var v:Float = raw;
var span = range.max - range.min;
if (span > 0)
v = (raw - range.min) / span;
v = if (v < 0) 0 else if (v > 1) 1 else v;
return Std.int((1 - v) * (size - 1));
}
static function createBitmapFromFunc(func:Dynamic, key:String, thickness:Int = 2):BitmapData
{
try
{
var size = EASE_CANVAS_SIZE;
var bd = new BitmapData(size, size, false, 0xFF202223);
if (key.toLowerCase() == "instant") return bd;
if (thickness < 1) thickness = 1;
var half = Std.int(thickness / 2);
var range = getEaseRange(func, size);
// Draw horizontal lines at y=1 and y=0 for reference.
var y1 = getEaseY(1, size, range);
var y0 = getEaseY(0, size, range);
bd.fillRect(new openfl.geom.Rectangle(0, y1 - half, size, thickness), 0xFF404040);
bd.fillRect(new openfl.geom.Rectangle(0, y0 - half, size, thickness), 0xFF404040);
var lastY:Int = -1;
for (i in 0...size)
{
var t:Float = if (size > 1) (i / (size - 1)) else 0.0;
var raw = func(t);
if (!Math.isNaN(raw))
{
var y:Int = getEaseY(raw, size, range);
if (lastY == -1)
{
for (xx in (i - half)...(i + half + 1))
if (xx >= 0 && xx < size) for (yy in (y - half)...(y + half + 1))
if (yy >= 0 && yy < size) bd.setPixel32(xx, yy, 0xFFFFFFFF);
}
else
{
var a = Std.int(Math.min(y, lastY));
var b = Std.int(Math.max(y, lastY));
for (xx in (i - half)...(i + half + 1))
if (xx >= 0 && xx < size) for (yy in a - half...b + half + 1)
if (yy >= 0 && yy < size) bd.setPixel32(xx, yy, 0xFFFFFFFF);
}
lastY = y;
}
}
return bd;
}
catch (e:Dynamic)
{
return null;
}
}
public static function createSpriteFromKey(key:String, displayW:Int, displayH:Int):FlxSprite
{
var bd = getEaseBitmap(key);
if (bd == null) return null;
var graphicName = "easegfx_" + key;
var gfx = FlxG.bitmap.add(bd, true, graphicName);
final spr = new FlxSprite();
spr.loadGraphic(gfx);
if (bd.width > 0 && bd.height > 0)
{
var sx = displayW / bd.width;
var sy = displayH / bd.height;
spr.scale.set(sx, sy);
}
spr.updateHitbox();
spr.antialiasing = false;
return spr;
}
public static function getOrCreateEaseDotSprites(key:String, frameCount:Int = 30, dotRadius:Int = 3, dotWidth:Int = 16):Array<FlxSprite>
{
if (easeDotCache.exists(key)) return easeDotCache.get(key);
var baseBd:BitmapData = getEaseBitmap(key);
if (baseBd == null) return null;
var easeFunc:Dynamic = resolveEaseFuncForKey(key);
var sizeH:Int = baseBd.height;
var range = getEaseRange(easeFunc, sizeH);
var sprites:Array<FlxSprite> = [];
for (f in 0...frameCount)
{
var t:Float = if (frameCount > 1) (f / (frameCount - 1.0)) else 0.0;
var raw:Float = 0.0;
try
{
raw = if (easeFunc != null) easeFunc(t) else 0.0;
}
catch (e:Dynamic)
{
raw = FlxEase.linear(t);
}
if (Math.isNaN(raw)) raw = 0.0;
var y:Int = getEaseY(raw, sizeH, range);
var bd:BitmapData = new BitmapData(dotWidth, sizeH, false, 0xFF202223);
var centerX:Int = Std.int(dotWidth / 2);
for (dx in -dotRadius...dotRadius + 1)
for (dy in -dotRadius...dotRadius + 1)
{
var px = centerX + dx;
var py = y + dy;
if (px >= 0 && px < dotWidth && py >= 0 && py < sizeH) if (dx * dx + dy * dy <= dotRadius * dotRadius) bd.setPixel32(px, py, 0xFFFFFFFF);
}
var gfxName = "ease_dot_" + key + "_" + f;
var gfx = FlxG.bitmap.add(bd, true, gfxName);
var spr = new FlxSprite();
spr.loadGraphic(gfx);
sprites.push(spr);
}
easeDotCache.set(key, sprites);
return sprites;
}
static function resolveEaseFuncForKey(key:String):Dynamic
{
var lk = key;
if (lk == null || lk.toLowerCase() == "linear") return FlxEase.linear;
if (lk.toLowerCase() == "instant") return null;
for (dir in easeDirs)
{
if (lk.length >= dir.length && lk.substr(lk.length - dir.length, dir.length) == dir)
{
var base = lk.substr(0, lk.length - dir.length);
return getEaseFunc(base, dir);
}
}
return FlxEase.linear;
}
}