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 funkin.audio.FunkinSound; import openfl.display.Bitmap; import openfl.display.Sprite; import openfl.display.BitmapData; import openfl.display.PNGEncoderOptions; import openfl.geom.Matrix; import openfl.geom.Rectangle; import openfl.utils.ByteArray; import openfl.events.MouseEvent; 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 (hasPressedScreenshot()) { 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.F3], shouldHideMouse: false, fancyPreview: true, })); } public function hasPressedScreenshot():Bool { return PlayerSettings.player1.controls.SCREENSHOT; } 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 { var flashBitmap = new Bitmap(new BitmapData(Std.int(FlxG.stage.width), Std.int(FlxG.stage.height), false, 0xFFFFFFFF)); var flashSpr = new Sprite(); flashSpr.addChild(flashBitmap); FlxG.stage.addChild(flashSpr); FlxTween.tween(flashSpr, {alpha: 0}, 0.15, {ease: FlxEase.quadOut, onComplete: _ -> FlxG.stage.removeChild(flashSpr)}); // Play a sound (auto-play is true). FunkinSound.playOnce(Paths.sound('screenshot'), 1.0); } 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 = 1.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 { // ermmm stealing this?? var wasMouseHidden = false; if (!FlxG.mouse.visible) { wasMouseHidden = true; Cursor.show(); } // so that it doesnt change the alpha when tweening in/out var changingAlpha:Bool = false; // fuck it, cursed locally scoped functions, purely because im lazy // (and so we can check changingAlpha, which is locally scoped.... because I'm lazy...) var onHover = function(e:MouseEvent) { if (!changingAlpha) e.target.alpha = 0.6; }; var onHoverOut = function(e:MouseEvent) { if (!changingAlpha) e.target.alpha = 1; } var scale:Float = 0.25; var w:Int = Std.int(bitmap.bitmapData.width * scale); var h:Int = Std.int(bitmap.bitmapData.height * scale); var preview:BitmapData = new BitmapData(w, h, true); var matrix:openfl.geom.Matrix = new openfl.geom.Matrix(); matrix.scale(scale, scale); preview.draw(bitmap.bitmapData, matrix); // used for movement + button stuff var previewSprite = new Sprite(); previewSprite.buttonMode = true; previewSprite.addEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder); previewSprite.addEventListener(MouseEvent.MOUSE_OVER, onHover); previewSprite.addEventListener(MouseEvent.MOUSE_OUT, onHoverOut); FlxG.stage.addChild(previewSprite); previewSprite.alpha = 0.0; previewSprite.y -= 10; var previewBitmap = new Bitmap(preview); previewSprite.addChild(previewBitmap); // Wait to fade in. new FlxTimer().start(PREVIEW_INITIAL_DELAY, function(_) { // Fade in. changingAlpha = true; FlxTween.tween(previewSprite, {alpha: 1.0, y: 0}, PREVIEW_FADE_IN_DURATION, { ease: FlxEase.quartOut, onComplete: function(_) { changingAlpha = false; // Wait to fade out. new FlxTimer().start(PREVIEW_FADE_OUT_DELAY, function(_) { changingAlpha = true; // Fade out. FlxTween.tween(previewSprite, {alpha: 0.0, y: 10}, PREVIEW_FADE_OUT_DURATION, { ease: FlxEase.quartInOut, onComplete: function(_) { if (wasMouseHidden) { Cursor.hide(); } previewSprite.removeEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder); previewSprite.removeEventListener(MouseEvent.MOUSE_OVER, onHover); previewSprite.removeEventListener(MouseEvent.MOUSE_OUT, onHoverOut); FlxG.stage.removeChild(previewSprite); } }); }); } }); }); } function openScreenshotsFolder(e:MouseEvent):Void { FileUtil.openFolder(SCREENSHOT_FOLDER); } 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); } } }