diff --git a/extras/flas/TechNotDrip/boyfriend freeplay animations v5_FixedTurntableLights_NewUnlock_ColorTransform.fla b/extras/flas/TechNotDrip/boyfriend freeplay animations v5_FixedTurntableLights_NewUnlock_ColorTransform.fla new file mode 100644 index 0000000..7c61970 Binary files /dev/null and b/extras/flas/TechNotDrip/boyfriend freeplay animations v5_FixedTurntableLights_NewUnlock_ColorTransform.fla differ diff --git a/src/funkin/objects/FunkinSprite.hx b/src/funkin/objects/FunkinSprite.hx index c575573..ce7051f 100644 --- a/src/funkin/objects/FunkinSprite.hx +++ b/src/funkin/objects/FunkinSprite.hx @@ -1,14 +1,18 @@ package funkin.objects; +import animate.FlxAnimate; +import animate.FlxAnimateFrames; +import flixel.animation.FlxAnimation; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.util.FlxSignal.FlxTypedSignal; -import flxanimate.FlxAnimate; import funkin.structures.ObjectStructure; +using funkin.util.FlxAnimateUtil; + /** * This is a sprite class that adds on to the already existing FlxSprite. */ -class FunkinSprite extends FlxSprite +class FunkinSprite extends FlxAnimate { /** * Draws this `FunkinSprite`, but invisible. @@ -17,15 +21,20 @@ class FunkinSprite extends FlxSprite public var doInvisibleDraw:Bool = false; /** - * The Animate Atlas Object if the animation is one. + * Settings to use when initializing texture atlases. */ - public var atlas:FlxAnimate; + public var atlasSettings:FlxAnimateSettings = {}; public function new(x:Float = 0, y:Float = 0) { super(x, y); } + override function initVars():Void + { + super.initVars(); + } + /** * Loads or creates a texture and applies it to this sprite. * @param path The asset path. (If `path` starts with a **#** then a color will be made instead and rectWidth + rectHeight will determine its size.) @@ -57,58 +66,29 @@ class FunkinSprite extends FlxSprite */ public function loadFrames(path:String, ?forcedType:Null):FunkinSprite { - if (atlas != null) - { - atlas.destroy(); - atlas = null; - } - - // TODO: make forcedType work, im too lazy - if (Paths.location.exists(path + '.xml')) { frames = Paths.content.sparrowAtlas(path); } else if (Paths.location.exists(path + '/Animation.json')) { - atlas = new FlxAnimate(0, 0, Paths.location.get(path), { - ShowPivot: false - }); + frames = Paths.content.animateAtlas(path, atlasSettings); } return this; } - override public function destroy():Void - { - if (atlas != null) - atlas.destroy(); - - super.destroy(); - } - override public function draw():Void { var oldAlpha:Float = alpha; if (doInvisibleDraw) alpha = 0.0001; - if (atlas != null) - { - updateAtlasDummy(); - atlas.draw(); - } - else - { - super.draw(); - } + super.draw(); if (doInvisibleDraw) { alpha = oldAlpha; - - if (atlas != null) - atlas.alpha = alpha; } } @@ -122,37 +102,6 @@ class FunkinSprite extends FlxSprite } #end - override public function update(elapsed:Float):Void - { - if (atlas != null) - atlas.update(elapsed); - - super.update(elapsed); - } - - /** - * Updates the Atlas dummy's values, so it looks like it belongs to this sprite. - * @see The Original Code: https://github.com/CodenameCrew/CodenameEngine/blob/f6deda2c84984202effdfc5f6b577c2d956aa7b5/source/funkin/backend/FunkinSprite.hx#L209C2-L232C3 - */ - @:privateAccess - public function updateAtlasDummy():Void - { - atlas.cameras = cameras; - atlas.scrollFactor = scrollFactor; - atlas.scale = scale; - atlas.offset = offset; - atlas.x = x; - atlas.y = y; - atlas.angle = angle; - atlas.alpha = alpha; - atlas.visible = visible; - atlas.flipX = flipX; - atlas.flipY = flipY; - atlas.shader = shader; - atlas.antialiasing = antialiasing; - atlas.colorTransform = colorTransform; - } - // ANIMATION BINDINGS /** @@ -174,11 +123,7 @@ class FunkinSprite extends FlxSprite if (animationStunned) return; - if (atlas != null) - atlas.anim.play(name, restart, reversed); - else - animation.play(name, restart, reversed); - + animation.play(name, restart, reversed); animationStunned = stunAnimations; currentAnim = name; } @@ -193,20 +138,18 @@ class FunkinSprite extends FlxSprite */ public function addAnimation(name:String, anim:String, ?indices:Array = null, ?frameRate:Float = 24, ?looped:Bool = true):Void { - if (atlas != null) + var atlasAnimList:Array = super.getAnimateAnimations(); + + if (atlasAnimList.contains(anim)) { - if (indices != null && indices.length > 0) - atlas.anim.addBySymbolIndices(name, anim + '\\', indices, frameRate, looped); - else - atlas.anim.addBySymbol(name, anim + '\\', frameRate, looped); + super.addAnimateAtlasAnimation(name, anim, indices, frameRate, looped); + return; } + + if (indices != null && indices.length > 0) + animation.addByIndices(name, anim + '0', indices, '', frameRate, looped); else - { - if (indices != null && indices.length > 0) - animation.addByIndices(name, anim + '0', indices, '', frameRate, looped); - else - animation.addByPrefix(name, anim + '0', frameRate, looped); - } + animation.addByPrefix(name, anim + '0', frameRate, looped); } /** @@ -216,7 +159,7 @@ class FunkinSprite extends FlxSprite function get_animationIsNull():Bool { - return (atlas != null) ? atlas.anim.curSymbol == null : animation.curAnim == null; + return animation.curAnim == null; } /** @@ -226,10 +169,7 @@ class FunkinSprite extends FlxSprite function get_animFinished():Bool { - if (animationIsNull) - return false; - - return ((atlas != null) ? atlas.anim.finished : animation.curAnim.finished) ?? false; + return animation?.curAnim?.finished ?? false; } /** @@ -240,10 +180,7 @@ class FunkinSprite extends FlxSprite if (animationIsNull) return; - if (atlas != null) - atlas.anim.curFrame = atlas.anim.length - 1; - else - animation.curAnim.finish(); + animation.curAnim.finish(); } /** @@ -256,7 +193,7 @@ class FunkinSprite extends FlxSprite if (animationIsNull) return false; - return ((atlas != null) ? atlas.anim.isPlaying : animation.curAnim.paused) ?? false; + return animation?.curAnim?.paused ?? false; } function set_animPaused(value:Bool):Bool @@ -264,20 +201,10 @@ class FunkinSprite extends FlxSprite if (animationIsNull) return value; - if (atlas != null) - { - if (value) - atlas.anim.pause(); - else - atlas.anim.resume(); - } + if (value) + animation.curAnim.pause(); else - { - if (value) - animation.curAnim.pause(); - else - animation.curAnim.resume(); - } + animation.curAnim.resume(); return value; } @@ -287,13 +214,13 @@ class FunkinSprite extends FlxSprite * @param name The animation name to check for. * @return If the animation exists. */ - @:privateAccess public function animationExists(name:String):Bool { - if (atlas != null) - return atlas.anim.symbolDictionary.get(name) != null; - else - return animation?.exists(name) ?? false; + var atlasAnimList:Array = super.getAnimateAnimations(); + if (atlasAnimList.contains(name)) + return true; + + return animation?.exists(name) ?? false; } /** @@ -308,21 +235,7 @@ class FunkinSprite extends FlxSprite if (_onAnimFinished == null) { _onAnimFinished = new FlxTypedSignalVoid>(); - - if (atlas != null) - { - atlas.anim.onComplete.add(() -> - { - _onAnimFinished.dispatch(currentAnim); - }); - } - else - { - animation.onFinish.add((_) -> - { - _onAnimFinished.dispatch(currentAnim); - }); - } + animation.onFinish.add((_) -> _onAnimFinished.dispatch(currentAnim)); } return _onAnimFinished; @@ -337,65 +250,11 @@ class FunkinSprite extends FlxSprite if (animationIsNull) return false; - if (atlas != null) - { - var animData = atlas.anim.symbolDictionary.get(id); - if (animData == null) - return false; - return animData.length > 1; - } - else - { - var animData = animation.getByName(id); - if (animData == null) - return false; - return animData.numFrames > 1; - } - } - - @:noCompletion - override function set_width(value:Float):Float - { - if (atlas != null) - { - atlas.width = value; - return atlas.width; - } - - return super.set_width(value); - } - - @:noCompletion - override function get_width():Float - { - if (atlas != null) - { - return atlas.width; - } - - return super.get_width(); - } - - @:noCompletion - override function set_height(value:Float):Float - { - if (atlas != null) - { - atlas.height = value; - return atlas.height; - } - - return super.set_height(value); - } - - override function get_height():Float - { - if (atlas != null) - { - return atlas.height; - } + var animData:Null = animation.getByName(id); + if (animData == null) + return false; - return super.get_height(); + return animData.numFrames > 1; } } diff --git a/src/funkin/objects/ui/freeplay/FreeplayDJ.hx b/src/funkin/objects/ui/freeplay/FreeplayDJ.hx index 5893c7e..8cb19ec 100644 --- a/src/funkin/objects/ui/freeplay/FreeplayDJ.hx +++ b/src/funkin/objects/ui/freeplay/FreeplayDJ.hx @@ -1,12 +1,26 @@ package funkin.objects.ui.freeplay; +import animate.FlxAnimateFrames; +import animate.internal.Frame; +import animate.internal.Layer; +import animate.internal.SymbolItem; +import animate.internal.Timeline; +import animate.internal.elements.FlxSpriteElement; +import animate.internal.elements.SymbolInstance; +import flixel.math.FlxMatrix; import flixel.util.FlxSignal; +import funkin.vis.dsp.SpectralAnalyzer; /* TODO: MAKE THIS LESS HARDCODED! */ class FreeplayDJ extends FunkinSprite { + /** + * Turn table light symbol names. + */ + public static final LIGHT_SYMBOL_NAMES:Array = ['table eq lights/left', 'table eq lights/middle', 'table eq lights/right']; + /** * Represents the sprite's current status. */ @@ -17,10 +31,20 @@ class FreeplayDJ extends FunkinSprite */ public var introDone:FlxSignal = new FlxSignal(); + /** + * The sound to use for an optional visualizer. + */ + public var sound(default, set):FlxSound; + public function new(x:Float, y:Float, id:String) { super(x, y); + atlasSettings = { + swfMode: true, + filterQuality: HIGH + }; + // todo: softcode this switch (id) { @@ -32,6 +56,8 @@ class FreeplayDJ extends FunkinSprite addAnimation('intro', 'boyfriend dj intro', 24, false); } + initVisualizer(); + onAnimFinished.add(onFinishAnim); } @@ -46,7 +72,8 @@ class FreeplayDJ extends FunkinSprite case Intro: currentState = Idle; introDone.dispatch(); - cast(atlas, FreeplayDJAtlas)?.initVisualizer(FlxG.sound.music); + sound = FlxG.sound.music; + default: } } @@ -116,26 +143,38 @@ class FreeplayDJ extends FunkinSprite #end } - override public function loadFrames(path:String, ?forcedType:Null):FunkinSprite + var turntableLights:Array = []; + var analyzer:Null; + var analyzerLevelsCache:Array = []; + var barCount:Int = 0; + + override public function draw():Void { - if (atlas != null) - { - atlas.destroy(); - atlas = null; - } + analyzerLevelsCache = analyzer?.getLevels(analyzerLevelsCache) ?? []; - if (Paths.location.exists(path + '/Animation.json')) - { - atlas = new FreeplayDJAtlas(0, 0, Paths.location.get(path), { - ShowPivot: false - }); + for (i => light in turntableLights) + light.analyzerLevel = analyzerLevelsCache[i]?.value ?? 0; - return this; - } - else - { - return super.loadFrames(path, forcedType); - } + super.draw(); + } + + function set_sound(snd:FlxSound):FlxSound + { + if (snd == null) + return snd; + + @:privateAccess + analyzer = new SpectralAnalyzer(snd._channel.__audioSource, barCount, 0.1, 40); + analyzer.minDb = -65; + analyzer.maxDb = -25; + analyzer.maxFreq = 22000; + analyzer.minFreq = 10; + + #if sys + analyzer.fftN = 256; + #end + + return snd; } override public function playAnimation(name:String, ?restart:Bool = false, ?stunAnimations:Bool = false, ?reversed:Bool = false):Void @@ -153,6 +192,55 @@ class FreeplayDJ extends FunkinSprite offset.set(); } } + + /** + * Initializes the visualizer. + * This will only work on animate atlases! + */ + public function initVisualizer():Void + { + var symbols:Array = LIGHT_SYMBOL_NAMES.map((symbolName:String) -> library.getSymbol(symbolName)); + + for (i => symbol in symbols) + { + trace('${i}: ${symbol.name}'); + + var timeline:Timeline = new Timeline(null, this.library, symbol.timeline.name); + timeline.frameCount = symbol.timeline.frameCount; + + var layer:Layer = new Layer(timeline); + timeline.layers.push(layer); + + var frame:Frame = new Frame(layer); + frame.duration = symbol.timeline.frameCount; + + var eqLight:EqLightInstance = new EqLightInstance(symbol.timeline, this.library, frame); + turntableLights.push(eqLight); + frame.add(eqLight); + + layer.frames.push(frame); + + @:privateAccess + { + for (_ in 0...frame.duration) + layer.frameIndices.push(0); + + timeline._bounds = timeline.getWholeBounds(false, timeline._bounds); + } + + symbol.timeline = timeline; + } + + barCount = turntableLights.length; + } + + override public function destroy():Void + { + super.destroy(); + + while (turntableLights.length > 0) + turntableLights.shift()?.destroy(); + } } enum FreeplayDJState @@ -205,3 +293,28 @@ enum FreeplayDJState */ CharSelect; } + +class EqLightInstance extends SymbolInstance +{ + /** + * The analyzer level. + * Goes from 0 to 1. + */ + public var analyzerLevel:Float = 0; + + public function new(timeline:Timeline, parent:FlxAnimateFrames, frame:Frame) + { + super(null, parent, frame); + + this.libraryItem = new SymbolItem(timeline); + this.libraryItem.name += " copy"; + this.matrix = new FlxMatrix(); + this.isColored = false; + } + + override public function getFrameIndex(index:Int, frameIndex:Int = 0):Int + { + final lastIndex:Int = libraryItem.timeline.frameCount - 1; + return Math.floor(FlxMath.remapToRange(analyzerLevel, 0, 1, lastIndex, 0)); + } +} diff --git a/src/funkin/util/FlxAnimateUtil.hx b/src/funkin/util/FlxAnimateUtil.hx new file mode 100644 index 0000000..6fb4bda --- /dev/null +++ b/src/funkin/util/FlxAnimateUtil.hx @@ -0,0 +1,124 @@ +package funkin.util; + +import animate.FlxAnimate; +import animate.FlxAnimateJson; +import animate.internal.Frame; +import animate.internal.Timeline; + +class FlxAnimateUtil +{ + /** + * Gets every possible animation the texture atlas provides. + * @param sprite The sprite to check for animations. + * @return A list of all possible animations. + */ + public static function getAnimateAnimations(sprite:FlxAnimate):Array + { + var toReturn:Array = []; + + var defaultTimeline:Null = sprite.anim.getDefaultTimeline(); + if (defaultTimeline == null) + return toReturn; + + var timelines:Array = sprite.anim.getCollectionTimelines() ?? []; + timelines.unshift(defaultTimeline); + + var timelineAnims:Array = []; + for (timeline in timelines) + { + timelineAnims.resize(0); + + var frames:Array = [for (l in timeline.layers) for (i in l.frames) i]; + + for (frame in frames) + { + if (frame.name.length < 1) + continue; + + timelineAnims.push(frame.name.rtrim()); + } + + @:privateAccess + { + for (symbolItem in sprite.library.dictionary.iterator()) + timelineAnims.push(symbolItem.name); + + // Are the loops below needed?? + // I feel all symbols are already loaded in already due to the master symbol containing them. + + if (sprite.library._isInlined) + { + for (i in 0...sprite.library._symbolDictionary?.length ?? 0) + timelineAnims.push(sprite.library._symbolDictionary[i].SN); + } + else + { + for (symbol in sprite.library._libraryList) + timelineAnims.push(symbol); + } + } + + for (animName in timelineAnims) + { + var fullName:String = '${timeline.name}\\\\$animName'; + + if (!toReturn.contains(animName)) + toReturn.push(animName); + + if (!toReturn.contains(fullName)) + toReturn.push(fullName); + } + } + + return toReturn; + } + + /** + * Adds a Texture Atlas Animation to a sprite. + * Frame Labels and Symbols are supported. + * @param sprite The sprite to apply this animation to. + * @param name What this animation should be called (e.g. `"run"`). + * @param prefix The name of the Texture Atlas animation internally. + * @param indices An array of numbers indicating what frames to play in what order (e.g. `[0, 1, 2]`). + * @param frameRate The speed in frames per second that the animation should play at (e.g. `40` fps), leave ``null`` to use the default framerate. + * @param looped Whether or not the animation is looped or just plays once. + * @param flipX Whether the frames should be flipped horizontally. + * @param flipY Whether the frames should be flipped vertically. + */ + public static function addAnimateAtlasAnimation(sprite:FlxAnimate, name:String, prefix:String, ?indices:Array, ?frameRate:Float, ?looped:Bool = true, + ?flipX:Bool, ?flipY:Bool):Void + { + if (sprite.library == null) + return; + + var slashIndex:Int = prefix.indexOf('\\\\'); + var timeline:Null = null; + + if (slashIndex >= 0) + { + var timelines:Array = sprite.anim.getCollectionTimelines() ?? []; + timelines.unshift(sprite.anim.getDefaultTimeline()); + + var timelineName:String = prefix.substring(0, slashIndex); + timeline = timelines?.filter((timeline:Timeline) -> timeline.name == timelineName)[0]; + } + + var newPrefix:String = slashIndex != -1 ? prefix.substring(slashIndex + '\\\\'.length) : prefix; + var foundLabelFrames:Array = sprite.anim.findFrameLabelIndices(newPrefix, timeline); + + if (foundLabelFrames.length > 0) + { + if (indices != null) + sprite.anim.addByFrameLabelIndices(name, newPrefix, indices, frameRate, looped, flipX, flipY); + else + sprite.anim.addByFrameLabel(name, newPrefix, frameRate, looped, flipX, flipY); + } + else + { + if (indices != null) + sprite.anim.addBySymbolIndices(name, newPrefix, indices, frameRate, looped, flipX, flipY); + else + sprite.anim.addBySymbol(name, newPrefix, frameRate, looped, flipX, flipY); + } + } +} diff --git a/src/funkin/util/visualizer/AnalyzerAnimationHelper.hx b/src/funkin/util/visualizer/AnalyzerAnimationHelper.hx deleted file mode 100644 index 3960c6c..0000000 --- a/src/funkin/util/visualizer/AnalyzerAnimationHelper.hx +++ /dev/null @@ -1,83 +0,0 @@ -package funkin.util.visualizer; - -import funkin.vis.dsp.SpectralAnalyzer; - -class AnalyzerAnimationHelper -{ - var analyzer:SpectralAnalyzer; - - var barCount:Int; - var barHeight:Int; - - /** - * The sound to use for this analyzer. - */ - public var snd:FlxSound; - - /** - * Whether this analyzer is ready or not. - */ - public var ready(get, null):Bool; - - function get_ready():Bool - { - return snd != null && analyzer != null; - } - - public function new(snd:FlxSound, barCount:Int, barHeight:Int) - { - this.snd = snd; - this.barCount = barCount; - this.barHeight = barHeight; - } - - public function initAnalyzer(?peakHold:Int = 30):Void - { - @:privateAccess - analyzer = new SpectralAnalyzer(snd._channel.__audioSource, barCount, 0.1, peakHold); - - #if desktop - // On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5 - // So we want to manually change it! - analyzer.fftN = 256; - #end - } - - public var frameMap:Array = []; - - public function updateFFT():Void - { - var levels; - try - { - levels = analyzer.getLevels(); - } - catch (e) - { - trace('Couldn\'t load levels! $e'); - return; - } - - frameMap = []; - - var len:Int = cast Math.min(barCount, levels.length); - - for (i in 0...len) - { - var animFrame:Int = Math.round(levels[i].value * barHeight); - - #if desktop - // Web version scales with the Flixel volume level. - // This line brings platform parity but looks worse. - // animFrame = Math.round(animFrame * FlxG.sound.volume); - #end - - animFrame = Math.floor(Math.min(barHeight, animFrame)); - animFrame = Math.floor(Math.max(0, animFrame)); - - animFrame = Std.int(Math.abs(animFrame - barHeight)); // shitty dumbass flip, cuz dave got da shit backwards lol! - - frameMap[i] = animFrame; - } - } -}