mirror of
https://git.huckle.dev/Huckles-Minecraft-Archive/PCK-Studio.git
synced 2026-07-12 10:48:09 +00:00
Refactored AnimationEditor
This commit is contained in:
17
PCK-Studio/Classes/Extentions/EnumerableExtentions.cs
Normal file
17
PCK-Studio/Classes/Extentions/EnumerableExtentions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PckStudio.Classes.Extentions
|
||||
{
|
||||
internal static class EnumerableExtentions
|
||||
{
|
||||
public static IEnumerable<(int index, T type)>enumerate<T>(this IEnumerable<T> array)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var item in array)
|
||||
{
|
||||
yield return (i++, item);
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Size of sub section of the image
|
||||
/// </summary>
|
||||
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<Image> 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<Image> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
150
PCK-Studio/Forms/Editor/Animation.cs
Normal file
150
PCK-Studio/Forms/Editor/Animation.cs
Normal file
@@ -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<Image> frameTextures;
|
||||
|
||||
private readonly List<Frame> frames = new List<Frame>();
|
||||
|
||||
|
||||
public Animation(IEnumerable<Image> image)
|
||||
{
|
||||
frameTextures = new List<Image>(image);
|
||||
}
|
||||
|
||||
public Animation(IEnumerable<Image> 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<Frame> GetFrames()
|
||||
{
|
||||
return frames;
|
||||
}
|
||||
|
||||
public List<Image> 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<Image> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Image> frameTextures;
|
||||
|
||||
private readonly List<Frame> frames = new List<Frame>();
|
||||
|
||||
public Frame this[int frameIndex] => frames[frameIndex];
|
||||
|
||||
// not implemented rn...
|
||||
public bool Interpolate { get; set; } = false;
|
||||
|
||||
public Animation(Image image)
|
||||
{
|
||||
frameTextures = new List<Image>(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<Frame> GetFrames()
|
||||
{
|
||||
return frames;
|
||||
}
|
||||
|
||||
public List<Image> 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<Image> linearImages = new List<Image>();
|
||||
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");
|
||||
}
|
||||
|
||||
76
PCK-Studio/Forms/Editor/AnimationPlayer.cs
Normal file
76
PCK-Studio/Forms/Editor/AnimationPlayer.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,7 @@
|
||||
<Compile Include="Classes\API\PCKCenter\PCKCollections.cs" />
|
||||
<Compile Include="Classes\API\PCKCenter\PCKCollectionsLocal.cs" />
|
||||
<Compile Include="Classes\API\PCKCenter\SaveLocalJSON.cs" />
|
||||
<Compile Include="Classes\Extentions\EnumerableExtentions.cs" />
|
||||
<Compile Include="Classes\FileTypes\CSMBFile.cs" />
|
||||
<Compile Include="Classes\FileTypes\PCKAudioFile.cs" />
|
||||
<Compile Include="Classes\FileTypes\Binka.cs" />
|
||||
@@ -259,6 +260,8 @@
|
||||
<Compile Include="Forms\Additional-Popups\TextPrompt.Designer.cs">
|
||||
<DependentUpon>TextPrompt.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Forms\Editor\Animation.cs" />
|
||||
<Compile Include="Forms\Editor\AnimationPlayer.cs" />
|
||||
<Compile Include="Forms\Editor\MaterialsEditor.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
|
||||
Reference in New Issue
Block a user