SkinColor.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. using System.Security.Cryptography;
  2. using Microsoft.VisualBasic.CompilerServices;
  3. namespace Content.Shared.Humanoid;
  4. public static class SkinColor
  5. {
  6. public const float MaxTintedHuesSaturation = 0.1f;
  7. public const float MinTintedHuesLightness = 0.85f;
  8. public const float MinHuesLightness = 0.175f;
  9. public const float MinFeathersHue = 29f / 360;
  10. public const float MaxFeathersHue = 174f / 360;
  11. public const float MinFeathersSaturation = 20f / 100;
  12. public const float MaxFeathersSaturation = 88f / 100;
  13. public const float MinFeathersValue = 36f / 100;
  14. public const float MaxFeathersValue = 55f / 100;
  15. public static Color ValidHumanSkinTone => Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f));
  16. /// <summary>
  17. /// Turn a color into a valid tinted hue skin tone.
  18. /// </summary>
  19. /// <param name="color">The color to validate</param>
  20. /// <returns>Validated tinted hue skin tone</returns>
  21. public static Color ValidTintedHuesSkinTone(Color color)
  22. {
  23. return TintedHues(color);
  24. }
  25. /// <summary>
  26. /// Get a human skin tone based on a scale of 0 to 100. The value is clamped between 0 and 100.
  27. /// </summary>
  28. /// <param name="tone">Skin tone. Valid range is 0 to 100, inclusive. 0 is gold/yellowish, 100 is dark brown.</param>
  29. /// <returns>A human skin tone.</returns>
  30. public static Color HumanSkinTone(int tone)
  31. {
  32. // 0 - 100, 0 being gold/yellowish and 100 being dark
  33. // HSV based
  34. //
  35. // 0 - 20 changes the hue
  36. // 20 - 100 changes the value
  37. // 0 is 45 - 20 - 100
  38. // 20 is 25 - 20 - 100
  39. // 100 is 25 - 100 - 20
  40. tone = Math.Clamp(tone, 0, 100);
  41. var rangeOffset = tone - 20;
  42. float hue = 25;
  43. float sat = 20;
  44. float val = 100;
  45. if (rangeOffset <= 0)
  46. {
  47. hue += Math.Abs(rangeOffset);
  48. }
  49. else
  50. {
  51. sat += rangeOffset;
  52. val -= rangeOffset;
  53. }
  54. var color = Color.FromHsv(new Vector4(hue / 360, sat / 100, val / 100, 1.0f));
  55. return color;
  56. }
  57. /// <summary>
  58. /// Gets a human skin tone from a given color.
  59. /// </summary>
  60. /// <param name="color"></param>
  61. /// <returns></returns>
  62. /// <remarks>
  63. /// Does not cause an exception if the color is not originally from the human color range.
  64. /// Instead, it will return the approximation of the skin tone value.
  65. /// </remarks>
  66. public static float HumanSkinToneFromColor(Color color)
  67. {
  68. var hsv = Color.ToHsv(color);
  69. // check for hue/value first, if hue is lower than this percentage
  70. // and value is 1.0
  71. // then it'll be hue
  72. if (Math.Clamp(hsv.X, 25f / 360f, 1) > 25f / 360f
  73. && hsv.Z == 1.0)
  74. {
  75. return Math.Abs(45 - (hsv.X * 360));
  76. }
  77. // otherwise it'll directly be the saturation
  78. else
  79. {
  80. return hsv.Y * 100;
  81. }
  82. }
  83. /// <summary>
  84. /// Verify if a color is in the human skin tone range.
  85. /// </summary>
  86. /// <param name="color">The color to verify</param>
  87. /// <returns>True if valid, false otherwise.</returns>
  88. public static bool VerifyHumanSkinTone(Color color)
  89. {
  90. var colorValues = Color.ToHsv(color);
  91. var hue = Math.Round(colorValues.X * 360f);
  92. var sat = Math.Round(colorValues.Y * 100f);
  93. var val = Math.Round(colorValues.Z * 100f);
  94. // rangeOffset makes it so that this value
  95. // is 25 <= hue <= 45
  96. if (hue < 25 || hue > 45)
  97. {
  98. return false;
  99. }
  100. // rangeOffset makes it so that these two values
  101. // are 20 <= sat <= 100 and 20 <= val <= 100
  102. // where saturation increases to 100 and value decreases to 20
  103. if (sat < 20 || val < 20)
  104. {
  105. return false;
  106. }
  107. return true;
  108. }
  109. /// <summary>
  110. /// Convert a color to the 'tinted hues' skin tone type.
  111. /// </summary>
  112. /// <param name="color">Color to convert</param>
  113. /// <returns>Tinted hue color</returns>
  114. public static Color TintedHues(Color color)
  115. {
  116. var newColor = Color.ToHsl(color);
  117. newColor.Y *= MaxTintedHuesSaturation;
  118. newColor.Z = MathHelper.Lerp(MinTintedHuesLightness, 1f, newColor.Z);
  119. return Color.FromHsv(newColor);
  120. }
  121. /// <summary>
  122. /// Verify if this color is a valid tinted hue color type, or not.
  123. /// </summary>
  124. /// <param name="color">The color to verify</param>
  125. /// <returns>True if valid, false otherwise</returns>
  126. public static bool VerifyTintedHues(Color color)
  127. {
  128. // tinted hues just ensures saturation is always .1, or 10% saturation at all times
  129. return Color.ToHsl(color).Y <= MaxTintedHuesSaturation && Color.ToHsl(color).Z >= MinTintedHuesLightness;
  130. }
  131. /// <summary>
  132. /// Converts a Color proportionally to the allowed vox color range.
  133. /// Will NOT preserve the specific input color even if it is within the allowed vox color range.
  134. /// </summary>
  135. /// <param name="color">Color to convert</param>
  136. /// <returns>Vox feather coloration</returns>
  137. public static Color ProportionalVoxColor(Color color)
  138. {
  139. var newColor = Color.ToHsv(color);
  140. newColor.X = newColor.X * (MaxFeathersHue - MinFeathersHue) + MinFeathersHue;
  141. newColor.Y = newColor.Y * (MaxFeathersSaturation - MinFeathersSaturation) + MinFeathersSaturation;
  142. newColor.Z = newColor.Z * (MaxFeathersValue - MinFeathersValue) + MinFeathersValue;
  143. return Color.FromHsv(newColor);
  144. }
  145. // /// <summary>
  146. // /// Ensures the input Color is within the allowed vox color range.
  147. // /// </summary>
  148. // /// <param name="color">Color to convert</param>
  149. // /// <returns>The same Color if it was within the allowed range, or the closest matching Color otherwise</returns>
  150. public static Color ClosestVoxColor(Color color)
  151. {
  152. var hsv = Color.ToHsv(color);
  153. hsv.X = Math.Clamp(hsv.X, MinFeathersHue, MaxFeathersHue);
  154. hsv.Y = Math.Clamp(hsv.Y, MinFeathersSaturation, MaxFeathersSaturation);
  155. hsv.Z = Math.Clamp(hsv.Z, MinFeathersValue, MaxFeathersValue);
  156. return Color.FromHsv(hsv);
  157. }
  158. /// <summary>
  159. /// Verify if this color is a valid vox feather coloration, or not.
  160. /// </summary>
  161. /// <param name="color">The color to verify</param>
  162. /// <returns>True if valid, false otherwise</returns>
  163. public static bool VerifyVoxFeathers(Color color)
  164. {
  165. var colorHsv = Color.ToHsv(color);
  166. if (colorHsv.X < MinFeathersHue || colorHsv.X > MaxFeathersHue)
  167. return false;
  168. if (colorHsv.Y < MinFeathersSaturation || colorHsv.Y > MaxFeathersSaturation)
  169. return false;
  170. if (colorHsv.Z < MinFeathersValue || colorHsv.Z > MaxFeathersValue)
  171. return false;
  172. return true;
  173. }
  174. /// <summary>
  175. /// This takes in a color, and returns a color guaranteed to be above MinHuesLightness
  176. /// </summary>
  177. /// <param name="color"></param>
  178. /// <returns>Either the color as-is if it's above MinHuesLightness, or the color with luminosity increased above MinHuesLightness</returns>
  179. public static Color MakeHueValid(Color color)
  180. {
  181. var manipulatedColor = Color.ToHsv(color);
  182. manipulatedColor.Z = Math.Max(manipulatedColor.Z, MinHuesLightness);
  183. return Color.FromHsv(manipulatedColor);
  184. }
  185. /// <summary>
  186. /// Verify if this color is above a minimum luminosity
  187. /// </summary>
  188. /// <param name="color"></param>
  189. /// <returns>True if valid, false if not</returns>
  190. public static bool VerifyHues(Color color)
  191. {
  192. return Color.ToHsv(color).Z >= MinHuesLightness;
  193. }
  194. public static bool VerifySkinColor(HumanoidSkinColor type, Color color)
  195. {
  196. return type switch
  197. {
  198. HumanoidSkinColor.HumanToned => VerifyHumanSkinTone(color),
  199. HumanoidSkinColor.TintedHues => VerifyTintedHues(color),
  200. HumanoidSkinColor.Hues => VerifyHues(color),
  201. HumanoidSkinColor.VoxFeathers => VerifyVoxFeathers(color),
  202. _ => false,
  203. };
  204. }
  205. public static Color ValidSkinTone(HumanoidSkinColor type, Color color)
  206. {
  207. return type switch
  208. {
  209. HumanoidSkinColor.HumanToned => ValidHumanSkinTone,
  210. HumanoidSkinColor.TintedHues => ValidTintedHuesSkinTone(color),
  211. HumanoidSkinColor.Hues => MakeHueValid(color),
  212. HumanoidSkinColor.VoxFeathers => ClosestVoxColor(color),
  213. _ => color
  214. };
  215. }
  216. }
  217. public enum HumanoidSkinColor : byte
  218. {
  219. HumanToned,
  220. Hues,
  221. VoxFeathers, // Vox feathers are limited to a specific color range
  222. TintedHues, //This gives a color tint to a humanoid's skin (10% saturation with full hue range).
  223. }