You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
390 lines
16 KiB
390 lines
16 KiB
using System.Drawing;
|
|
using System.Text;
|
|
using ImageMagick;
|
|
using Kynareth.Extensions;
|
|
using CollectionExtensions = Kynareth.Extensions.CollectionExtensions;
|
|
|
|
namespace Kynareth.Helpers.AmongUs;
|
|
|
|
public class AmongUsGenerator
|
|
{
|
|
public static string IdleRoot, EjectRoot, CommonRoot;
|
|
|
|
public const string Arial = "Arial";
|
|
|
|
public AmongUsGenerator(IConfiguration configuration, IWebHostEnvironment appEnvironment)
|
|
{
|
|
string amogusBase = Path.Combine(appEnvironment.WebRootPath, "amogus");
|
|
|
|
IdleRoot = Path.Combine(amogusBase, "idle");
|
|
EjectRoot = Path.Combine(amogusBase, "eject");
|
|
CommonRoot = Path.Combine(amogusBase, "common");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a base image for an AmongUs Asset that needs color replacement
|
|
/// The default image is the standing crewmate (idle) and the default colour is blue
|
|
/// </summary>
|
|
/// <param name="baseImageName">The image to load up</param>
|
|
/// <param name="colour">The colour to make the base image, must be one <see cref="AmogusColour"/></param>
|
|
/// <returns>A <see cref="MagickImage"/> generated from <paramref name="baseImageName"/></returns>
|
|
static MagickImage GenerateBase(string baseImageName = "idle.png", AmogusColour colour = AmogusColour.Blue)
|
|
{
|
|
string baseIdle = Path.Combine(CommonRoot, baseImageName);
|
|
MagickImage body = new(baseIdle);
|
|
|
|
body = ColourReplace(body, colour);
|
|
body.BackgroundColor = MagickColors.Transparent;
|
|
|
|
return body;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces the Colour of an AmogieCrewMate with the target colour
|
|
/// </summary>
|
|
/// <param name="image">Image to Colour Replace</param>
|
|
/// <param name="colour">Colour to replace with</param>
|
|
/// <returns>Colour Replaced Amogie Crewmate</returns>
|
|
static MagickImage ColourReplace(MagickImage image, AmogusColour colour = AmogusColour.Blue)
|
|
{
|
|
ColourHelper colours = new(colour);
|
|
|
|
var pixels = image.GetPixels();
|
|
|
|
for (int y = 0; y < image.Height; y++)
|
|
{
|
|
for (int x = 0; x < image.Width; x++)
|
|
{
|
|
var pixel = pixels.GetPixel(x, y);
|
|
var pixelColor = pixel.ToColor();
|
|
|
|
if (pixelColor is null) continue;
|
|
|
|
float pixelSum = pixelColor.R + pixelColor.G + pixelColor.B;
|
|
|
|
var areEqual = pixelColor.R == pixelColor.G && pixelColor.G == pixelColor.B;
|
|
|
|
if (pixelColor.A == 0 || areEqual) continue;
|
|
|
|
var pixBr = pixelColor.ToByteArray();
|
|
|
|
int SumWithinRange(byte lower, byte upper)
|
|
{
|
|
int count = 0;
|
|
|
|
if (pixelColor.R >= lower && pixelColor.R <= upper) count++;
|
|
if (pixelColor.G >= lower && pixelColor.G <= upper) count++;
|
|
if (pixelColor.B >= lower && pixelColor.B <= upper) count++;
|
|
|
|
return count;
|
|
}
|
|
|
|
if (SumWithinRange(45, 65) < 3)
|
|
{
|
|
byte[]
|
|
newR = Enumerable.Repeat((byte)(pixelColor.R / 255), 3).ToArray(),
|
|
newG = new byte[3],
|
|
newB = Enumerable.Repeat((byte)(pixelColor.B / 255), 3).ToArray();
|
|
|
|
var rBArr = colours.Red.ToByteArray();
|
|
var gBArr = colours.Green.ToByteArray();
|
|
var bBArr = colours.Blue.ToByteArray();
|
|
|
|
int m = 0;
|
|
foreach (var mask in gBArr)
|
|
{
|
|
if (m == 3) break;
|
|
newG[m] = (byte)(mask * pixelColor.G / 255);
|
|
m++;
|
|
}
|
|
|
|
if (pixelColor.G == 0 || pixelSum is >= 508 and <= 520)
|
|
{
|
|
m = 0;
|
|
foreach (var mask in rBArr)
|
|
{
|
|
if (m == 3) break;
|
|
newR[m] = (byte)(mask * pixelColor.R / 255);
|
|
m++;
|
|
}
|
|
|
|
|
|
m = 0;
|
|
foreach (var mask in bBArr)
|
|
{
|
|
if (m == 3) break;
|
|
newB[m] = (byte)(mask * pixelColor.B / 255);
|
|
m++;
|
|
}
|
|
}
|
|
else if (pixelColor.R == pixelColor.B && pixelSum > 512)
|
|
{
|
|
m = 0;
|
|
foreach (var mask in newG)
|
|
{
|
|
if (m == 3) break;
|
|
byte val = (byte)(((pixelColor.R / 255) * (255 - mask)) / 2);
|
|
newR[m] = val;
|
|
newB[m] = val;
|
|
}
|
|
}
|
|
|
|
byte minR = (byte)Math.Min(newR[0] + newG[0] + newB[0], 255);
|
|
byte minG = (byte)Math.Min(newR[1] + newG[1] + newB[1], 255);
|
|
byte minB = (byte)Math.Min(newR[2] + newG[2] + newB[2], 255);
|
|
|
|
byte[] newColors = { minR, minG, minB, pixelColor.A };
|
|
|
|
pixels.SetPixel(x, y, newColors);
|
|
}
|
|
}
|
|
}
|
|
|
|
image.ImportPixels(pixels.ToArray(), new PixelImportSettings(0, 0, image.Width, image.Height, StorageType.Quantum, PixelMapping.RGBA));
|
|
|
|
return image;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the Eject variant of the Skin
|
|
/// </summary>
|
|
/// <param name="baseImage">Base Crewmate Image</param>
|
|
/// <param name="layerImage">Skin to Apply</param>
|
|
/// <param name="idleOffset">Skin offset from <see cref="baseImage"/></param>
|
|
/// <param name="baseOrigin">Origin of <see cref="baseImage"/></param>
|
|
/// <returns>Crewmate with Skin Applied</returns>
|
|
static MagickImage ApplyLayer(MagickImage baseImage, MagickImage layerImage, Point layerOrigin, Point baseOrigin)
|
|
{
|
|
int left = layerOrigin.X + baseOrigin.X;
|
|
int top = layerOrigin.Y + baseOrigin.Y;
|
|
int right = layerOrigin.X + layerImage.Width - baseImage.Width;
|
|
int bottom = layerOrigin.Y + layerImage.Height - baseImage.Height;
|
|
|
|
left = left < 0 ? Math.Abs(left) : 0;
|
|
top = top < 0 ? Math.Abs(top) : 0;
|
|
|
|
MagickImage newImage = new MagickImage(MagickColors.Transparent, left + right + baseImage.Width, top + bottom + baseImage.Height);
|
|
newImage.BackgroundColor = MagickColors.Transparent;
|
|
|
|
baseOrigin = new Point(left, top);
|
|
Point newLayerOrigin = new Point(baseOrigin.X + layerOrigin.X, baseOrigin.Y + layerOrigin.Y);
|
|
|
|
newImage.Composite(baseImage, baseOrigin.X, baseOrigin.Y, CompositeOperator.Over);
|
|
newImage.Composite(layerImage, newLayerOrigin.X, newLayerOrigin.Y, CompositeOperator.Over);
|
|
|
|
return newImage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate a crewmate with the given settings
|
|
/// </summary>
|
|
/// <param name="colour">Colour of the crewmate</param>
|
|
/// <param name="skin">Skin that the crewmate is wearing</param>
|
|
/// <param name="ejected">If the crewmate has been ejected or not</param>
|
|
/// <returns>Generated crewmate with the given settings</returns>
|
|
static MagickImage GenerateCrewmate(AmogusColour colour, AmogusSkin skin, bool ejected = true)
|
|
{
|
|
MagickImage baseImage = GenerateBase(ejected ? "ejected.png" : "idle.png", colour);
|
|
MagickImage skinImage = null;
|
|
|
|
if (skin != AmogusSkin.None)
|
|
{
|
|
skinImage = new MagickImage(
|
|
Path.Combine(
|
|
ejected ? EjectRoot : IdleRoot,
|
|
ejected ? AmongUsGeneratorHelper.EjectSkins[skin] : AmongUsGeneratorHelper.IdleSkins[skin]
|
|
)
|
|
);
|
|
}
|
|
|
|
var outImage = skinImage != null ?
|
|
ApplyLayer(
|
|
baseImage,
|
|
skinImage,
|
|
ejected ? AmongUsGeneratorHelper.EjectOffset[skin] : AmongUsGeneratorHelper.IdleOffset[skin], new Point(0, 0)
|
|
) : baseImage;
|
|
|
|
outImage.Trim();
|
|
|
|
return outImage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate Amongus Stars
|
|
/// </summary>
|
|
/// <param name="width">Width of the stars</param>
|
|
/// <returns>Generated image of stars</returns>
|
|
static MagickImage GenerateStars(int width = 2000)
|
|
{
|
|
MagickImage baseImage = new MagickImage("xc:transparent", width, 588);
|
|
|
|
using MagickImage stars = new MagickImage(Path.Combine(CommonRoot, "Stars.png"));
|
|
stars.Distort(DistortMethod.ScaleRotateTranslate, 90);
|
|
using MagickImage stars2 = new MagickImage(stars);
|
|
stars2.Distort(DistortMethod.ScaleRotateTranslate, 180);
|
|
|
|
foreach (var x in CollectionExtensions.Range(-800, width, 320))
|
|
{
|
|
baseImage.Composite(stars2, Gravity.Northwest, x, (int)(x / 60f) - 200, CompositeOperator.Over);
|
|
baseImage.Composite(stars, Gravity.Northwest, x, 100 + (int)(x / 120f), CompositeOperator.Over);
|
|
baseImage.Composite(stars2, Gravity.Northwest, x, 300 - (int)(x / 60f), CompositeOperator.Over);
|
|
baseImage.Composite(stars, Gravity.Northwest, x, 500 + (int)(x / 120f), CompositeOperator.Over);
|
|
baseImage.Composite(stars, Gravity.Northwest, x + 160 + (int)(x / 80f), (int)(x / 100f) - 100, CompositeOperator.Over);
|
|
baseImage.Composite(stars2, Gravity.Northwest, x + 160 - (int)(x / 80f), 100 + (int)(x / 120f), CompositeOperator.Over);
|
|
baseImage.Composite(stars, Gravity.Northwest, x + 160 + (int)(x / 80f), 300 - (int)(x / 100f), CompositeOperator.Over);
|
|
baseImage.Composite(stars2, Gravity.Northwest, x + 160 - (int)(x / 80f), 500 - (int)(x / 100f), CompositeOperator.Over);
|
|
}
|
|
|
|
return baseImage;
|
|
}
|
|
|
|
static string GenerateEjectionMessage(string name = "I", AmogusRole role = AmogusRole.Unknown)
|
|
{
|
|
StringBuilder message = new StringBuilder(name);
|
|
message.Append(" was");
|
|
|
|
if (role == AmogusRole.Unknown)
|
|
{
|
|
message.Append(" ejected.");
|
|
return message.ToString();
|
|
}
|
|
|
|
if (role == AmogusRole.Crewmate)
|
|
{
|
|
message.Append(" not");
|
|
}
|
|
|
|
message.Append(" An Impostor.");
|
|
|
|
return message.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates the Ejection Gif with all modifications applied
|
|
/// </summary>
|
|
/// <param name="name"></param>
|
|
/// <param name="colour"></param>
|
|
/// <param name="skin"></param>
|
|
/// <param name="role"></param>
|
|
/// <returns>A <see cref="MemoryStream"/> containing the Amogus Ejected Gif</returns>
|
|
public MagickImageCollection GenerateEjectionGif(string name, AmogusColour? colour = null, AmogusRole? role = null, AmogusSkin? skin = null, bool addStars = true)
|
|
{
|
|
if (role is null) role = EnumHelper.GetRandomEnumValue<AmogusRole>();
|
|
|
|
return GenerateCustomEjectionGif(GenerateEjectionMessage(name, role.Value), colour, role, skin, addStars);
|
|
}
|
|
|
|
public MagickImageCollection GenerateCustomEjectionGif(string text, AmogusColour? colour = null, AmogusRole? role = null, AmogusSkin? skin = null, bool addStars = true)
|
|
{
|
|
if (colour is null) colour = EnumHelper.GetRandomEnumValue<AmogusColour>();
|
|
if (role is null) role = EnumHelper.GetRandomEnumValue<AmogusRole>();
|
|
if (skin is null) skin = EnumHelper.GetRandomEnumValue<AmogusSkin>();
|
|
|
|
var crewmate = GenerateCrewmate(colour.Value, skin.Value);
|
|
crewmate = MakeSquare(crewmate);
|
|
var textImg = new MagickImage(MagickColors.Transparent, 4000, 64);
|
|
textImg.WriteText(text, 0, 0, MagickColors.White, Encoding.Default, 24, Arial, 0, MagickColors.Transparent);
|
|
textImg.Crop(textImg.BoundingBox);
|
|
textImg.RePage();
|
|
|
|
(int X, int Y) center = new(crewmate.Width / 2, crewmate.Height / 2);
|
|
|
|
using var background = new MagickImage(MagickColors.Black, Math.Max(textImg.Width + 100, 600), 300);
|
|
|
|
MagickImage stars = null, stars1 = null, stars2 = null, stars3 = null;
|
|
|
|
if (addStars)
|
|
{
|
|
stars = GenerateStars(background.Width * 3);
|
|
|
|
stars1 = stars.Clone() as MagickImage;
|
|
stars1.BackgroundColor = MagickColors.Transparent;
|
|
stars1.Resize((int)(stars.Width * 3.5f), (int)(stars.Height * 3.5f));
|
|
|
|
stars2 = stars1.Clone() as MagickImage;
|
|
stars2.BackgroundColor = MagickColors.Transparent;
|
|
|
|
stars3 = stars1.Clone() as MagickImage;
|
|
stars3.BackgroundColor = MagickColors.Transparent;
|
|
}
|
|
|
|
var range = CollectionExtensions.Range(-120, background.Width * 3, 12);
|
|
|
|
MagickImageCollection gifFrames = new MagickImageCollection();
|
|
|
|
foreach (var bodyX in range)
|
|
{
|
|
var image = background.Clone() as MagickImage;
|
|
|
|
if (addStars)
|
|
{
|
|
image.Composite(stars1, -background.Width + (int)(bodyX / 24f) - 100, -200, CompositeOperator.Over);
|
|
image.Composite(stars2, -background.Width + (int)(bodyX / 12f) - 200, -800, CompositeOperator.Over);
|
|
image.Composite(stars3, -background.Width + (int)(bodyX / 6f) - 100, -425, CompositeOperator.Over);
|
|
}
|
|
|
|
using var rotatedBody = crewmate.Clone() as MagickImage;
|
|
rotatedBody.BackgroundColor = MagickColors.Transparent;
|
|
rotatedBody.Distort(DistortMethod.ScaleRotateTranslate, -(2 * bodyX / 3));
|
|
rotatedBody.RePage();
|
|
|
|
var rotateX = bodyX - center.X;
|
|
var rotateY = background.Height / 2 - center.Y;
|
|
|
|
image.Composite(rotatedBody, Gravity.Northwest, rotateX, rotateY, CompositeOperator.Atop);
|
|
|
|
using var txt = textImg.Clone() as MagickImage;
|
|
txt.BackgroundColor = MagickColors.Transparent;
|
|
|
|
if (bodyX > background.Width / 2 && bodyX <= 3 * background.Width / 2)
|
|
{
|
|
var midX = background.Width / 2;
|
|
var textY = background.Height / 2 - txt.Height / 2;
|
|
var textX = Convert.ToInt32(midX - ((float)bodyX - midX) / background.Width * txt.Width / 2f);
|
|
var textWidth = (midX - textX) * 2;
|
|
|
|
txt.Crop(new MagickGeometry(0, 0, textWidth, txt.Height));
|
|
|
|
image.Composite(txt, Gravity.Northwest, textX, textY, CompositeOperator.Over);
|
|
}
|
|
else if (bodyX > (int)(background.Width * 1.5f) && bodyX <= (int)(background.Width * 2.5f))
|
|
{
|
|
image.Composite(txt, (int)(background.Width / 2f - txt.Width / 2f), (int)(background.Height / 2f - txt.Height / 2f), CompositeOperator.Atop);
|
|
}
|
|
|
|
image.AnimationIterations = 0;
|
|
image.AnimationDelay = 4;
|
|
|
|
gifFrames.Add(image);
|
|
}
|
|
|
|
if (addStars)
|
|
{
|
|
stars.Dispose();
|
|
stars1.Dispose();
|
|
stars2.Dispose();
|
|
stars3.Dispose();
|
|
}
|
|
|
|
crewmate.Dispose();
|
|
|
|
return gifFrames;
|
|
}
|
|
|
|
#region Utilities
|
|
static MagickImage MakeSquare(MagickImage image)
|
|
{
|
|
int size = Math.Max(image.Width, image.Height);
|
|
|
|
MagickImage newImage = new MagickImage("xc:transparent", size, size);
|
|
newImage.Format = MagickFormat.Png;
|
|
|
|
int x = (size - image.Width) / 2;
|
|
int y = (size - image.Height) / 2;
|
|
|
|
newImage.Composite(image, Gravity.Center, x, y, CompositeOperator.Over);
|
|
|
|
return newImage;
|
|
}
|
|
#endregion Utilities
|
|
} |