Last active
March 11, 2021 18:42
-
-
Save Jjagg/b1dce39b23192f46205a75d3a6482073 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 ImageSharpMg | |
{ | |
/// <summary> | |
/// Store a number of frames as <see cref="Color"/> arrays. Supports exporting single | |
/// frames as a png image or multiple frames as a gif using ImageSharp. | |
/// </summary> | |
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 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 Color[capacity][]; | |
var frameSize = width * height; | |
for (var i = 0; i < capacity; i++) | |
Frames[i] = new 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(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.FrameDelay = frameDelay; | |
} | |
// remove the frame created with image creation | |
frames.RemoveFrame(0); | |
var encoder = new GifEncoder(); | |
image.SaveAsGif(output, encoder); | |
} | |
} | |
private static void ConvertColorData(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 System; | |
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Graphics; | |
using Microsoft.Xna.Framework.Input; | |
namespace ImageSharpMg | |
{ | |
/// <summary> | |
/// Store old frames and create a GIF by pressing 'S', PNG by pressing 'P'. | |
/// </summary> | |
public class Game1 : Game | |
{ | |
[STAThread] | |
static void Main() | |
{ | |
using (var game = new Game1()) | |
game.Run(); | |
} | |
private const int FrameCount = 180; | |
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; | |
public Game1() | |
{ | |
var 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) | |
{ | |
// 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