From 3e5958de976fdc2330eed1f89a26ee28f29b0e00 Mon Sep 17 00:00:00 2001 From: miku-666 <74728189+NessieHax@users.noreply.github.com> Date: Wed, 10 May 2023 15:22:19 +0200 Subject: [PATCH] AnimationEditor - Added Interpolation to animation --- PCK-Studio/Extensions/ColorExtensions.cs | 21 ++++- PCK-Studio/Extensions/ImageExtensions.cs | 38 ++++++++ PCK-Studio/Forms/Editor/Animation.cs | 29 +++---- .../Forms/Editor/AnimationEditor.Designer.cs | 27 +++--- PCK-Studio/Forms/Editor/AnimationEditor.cs | 21 ++--- PCK-Studio/Forms/Editor/AnimationPlayer.cs | 87 +++++++++++++------ PCK-Studio/PckStudio.csproj | 4 +- 7 files changed, 152 insertions(+), 75 deletions(-) diff --git a/PCK-Studio/Extensions/ColorExtensions.cs b/PCK-Studio/Extensions/ColorExtensions.cs index e2bb0ed7..075aa2fe 100644 --- a/PCK-Studio/Extensions/ColorExtensions.cs +++ b/PCK-Studio/Extensions/ColorExtensions.cs @@ -10,12 +10,12 @@ namespace PckStudio.Extensions /// Normalizes the Color between 0.0 - 1.0 /// /// - 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 value, T min, T max) where T : IComparable + public static T Clamp(T value, T min, T max) where T : IComparable { 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) + ); + } } } diff --git a/PCK-Studio/Extensions/ImageExtensions.cs b/PCK-Studio/Extensions/ImageExtensions.cs index 21992942..a8e29e6b 100644 --- a/PCK-Studio/Extensions/ImageExtensions.cs +++ b/PCK-Studio/Extensions/ImageExtensions.cs @@ -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; + } } } diff --git a/PCK-Studio/Forms/Editor/Animation.cs b/PCK-Studio/Forms/Editor/Animation.cs index fc531fd3..11c1c1b2 100644 --- a/PCK-Studio/Forms/Editor/Animation.cs +++ b/PCK-Studio/Forms/Editor/Animation.cs @@ -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 frameTextures; + private readonly List textures; private readonly List frames = new List(); public Animation(IEnumerable image) { - frameTextures = new List(image); + textures = new List(image); } public Animation(IEnumerable 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 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 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); } } diff --git a/PCK-Studio/Forms/Editor/AnimationEditor.Designer.cs b/PCK-Studio/Forms/Editor/AnimationEditor.Designer.cs index 125866f6..ef796ec5 100644 --- a/PCK-Studio/Forms/Editor/AnimationEditor.Designer.cs +++ b/PCK-Studio/Forms/Editor/AnimationEditor.Designer.cs @@ -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; diff --git a/PCK-Studio/Forms/Editor/AnimationEditor.cs b/PCK-Studio/Forms/Editor/AnimationEditor.cs index 06970be2..e4af03cf 100644 --- a/PCK-Studio/Forms/Editor/AnimationEditor.cs +++ b/PCK-Studio/Forms/Editor/AnimationEditor.cs @@ -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(); } } } diff --git a/PCK-Studio/Forms/Editor/AnimationPlayer.cs b/PCK-Studio/Forms/Editor/AnimationPlayer.cs index d1488575..216239eb 100644 --- a/PCK-Studio/Forms/Editor/AnimationPlayer.cs +++ b/PCK-Studio/Forms/Editor/AnimationPlayer.cs @@ -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; + } } } diff --git a/PCK-Studio/PckStudio.csproj b/PCK-Studio/PckStudio.csproj index 99d929f5..913166db 100644 --- a/PCK-Studio/PckStudio.csproj +++ b/PCK-Studio/PckStudio.csproj @@ -281,7 +281,9 @@ TextPrompt.cs - + + Component + Form