AnimationEditor - Added Interpolation to animation

This commit is contained in:
miku-666
2023-05-10 15:22:19 +02:00
parent a8d958348d
commit 3e5958de97
7 changed files with 152 additions and 75 deletions

View File

@@ -10,12 +10,12 @@ namespace PckStudio.Extensions
/// Normalizes the Color between 0.0 - 1.0
/// </summary>
/// <returns></returns>
public static Vector3 Normalize(this Color color)
public static Vector4 Normalize(this Color color)
{
return new Vector3(color.R / 255f, color.G / 255f, color.B / 255f);
return new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
}
private static T Clamp<T>(T value, T min, T max) where T : IComparable<T>
public static T Clamp<T>(T value, T min, T max) where T : IComparable<T>
{
if (value.CompareTo(min) < 0) return min;
if (value.CompareTo(max) > 0) return max;
@@ -26,7 +26,6 @@ namespace PckStudio.Extensions
{
source = Clamp(source, 0.0f, 1.0f);
overlay = Clamp(overlay, 0.0f, 1.0f);
float resultValue = blendType switch
{
BlendMode.Add => source + overlay,
@@ -41,5 +40,19 @@ namespace PckStudio.Extensions
return (byte)Clamp(resultValue * 255, 0, 255);
}
public static byte Mix(double ratio, byte val1, byte val2)
{
return (byte)(ratio * val1 + (1.0 - ratio) * val2);
}
public static Color Mix(this Color c1, Color c2, double ratio)
{
ratio = Clamp(ratio, 0.0, 1.0);
return Color.FromArgb(c1.A,
Mix(ratio, c1.R, c2.R),
Mix(ratio, c1.G, c2.G),
Mix(ratio, c1.B, c2.B)
);
}
}
}

View File

@@ -229,5 +229,43 @@ namespace PckStudio.Extensions
overlayImage.UnlockBits(overlayImageData);
return bitmapResult;
}
public static Image Interpolate(this Image image1, Image image2, double delta)
{
delta = ColorExtensions.Clamp(delta, 0.0, 1.0);
if (image1 is not Bitmap baseImage || image2 is not Bitmap overlayImage ||
image1.Width != image2.Width || image1.Height != image2.Height)
return image1;
BitmapData baseImageData = baseImage.LockBits(new Rectangle(Point.Empty, baseImage.Size),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
byte[] baseImageBuffer = new byte[baseImageData.Stride * baseImageData.Height];
Marshal.Copy(baseImageData.Scan0, baseImageBuffer, 0, baseImageBuffer.Length);
BitmapData overlayImageData = overlayImage.LockBits(new Rectangle(Point.Empty, overlayImage.Size),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte[] overlayImageBuffer = new byte[overlayImageData.Stride * overlayImageData.Height];
Marshal.Copy(overlayImageData.Scan0, overlayImageBuffer, 0, overlayImageBuffer.Length);
for (int k = 0; k < baseImageBuffer.Length && k < overlayImageBuffer.Length; k += 4)
{
baseImageBuffer[k + 0] = ColorExtensions.Mix(delta, baseImageBuffer[k + 0], overlayImageBuffer[k + 0]);
baseImageBuffer[k + 1] = ColorExtensions.Mix(delta, baseImageBuffer[k + 1], overlayImageBuffer[k + 1]);
baseImageBuffer[k + 2] = ColorExtensions.Mix(delta, baseImageBuffer[k + 2], overlayImageBuffer[k + 2]);
}
Bitmap bitmapResult = new Bitmap(baseImage.Width, baseImage.Height, PixelFormat.Format32bppArgb);
BitmapData resultImageData = bitmapResult.LockBits(new Rectangle(Point.Empty, bitmapResult.Size),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(baseImageBuffer, 0, resultImageData.Scan0, baseImageBuffer.Length);
bitmapResult.UnlockBits(resultImageData);
baseImage.UnlockBits(baseImageData);
overlayImage.UnlockBits(overlayImageData);
return bitmapResult;
}
}
}

View File

@@ -14,21 +14,20 @@ namespace PckStudio.Forms.Editor
public int FrameCount => frames.Count;
public int TextureCount => frameTextures.Count;
public int TextureCount => textures.Count;
public Frame this[int frameIndex] => frames[frameIndex];
// TODO: implement this
public bool Interpolate { get; set; } = false;
private readonly List<Image> frameTextures;
private readonly List<Image> textures;
private readonly List<Frame> frames = new List<Frame>();
public Animation(IEnumerable<Image> image)
{
frameTextures = new List<Image>(image);
textures = new List<Image>(image);
}
public Animation(IEnumerable<Image> frameTextures, string ANIM) : this(frameTextures)
@@ -67,8 +66,6 @@ namespace PckStudio.Forms.Editor
foreach (string frameInfo in animData)
{
string[] frameData = frameInfo.Split('*');
//if (frameData.Length < 2)
// continue; // shouldn't happen
int currentFrameIndex = 0;
int.TryParse(frameData[0], out currentFrameIndex);
@@ -77,7 +74,7 @@ namespace PckStudio.Forms.Editor
// This will detect that and place the last frame time in its place.
// This is accurate to console edition behavior.
// - MattNL
int currentFrameTime = string.IsNullOrEmpty(frameData[1]) ? lastFrameTime : int.Parse(frameData[1]);
int currentFrameTime = frameData.Length < 2 || string.IsNullOrEmpty(frameData[1]) ? lastFrameTime : int.Parse(frameData[1]);
AddFrame(currentFrameIndex, currentFrameTime);
lastFrameTime = currentFrameTime;
}
@@ -86,9 +83,9 @@ namespace PckStudio.Forms.Editor
public Frame AddFrame(int frameTextureIndex) => AddFrame(frameTextureIndex, MinimumFrameTime);
public Frame AddFrame(int frameTextureIndex, int frameTime)
{
if (frameTextureIndex < 0 || frameTextureIndex >= frameTextures.Count)
if (frameTextureIndex < 0 || frameTextureIndex >= textures.Count)
throw new ArgumentOutOfRangeException(nameof(frameTextureIndex));
Frame f = new Frame(frameTextures[frameTextureIndex], frameTime);
Frame f = new Frame(textures[frameTextureIndex], frameTime);
frames.Add(f);
return f;
}
@@ -108,20 +105,20 @@ namespace PckStudio.Forms.Editor
public List<Image> GetFrameTextures()
{
return frameTextures;
return textures;
}
public int GetTextureIndex(Image frameTexture)
{
_ = frameTexture ?? throw new ArgumentNullException(nameof(frameTexture));
return frameTextures.IndexOf(frameTexture);
return textures.IndexOf(frameTexture);
}
public void SetFrame(Frame frame, int frameTextureIndex, int frameTime = MinimumFrameTime)
=> SetFrame(frames.IndexOf(frame), frameTextureIndex, frameTime);
public void SetFrame(int frameIndex, int frameTextureIndex, int frameTime = MinimumFrameTime)
{
frames[frameIndex] = new Frame(frameTextures[frameTextureIndex], frameTime);
frames[frameIndex] = new Frame(textures[frameTextureIndex], frameTime);
}
public string BuildAnim()
@@ -134,13 +131,11 @@ namespace PckStudio.Forms.Editor
public Image BuildTexture(bool isClockOrCompass, List<Image> linearImages = default!)
{
int width = frameTextures[0].Width;
int height = frameTextures[0].Height;
if (width != height)
var textures = isClockOrCompass ? linearImages : this.textures;
if (textures[0].Width != textures[0].Height)
throw new Exception("Invalid size");
var textures = isClockOrCompass ? linearImages : frameTextures;
return ImageExtensions.CombineImages(textures, ImageLayoutDirection.Vertical);
}
}

View File

@@ -53,11 +53,11 @@
this.AnimationStopBtn = new MetroFramework.Controls.MetroButton();
this.tileLabel = new MetroFramework.Controls.MetroLabel();
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.pictureBoxWithInterpolationMode1 = new PckStudio.PictureBoxWithInterpolationMode();
this.animationPictureBox = new PckStudio.Forms.Editor.AnimationPictureBox();
this.contextMenuStrip1.SuspendLayout();
this.menuStrip.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.pictureBoxWithInterpolationMode1)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.animationPictureBox)).BeginInit();
this.SuspendLayout();
//
// frameTreeView
@@ -282,18 +282,17 @@
this.pictureBox1.TabIndex = 21;
this.pictureBox1.TabStop = false;
//
// pictureBoxWithInterpolationMode1
// animationPictureBox
//
this.pictureBoxWithInterpolationMode1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
this.animationPictureBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.pictureBoxWithInterpolationMode1.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
this.pictureBoxWithInterpolationMode1.Location = new System.Drawing.Point(157, 88);
this.pictureBoxWithInterpolationMode1.Name = "pictureBoxWithInterpolationMode1";
this.pictureBoxWithInterpolationMode1.Size = new System.Drawing.Size(235, 223);
this.pictureBoxWithInterpolationMode1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
this.pictureBoxWithInterpolationMode1.TabIndex = 16;
this.pictureBoxWithInterpolationMode1.TabStop = false;
this.animationPictureBox.Location = new System.Drawing.Point(157, 88);
this.animationPictureBox.Name = "animationPictureBox";
this.animationPictureBox.Size = new System.Drawing.Size(235, 223);
this.animationPictureBox.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
this.animationPictureBox.TabIndex = 16;
this.animationPictureBox.TabStop = false;
//
// AnimationEditor
//
@@ -305,7 +304,7 @@
this.Controls.Add(this.AnimationStopBtn);
this.Controls.Add(this.AnimationPlayBtn);
this.Controls.Add(this.tileLabel);
this.Controls.Add(this.pictureBoxWithInterpolationMode1);
this.Controls.Add(this.animationPictureBox);
this.Controls.Add(this.frameTreeView);
this.Controls.Add(this.menuStrip);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
@@ -319,7 +318,7 @@
this.menuStrip.ResumeLayout(false);
this.menuStrip.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.pictureBoxWithInterpolationMode1)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.animationPictureBox)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
@@ -331,7 +330,7 @@
private System.Windows.Forms.MenuStrip menuStrip;
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem1;
private PictureBoxWithInterpolationMode pictureBoxWithInterpolationMode1;
private PckStudio.Forms.Editor.AnimationPictureBox animationPictureBox;
private MetroFramework.Controls.MetroCheckBox InterpolationCheckbox;
private MetroFramework.Controls.MetroButton AnimationPlayBtn;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;

View File

@@ -18,7 +18,6 @@ namespace PckStudio.Forms.Editor
{
PckFile.FileData animationFile;
Animation currentAnimation;
AnimationPlayer player;
bool isItem = false;
string animationSection => AnimationResources.GetAnimationSection(isItem);
@@ -54,7 +53,6 @@ namespace PckStudio.Forms.Editor
currentAnimation = animationFile.Properties.HasProperty("ANIM")
? new Animation(frameTextures, animationFile.Properties.GetPropertyValue("ANIM"))
: new Animation(frameTextures);
player = new AnimationPlayer(pictureBoxWithInterpolationMode1);
foreach (JObject content in AnimationResources.tileData[animationSection].Children())
{
@@ -84,14 +82,14 @@ namespace PckStudio.Forms.Editor
SelectedImageIndex = imageIndex,
});
}
player.SelectFrame(currentAnimation, 0);
animationPictureBox.SelectFrame(currentAnimation, 0);
}
private void frameTreeView_AfterSelect(object sender, TreeViewEventArgs e)
{
if (player.IsPlaying && !AnimationPlayBtn.Enabled)
if (animationPictureBox.IsPlaying && !AnimationPlayBtn.Enabled)
AnimationPlayBtn.Enabled = !(AnimationStopBtn.Enabled = !AnimationStopBtn.Enabled);
player.SelectFrame(currentAnimation, frameTreeView.SelectedNode.Index);
animationPictureBox.SelectFrame(currentAnimation, frameTreeView.SelectedNode.Index);
}
private int mix(double ratio, int val1, int val2) // Ported from Java Edition code
@@ -101,20 +99,19 @@ namespace PckStudio.Forms.Editor
private void StartAnimationBtn_Click(object sender, EventArgs e)
{
// prevent player from crashing
player.Stop();
// prevent player from crashing
animationPictureBox.Stop();
AnimationPlayBtn.Enabled = !(AnimationStopBtn.Enabled = !AnimationStopBtn.Enabled);
if (currentAnimation.FrameCount > 1)
{
player.SetContext(pictureBoxWithInterpolationMode1);
player.Start(currentAnimation);
animationPictureBox.Start(currentAnimation);
}
}
private void StopAnimationBtn_Click(object sender, EventArgs e)
{
AnimationPlayBtn.Enabled = !(AnimationStopBtn.Enabled = !AnimationStopBtn.Enabled);
player.Stop();
animationPictureBox.Stop();
}
private void frameTreeView_KeyDown(object sender, KeyEventArgs e)
@@ -436,9 +433,9 @@ namespace PckStudio.Forms.Editor
private void AnimationEditor_FormClosing(object sender, FormClosingEventArgs e)
{
if (player.IsPlaying)
if (animationPictureBox.IsPlaying)
{
player.Stop();
animationPictureBox.Stop();
}
}
}

View File

@@ -1,44 +1,76 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Windows.Forms;
using System.Threading.Tasks;
using System.Drawing.Drawing2D;
using System.Runtime.CompilerServices;
using PckStudio.Extensions;
namespace PckStudio.Forms.Editor
{
// TODO: write as a UI control ??
sealed class AnimationPlayer
{
internal class AnimationPictureBox : PictureBox
{
private const int TickInMillisecond = 50; // 1 InGame tick
public bool IsPlaying { get; private set; } = false;
private int currentAnimationFrameIndex = 0;
private PictureBox display;
private Animation.Frame currentFrame;
private Animation _animation;
private CancellationTokenSource cts = new CancellationTokenSource();
public AnimationPlayer(PictureBox display)
{
SetContext(display);
}
protected override void OnPaint(PaintEventArgs pe)
{
pe.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
pe.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
base.OnPaint(pe);
}
private async void DoAnimate()
private async void DoAnimate()
{
_ = display ?? throw new ArgumentNullException(nameof(display));
_ = _animation ?? throw new ArgumentNullException(nameof(_animation));
IsPlaying = true;
Animation.Frame nextFrame;
while (!cts.IsCancellationRequested)
{
if (currentAnimationFrameIndex >= _animation.FrameCount)
{
currentAnimationFrameIndex = 0;
Animation.Frame frame = SetDisplayFrame(currentAnimationFrameIndex++);
await Task.Delay(TickInMillisecond * frame.Ticks);
}
}
if (currentAnimationFrameIndex + 1 >= _animation.FrameCount)
{
nextFrame = _animation[0];
}
else
{
nextFrame = _animation[currentAnimationFrameIndex + 1];
}
currentFrame = _animation[currentAnimationFrameIndex++];
if (_animation.Interpolate)
{
await InterpolateFrame(currentFrame, nextFrame);
continue;
}
SetAnimationFrame(currentFrame);
await Task.Delay(TickInMillisecond * currentFrame.Ticks);
}
IsPlaying = false;
}
public void Start(Animation animation)
private async Task InterpolateFrame(Animation.Frame currentFrame, Animation.Frame nextFrame)
{
for (int i = 0; i < currentFrame.Ticks; i++)
{
double delta = 1.0f - i / (double)currentFrame.Ticks;
Image = currentFrame.Texture.Interpolate(nextFrame.Texture, delta);
await Task.Delay(TickInMillisecond);
}
}
public void Start(Animation animation)
{
_animation = animation;
cts = new CancellationTokenSource();
@@ -47,30 +79,31 @@ namespace PckStudio.Forms.Editor
public void Stop([CallerMemberName] string callerName = default!)
{
Debug.WriteLine($"{nameof(AnimationPlayer.Stop)} called from {callerName}!");
Debug.WriteLine($"{nameof(AnimationPictureBox.Stop)} called from {callerName}!");
cts.Cancel();
}
public Animation.Frame GetCurrentFrame() => _animation[currentAnimationFrameIndex];
public void SetContext(PictureBox display) => this.display = display;
public void SelectFrame(Animation animation, int index)
{
_animation = animation;
if (IsPlaying)
Stop();
SetDisplayFrame(index);
_animation = animation;
currentAnimationFrameIndex = index;
currentFrame = SetAnimationFrame(index);
}
private Animation.Frame SetDisplayFrame(int frameIndex)
private Animation.Frame SetAnimationFrame(int frameIndex)
{
Monitor.Enter(_animation);
Animation.Frame frame = _animation[frameIndex];
display.Image = frame.Texture;
Monitor.Exit(_animation);
var frame = _animation[frameIndex];
SetAnimationFrame(frame);
return frame;
}
private void SetAnimationFrame(Animation.Frame frame)
{
Image = frame.Texture;
}
}
}

View File

@@ -281,7 +281,9 @@
<DependentUpon>TextPrompt.cs</DependentUpon>
</Compile>
<Compile Include="Forms\Editor\Animation.cs" />
<Compile Include="Forms\Editor\AnimationPlayer.cs" />
<Compile Include="Forms\Editor\AnimationPlayer.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Forms\Editor\MaterialsEditor.cs">
<SubType>Form</SubType>
</Compile>