if (Profile is null) return;
var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
+ var strategy = _prototypeManager.Index(skin).Strategy;
- switch (skin)
+ switch (strategy.InputType)
{
- case HumanoidSkinColor.HumanToned:
+ case SkinColorationStrategyInput.Unary:
{
if (!Skin.Visible)
{
RgbSkinColorContainer.Visible = false;
}
- var color = SkinColor.HumanSkinTone((int) Skin.Value);
+ var color = strategy.FromUnary(Skin.Value);
Markings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
- break;
- }
- case HumanoidSkinColor.Hues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
- Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
break;
}
- case HumanoidSkinColor.TintedHues:
+ case SkinColorationStrategyInput.Color:
{
if (!RgbSkinColorContainer.Visible)
{
RgbSkinColorContainer.Visible = true;
}
- var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
+ var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
- break;
- }
- case HumanoidSkinColor.VoxFeathers:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
-
- var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
- Markings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
}
}
return;
var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
+ var strategy = _prototypeManager.Index(skin).Strategy;
- switch (skin)
+ switch (strategy.InputType)
{
- case HumanoidSkinColor.HumanToned:
+ case SkinColorationStrategyInput.Unary:
{
if (!Skin.Visible)
{
RgbSkinColorContainer.Visible = false;
}
- Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
+ Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor);
break;
}
- case HumanoidSkinColor.Hues:
+ case SkinColorationStrategyInput.Color:
{
if (!RgbSkinColorContainer.Visible)
{
RgbSkinColorContainer.Visible = true;
}
- // set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- case HumanoidSkinColor.TintedHues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
-
- // set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- case HumanoidSkinColor.VoxFeathers:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
-
- _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
+ _rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor);
break;
}
}
-
}
public void UpdateSpeciesGuidebookIcon()
using System.Linq;
+using System.Numerics;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Prototypes;
public Color EyeColor { get; set; } = Color.Black;
[DataField]
- public Color SkinColor { get; set; } = Humanoid.SkinColor.ValidHumanSkinTone;
+ public Color SkinColor { get; set; } = Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f));
[DataField]
public List<Marking> Markings { get; set; } = new();
public static HumanoidCharacterAppearance DefaultWithSpecies(string species)
{
- var speciesPrototype = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species);
- var skinColor = speciesPrototype.SkinColoration switch
+ var protoMan = IoCManager.Resolve<IPrototypeManager>();
+ var speciesPrototype = protoMan.Index<SpeciesPrototype>(species);
+ var skinColoration = protoMan.Index(speciesPrototype.SkinColoration).Strategy;
+ var skinColor = skinColoration.InputType switch
{
- HumanoidSkinColor.HumanToned => Humanoid.SkinColor.HumanSkinTone(speciesPrototype.DefaultHumanSkinTone),
- HumanoidSkinColor.Hues => speciesPrototype.DefaultSkinTone,
- HumanoidSkinColor.TintedHues => Humanoid.SkinColor.TintedHues(speciesPrototype.DefaultSkinTone),
- HumanoidSkinColor.VoxFeathers => Humanoid.SkinColor.ClosestVoxColor(speciesPrototype.DefaultSkinTone),
- _ => Humanoid.SkinColor.ValidHumanSkinTone,
+ SkinColorationStrategyInput.Unary => skinColoration.FromUnary(speciesPrototype.DefaultHumanSkinTone),
+ SkinColorationStrategyInput.Color => skinColoration.ClosestSkinColor(speciesPrototype.DefaultSkinTone),
};
return new(
var newEyeColor = random.Pick(RealisticEyeColors);
- var skinType = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species).SkinColoration;
+ var protoMan = IoCManager.Resolve<IPrototypeManager>();
+ var skinType = protoMan.Index<SpeciesPrototype>(species).SkinColoration;
+ var strategy = protoMan.Index(skinType).Strategy;
- var newSkinColor = new Color(random.NextFloat(1), random.NextFloat(1), random.NextFloat(1), 1);
- switch (skinType)
+ var newSkinColor = strategy.InputType switch
{
- case HumanoidSkinColor.HumanToned:
- newSkinColor = Humanoid.SkinColor.HumanSkinTone(random.Next(0, 101));
- break;
- case HumanoidSkinColor.Hues:
- break;
- case HumanoidSkinColor.TintedHues:
- newSkinColor = Humanoid.SkinColor.ValidTintedHuesSkinTone(newSkinColor);
- break;
- case HumanoidSkinColor.VoxFeathers:
- newSkinColor = Humanoid.SkinColor.ProportionalVoxColor(newSkinColor);
- break;
- }
+ SkinColorationStrategyInput.Unary => strategy.FromUnary(random.NextFloat(0f, 100f)),
+ SkinColorationStrategyInput.Color => strategy.ClosestSkinColor(new Color(random.NextFloat(1), random.NextFloat(1), random.NextFloat(1), 1)),
+ };
return new HumanoidCharacterAppearance(newHairStyle, newHairColor, newFacialHairStyle, newHairColor, newEyeColor, newSkinColor, new ());
markingSet = new MarkingSet(appearance.Markings, speciesProto.MarkingPoints, markingManager, proto);
markingSet.EnsureValid(markingManager);
- if (!Humanoid.SkinColor.VerifySkinColor(speciesProto.SkinColoration, skinColor))
- {
- skinColor = Humanoid.SkinColor.ValidSkinTone(speciesProto.SkinColoration, skinColor);
- }
+ var strategy = proto.Index(speciesProto.SkinColoration).Strategy;
+ skinColor = strategy.EnsureVerified(skinColor);
markingSet.EnsureSpecies(species, skinColor, markingManager);
markingSet.EnsureSexes(sex, markingManager);
/// Method of skin coloration used by the species.
/// </summary>
[DataField(required: true)]
- public HumanoidSkinColor SkinColoration { get; private set; }
+ public ProtoId<SkinColorationPrototype> SkinColoration { get; private set; }
[DataField]
public ProtoId<LocalizedDatasetPrototype> MaleFirstNames { get; private set; } = "NamesFirstMale";
return;
}
- if (verify && !SkinColor.VerifySkinColor(species.SkinColoration, skinColor))
+ if (verify && _proto.Resolve(species.SkinColoration, out var index))
{
- skinColor = SkinColor.ValidSkinTone(species.SkinColoration, skinColor);
+ var strategy = index.Strategy;
+ skinColor = strategy.EnsureVerified(skinColor);
}
humanoid.SkinColor = skinColor;
+++ /dev/null
-using System.Numerics;
-using System.Security.Cryptography;
-using Microsoft.VisualBasic.CompilerServices;
-
-namespace Content.Shared.Humanoid;
-
-public static class SkinColor
-{
- public const float MaxTintedHuesSaturation = 0.1f;
- public const float MinTintedHuesLightness = 0.85f;
-
- public const float MinHuesLightness = 0.175f;
-
- public const float MinFeathersHue = 29f / 360;
- public const float MaxFeathersHue = 174f / 360;
- public const float MinFeathersSaturation = 20f / 100;
- public const float MaxFeathersSaturation = 88f / 100;
- public const float MinFeathersValue = 36f / 100;
- public const float MaxFeathersValue = 55f / 100;
-
- public static Color ValidHumanSkinTone => Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f));
-
- /// <summary>
- /// Turn a color into a valid tinted hue skin tone.
- /// </summary>
- /// <param name="color">The color to validate</param>
- /// <returns>Validated tinted hue skin tone</returns>
- public static Color ValidTintedHuesSkinTone(Color color)
- {
- return TintedHues(color);
- }
-
- /// <summary>
- /// Get a human skin tone based on a scale of 0 to 100. The value is clamped between 0 and 100.
- /// </summary>
- /// <param name="tone">Skin tone. Valid range is 0 to 100, inclusive. 0 is gold/yellowish, 100 is dark brown.</param>
- /// <returns>A human skin tone.</returns>
- public static Color HumanSkinTone(int tone)
- {
- // 0 - 100, 0 being gold/yellowish and 100 being dark
- // HSV based
- //
- // 0 - 20 changes the hue
- // 20 - 100 changes the value
- // 0 is 45 - 20 - 100
- // 20 is 25 - 20 - 100
- // 100 is 25 - 100 - 20
-
- tone = Math.Clamp(tone, 0, 100);
-
- var rangeOffset = tone - 20;
-
- float hue = 25;
- float sat = 20;
- float val = 100;
-
- if (rangeOffset <= 0)
- {
- hue += Math.Abs(rangeOffset);
- }
- else
- {
- sat += rangeOffset;
- val -= rangeOffset;
- }
-
- var color = Color.FromHsv(new Vector4(hue / 360, sat / 100, val / 100, 1.0f));
-
- return color;
- }
-
- /// <summary>
- /// Gets a human skin tone from a given color.
- /// </summary>
- /// <param name="color"></param>
- /// <returns></returns>
- /// <remarks>
- /// Does not cause an exception if the color is not originally from the human color range.
- /// Instead, it will return the approximation of the skin tone value.
- /// </remarks>
- public static float HumanSkinToneFromColor(Color color)
- {
- var hsv = Color.ToHsv(color);
- // check for hue/value first, if hue is lower than this percentage
- // and value is 1.0
- // then it'll be hue
- if (Math.Clamp(hsv.X, 25f / 360f, 1) > 25f / 360f
- && hsv.Z == 1.0)
- {
- return Math.Abs(45 - (hsv.X * 360));
- }
- // otherwise it'll directly be the saturation
- else
- {
- return hsv.Y * 100;
- }
- }
-
- /// <summary>
- /// Verify if a color is in the human skin tone range.
- /// </summary>
- /// <param name="color">The color to verify</param>
- /// <returns>True if valid, false otherwise.</returns>
- public static bool VerifyHumanSkinTone(Color color)
- {
- var colorValues = Color.ToHsv(color);
-
- var hue = Math.Round(colorValues.X * 360f);
- var sat = Math.Round(colorValues.Y * 100f);
- var val = Math.Round(colorValues.Z * 100f);
- // rangeOffset makes it so that this value
- // is 25 <= hue <= 45
- if (hue < 25 || hue > 45)
- {
- return false;
- }
-
- // rangeOffset makes it so that these two values
- // are 20 <= sat <= 100 and 20 <= val <= 100
- // where saturation increases to 100 and value decreases to 20
- if (sat < 20 || val < 20)
- {
- return false;
- }
-
- return true;
- }
-
- /// <summary>
- /// Convert a color to the 'tinted hues' skin tone type.
- /// </summary>
- /// <param name="color">Color to convert</param>
- /// <returns>Tinted hue color</returns>
- public static Color TintedHues(Color color)
- {
- var newColor = Color.ToHsl(color);
- newColor.Y *= MaxTintedHuesSaturation;
- newColor.Z = MathHelper.Lerp(MinTintedHuesLightness, 1f, newColor.Z);
-
- return Color.FromHsv(newColor);
- }
-
- /// <summary>
- /// Verify if this color is a valid tinted hue color type, or not.
- /// </summary>
- /// <param name="color">The color to verify</param>
- /// <returns>True if valid, false otherwise</returns>
- public static bool VerifyTintedHues(Color color)
- {
- // tinted hues just ensures saturation is always .1, or 10% saturation at all times
- return Color.ToHsl(color).Y <= MaxTintedHuesSaturation && Color.ToHsl(color).Z >= MinTintedHuesLightness;
- }
-
- /// <summary>
- /// Converts a Color proportionally to the allowed vox color range.
- /// Will NOT preserve the specific input color even if it is within the allowed vox color range.
- /// </summary>
- /// <param name="color">Color to convert</param>
- /// <returns>Vox feather coloration</returns>
- public static Color ProportionalVoxColor(Color color)
- {
- var newColor = Color.ToHsv(color);
-
- newColor.X = newColor.X * (MaxFeathersHue - MinFeathersHue) + MinFeathersHue;
- newColor.Y = newColor.Y * (MaxFeathersSaturation - MinFeathersSaturation) + MinFeathersSaturation;
- newColor.Z = newColor.Z * (MaxFeathersValue - MinFeathersValue) + MinFeathersValue;
-
- return Color.FromHsv(newColor);
- }
-
- // /// <summary>
- // /// Ensures the input Color is within the allowed vox color range.
- // /// </summary>
- // /// <param name="color">Color to convert</param>
- // /// <returns>The same Color if it was within the allowed range, or the closest matching Color otherwise</returns>
- public static Color ClosestVoxColor(Color color)
- {
- var hsv = Color.ToHsv(color);
-
- hsv.X = Math.Clamp(hsv.X, MinFeathersHue, MaxFeathersHue);
- hsv.Y = Math.Clamp(hsv.Y, MinFeathersSaturation, MaxFeathersSaturation);
- hsv.Z = Math.Clamp(hsv.Z, MinFeathersValue, MaxFeathersValue);
-
- return Color.FromHsv(hsv);
- }
-
- /// <summary>
- /// Verify if this color is a valid vox feather coloration, or not.
- /// </summary>
- /// <param name="color">The color to verify</param>
- /// <returns>True if valid, false otherwise</returns>
- public static bool VerifyVoxFeathers(Color color)
- {
- var colorHsv = Color.ToHsv(color);
-
- if (colorHsv.X < MinFeathersHue || colorHsv.X > MaxFeathersHue)
- return false;
-
- if (colorHsv.Y < MinFeathersSaturation || colorHsv.Y > MaxFeathersSaturation)
- return false;
-
- if (colorHsv.Z < MinFeathersValue || colorHsv.Z > MaxFeathersValue)
- return false;
-
- return true;
- }
-
- /// <summary>
- /// This takes in a color, and returns a color guaranteed to be above MinHuesLightness
- /// </summary>
- /// <param name="color"></param>
- /// <returns>Either the color as-is if it's above MinHuesLightness, or the color with luminosity increased above MinHuesLightness</returns>
- public static Color MakeHueValid(Color color)
- {
- var manipulatedColor = Color.ToHsv(color);
- manipulatedColor.Z = Math.Max(manipulatedColor.Z, MinHuesLightness);
- return Color.FromHsv(manipulatedColor);
- }
-
- /// <summary>
- /// Verify if this color is above a minimum luminosity
- /// </summary>
- /// <param name="color"></param>
- /// <returns>True if valid, false if not</returns>
- public static bool VerifyHues(Color color)
- {
- return Color.ToHsv(color).Z >= MinHuesLightness;
- }
-
- public static bool VerifySkinColor(HumanoidSkinColor type, Color color)
- {
- return type switch
- {
- HumanoidSkinColor.HumanToned => VerifyHumanSkinTone(color),
- HumanoidSkinColor.TintedHues => VerifyTintedHues(color),
- HumanoidSkinColor.Hues => VerifyHues(color),
- HumanoidSkinColor.VoxFeathers => VerifyVoxFeathers(color),
- _ => false,
- };
- }
-
- public static Color ValidSkinTone(HumanoidSkinColor type, Color color)
- {
- return type switch
- {
- HumanoidSkinColor.HumanToned => ValidHumanSkinTone,
- HumanoidSkinColor.TintedHues => ValidTintedHuesSkinTone(color),
- HumanoidSkinColor.Hues => MakeHueValid(color),
- HumanoidSkinColor.VoxFeathers => ClosestVoxColor(color),
- _ => color
- };
- }
-}
-
-public enum HumanoidSkinColor : byte
-{
- HumanToned,
- Hues,
- VoxFeathers, // Vox feathers are limited to a specific color range
- TintedHues, //This gives a color tint to a humanoid's skin (10% saturation with full hue range).
-}
--- /dev/null
+using System.Numerics;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Humanoid;
+
+/// <summary>
+/// A prototype containing a SkinColorationStrategy
+/// </summary>
+[Prototype]
+public sealed partial class SkinColorationPrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
+ /// <summary>
+ /// The skin coloration strategy specified by this prototype
+ /// </summary>
+ [DataField(required: true)]
+ public ISkinColorationStrategy Strategy = default!;
+}
+
+/// <summary>
+/// The type of input taken by a <see cref="SkinColorationStrategy" />
+/// </summary>
+[Serializable, NetSerializable]
+public enum SkinColorationStrategyInput
+{
+ /// <summary>
+ /// A single floating point number from 0 to 100 (inclusive)
+ /// </summary>
+ Unary,
+
+ /// <summary>
+ /// A <see cref="Color" />
+ /// </summary>
+ Color,
+}
+
+/// <summary>
+/// Takes in the given <see cref="SkinColorationStrategyInput" /> and returns an adjusted Color
+/// </summary>
+public interface ISkinColorationStrategy
+{
+ /// <summary>
+ /// The type of input expected by the implementor; callers should consult InputType before calling the methods that require a given input
+ /// </summary>
+ SkinColorationStrategyInput InputType { get; }
+
+ /// <summary>
+ /// Returns whether or not the provided <see cref="Color" /> is within bounds of this strategy
+ /// </summary>
+ bool VerifySkinColor(Color color);
+
+ /// <summary>
+ /// Returns the closest skin color that this strategy would provide to the given <see cref="Color" />
+ /// </summary>
+ Color ClosestSkinColor(Color color);
+
+ /// <summary>
+ /// Returns the input if it passes <see cref="VerifySkinColor">, otherwise returns <see cref="ClosestSkinColor" />
+ /// </summary>
+ Color EnsureVerified(Color color)
+ {
+ if (VerifySkinColor(color))
+ {
+ return color;
+ }
+
+ return ClosestSkinColor(color);
+ }
+
+ /// <summary>
+ /// Returns a colour representation of the given unary input
+ /// </summary>
+ Color FromUnary(float unary)
+ {
+ throw new InvalidOperationException("This coloration strategy does not support unary input");
+ }
+
+ /// <summary>
+ /// Returns a colour representation of the given unary input
+ /// </summary>
+ float ToUnary(Color color)
+ {
+ throw new InvalidOperationException("This coloration strategy does not support unary input");
+ }
+}
+
+/// <summary>
+/// Unary coloration strategy that returns human skin tones, with 0 being lightest and 100 being darkest
+/// </summary>
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy
+{
+ [DataField]
+ public Color ValidHumanSkinTone = Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f));
+
+ public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Unary;
+
+ public bool VerifySkinColor(Color color)
+ {
+ var colorValues = Color.ToHsv(color);
+
+ var hue = Math.Round(colorValues.X * 360f);
+ var sat = Math.Round(colorValues.Y * 100f);
+ var val = Math.Round(colorValues.Z * 100f);
+ // rangeOffset makes it so that this value
+ // is 25 <= hue <= 45
+ if (hue < 25f || hue > 45f)
+ {
+ return false;
+ }
+
+ // rangeOffset makes it so that these two values
+ // are 20 <= sat <= 100 and 20 <= val <= 100
+ // where saturation increases to 100 and value decreases to 20
+ if (sat < 20f || val < 20f)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public Color ClosestSkinColor(Color color)
+ {
+ return ValidHumanSkinTone;
+ }
+
+ public Color FromUnary(float color)
+ {
+ // 0 - 100, 0 being gold/yellowish and 100 being dark
+ // HSV based
+ //
+ // 0 - 20 changes the hue
+ // 20 - 100 changes the value
+ // 0 is 45 - 20 - 100
+ // 20 is 25 - 20 - 100
+ // 100 is 25 - 100 - 20
+
+ var tone = Math.Clamp(color, 0f, 100f);
+
+ var rangeOffset = tone - 20f;
+
+ var hue = 25f;
+ var sat = 20f;
+ var val = 100f;
+
+ if (rangeOffset <= 0)
+ {
+ hue += Math.Abs(rangeOffset);
+ }
+ else
+ {
+ sat += rangeOffset;
+ val -= rangeOffset;
+ }
+
+ return Color.FromHsv(new Vector4(hue / 360f, sat / 100f, val / 100f, 1.0f));
+ }
+
+ public float ToUnary(Color color)
+ {
+ var hsv = Color.ToHsv(color);
+ // check for hue/value first, if hue is lower than this percentage
+ // and value is 1.0
+ // then it'll be hue
+ if (Math.Clamp(hsv.X, 25f / 360f, 1) > 25f / 360f
+ && hsv.Z == 1.0)
+ {
+ return Math.Abs(45 - (hsv.X * 360));
+ }
+ // otherwise it'll directly be the saturation
+ else
+ {
+ return hsv.Y * 100;
+ }
+ }
+}
+
+/// <summary>
+/// Unary coloration strategy that clamps the color within the HSV colorspace
+/// </summary>
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class ClampedHsvColoration : ISkinColorationStrategy
+{
+ /// <summary>
+ /// The (min, max) of the hue channel.
+ /// </summary>
+ [DataField]
+ public (float, float)? Hue;
+
+ /// <summary>
+ /// The (min, max) of the saturation channel.
+ /// </summary>
+ [DataField]
+ public (float, float)? Saturation;
+
+ /// <summary>
+ /// The (min, max) of the value channel.
+ /// </summary>
+ [DataField]
+ public (float, float)? Value;
+
+ public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color;
+
+ public bool VerifySkinColor(Color color)
+ {
+ var hsv = Color.ToHsv(color);
+
+ if (Hue is (var minHue, var maxHue) && (hsv.X < minHue || hsv.X > maxHue))
+ return false;
+
+ if (Saturation is (var minSaturation, var maxSaturation) && (hsv.Y < minSaturation || hsv.Y > maxSaturation))
+ return false;
+
+ if (Value is (var minValue, var maxValue) && (hsv.Z < minValue || hsv.Z > maxValue))
+ return false;
+
+ return true;
+ }
+
+ public Color ClosestSkinColor(Color color)
+ {
+ var hsv = Color.ToHsv(color);
+
+ if (Hue is (var minHue, var maxHue))
+ hsv.X = Math.Clamp(hsv.X, minHue, maxHue);
+
+ if (Saturation is (var minSaturation, var maxSaturation))
+ hsv.Y = Math.Clamp(hsv.Y, minSaturation, maxSaturation);
+
+ if (Value is (var minValue, var maxValue))
+ hsv.Z = Math.Clamp(hsv.Z, minValue, maxValue);
+
+ return Color.FromHsv(hsv);
+ }
+}
+
+/// <summary>
+/// Unary coloration strategy that clamps the color within the HSL colorspace
+/// </summary>
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class ClampedHslColoration : ISkinColorationStrategy
+{
+ /// <summary>
+ /// The (min, max) of the hue channel.
+ /// </summary>
+ [DataField]
+ public (float, float)? Hue;
+
+ /// <summary>
+ /// The (min, max) of the saturation channel.
+ /// </summary>
+ [DataField]
+ public (float, float)? Saturation;
+
+ /// <summary>
+ /// The (min, max) of the lightness channel.
+ /// </summary>
+ [DataField]
+ public (float, float)? Lightness;
+
+ public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color;
+
+ public bool VerifySkinColor(Color color)
+ {
+ var hsl = Color.ToHsl(color);
+
+ if (Hue is (var minHue, var maxHue) && (hsl.X < minHue || hsl.X > maxHue))
+ return false;
+
+ if (Saturation is (var minSaturation, var maxSaturation) && (hsl.Y < minSaturation || hsl.Y > maxSaturation))
+ return false;
+
+ if (Lightness is (var minValue, var maxValue) && (hsl.Z < minValue || hsl.Z > maxValue))
+ return false;
+
+ return true;
+ }
+
+ public Color ClosestSkinColor(Color color)
+ {
+ var hsl = Color.ToHsl(color);
+
+ if (Hue is (var minHue, var maxHue))
+ hsl.X = Math.Clamp(hsl.X, minHue, maxHue);
+
+ if (Saturation is (var minSaturation, var maxSaturation))
+ hsl.Y = Math.Clamp(hsl.Y, minSaturation, maxSaturation);
+
+ if (Lightness is (var minValue, var maxValue))
+ hsl.Z = Math.Clamp(hsl.Z, minValue, maxValue);
+
+ return Color.FromHsl(hsl);
+ }
+}
[Test]
public void TestHumanSkinToneValidity()
{
+ var strategy = new HumanTonedSkinColoration();
+
for (var i = 0; i <= 100; i++)
{
- var color = SkinColor.HumanSkinTone(i);
- Assert.That(SkinColor.VerifyHumanSkinTone(color));
+ var color = strategy.FromUnary(i);
+ Assert.That(strategy.VerifySkinColor(color));
}
}
[Test]
public void TestDefaultSkinToneValid()
{
- Assert.That(SkinColor.VerifyHumanSkinTone(SkinColor.ValidHumanSkinTone));
+ var strategy = new HumanTonedSkinColoration();
+
+ Assert.That(strategy.VerifySkinColor(strategy.ValidHumanSkinTone));
}
}
--- /dev/null
+- type: skinColoration
+ id: Hues
+ strategy: !type:ClampedHsvColoration
+ value: [0.175, 1]
+
+- type: skinColoration
+ id: TintedHues
+ strategy: !type:ClampedHslColoration
+ saturation: [0, 0.1]
+ lightness: [0.85, 1]
+
+- type: skinColoration
+ id: VoxFeathers
+ strategy: !type:ClampedHsvColoration
+ hue: [0.081, 0.48]
+ saturation: [0.2, 0.8]
+ value: [0.36, 0.55]
+
+- type: skinColoration
+ id: HumanToned
+ strategy: !type:HumanTonedSkinColoration {}