From 7999900152bc0e763b9bd79c18c9daf9b8113cf3 Mon Sep 17 00:00:00 2001 From: miku-666 <74728189+NessieHax@users.noreply.github.com> Date: Fri, 17 Mar 2023 19:53:10 +0100 Subject: [PATCH] Refactored AnimationEditor --- .../Extentions/EnumerableExtentions.cs | 17 + .../Classes/Extentions/ImageExtentions.cs | 75 +++- PCK-Studio/Forms/Editor/Animation.cs | 150 ++++++++ PCK-Studio/Forms/Editor/AnimationEditor.cs | 325 ++++-------------- PCK-Studio/Forms/Editor/AnimationPlayer.cs | 76 ++++ PCK-Studio/PckStudio.csproj | 3 + 6 files changed, 372 insertions(+), 274 deletions(-) create mode 100644 PCK-Studio/Classes/Extentions/EnumerableExtentions.cs create mode 100644 PCK-Studio/Forms/Editor/Animation.cs create mode 100644 PCK-Studio/Forms/Editor/AnimationPlayer.cs diff --git a/PCK-Studio/Classes/Extentions/EnumerableExtentions.cs b/PCK-Studio/Classes/Extentions/EnumerableExtentions.cs new file mode 100644 index 00000000..cda71bba --- /dev/null +++ b/PCK-Studio/Classes/Extentions/EnumerableExtentions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace PckStudio.Classes.Extentions +{ + internal static class EnumerableExtentions + { + public static IEnumerable<(int index, T type)>enumerate(this IEnumerable array) + { + int i = 0; + foreach (var item in array) + { + yield return (i++, item); + } + yield break; + } + } +} diff --git a/PCK-Studio/Classes/Extentions/ImageExtentions.cs b/PCK-Studio/Classes/Extentions/ImageExtentions.cs index d866b661..f2e3dd39 100644 --- a/PCK-Studio/Classes/Extentions/ImageExtentions.cs +++ b/PCK-Studio/Classes/Extentions/ImageExtentions.cs @@ -2,17 +2,48 @@ using System.Drawing.Drawing2D; using System.Drawing; using System.Diagnostics; +using System; namespace PckStudio.Classes.Extentions { internal static class ImageExtentions { + public enum ImageLayoutDirection { Horizontal, Vertical } + private struct ImageLayoutInfo + { + /// + /// Size of sub section of the image + /// + public Size SectionSize; + public Point SectionPoint; + + public ImageLayoutInfo(int width, int height, int index, ImageLayoutDirection layoutDirection) + { + bool horizontal = layoutDirection == ImageLayoutDirection.Horizontal; + SectionSize = horizontal ? new Size(width, width) : new Size(height, height); + SectionPoint = horizontal ? new Point(0, index * width) : new Point(index * height, 0); + } + } + + private static Image GetTileImage(Image source, Rectangle area, Size size, GraphicsUnit unit = GraphicsUnit.Pixel) + { + Image tileImage = new Bitmap(size.Width, size.Height); + using (Graphics gfx = Graphics.FromImage(tileImage)) + { + gfx.SmoothingMode = SmoothingMode.None; + gfx.InterpolationMode = InterpolationMode.NearestNeighbor; + gfx.PixelOffsetMode = PixelOffsetMode.HighQuality; + gfx.DrawImage(source, new Rectangle(Point.Empty, size), area, unit); + } + return tileImage; + } + public static IEnumerable CreateImageList(this Image source, Size size) { int img_row_count = source.Width / size.Width; @@ -28,7 +59,6 @@ namespace PckStudio.Classes.Extentions yield break; } - public static IEnumerable CreateImageList(this Image source, int scalar) { return CreateImageList(source, new Size(scalar, scalar)); @@ -38,26 +68,43 @@ namespace PckStudio.Classes.Extentions { for (int i = 0; i < source.Height / source.Width; i++) { - (Size size, Point point) locationInfo = layoutDirection == ImageLayoutDirection.Horizontal - ? (new Size(source.Width, source.Width), new Point(0, i * source.Width)) - : (new Size(source.Height, source.Height), new Point(i * source.Height, 0)); - Rectangle tileArea = new Rectangle(locationInfo.point, locationInfo.size); - yield return GetTileImage(source, tileArea, locationInfo.size); + ImageLayoutInfo locationInfo = new ImageLayoutInfo(source.Width, source.Height, i, layoutDirection); + Rectangle tileArea = new Rectangle(locationInfo.SectionPoint, locationInfo.SectionSize); + yield return GetTileImage(source, tileArea, locationInfo.SectionSize); } yield break; } - private static Image GetTileImage(Image source, Rectangle area, Size size, GraphicsUnit unit = GraphicsUnit.Pixel) + public static Image ImageFromImageArray(Image[] sources, ImageLayoutDirection layoutDirection) { - Image tileImage = new Bitmap(size.Width, size.Height); - using (Graphics gfx = Graphics.FromImage(tileImage)) + Size imageSize = CalculateImageSize(sources, layoutDirection); + var result = new Bitmap(imageSize.Width, imageSize.Height); + + using (var graphic = Graphics.FromImage(result)) { - gfx.SmoothingMode = SmoothingMode.None; - gfx.InterpolationMode = InterpolationMode.NearestNeighbor; - gfx.PixelOffsetMode = PixelOffsetMode.HighQuality; - gfx.DrawImage(source, new Rectangle(Point.Empty, size), area, unit); + foreach (var (i, texture) in sources.enumerate()) + { + var info = new ImageLayoutInfo(imageSize.Width, imageSize.Height, i, layoutDirection); + graphic.DrawImage(texture, info.SectionPoint); + }; } - return tileImage; + return result; + } + + private static Size CalculateImageSize(Image[] sources, ImageLayoutDirection layoutDirection) + { + var horizontal = layoutDirection == ImageLayoutDirection.Horizontal; + + // TODO: Validate all source images to be the same size. + int width = sources[0].Width; + int heigh = sources[0].Height; + + if (horizontal) + width *= sources.Length; + else + heigh *= sources.Length; + + return new Size(width, heigh); } } } diff --git a/PCK-Studio/Forms/Editor/Animation.cs b/PCK-Studio/Forms/Editor/Animation.cs new file mode 100644 index 00000000..7ef4b203 --- /dev/null +++ b/PCK-Studio/Forms/Editor/Animation.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using PckStudio.Classes.Extentions; +using System.Text; + + +// TODO: change namespace +namespace PckStudio.Forms.Editor +{ + sealed class Animation + { + public const int MinimumFrameTime = 1; + + public int FrameCount => frames.Count; + + public int FrameTextureCount => frameTextures.Count; + + public Frame this[int frameIndex] => frames[frameIndex]; + + // not implemented rn... + public bool Interpolate { get; set; } = false; + + private readonly List frameTextures; + + private readonly List frames = new List(); + + + public Animation(IEnumerable image) + { + frameTextures = new List(image); + } + + public Animation(IEnumerable frameTextures, string ANIM) : this(frameTextures) + { + ParseAnim(ANIM); + } + + public struct Frame + { + public readonly Image Texture; + public int Ticks; + + public static implicit operator Image(Frame f) => f.Texture; + + public Frame(Image texture) : this(texture, MinimumFrameTime) + { } + + public Frame(Image texture, int frameTime) + { + Texture = texture; + Ticks = frameTime; + } + } + + public void ParseAnim(string ANIM) + { + _ = ANIM ?? throw new ArgumentNullException(nameof(ANIM)); + ANIM = (Interpolate = ANIM.StartsWith("#")) ? ANIM.Substring(1) : ANIM; + string[] animData = ANIM.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + int lastFrameTime = MinimumFrameTime; + if (animData.Length <= 0) + { + for (int i = 0; i < FrameTextureCount; i++) + { + AddFrame(i, MinimumFrameTime); + } + } + else + { + foreach (string frameInfo in animData) + { + string[] frameData = frameInfo.Split('*'); + //if (frameData.Length < 2) + // continue; // shouldn't happen + int currentFrameIndex = int.TryParse(frameData[0], out currentFrameIndex) ? currentFrameIndex : 0; + + // Some textures like the Halloween 2015's Lava texture don't have a + // frame time parameter for certain frames. + // 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]); + AddFrame(currentFrameIndex, currentFrameTime); + lastFrameTime = currentFrameTime; + } + } + } + public Frame AddFrame(int frameTextureIndex) => AddFrame(frameTextureIndex, MinimumFrameTime); + public Frame AddFrame(int frameTextureIndex, int frameTime) + { + if (frameTextureIndex < 0 || frameTextureIndex >= frameTextures.Count) + throw new ArgumentOutOfRangeException(nameof(frameTextureIndex)); + Frame f = new Frame(frameTextures[frameTextureIndex], frameTime); + frames.Add(f); + return f; + } + + public bool RemoveFrame(int frameIndex) + { + frames.RemoveAt(frameIndex); + return true; + } + + public Frame GetFrame(int index) => frames[index]; + + public List GetFrames() + { + return frames; + } + + public List GetFrameTextures() + { + return frameTextures; + } + + public int GetTextureIndex(Image frameTexture) + { + _ = frameTexture ?? throw new ArgumentNullException(nameof(frameTexture)); + return frameTextures.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); + } + + public string BuildAnim() + { + StringBuilder stringBuilder = new StringBuilder(Interpolate ? "#" : string.Empty); + foreach (var frame in frames) + stringBuilder.Append($"{GetTextureIndex(frame)}*{frame.Ticks},"); + return stringBuilder.ToString(0, stringBuilder.Length - 1); + } + + public Image BuildTexture(bool isClockOrCompass, List linearImages = default!) + { + int width = frameTextures[0].Width; + int height = frameTextures[0].Height; + if (width != height) + throw new Exception("Invalid size"); + + var textures = isClockOrCompass ? linearImages : frameTextures; + + return ImageExtentions.ImageFromImageArray(textures.ToArray(), ImageExtentions.ImageLayoutDirection.Vertical); + } + } +} diff --git a/PCK-Studio/Forms/Editor/AnimationEditor.cs b/PCK-Studio/Forms/Editor/AnimationEditor.cs index 9e22f9a1..a10c232e 100644 --- a/PCK-Studio/Forms/Editor/AnimationEditor.cs +++ b/PCK-Studio/Forms/Editor/AnimationEditor.cs @@ -2,26 +2,21 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using System.Windows.Forms; using PckStudio.Classes.FileTypes; using PckStudio.Forms.Additional_Popups.Animation; using PckStudio.Forms.Utilities; using PckStudio.Classes.Extentions; -using System.Runtime.CompilerServices; -using System.Diagnostics; using OMI.Formats.Pck; namespace PckStudio.Forms.Editor { - public partial class AnimationEditor : MetroForm + public partial class AnimationEditor : MetroForm { PckFile.FileData animationFile; Animation currentAnimation; @@ -32,218 +27,12 @@ namespace PckStudio.Forms.Editor public string TileName = string.Empty; - sealed class Animation - { - public const int MinimumFrameTime = 1; + private bool IsEditingSpecial => IsSpecialTile(TileName); - private readonly List frameTextures; - - private readonly List frames = new List(); - - public Frame this[int frameIndex] => frames[frameIndex]; - - // not implemented rn... - public bool Interpolate { get; set; } = false; - - public Animation(Image image) - { - frameTextures = new List(image.CreateImageList(ImageExtentions.ImageLayoutDirection.Horizontal)); - } - - public Animation(Image image, string ANIM) : this(image) - { - ParseAnim(ANIM); - } - - public struct Frame - { - public readonly Image Texture; - public int Ticks; - - public static implicit operator Image(Frame f) => f.Texture; - - public Frame(Image texture) : this(texture, MinimumFrameTime) - {} - - public Frame(Image texture, int frameTime) - { - Texture = texture; - Ticks = frameTime; - } - } - - public void ParseAnim(string ANIM) - { - _ = ANIM ?? throw new ArgumentNullException(nameof(ANIM)); - ANIM = (Interpolate = ANIM.StartsWith("#")) ? ANIM.Substring(1) : ANIM; - string[] animData = ANIM.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - int lastFrameTime = MinimumFrameTime; - if (animData.Length <= 0) - { - for(int i = 0; i < FrameTextureCount; i++) - { - AddFrame(i, 1); - } - } - else - { - foreach (string frameInfo in animData) - { - string[] frameData = frameInfo.Split('*'); - //if (frameData.Length < 2) - // continue; // shouldn't happen - int currentFrameIndex = int.TryParse(frameData[0], out currentFrameIndex) ? currentFrameIndex : 0; - - // Some textures like the Halloween 2015's Lava texture don't have a - // frame time parameter for certain frames. - // 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]); - AddFrame(currentFrameIndex, currentFrameTime); - lastFrameTime = currentFrameTime; - } - } - } - public Frame AddFrame(int frameTextureIndex) => AddFrame(frameTextureIndex, MinimumFrameTime); - public Frame AddFrame(int frameTextureIndex, int frameTime) - { - if (frameTextureIndex < 0 || frameTextureIndex >= frameTextures.Count) - throw new ArgumentOutOfRangeException(nameof(frameTextureIndex)); - Frame f = new Frame(frameTextures[frameTextureIndex], frameTime); - frames.Add(f); - return f; - } - - public bool RemoveFrame(int frameIndex) - { - frames.RemoveAt(frameIndex); - return true; - } - - public Frame GetFrame(int index) => frames[index]; - - public List GetFrames() - { - return frames; - } - - public List GetFrameTextures() - { - return frameTextures; - } - - public int GetFrameIndex(Image frameTexture) - { - _ = frameTexture ?? throw new ArgumentNullException(nameof(frameTexture)); - return frameTextures.IndexOf(frameTexture); - } - - public int FrameCount => frames.Count; - public int FrameTextureCount => frameTextures.Count; - - 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); - } - - public string BuildAnim() - { - string animationData = Interpolate ? "#" : string.Empty; - frames.ForEach(frame => animationData += $"{GetFrameIndex(frame)}*{frame.Ticks},"); - return animationData.TrimEnd(','); - } - - public Image BuildTexture(AnimationEditor ae) - { - int width = frameTextures[0].Width; - int height = frameTextures[0].Height; - if (width != height) throw new Exception("Invalid size"); - - bool isClockOrCompass = ae.TileName == "clock" || ae.TileName == "compass"; - - List linearImages = new List(); - foreach (TreeNode n in ae.frameTreeView.Nodes) linearImages.Add(frameTextures[n.ImageIndex]); - - var img = new Bitmap(width, height * (isClockOrCompass ? linearImages.Count : FrameTextureCount)); - int pos_y = 0; - - using (var g = Graphics.FromImage(img)) - (isClockOrCompass ? linearImages : frameTextures).ForEach(texture => - { - g.DrawImage(texture, 0, pos_y); - pos_y += height; - }); - return img; - } - } - - sealed class AnimationPlayer + private bool IsSpecialTile(string tileName) { - public const int BaseTickSpeed = 48; - public bool IsPlaying { get; private set; } = false; - - private int currentAnimationFrameIndex = 0; - private PictureBox display; - private Animation _animation; - private CancellationTokenSource cts = new CancellationTokenSource(); - - public AnimationPlayer(PictureBox display) - { - SetContext(display); - } - - private async void DoAnimate() - { - _ = display ?? throw new ArgumentNullException(nameof(display)); - _ = _animation ?? throw new ArgumentNullException(nameof(_animation)); - IsPlaying = true; - while (!cts.IsCancellationRequested) - { - if (currentAnimationFrameIndex >= _animation.FrameCount) - currentAnimationFrameIndex = 0; - Animation.Frame frame = SetFrameDisplayed(currentAnimationFrameIndex++); - await Task.Delay(BaseTickSpeed * frame.Ticks); - } - IsPlaying = false; - } - - public void Start(Animation animation) - { - _animation = animation; - cts = new CancellationTokenSource(); - Task.Run(DoAnimate, cts.Token); - } - - public void Stop([CallerMemberName] string callerName = default!) - { - Debug.WriteLine($"{nameof(AnimationPlayer.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(); - SetFrameDisplayed(index); - currentAnimationFrameIndex = index; - } - - private Animation.Frame SetFrameDisplayed(int i) - { - Monitor.Enter(_animation); - Animation.Frame frame = _animation[i]; - display.Image = frame; - Monitor.Exit(_animation); - return frame; - } - } + return tileName == "clock" || tileName == "compass"; + } public AnimationEditor(PckFile.FileData file) { @@ -252,7 +41,7 @@ namespace PckStudio.Forms.Editor isItem = file.Filename.Split('/').Contains("items"); TileName = Path.GetFileNameWithoutExtension(file.Filename); - InterpolationCheckbox.Visible = !(TileName == "clock" || TileName == "compass"); + InterpolationCheckbox.Visible = !IsEditingSpecial; InterpolationCheckbox.Checked = InterpolationCheckbox.Visible; bulkAnimationSpeedToolStripMenuItem.Enabled = InterpolationCheckbox.Visible; importJavaAnimationToolStripMenuItem.Enabled = InterpolationCheckbox.Visible; @@ -262,9 +51,11 @@ namespace PckStudio.Forms.Editor using MemoryStream textureMem = new MemoryStream(animationFile.Data); var texture = new Bitmap(textureMem); - currentAnimation = animationFile.Properties.HasProperty("ANIM") - ? new Animation(texture, animationFile.Properties.GetPropertyValue("ANIM")) - : new Animation(texture); + var frameTextures = texture.CreateImageList(ImageExtentions.ImageLayoutDirection.Horizontal); + + currentAnimation = animationFile.Properties.HasProperty("ANIM") + ? new Animation(frameTextures, animationFile.Properties.GetPropertyValue("ANIM")) + : new Animation(frameTextures); player = new AnimationPlayer(pictureBoxWithInterpolationMode1); foreach (JObject content in AnimationUtil.tileData[animationSection].Children()) @@ -286,7 +77,15 @@ namespace PckStudio.Forms.Editor // $"Frame: {i}, Frame Time: {Animation.MinimumFrameTime}" TextureIcons.Images.Clear(); TextureIcons.Images.AddRange(currentAnimation.GetFrameTextures().ToArray()); - currentAnimation.GetFrames().ForEach(f => frameTreeView.Nodes.Add("", $"for {f.Ticks} frames", currentAnimation.GetFrameIndex(f.Texture), currentAnimation.GetFrameIndex(f.Texture))); + foreach (var frame in currentAnimation.GetFrames()) + { + var imageIndex = currentAnimation.GetTextureIndex(frame.Texture); + frameTreeView.Nodes.Add(new TreeNode($"for {frame.Ticks} frames") + { + ImageIndex = imageIndex, + SelectedImageIndex = imageIndex, + }); + } player.SelectFrame(currentAnimation, 0); } @@ -304,8 +103,8 @@ namespace PckStudio.Forms.Editor private void StartAnimationBtn_Click(object sender, EventArgs e) { - // crash fix: when pushing the play button on occasions, the animation will play twice the intended speed and crash PCK Studio after one iteration - player.Stop(); // force the player to stop before starting + // prevent player from crashing + player.Stop(); AnimationPlayBtn.Enabled = !(AnimationStopBtn.Enabled = !AnimationStopBtn.Enabled); if (currentAnimation.FrameCount > 1) { @@ -338,12 +137,13 @@ namespace PckStudio.Forms.Editor private void saveToolStripMenuItem1_Click(object sender, EventArgs e) { - string anim = currentAnimation.BuildAnim(); - animationFile.Properties.SetProperty("ANIM", TileName == "clock" || TileName == "compass" ? "" : anim); + + animationFile.Properties.SetProperty("ANIM", IsEditingSpecial ? "" : anim); using (var stream = new MemoryStream()) { - currentAnimation.BuildTexture(this).Save(stream, ImageFormat.Png); + var texture = currentAnimation.BuildTexture(IsEditingSpecial); + texture.Save(stream, ImageFormat.Png); animationFile.SetData(stream.ToArray()); } //Reusing this for the tile path @@ -431,7 +231,7 @@ namespace PckStudio.Forms.Editor private void treeView1_doubleClick(object sender, EventArgs e) { var frame = currentAnimation.GetFrame(frameTreeView.SelectedNode.Index); - using FrameEditor diag = new FrameEditor(frame.Ticks, currentAnimation.GetFrameIndex(frame.Texture), TextureIcons); + using FrameEditor diag = new FrameEditor(frame.Ticks, currentAnimation.GetTextureIndex(frame.Texture), TextureIcons); if (diag.ShowDialog(this) == DialogResult.OK) { /* Found a bug here. When passing the frame variable, it would replace the first instance of that frame and time @@ -471,7 +271,7 @@ namespace PckStudio.Forms.Editor for (int i = 0; i < list.Count; i++) { Animation.Frame f = list[i]; - currentAnimation.SetFrame(f, currentAnimation.GetFrameIndex(f), diag.time); + currentAnimation.SetFrame(f, currentAnimation.GetTextureIndex(f), diag.time); } LoadAnimationTreeView(); } @@ -485,15 +285,12 @@ namespace PckStudio.Forms.Editor if (query == DialogResult.No) return; OpenFileDialog fileDialog = new OpenFileDialog(); - fileDialog.Multiselect = false; fileDialog.Title = "Please select a valid Minecaft: Java Edition animation script"; // It's marked as .png.mcmeta just in case // some weirdo tries to pass a pack.mcmeta or something // -MattNL fileDialog.Filter = "Animation Scripts (*.mcmeta)|*.png.mcmeta"; - fileDialog.CheckPathExists = true; - fileDialog.CheckFileExists = true; if (fileDialog.ShowDialog(this) != DialogResult.OK) return; Console.WriteLine("Selected Animation Script: " + fileDialog.FileName); @@ -504,8 +301,8 @@ namespace PckStudio.Forms.Editor return; } using MemoryStream textureMem = new MemoryStream(File.ReadAllBytes(textureFile)); - var new_animation = new Animation(Image.FromStream(textureMem)); - + var textures = Image.FromStream(textureMem).CreateImageList(ImageExtentions.ImageLayoutDirection.Horizontal); + var new_animation = new Animation(textures); try { JObject mcmeta = JObject.Parse(File.ReadAllText(fileDialog.FileName)); @@ -566,11 +363,11 @@ namespace PckStudio.Forms.Editor if (TileName != diag.SelectedTile) isItem = diag.IsItem; TileName = diag.SelectedTile; - InterpolationCheckbox.Visible = !(TileName == "clock" || TileName == "compass"); - InterpolationCheckbox.Checked = InterpolationCheckbox.Visible; - bulkAnimationSpeedToolStripMenuItem.Enabled = InterpolationCheckbox.Visible; - importJavaAnimationToolStripMenuItem.Enabled = InterpolationCheckbox.Visible; - exportJavaAnimationToolStripMenuItem.Enabled = InterpolationCheckbox.Visible; + InterpolationCheckbox.Checked = + bulkAnimationSpeedToolStripMenuItem.Enabled = + importJavaAnimationToolStripMenuItem.Enabled = + exportJavaAnimationToolStripMenuItem.Enabled = + InterpolationCheckbox.Visible = !IsEditingSpecial; foreach (JObject content in AnimationUtil.tileData[animationSection].Children()) { @@ -584,31 +381,39 @@ namespace PckStudio.Forms.Editor { SaveFileDialog fileDialog = new SaveFileDialog(); fileDialog.Title = "Please choose where you want to save your new animation"; - fileDialog.Filter = "Animation Scripts (*.mcmeta)|*.png.mcmeta"; - fileDialog.CheckPathExists = true; - if (fileDialog.ShowDialog(this) != DialogResult.OK) return; + if (fileDialog.ShowDialog(this) == DialogResult.OK) + { + JObject mcmeta = ConvertAnimationToJava(currentAnimation); + string jsondata = JsonConvert.SerializeObject(mcmeta, Formatting.Indented); + string filename = fileDialog.FileName; + File.WriteAllText(filename, jsondata); + var finalTexture = currentAnimation.BuildTexture(isClockOrCompass: false); + finalTexture.Save(Path.GetFileNameWithoutExtension(filename)); // removes ".mcmeta" from filename! + MessageBox.Show("Animation was successfully exported to " + filename, "Export successful!"); + } + } - JObject mcmeta = new JObject(); - JObject animation = new JObject(); - JArray frames = new JArray(); - currentAnimation.GetFrames().ForEach(f => { - JObject frame = new JObject(); - frame["index"] = currentAnimation.GetFrameIndex(f.Texture); - frame["time"] = f.Ticks; - frames.Add(frame); - }); - animation["interpolation"] = InterpolationCheckbox.Checked; - animation["frames"] = frames; - mcmeta["comment"] = "Animation converted via PCK Studio"; - mcmeta["animation"] = animation; - File.WriteAllText(fileDialog.FileName, JsonConvert.SerializeObject(mcmeta, Formatting.Indented)); - string fn = fileDialog.FileName; - currentAnimation.BuildTexture(this).Save(fn.Remove(fn.Length - 7)); - MessageBox.Show("Your animation was successfully exported at " + fn, "Successful export"); - } + private JObject ConvertAnimationToJava(Animation animation) + { + JObject janimation = new JObject(); + JObject mcmeta = new JObject(); + mcmeta["comment"] = $"Animation converted by {ProductName}"; + mcmeta["animation"] = janimation; + JArray jframes = new JArray(); + foreach (var frame in animation.GetFrames()) + { + JObject jframe = new JObject(); + jframe["index"] = animation.GetTextureIndex(frame.Texture); + jframe["time"] = frame.Ticks; + jframes.Add(jframe); + }; + janimation["interpolation"] = InterpolationCheckbox.Checked; + janimation["frames"] = jframes; + return mcmeta; + } - private void howToInterpolation_Click(object sender, EventArgs e) + private void howToInterpolation_Click(object sender, EventArgs e) { MessageBox.Show("The Interpolation effect is when the animtion smoothly translates between the frames instead of simply displaying the next one. This can be seen with some vanilla Minecraft textures such as Magma and Prismarine.\n\nThe \"Interpolates\" checkbox at the bottom controls this.", "Interpolation"); } diff --git a/PCK-Studio/Forms/Editor/AnimationPlayer.cs b/PCK-Studio/Forms/Editor/AnimationPlayer.cs new file mode 100644 index 00000000..4a87299d --- /dev/null +++ b/PCK-Studio/Forms/Editor/AnimationPlayer.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.Runtime.CompilerServices; +using System.Diagnostics; + +namespace PckStudio.Forms.Editor +{ + // TODO: write as a UI control ?? + sealed class AnimationPlayer + { + public const int BaseTickSpeed = 48; + public bool IsPlaying { get; private set; } = false; + + private int currentAnimationFrameIndex = 0; + private PictureBox display; + private Animation _animation; + private CancellationTokenSource cts = new CancellationTokenSource(); + + public AnimationPlayer(PictureBox display) + { + SetContext(display); + } + + private async void DoAnimate() + { + _ = display ?? throw new ArgumentNullException(nameof(display)); + _ = _animation ?? throw new ArgumentNullException(nameof(_animation)); + IsPlaying = true; + while (!cts.IsCancellationRequested) + { + if (currentAnimationFrameIndex >= _animation.FrameCount) + currentAnimationFrameIndex = 0; + Animation.Frame frame = SetDisplayFrame(currentAnimationFrameIndex++); + await Task.Delay(BaseTickSpeed * frame.Ticks); + } + IsPlaying = false; + } + + public void Start(Animation animation) + { + _animation = animation; + cts = new CancellationTokenSource(); + Task.Run(DoAnimate, cts.Token); + } + + public void Stop([CallerMemberName] string callerName = default!) + { + Debug.WriteLine($"{nameof(AnimationPlayer.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); + currentAnimationFrameIndex = index; + } + + private Animation.Frame SetDisplayFrame(int frameIndex) + { + Monitor.Enter(_animation); + Animation.Frame frame = _animation[frameIndex]; + display.Image = frame; + Monitor.Exit(_animation); + return frame; + } + } +} diff --git a/PCK-Studio/PckStudio.csproj b/PCK-Studio/PckStudio.csproj index 62dfa550..a0a6cf2c 100644 --- a/PCK-Studio/PckStudio.csproj +++ b/PCK-Studio/PckStudio.csproj @@ -170,6 +170,7 @@ + @@ -259,6 +260,8 @@ TextPrompt.cs + + Form