diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index 37977dbf4..066b77516 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -238,6 +238,82 @@ class Preferences return value; } + /** + * If enabled, the game will hide the mouse when taking a screenshot. + * @default `true` + */ + public static var shouldHideMouse(get, set):Bool; + + static function get_shouldHideMouse():Bool + { + return Save?.instance?.options?.screenshot?.shouldHideMouse ?? true; + } + + static function set_shouldHideMouse(value:Bool):Bool + { + var save:Save = Save.instance; + save.options.screenshot.shouldHideMouse = value; + save.flush(); + return value; + } + + /** + * If enabled, the game will show a preview after taking a screenshot. + * @default `true` + */ + public static var fancyPreview(get, set):Bool; + + static function get_fancyPreview():Bool + { + return Save?.instance?.options?.screenshot?.fancyPreview ?? true; + } + + static function set_fancyPreview(value:Bool):Bool + { + var save:Save = Save.instance; + save.options.screenshot.fancyPreview = value; + save.flush(); + return value; + } + + /** + * If enabled, the game will save any screenshots taken to this format. + * @default `PNG` + */ + public static var saveFormat(get, set):Any; + + static function get_saveFormat():Any + { + return Save?.instance?.options?.screenshot?.saveFormat ?? 'PNG'; + } + + static function set_saveFormat(value):Any + { + var save:Save = Save.instance; + save.options.screenshot.saveFormat = value; + save.flush(); + return value; + } + + /** + * If enabled, the game will save JEPG screenshots with this quality percentage. + * @default `80` + */ + public static var jpegQuality(get, set):Int; + + static function get_jpegQuality():Int + { + return Save?.instance?.options?.screenshot?.jpegQuality ?? 80; + } + + static function set_jpegQuality(value:Int):Int + { + var save:Save = Save.instance; + save.options.screenshot.jpegQuality = value; + save.flush(); + return value; + } + /** * Loads the user's preferences from the save data and apply them. */ diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 4d2bab9d0..a65988387 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -107,6 +107,14 @@ class Save audioVisualOffset: 0, unlockedFramerate: false, + screenshot: + { + shouldHideMouse: true, + fancyPreview: true, + saveFormat: 'PNG', + jpegQuality: 80, + }, + controls: { // Leave controls blank so defaults are loaded. @@ -1388,6 +1396,19 @@ typedef SaveDataOptions = */ var unlockedFramerate:Bool; + // screenshot options + var screenshot: + { + // Should the mouse be hidden when taking a screenshot? + var shouldHideMouse:Bool; + // Show a fancy preview + var fancyPreview:Bool; + // The save format of the screenshot, PNG or JPEG + var saveFormat:String; + // The JPEG Quality, if we're saving to the format + var jpegQuality:Int; + }; + var controls: { var p1: diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index ad4ddabc9..1e20dfb5f 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -119,6 +119,19 @@ class PreferencesMenu extends Page Preferences.autoFullscreen = value; }, Preferences.autoFullscreen); + createPrefItemCheckbox('Hide Mouse', 'Enable to hide the mouse when taking a screenshot', function(value:Bool):Void { + Preferences.shouldHideMouse = value; + }, Preferences.shouldHideMouse); + createPrefItemCheckbox('Fancy Preview', 'Enable to show a preview after taking a screenshot', function(value:Bool):Void { + Preferences.fancyPreview = value; + }, Preferences.fancyPreview); + createPrefItemEnum('Save Format', 'Save screenshots to this format', ['PNG' => 'PNG', 'JPEG' => 'JPEG'], function(value:String):Void { + Preferences.saveFormat = value; + }, Preferences.saveFormat); + createPrefItemNumber('JPEG Quality', 'The quality of JPEG screenshots', function(value:Float) { + Preferences.jpegQuality = Std.int(value); + }, null, Preferences.jpegQuality, 0, 100, 5, 0); + #if web createPrefItemCheckbox('Unlocked Framerate', 'Enable to unlock the framerate', function(value:Bool):Void { Preferences.unlockedFramerate = value; diff --git a/source/funkin/util/plugins/ScreenshotPlugin.hx b/source/funkin/util/plugins/ScreenshotPlugin.hx index 6ff7187f9..6234a0e89 100644 --- a/source/funkin/util/plugins/ScreenshotPlugin.hx +++ b/source/funkin/util/plugins/ScreenshotPlugin.hx @@ -20,14 +20,12 @@ import openfl.display.PNGEncoderOptions; import openfl.geom.Rectangle; import openfl.utils.ByteArray; import openfl.events.MouseEvent; +import funkin.Preferences; typedef ScreenshotPluginParams = { - hotkeys:Array, ?region:Rectangle, - shouldHideMouse:Bool, flashColor:Null, - fancyPreview:Bool, }; /** @@ -36,17 +34,37 @@ typedef ScreenshotPluginParams = */ class ScreenshotPlugin extends FlxBasic { + /** + * Current `ScreenshotPlugin` instance + */ + public static var instance:ScreenshotPlugin = null; + public static final SCREENSHOT_FOLDER = 'screenshots'; - var _hotkeys:Array; + var region:Null; - var _region:Null; + /** + * The color used for the flash + */ + public static var flashColor(default, set):Int = 0xFFFFFFFF; - var _shouldHideMouse:Bool; + public static function set_flashColor(v:Int):Int + { + flashColor = v; + if (instance != null && instance.flashBitmap != null) instance.flashBitmap.bitmapData = new BitmapData(lastWidth, lastHeight, true, v); + return flashColor; + } - var _flashColor:Null; + /** + * The save format used to save the screenshots, default to `PNG`. + * The current available formats are `PNG` and `JPEG` + */ + public static var saveFormat(get, null):FileFormatOption; - var _fancyPreview:Bool; + public static function get_saveFormat() + { + return Preferences.saveFormat; + } /** * A signal fired before the screenshot is taken. @@ -66,7 +84,7 @@ class ScreenshotPlugin extends FlxBasic var flashSprite:Sprite; var flashBitmap:Bitmap; var previewSprite:Sprite; - var shotDisplayBitmap:Bitmap; + var shotPreviewBitmap:Bitmap; var outlineBitmap:Bitmap; var wasMouseHidden = false; @@ -89,6 +107,14 @@ class ScreenshotPlugin extends FlxBasic { super(); + if (instance != null) + { + destroy(); + return; + } + + instance = this; + lastWidth = FlxG.width; lastHeight = FlxG.height; @@ -109,17 +135,14 @@ class ScreenshotPlugin extends FlxBasic outlineBitmap.y = 5; previewSprite.addChild(outlineBitmap); - shotDisplayBitmap = new Bitmap(); - shotDisplayBitmap.scaleX /= 5; - shotDisplayBitmap.scaleY /= 5; - previewSprite.addChild(shotDisplayBitmap); + shotPreviewBitmap = new Bitmap(); + shotPreviewBitmap.scaleX /= 5; + shotPreviewBitmap.scaleY /= 5; + previewSprite.addChild(shotPreviewBitmap); containerThing.addChild(flashSprite); - _hotkeys = params.hotkeys; - _region = params.region ?? null; - _shouldHideMouse = params.shouldHideMouse; - _flashColor = params.flashColor; - _fancyPreview = params.fancyPreview; + region = params.region ?? null; + flashColor = params.flashColor; onPreScreenshot = new FlxTypedSignalVoid>(); onPostScreenshot = new FlxTypedSignalVoid>(); @@ -137,7 +160,7 @@ class ScreenshotPlugin extends FlxBasic } else { - // if the loop has been started, and is finished, then we swich which groups are active + // if the loop has been started, and is finished, then we kill. it if (asyncLoop.finished) { trace("finished saving screenshot buffer"); @@ -155,33 +178,46 @@ class ScreenshotPlugin extends FlxBasic if (hasPressedScreenshot()) { - if (_shouldHideMouse && !wasMouseHidden && FlxG.mouse.visible) + if (Preferences.shouldHideMouse && !wasMouseHidden && FlxG.mouse.visible) { wasMouseHidden = true; // argh, even though this is happening, it's not happening fast enough before the screenshot is taken! // Whatever, I made the popup not show up, that's all I need + // If I just have enough things here and run this first, it'll eventually work, right? Cursor.hide(); } + if (FlxG.keys.pressed.SHIFT) + { + // If there's no preview and we have the option enabled we're not going to bother opening the folder + if (containerThing.alpha == 0 && Preferences.fancyPreview == true || previewSprite.alpha == 0 && Preferences.fancyPreview == true) return; + else + { + openScreenshotsFolder(); + return; // We're only opening the screenshots folder (we don't want to accidently take a screenshot after this) + } + } + // screenshot spamming timer if (screenshotSpammedTimer == null || screenshotSpammedTimer.finished == true) { screenshotSpammedTimer = new FlxTimer().start(1, function(_) { + // The player's stopped spamming shots, so we can stop the screenshot spam mode too containerThing.alpha = 1; screenshotBeingSpammed = false; if (screenshotBuffer[0] != null) saveBufferedScreenshots(screenshotBuffer, screenshotNameBuffer); - if (_shouldHideMouse && wasMouseHidden && !FlxG.mouse.visible) + if (wasMouseHidden && !FlxG.mouse.visible) { wasMouseHidden = false; Cursor.show(); } }); } - else + else // Pressing the screenshot key more than once every second enables the screenshot spam mode and resets the timer { screenshotBeingSpammed = true; containerThing.alpha = 0; screenshotSpammedTimer.reset(1); } - capture(); + capture(); // After all these checks, we finally try taking a screenshot } } @@ -193,11 +229,6 @@ class ScreenshotPlugin extends FlxBasic 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.F3], - shouldHideMouse: true, - fancyPreview: true, })); } @@ -210,16 +241,16 @@ class ScreenshotPlugin extends FlxBasic #end } - public function updatePreferences():Void + public function updateFlashColor():Void { - _flashColor = Preferences.flashingLights ? FlxColor.WHITE : null; + Preferences.flashingLights ? set_flashColor(FlxColor.WHITE) : null; } private function resizeBitmap(width:Int, height:Int) { lastWidth = width; lastHeight = height; - flashBitmap.bitmapData = new BitmapData(lastWidth, lastHeight, true, _flashColor); + flashBitmap.bitmapData = new BitmapData(lastWidth, lastHeight, true, flashColor); outlineBitmap.bitmapData = new BitmapData(Std.int(lastWidth / 5) + 10, Std.int(lastHeight / 5) + 10, true, 0xFFFFFFFF); } @@ -229,7 +260,7 @@ class ScreenshotPlugin extends FlxBasic */ public function defineCaptureRegion(x:Int, y:Int, width:Int, height:Int):Void { - _region = new Rectangle(x, y, width, height); + region = new Rectangle(x, y, width, height); } /** @@ -239,7 +270,7 @@ class ScreenshotPlugin extends FlxBasic { onPreScreenshot.dispatch(); - var captureRegion = _region != null ? _region : new Rectangle(0, 0, FlxG.stage.stageWidth, FlxG.stage.stageHeight); + // var captureRegion = region != null ? region : new Rectangle(0, 0, FlxG.stage.stageWidth, FlxG.stage.stageHeight); // The actual work. // var bitmap = new Bitmap(new BitmapData(Math.floor(captureRegion.width), Math.floor(captureRegion.height), true, 0x00000000)); // Create a transparent empty bitmap. @@ -274,7 +305,7 @@ class ScreenshotPlugin extends FlxBasic showFancyPreview(shot); - if (_shouldHideMouse && wasMouseHidden && !FlxG.mouse.visible) + if (wasMouseHidden && !FlxG.mouse.visible) { wasMouseHidden = false; Cursor.show(); @@ -322,9 +353,10 @@ class ScreenshotPlugin extends FlxBasic function showFancyPreview(shot:Bitmap):Void { - shotDisplayBitmap.bitmapData = shot.bitmapData; - shotDisplayBitmap.x = outlineBitmap.x + 5; - shotDisplayBitmap.y = outlineBitmap.y + 5; + if (!Preferences.fancyPreview) return; // Sorry, the previews' been cancelled + shotPreviewBitmap.bitmapData = shot.bitmapData; + shotPreviewBitmap.x = outlineBitmap.x + 5; + shotPreviewBitmap.y = outlineBitmap.y + 5; previewSprite.alpha = 1; FlxTween.tween(previewSprite, {alpha: 0}, 0.5, {startDelay: .5}); @@ -440,7 +472,7 @@ class ScreenshotPlugin extends FlxBasic */ } - function openScreenshotsFolder(e:MouseEvent):Void + function openScreenshotsFolder():Void { FileUtil.openFolder(SCREENSHOT_FOLDER); } @@ -466,11 +498,11 @@ class ScreenshotPlugin extends FlxBasic } /** - * Convert a Bitmap to a PNG ByteArray to save to a file. + * Convert a Bitmap to a PNG or JPEG ByteArray to save to a file. */ - static function encodePNG(bitmap:Bitmap):ByteArray + static function encode(bitmap:Bitmap):ByteArray { - return bitmap.bitmapData.encode(bitmap.bitmapData.rect, new PNGEncoderOptions()); + return bitmap.bitmapData.encode(bitmap.bitmapData.rect, saveFormat.returnEncoder()); } var previousScreenshotName:String; @@ -487,7 +519,8 @@ class ScreenshotPlugin extends FlxBasic if (previousScreenshotName != targetPath) { previousScreenshotName = targetPath; - targetPath = getScreenshotPath() + targetPath + '.png'; + targetPath = getScreenshotPath() + targetPath + '.' + Std.string(saveFormat) + .toLowerCase(); // This is stupid, I'd pefer it did this in the FileFormatOption but idk how previousScreenshotCopyNum = 2; trace(previousScreenshotName); trace(targetPath); @@ -501,13 +534,13 @@ class ScreenshotPlugin extends FlxBasic previousScreenshotCopyNum++; } previousScreenshotName = newTargetPath; - targetPath = getScreenshotPath() + newTargetPath + '.png'; + targetPath = getScreenshotPath() + newTargetPath + '.' + Std.string(saveFormat).toLowerCase(); } - var pngData = encodePNG(bitmap); + var pngData = encode(bitmap); if (pngData == null) { - trace('[WARN] Failed to encode PNG data.'); + trace('[WARN] Failed to encode ${saveFormat} data'); previousScreenshotName = null; return; } @@ -525,11 +558,12 @@ class ScreenshotPlugin extends FlxBasic } } - // I'm very happy with this code, all of it just works - Lasercar + // I' m very happy with this code, all of it just works - Lasercar function function saveBufferedScreenshots(screenshots:Array, screenshotNames) { trace('Saving screenshot buffer'); var i:Int = 0; + asyncLoop = new FlxAsyncLoop(screenshots.length, () -> { if (screenshots[i] != null) { @@ -539,9 +573,51 @@ class ScreenshotPlugin extends FlxBasic }, 1); // for (i in 0...screenshots.length) // { - // } getCurrentState().add(asyncLoop); showFancyPreview(screenshots[screenshots.length - 1]); // show the preview for the last screenshot } + + override public function destroy():Void + { + if (instance == this) instance = null; + + if (FlxG.plugins.list.contains(this)) FlxG.plugins.remove(this); + + FlxG.signals.gameResized.remove(this.resizeBitmap); + FlxG.stage.removeChild(containerThing); + + super.destroy(); + + if (containerThing == null) return; + + @:privateAccess + for (parent in [containerThing, flashSprite, previewSprite]) + for (child in parent.__children) + parent.removeChild(child); + + containerThing = null; + flashSprite = null; + flashBitmap = null; + previewSprite = null; + shotPreviewBitmap = null; + outlineBitmap = null; + } +} + +enum abstract FileFormatOption(String) from String +{ + var JPEG = ".jpg"; + var PNG = ".png"; + + public function returnEncoder():Any + { + return switch (this : FileFormatOption) + { + // I have to make this check the string value rather than the var because I can't get it to work as a var + // JPEG encoder causes the game to crash????? + // case 'JPEG': new openfl.display.JPEGEncoderOptions(Preferences.jpegQuality); + default: new openfl.display.JPEGEncoderOptions(); + } + } }