From e24c78ae160dbdd968ae5753f70ab5b4058b1e66 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 15 Feb 2024 21:34:24 -0500 Subject: [PATCH] Implemented a screenshot button. FancyPreview is broken. --- source/funkin/InitState.hx | 8 +- source/funkin/Preferences.hx | 30 +- source/funkin/save/Save.hx | 6 + source/funkin/util/FileUtil.hx | 6 + .../funkin/util/plugins/ScreenshotPlugin.hx | 269 ++++++++++++++++++ 5 files changed, 312 insertions(+), 7 deletions(-) create mode 100644 source/funkin/util/plugins/ScreenshotPlugin.hx diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 399f52498..ec6a4e6f0 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -50,11 +50,13 @@ class InitState extends FlxState */ public override function create():Void { + // Setup a bunch of important Flixel stuff. setupShit(); - // loadSaveData(); // Moved to Main.hx // Load player options from save data. + // Flixel has already loaded the save data, so we can just use it. Preferences.init(); + // Load controls from save data. PlayerSettings.init(); @@ -198,6 +200,10 @@ class InitState extends FlxState // // FLIXEL PLUGINS // + // Plugins provide a useful interface for globally active Flixel objects, + // that receive update events regardless of the current state. + // TODO: Move Module behavior to a Flixel plugin. + funkin.util.plugins.ScreenshotPlugin.initialize(); funkin.util.plugins.EvacuateDebugPlugin.initialize(); funkin.util.plugins.ReloadAssetsDebugPlugin.initialize(); funkin.util.plugins.WatchPlugin.initialize(); diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index 6b0911ede..039a4c285 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -20,7 +20,10 @@ class Preferences static function set_naughtyness(value:Bool):Bool { - return Save.get().options.naughtyness = value; + var save = Save.get(); + save.options.naughtyness = value; + save.flush(); + return value; } /** @@ -36,7 +39,10 @@ class Preferences static function set_downscroll(value:Bool):Bool { - return Save.get().options.downscroll = value; + var save = Save.get(); + save.options.downscroll = value; + save.flush(); + return value; } /** @@ -52,7 +58,10 @@ class Preferences static function set_flashingLights(value:Bool):Bool { - return Save.get().options.flashingLights = value; + var save = Save.get(); + save.options.flashingLights = value; + save.flush(); + return value; } /** @@ -68,7 +77,10 @@ class Preferences static function set_zoomCamera(value:Bool):Bool { - return Save.get().options.zoomCamera = value; + var save = Save.get(); + save.options.zoomCamera = value; + save.flush(); + return value; } /** @@ -89,7 +101,10 @@ class Preferences toggleDebugDisplay(value); } - return Save.get().options.debugDisplay = value; + var save = Save.get(); + save.options.debugDisplay = value; + save.flush(); + return value; } /** @@ -107,7 +122,10 @@ class Preferences { if (value != Save.get().options.autoPause) FlxG.autoPause = value; - return Save.get().options.autoPause = value; + var save = Save.get(); + save.options.autoPause = value; + save.flush(); + return value; } public static function init():Void diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 6a4dd048c..5bbded231 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -113,6 +113,9 @@ abstract Save(RawSaveData) }; } + /** + * NOTE: Modifications will not be saved without calling `Save.flush()`! + */ public var options(get, never):SaveDataOptions; function get_options():SaveDataOptions @@ -120,6 +123,9 @@ abstract Save(RawSaveData) return this.options; } + /** + * NOTE: Modifications will not be saved without calling `Save.flush()`! + */ public var modOptions(get, never):Map; function get_modOptions():Map diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 612737680..5d189c0e9 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -20,6 +20,7 @@ class FileUtil { public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc"); public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); + public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png"); public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo = { @@ -31,6 +32,11 @@ class FileUtil extension: 'zip', label: 'ZIP Archive', }; + public static final FILE_EXTENSION_INFO_PNG:FileDialogExtensionInfo = + { + extension: 'png', + label: 'PNG Image', + }; /** * Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected. diff --git a/source/funkin/util/plugins/ScreenshotPlugin.hx b/source/funkin/util/plugins/ScreenshotPlugin.hx new file mode 100644 index 000000000..a8b494fee --- /dev/null +++ b/source/funkin/util/plugins/ScreenshotPlugin.hx @@ -0,0 +1,269 @@ +package funkin.util.plugins; + +import flixel.FlxBasic; +import flixel.FlxCamera; +import flixel.FlxG; +import flixel.FlxState; +import flixel.graphics.FlxGraphic; +import flixel.input.keyboard.FlxKey; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxSignal; +import flixel.util.FlxTimer; +import funkin.graphics.FunkinSprite; +import funkin.input.Cursor; +import openfl.display.Bitmap; +import openfl.display.BitmapData; +import openfl.display.PNGEncoderOptions; +import openfl.geom.Matrix; +import openfl.geom.Rectangle; +import openfl.utils.ByteArray; + +typedef ScreenshotPluginParams = +{ + hotkeys:Array, + ?region:Rectangle, + shouldHideMouse:Bool, + flashColor:Null, + fancyPreview:Bool, +}; + +/** + * What if `flixel.addons.plugin.screengrab.FlxScreenGrab` but it's better? + * TODO: Contribute this upstream. + */ +class ScreenshotPlugin extends FlxBasic +{ + public static final SCREENSHOT_FOLDER = 'screenshots'; + + var _hotkeys:Array; + + var _region:Null; + + var _shouldHideMouse:Bool; + + var _flashColor:Null; + + var _fancyPreview:Bool; + + /** + * A signal fired before the screenshot is taken. + */ + public var onPreScreenshot(default, null):FlxTypedSignalVoid>; + + /** + * A signal fired after the screenshot is taken. + * @param bitmap The bitmap that was captured. + */ + public var onPostScreenshot(default, null):FlxTypedSignalVoid>; + + public function new(params:ScreenshotPluginParams) + { + super(); + + _hotkeys = params.hotkeys; + _region = params.region ?? null; + _shouldHideMouse = params.shouldHideMouse; + _flashColor = params.flashColor; + _fancyPreview = params.fancyPreview; + + onPreScreenshot = new FlxTypedSignalVoid>(); + onPostScreenshot = new FlxTypedSignalVoid>(); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (FlxG.keys.anyJustReleased(_hotkeys)) + { + capture(); + } + } + + /** + * Initialize the screenshot plugin. + */ + public static function initialize():Void + { + FlxG.plugins.addPlugin(new ScreenshotPlugin( + { + flashColor: Preferences.flashingLights ? FlxColor.WHITE : null, // Was originally a black flash. + + // TODO: Add a way to configure screenshots from the options menu. + hotkeys: [FlxKey.PRINTSCREEN], + shouldHideMouse: false, + fancyPreview: true, // TODO: Fancy preview is broken on substates. + })); + } + + public function updatePreferences():Void + { + _flashColor = Preferences.flashingLights ? FlxColor.WHITE : null; + } + + /** + * Defines the region of the screen that should be captured. + * You don't need to call this method if you want to capture the entire screen, that's the default behavior. + */ + public function defineCaptureRegion(x:Int, y:Int, width:Int, height:Int):Void + { + _region = new Rectangle(x, y, width, height); + } + + /** + * Capture the game screen as a bitmap. + */ + public function capture():Void + { + onPreScreenshot.dispatch(); + + var captureRegion = _region != null ? _region : new Rectangle(0, 0, FlxG.stage.stageWidth, FlxG.stage.stageHeight); + + var wasMouseHidden = false; + if (_shouldHideMouse && FlxG.mouse.visible) + { + wasMouseHidden = true; + Cursor.hide(); + } + + // The actual work. + // var bitmap = new Bitmap(new BitmapData(Math.floor(captureRegion.width), Math.floor(captureRegion.height), true, 0x00000000)); // Create a transparent empty bitmap. + // var drawMatrix = new Matrix(1, 0, 0, 1, -captureRegion.x, -captureRegion.y); // Modifying this will scale or skew the bitmap. + // bitmap.bitmapData.draw(FlxG.stage, drawMatrix); + var bitmap = new Bitmap(BitmapData.fromImage(FlxG.stage.window.readPixels())); + + if (wasMouseHidden) + { + Cursor.show(); + } + + // Save the bitmap to a file. + saveScreenshot(bitmap); + + // Show some feedback. + showCaptureFeedback(); + if (_fancyPreview) + { + showFancyPreview(bitmap); + } + + onPostScreenshot.dispatch(bitmap); + } + + final CAMERA_FLASH_DURATION = 0.25; + + /** + * Visual (and audio?) feedback when a screenshot is taken. + */ + function showCaptureFeedback():Void + { + if (_flashColor != null) + { + for (camera in FlxG.cameras.list) + { + camera.flash(_flashColor, CAMERA_FLASH_DURATION); + } + } + } + + static final PREVIEW_INITIAL_DELAY = 0.25; // How long before the preview starts fading in. + static final PREVIEW_FADE_IN_DURATION = 0.3; // How long the preview takes to fade in. + static final PREVIEW_FADE_OUT_DELAY = 0.25; // How long the preview stays on screen. + static final PREVIEW_FADE_OUT_DURATION = 0.3; // How long the preview takes to fade out. + + function showFancyPreview(bitmap:Bitmap):Void + { + // TODO: This function looks really nice but breaks substates. + var targetCamera = new FlxCamera(); + targetCamera.bgColor.alpha = 0; // Show the scene behind the camera. + FlxG.cameras.add(targetCamera); + + var flxGraphic = FlxGraphic.fromBitmapData(bitmap.bitmapData, false, "screenshot", false); + + var preview = new FunkinSprite(0, 0); + preview.frames = flxGraphic.imageFrame; + preview.setGraphicSize(bitmap.width / 4, bitmap.height / 4); + preview.x = FlxG.width - preview.width - 10; + preview.y = FlxG.height - preview.height - 10; + + preview.alpha = 0.0; + preview.cameras = [targetCamera]; + getCurrentState().add(preview); + + // Wait to fade in. + new FlxTimer().start(PREVIEW_INITIAL_DELAY, function(_) { + // Fade in. + FlxTween.tween(preview, {alpha: 1.0}, PREVIEW_FADE_IN_DURATION, + { + ease: FlxEase.elasticOut, + onComplete: function(_) { + // Wait to fade out. + new FlxTimer().start(PREVIEW_FADE_OUT_DELAY, function(_) { + // Fade out. + FlxTween.tween(preview, {alpha: 0.0}, PREVIEW_FADE_OUT_DURATION, + { + ease: FlxEase.elasticIn, + onComplete: function(_) { + preview.kill(); + } + }); + }); + } + }); + }); + } + + static function getCurrentState():FlxState + { + var state = FlxG.state; + while (state.subState != null) + { + state = state.subState; + } + return state; + } + + static function getScreenshotPath():String + { + return '$SCREENSHOT_FOLDER/screenshot-${DateUtil.generateTimestamp()}.png'; + } + + static function makeScreenshotPath():Void + { + FileUtil.createDirIfNotExists(SCREENSHOT_FOLDER); + } + + /** + * Convert a Bitmap to a PNG ByteArray to save to a file. + */ + static function encodePNG(bitmap:Bitmap):ByteArray + { + return bitmap.bitmapData.encode(bitmap.bitmapData.rect, new PNGEncoderOptions()); + } + + /** + * Save the generated bitmap to a file. + * @param bitmap The bitmap to save. + */ + static function saveScreenshot(bitmap:Bitmap) + { + makeScreenshotPath(); + var targetPath:String = getScreenshotPath(); + + var pngData = encodePNG(bitmap); + + if (pngData == null) + { + trace('[WARN] Failed to encode PNG data.'); + return; + } + else + { + trace('Saving screenshot to: ' + targetPath); + // TODO: Make this work on browser. + FileUtil.writeBytesToPath(targetPath, pngData); + } + } +}