Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions source/funkin/InitState.hx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import funkin.data.freeplay.style.FreeplayStyleRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongRegistry;
import funkin.data.stickers.StickerRegistry;
import funkin.play.event.SongEventHelper;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.story.level.LevelRegistry;
Expand Down Expand Up @@ -134,6 +135,8 @@ class InitState extends FlxState
funkin.mobile.util.FNFCUtil.init();
#end

SongEventHelper.generateEaseGraphsBitmaps();

// This ain't a pixel art game! (most of the time)
FlxSprite.defaultAntialiasing = true;

Expand Down
190 changes: 190 additions & 0 deletions source/funkin/play/event/SongEventHelper.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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 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 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 v:Float = raw;
if (v < 0) v = 0;
if (v > 1) v = 1;
var y:Int = Std.int((1 - v) * (size - 1));
if (lastY == -1)
{
for (yy in (y - half)...(y + half + 1))
if (yy >= 0 && yy < size) bd.setPixel32(i, yy, 0xFFFFFFFF);
}
else
{
var a = Std.int(Math.min(y, lastY));
var b = Std.int(Math.max(y, lastY));
for (yy in a - half...b + half + 1)
if (yy >= 0 && yy < size) bd.setPixel32(i, 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 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 v:Float = raw;
if (v < 0) v = 0;
if (v > 1) v = 1;
var y:Int = Std.int((1 - v) * (sizeH - 1));
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;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package funkin.ui.debug.charting.toolboxes;

#if FEATURE_CHART_EDITOR
import funkin.play.event.SongEventHelper;
import funkin.data.event.SongEventSchema;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.CheckBox;
Expand All @@ -16,6 +17,15 @@ import haxe.ui.containers.Frame;
import haxe.ui.events.UIEvent;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.containers.Grid;
import haxe.ui.components.Image;
import haxe.ui.backend.ImageData;
import openfl.display.Bitmap;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
import openfl.geom.Point;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import flixel.FlxG;

/**
* The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM.
Expand All @@ -29,6 +39,16 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
var toolboxEventsDataFrame:Frame;
var toolboxEventsDataGrid:Grid;

var easeGraphImage:Image;
var easeDotImage:Image;

var _easeGraphSprite:Null<flixel.FlxSprite> = null;
var _easeDotSprites:Array<flixel.FlxSprite> = [];
var _dotTimer:Null<FlxTimer> = null;
var _pauseTimer:Null<FlxTimer> = null;
var _dotIndex:Int = 0;
static var _dotInterval:Float = 1.0 / 30.0;
static var _loopPause:Float = 0.15;
var _initializing:Bool = true;

public function new(chartEditorState2:ChartEditorState)
Expand Down Expand Up @@ -163,6 +183,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
field.resumeEvent(UIEvent.CHANGE, true, true);
}

updateEasePreview();
toolboxEventsEventKind.resumeEvent(UIEvent.CHANGE, true, true);
}

Expand All @@ -178,6 +199,11 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
// Clear the frame.
target.removeAllComponents();

// Ensure we have a cleared preview reference for rebuilt form
easeGraphImage = null;
easeDotImage = null;
var _needEasePreview:Bool = false;

for (field in schema)
{
if (field == null) continue;
Expand Down Expand Up @@ -254,6 +280,11 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
var inputBox:HBox = new HBox();
inputBox.addComponent(input);

if (field.type == ENUM && (field.name == "ease" || field.name == "easeDir"))
{
_needEasePreview = true;
}

// Add a unit label if applicable.
if (field.units != null && field.units != "")
{
Expand All @@ -272,6 +303,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
{
var drp:DropDown = cast event.target;
value = drp.selectedItem?.value ?? field.defaultValue;
updateEasePreview();
}
else if (field.type == BOOL)
{
Expand Down Expand Up @@ -304,10 +336,93 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
chartEditorState.notePreviewDirty = true;
chartEditorState.noteTooltipsDirty = true;
}
updateEasePreview();
}
} // end for schema

if (_needEasePreview)
{
if (easeGraphImage == null)
{
easeGraphImage = new Image();
easeGraphImage.id = "easeGraph";
easeGraphImage.width = 100;
easeGraphImage.height = 100;
}
if (easeDotImage == null)
{
easeDotImage = new Image();
easeDotImage.id = "easeDot";
easeDotImage.width = 16;
easeDotImage.height = 100;
}
var easeHBox = new HBox();
easeHBox.addComponent(easeGraphImage);
easeHBox.addComponent(easeDotImage);
target.addComponent(easeHBox);
currentEaseHBox = easeHBox;
updateEasePreview();
}
}

var currentEaseHBox:HBox = null;
function updateEasePreview():Void
{
if (easeGraphImage == null || easeDotImage == null) return;

final easeVal:Null<String> = chartEditorState.eventDataToPlace.get("ease");
final easeDirVal:Null<String> = chartEditorState.eventDataToPlace.get("easeDir");
final easeStr:String = easeVal == null ? "linear" : easeVal;
final easeDirStr:String = easeDirVal == null ? "In" : easeDirVal;
final key:String = easeStr + (easeDirStr == "" ? "" : easeDirStr);

_dotTimer?.cancel();
_pauseTimer?.cancel();
_dotTimer = null;
_pauseTimer = null;
_easeDotSprites = [];
_dotIndex = 0;

final _graphBd:BitmapData = SongEventHelper.getEaseBitmap(key);
_easeGraphSprite = SongEventHelper.createSpriteFromKey(key, 100, 100);
easeGraphImage.resource = _easeGraphSprite?.frame;
if (_graphBd == null || easeGraphImage.resource == null)
{
easeDotImage.resource = null;
if (currentEaseHBox != null) currentEaseHBox.hidden = true;
return;
}
if (currentEaseHBox != null) currentEaseHBox.hidden = false;

var dotSprites:Array<flixel.FlxSprite> = SongEventHelper.getOrCreateEaseDotSprites(key, 30, 3, 16);
if (dotSprites == null || dotSprites.length == 0) return;
_easeDotSprites = dotSprites;
easeDotImage.resource = _easeDotSprites[0].frame;

var frameCallback:Dynamic = null;
frameCallback = function(tmr:FlxTimer):Void {
_dotIndex++;
if (_dotIndex >= _easeDotSprites.length)
{
_dotTimer?.cancel();
_pauseTimer ??= new FlxTimer();
_pauseTimer.start(_loopPause, function(p:FlxTimer):Void {
if (easeDotImage != null && !_initializing)
{
_dotIndex = 0;
easeDotImage.resource = _easeDotSprites[0].frame;
_dotTimer ??= new FlxTimer();
_dotTimer.start(_dotInterval, frameCallback, 0);
}
}, 1);
}
else if (easeDotImage != null && !_initializing) easeDotImage.resource = _easeDotSprites[_dotIndex].frame;
};

_dotTimer ??= new FlxTimer();
_dotTimer.start(_dotInterval, frameCallback, 0);
}

public static function build(chartEditorState:ChartEditorState):ChartEditorEventDataToolbox
{
return new ChartEditorEventDataToolbox(chartEditorState);
Expand Down