-
-
Save Kwyrky/140a31c0d5f612a71e4d54a500adcf1a to your computer and use it in GitHub Desktop.
Store and export old frames in MonoGame using ImageSharp. MIT license: https://opensource.org/licenses/MIT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Graphics; | |
using SixLabors.ImageSharp; | |
using SixLabors.ImageSharp.Formats.Gif; | |
using SixLabors.ImageSharp.PixelFormats; | |
namespace GifRecorderMG | |
{ | |
public class FrameStore | |
{ | |
/// <summary> | |
/// Width of the frames in pixels. | |
/// </summary> | |
public int Width { get; } | |
/// <summary> | |
/// Height of the frames in pixels. | |
/// </summary> | |
public int Height { get; } | |
/// <summary> | |
/// The stored frames. | |
/// </summary> | |
public Microsoft.Xna.Framework.Color[][] Frames { get; } | |
private readonly Rgba32[] _rgbaBuffer; | |
private int _frameIndex; | |
/// <summary> | |
/// Number of frames that can be stored. | |
/// </summary> | |
public int FrameCapacity => Frames.Length; | |
/// <summary> | |
/// Number of stored frames. | |
/// </summary> | |
public int FrameCount { get; private set; } | |
/// <summary> | |
/// Create a new <see cref="FrameStore"/>. | |
/// </summary> | |
/// <param name="capacity">Number of frames to store.</param> | |
/// <param name="width">Width of each frame.</param> | |
/// <param name="height">Height of each frame.</param> | |
/// <exception cref="ArgumentOutOfRangeException"> | |
/// If <paramref name="capacity"/>, <paramref name="width"/> or <paramref name="height"/> | |
/// are less than or equal to zero. | |
/// </exception> | |
public FrameStore(int capacity, int width, int height) | |
{ | |
if (capacity <= 0) | |
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity should be larger than zero."); | |
if (width <= 0) | |
throw new ArgumentOutOfRangeException(nameof(width), "Width should be larger than zero."); | |
if (height <= 0) | |
throw new ArgumentOutOfRangeException(nameof(height), "Height should be larger than zero."); | |
Width = width; | |
Height = height; | |
Frames = new Microsoft.Xna.Framework.Color[capacity][]; | |
var frameSize = width * height; | |
for (var i = 0; i < capacity; i++) | |
Frames[i] = new Microsoft.Xna.Framework.Color[frameSize]; | |
_rgbaBuffer = new Rgba32[frameSize]; | |
} | |
/// <summary> | |
/// Add a frame at the end of the store. | |
/// </summary> | |
/// <param name="frame">The frame to add.</param> | |
/// <exception cref="ArgumentException">If the frame size does not match.</exception> | |
public void PushFrame(Microsoft.Xna.Framework.Color[] frame) | |
{ | |
if (Frames.Length < Width * Height) | |
throw new ArgumentException("Frame has less pixels than expected.", nameof(Frames)); | |
for (var i = 0; i < Width * Height; i++) | |
Frames[_frameIndex][i] = frame[i]; | |
_frameIndex = (_frameIndex + 1) % FrameCapacity; | |
if (FrameCount < FrameCapacity) | |
FrameCount++; | |
} | |
/// <summary> | |
/// Add a frame at the end of the store. | |
/// </summary> | |
/// <param name="frame">The frame to add.</param> | |
public void PushFrame(Texture2D frame) | |
{ | |
if (frame == null) | |
throw new ArgumentNullException(nameof(frame)); | |
frame.GetData(Frames[_frameIndex]); | |
_frameIndex = (_frameIndex + 1) % FrameCapacity; | |
if (FrameCount < FrameCapacity) | |
FrameCount++; | |
} | |
/// <summary> | |
/// Export a frame as a PNG image. | |
/// </summary> | |
/// <param name="path">Path to write the image to.</param> | |
/// <param name="index">Index of the frame.</param> | |
public void ExportFrame(string path, int index) | |
{ | |
using (var stream = File.OpenWrite(path)) | |
ExportFrame(stream, index); | |
} | |
/// <summary> | |
/// Export a frame as a PNG image. | |
/// </summary> | |
/// <param name="output">Stream to write the image to.</param> | |
/// <param name="index">Index of the frame.</param> | |
public void ExportFrame(Stream output, int index) | |
{ | |
if (index < 0) | |
throw new ArgumentOutOfRangeException(); | |
if (index >= FrameCount) | |
throw new ArgumentOutOfRangeException(); | |
var frameIndex = (_frameIndex + index) % FrameCapacity; | |
ConvertColorData(Frames[frameIndex], _rgbaBuffer); | |
using (var image = Image.LoadPixelData(_rgbaBuffer, Width, Height)) | |
image.SaveAsPng(output); | |
} | |
/// <summary> | |
/// Export frames from the store as a GIF. | |
/// </summary> | |
/// <param name="path">Path to write the GIF to.</param> | |
/// <param name="frameDelay">Delay between frames in units of 10ms.</param> | |
/// <param name="start">First frame to export.</param> | |
/// <param name="count">Number of frames to export.</param> | |
public void ExportGif(string path, int frameDelay, int start = 0, int count = -1) | |
{ | |
using (var stream = File.OpenWrite(path)) | |
ExportGif(stream, frameDelay, start, count); | |
} | |
/// <summary> | |
/// Export frames from the store as a GIF. | |
/// </summary> | |
/// <param name="output">Stream to write the GIF to.</param> | |
/// <param name="frameDelay">Delay between frames in units of 10ms.</param> | |
/// <param name="start">First frame to export.</param> | |
/// <param name="count">Number of frames to export.</param> | |
public void ExportGif(Stream output, int frameDelay, int start = 0, int count = -1) | |
{ | |
if (start < 0) | |
throw new ArgumentOutOfRangeException(); | |
if (start + count > FrameCount) | |
throw new ArgumentOutOfRangeException(); | |
if (count < 0) | |
count = FrameCapacity; | |
using (var image = new Image<Rgba32>(Width, Height)) | |
{ | |
var frames = image.Frames; | |
for (var i = start + 1; i <= count; i++) | |
{ | |
var frameIndex = (_frameIndex + i) % FrameCapacity; | |
ConvertColorData(Frames[frameIndex], _rgbaBuffer); | |
var frame = frames.AddFrame(_rgbaBuffer); | |
frame.Metadata.GetFormatMetadata(GifFormat.Instance).FrameDelay = frameDelay; | |
} | |
// remove the frame created with image creation | |
frames.RemoveFrame(0); | |
var encoder = new GifEncoder(); | |
image.SaveAsGif(output, encoder); | |
} | |
} | |
private static void ConvertColorData(Microsoft.Xna.Framework.Color[] mgBuffer, Rgba32[] isBuffer) | |
{ | |
for (var i = 0; i < mgBuffer.Length; i++) | |
{ | |
var c = mgBuffer[i]; | |
isBuffer[i] = new Rgba32(c.R, c.G, c.B, c.A); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Graphics; | |
using Microsoft.Xna.Framework.Input; | |
namespace GifRecorderMG | |
{ | |
public class Game1 : Game | |
{ | |
GraphicsDeviceManager graphics; | |
private const int FrameCount = 60; | |
private const int Width = 320; | |
private const int Height = 180; | |
private const float Speed = 0.5f; | |
private FrameStore _frameStore; | |
private SpriteBatch _spriteBatch; | |
private Texture2D _blank; | |
private RenderTarget2D _renderTarget; | |
private const int SquareSize = 32; | |
private float _squarePos = -32; | |
private KeyboardState _prevKeyboardState; | |
private KeyboardState _keyboardState; | |
/// <summary> | |
/// Store old frames and create a GIF by pressing 'S', PNG by pressing 'P'. | |
/// </summary> | |
public Game1() | |
{ | |
graphics = new GraphicsDeviceManager(this); | |
Content.RootDirectory = "Content"; | |
IsMouseVisible = true; | |
graphics.PreferredBackBufferWidth = Width; | |
graphics.PreferredBackBufferHeight = Height; | |
} | |
protected override void LoadContent() | |
{ | |
_spriteBatch = new SpriteBatch(GraphicsDevice); | |
_blank = new Texture2D(GraphicsDevice, 1, 1); | |
_blank.SetData(new[] { Color.White.PackedValue }); | |
_renderTarget = new RenderTarget2D(GraphicsDevice, Width, Height); | |
_frameStore = new FrameStore(FrameCount, Width, Height); | |
} | |
protected override void Update(GameTime gameTime) | |
{ | |
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || | |
Keyboard.GetState().IsKeyDown(Keys.Escape)) | |
Exit(); | |
_prevKeyboardState = _keyboardState; | |
_keyboardState = Keyboard.GetState(); | |
if (_prevKeyboardState.IsKeyUp(Keys.S) && _keyboardState.IsKeyDown(Keys.S)) | |
_frameStore.ExportGif("test.gif", 2); | |
if (_prevKeyboardState.IsKeyUp(Keys.P) && _keyboardState.IsKeyDown(Keys.P)) | |
_frameStore.ExportFrame("test.png", _frameStore.FrameCount - 1); | |
_squarePos += Speed; | |
if (_squarePos > Width) | |
_squarePos = -32f; | |
base.Update(gameTime); | |
} | |
protected override void Draw(GameTime gameTime) | |
{ | |
GraphicsDevice.Clear(Color.CornflowerBlue); | |
// render the scene to the active render target | |
GraphicsDevice.SetRenderTarget(_renderTarget); | |
GraphicsDevice.Clear(Color.CornflowerBlue); | |
_spriteBatch.Begin(); | |
_spriteBatch.Draw(_blank, new Rectangle((int)_squarePos, 74, SquareSize, SquareSize), Color.White); | |
_spriteBatch.End(); | |
// Render it to the backbuffer | |
GraphicsDevice.SetRenderTarget(null); | |
_spriteBatch.Begin(); | |
_spriteBatch.Draw(_renderTarget, Vector2.Zero, Color.White); | |
_spriteBatch.End(); | |
// store the pixel data | |
_frameStore.PushFrame(_renderTarget); | |
base.Draw(gameTime); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment