From 04f58c01dbf7b31f4d21b088b27ec50a094259df Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 15 Jul 2023 19:39:25 +0700 Subject: [PATCH] Add HSVColor, massively overhaul RGBAColor class --- .../ru/dbotthepony/mc/otm/core/math/Colors.kt | 388 ++++++++++++++++++ .../dbotthepony/mc/otm/core/math/RGBAColor.kt | 127 ------ 2 files changed, 388 insertions(+), 127 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Colors.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/core/math/RGBAColor.kt diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Colors.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Colors.kt new file mode 100644 index 000000000..98741982c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Colors.kt @@ -0,0 +1,388 @@ +package ru.dbotthepony.mc.otm.core.math + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import it.unimi.dsi.fastutil.chars.CharAVLTreeSet +import net.minecraft.ChatFormatting +import kotlin.math.roundToInt + +class HSVColor(hue: Float, saturation: Float, value: Float) : Comparable { + val hue = (hue % 360f).let { if (it < 0f) it + 360f else it } + val saturation = saturation.coerceIn(0f, 1f) + val value = value.coerceIn(0f, 1f) + + override fun equals(other: Any?): Boolean { + return other === this || other is HSVColor && other.hue == hue && other.saturation == saturation && other.value == value + } + + override fun hashCode(): Int { + return hue.hashCode() + saturation.hashCode() * 31 + value.hashCode() * 31 * 31 + } + + fun copy(hue: Float = this.hue, saturation: Float = this.saturation, value: Float = this.value): HSVColor { + return HSVColor(hue, saturation, value) + } + + operator fun component1() = hue + operator fun component2() = saturation + operator fun component3() = value + + fun toRGBA(alpha: Float = 1f): RGBAColor { + val valueMin = (1f - saturation) * value + val delta = (value - valueMin) * (hue % 60f) / 60f + val valueInc = valueMin + delta + val valueDec = value - delta + + return when ((hue / 60f).toInt()) { + 0 -> RGBAColor(value, valueInc, valueMin, alpha) + 1 -> RGBAColor(valueDec, value, valueMin, alpha) + 2 -> RGBAColor(valueMin, value, valueInc, alpha) + 3 -> RGBAColor(valueMin, valueDec, value, alpha) + 4 -> RGBAColor(valueInc, valueMin, value, alpha) + 5 -> RGBAColor(value, valueMin, valueDec, alpha) + else -> throw IllegalStateException("whut") + } + } + + override fun compareTo(other: HSVColor): Int { + return comparator.compare(this, other) + } + + companion object { + @JvmField val WHITE = HSVColor(0f, 1f, 1f) + + private val comparator = Comparator + .comparing(HSVColor::hue) + .thenComparing(HSVColor::saturation) + .thenComparing(HSVColor::value) + } +} + +private fun hex(value: Int): String { + require(value in 0 .. 255) + val v = value.toString(16) + + if (v.length == 1) + return "0$v" + else + return v +} + +class RGBAColor(red: Float, green: Float, blue: Float, alpha: Float = 1f) : Comparable { + constructor(r: Int, g: Int, b: Int) : this((r / 255f), (g / 255f), (b / 255f), 1f) + constructor(r: Int, g: Int, b: Int, a: Int) : this((r / 255f), (g / 255f), (b / 255f), (a / 255f)) + constructor(r: Int, g: Int, b: Int, a: Float) : this((r / 255f), (g / 255f), (b / 255f), a) + + val red = red.coerceIn(0f, 1f) + val green = green.coerceIn(0f, 1f) + val blue = blue.coerceIn(0f, 1f) + val alpha = alpha.coerceIn(0f, 1f) + + val redInt get() = (red * 255f).roundToInt() + val greenInt get() = (green * 255f).roundToInt() + val blueInt get() = (blue * 255f).roundToInt() + val alphaInt get() = (alpha * 255f).roundToInt() + + fun toRGBA(): Int { + return (redInt shl 24) or (greenInt shl 16) or (blueInt shl 8) or alphaInt + } + + fun toARGB(): Int { + return (alphaInt shl 24) or (redInt shl 16) or (greenInt shl 8) or blueInt + } + + fun toBGRA(): Int { + return (blueInt shl 24) or (greenInt shl 16) or (redInt shl 8) or alphaInt + } + + val isFullyTransparent get() = alpha <= 0f + val isWhite: Boolean get() = red >= 1f && green >= 1f && blue >= 1f && alpha >= 1f + + fun canRepresentHue(): Boolean { + val min = red.coerceAtMost(green).coerceAtMost(blue) + val max = red.coerceAtLeast(green).coerceAtLeast(blue) + return min != max + } + + fun hue(ifNoHue: Float = 0f): Float { + val min = red.coerceAtMost(green).coerceAtMost(blue) + val max = red.coerceAtLeast(green).coerceAtLeast(blue) + + if (min == max) { + return ifNoHue + } + + val diff = max - min + + return if (max == red && green >= blue) { + 60f * (green - blue) / diff + } else if (max == red) { + 60f * (green - blue) / diff + 360f + } else if (max == green) { + 60f * (blue - red) / diff + 120f + } else if (max == blue) { + 60f * (red - green) / diff + 240f + } else { + throw IllegalStateException("Whut $red $green $blue ($min / $max)") + } + } + + fun toHSV(): HSVColor { + val min = red.coerceAtMost(green).coerceAtMost(blue) + val max = red.coerceAtLeast(green).coerceAtLeast(blue) + + if (min == max) { + return HSVColor(0f, if (max == 0f) 0f else 1f - min / max, max) + } + + val diff = max - min + + val hue = if (max == red && green >= blue) { + 60f * (green - blue) / diff + } else if (max == red) { + 60f * (green - blue) / diff + 360f + } else if (max == green) { + 60f * (blue - red) / diff + 120f + } else if (max == blue) { + 60f * (red - green) / diff + 240f + } else { + throw IllegalStateException("Whut $red $green $blue ($min / $max)") + } + + return HSVColor(hue, 1f - min / max, max) + } + + fun toHexStringRGB(): String { + return "#${hex(redInt)}${hex(greenInt)}${hex(blueInt)}" + } + + fun toHexStringRGBA(): String { + return "#${hex(redInt)}${hex(greenInt)}${hex(blueInt)}${hex(alphaInt)}" + } + + fun toHexStringARGB(): String { + return "#${hex(alphaInt)}${hex(redInt)}${hex(greenInt)}${hex(blueInt)}" + } + + override fun toString(): String { + return "RGBAColor[$red $green $blue $alpha]" + } + + operator fun component1() = red + operator fun component2() = green + operator fun component3() = blue + operator fun component4() = alpha + + fun toIntInv(): Int { + return (blueInt shl 16) or (greenInt shl 8) or redInt + } + + fun toRGB(): Int { + return (redInt shl 16) or (greenInt shl 8) or blueInt + } + + fun copy(red: Float = this.red, green: Float = this.green, blue: Float = this.blue, alpha: Float = this.alpha): RGBAColor { + return RGBAColor(red, green, blue, alpha) + } + + override fun compareTo(other: RGBAColor): Int { + if (canRepresentHue() && other.canRepresentHue()) + return hue().compareTo(other.hue()).let { + if (it != 0) + it + else + toHSV().compareTo(other.toHSV()) + } + + return comparator.compare(this, other) + } + + override fun equals(other: Any?): Boolean { + return other === this || other is RGBAColor && comparator.compare(this, other) == 0 + } + + override fun hashCode(): Int { + return red.hashCode() + green.hashCode() * 31 + blue.hashCode() * 31 * 31 + alpha.hashCode() * 31 * 31 * 31 + } + + fun linearInterpolation(t: Float, other: RGBAColor, interpolateAlpha: Boolean = true): RGBAColor { + return RGBAColor( + linearInterpolation(t, red, other.red), + linearInterpolation(t, green, other.green), + linearInterpolation(t, blue, other.blue), + if (interpolateAlpha) linearInterpolation(t, alpha, other.alpha) else alpha, + ) + } + + operator fun times(other: RGBAColor): RGBAColor { + if (isWhite) + return other + else if (other.isWhite) + return this + + return RGBAColor(red * other.red, green * other.green, blue * other.blue, alpha * other.alpha) + } + + @Suppress("unused") + companion object { + private val comparator = Comparator + .comparing(RGBAColor::red) + .thenComparing(RGBAColor::green) + .thenComparing(RGBAColor::blue) + .thenComparing(RGBAColor::alpha) + + @JvmField val TRANSPARENT_BLACK = RGBAColor(0f, 0f, 0f, 0f) + @JvmField val TRANSPARENT_WHITE = RGBAColor(1f, 1f, 1f, 0f) + + @JvmField val BLACK = RGBAColor(0f, 0f, 0f) + @JvmField val WHITE = RGBAColor(1f, 1f, 1f) + @JvmField val RED = RGBAColor(1f, 0f, 0f) + @JvmField val GREEN = RGBAColor(0f, 1f, 0f) + @JvmField val LIGHT_GREEN = RGBAColor(136, 255, 124) + @JvmField val SLATE_GRAY = RGBAColor(64, 64, 64) + @JvmField val GRAY = rgb(0x2C2C2CL) + + @JvmField val DARK_BLUE = rgb(ChatFormatting.DARK_BLUE.color!!) + @JvmField val DARK_GREEN = rgb(ChatFormatting.DARK_GREEN.color!!) + @JvmField val DARK_AQUA = rgb(ChatFormatting.DARK_AQUA.color!!) + @JvmField val DARK_RED = rgb(ChatFormatting.DARK_RED.color!!) + @JvmField val DARK_PURPLE = rgb(ChatFormatting.DARK_PURPLE.color!!) + @JvmField val GOLD = rgb(ChatFormatting.GOLD.color!!) + @JvmField val DARK_GRAY = rgb(ChatFormatting.DARK_GRAY.color!!) + @JvmField val BLUE = rgb(ChatFormatting.BLUE.color!!) + @JvmField val AQUA = rgb(ChatFormatting.AQUA.color!!) + @JvmField val LIGHT_PURPLE = rgb(ChatFormatting.LIGHT_PURPLE.color!!) + @JvmField val YELLOW = rgb(ChatFormatting.YELLOW.color!!) + + @JvmField val LOW_POWER = RGBAColor(173, 41, 41) + @JvmField val FULL_POWER = RGBAColor(255, 242, 40) + @JvmField val LOW_MATTER = RGBAColor(0, 24, 148) + @JvmField val FULL_MATTER = RGBAColor(72, 90, 255) + @JvmField val LOW_PATTERNS = RGBAColor(44, 104, 57) + @JvmField val FULL_PATTERNS = RGBAColor(65, 255, 87) + + @JvmField val HALF_TRANSPARENT = RGBAColor(1f, 1f, 1f, 0.5f) + @JvmField val REDDISH = RGBAColor(1f, 0.4f, 0.4f) + + @JvmField + val CODECRGBA: Codec = RecordCodecBuilder.create { + it.group( + Codec.floatRange(0f, 1f).fieldOf("red").forGetter(RGBAColor::red), + Codec.floatRange(0f, 1f).fieldOf("green").forGetter(RGBAColor::green), + Codec.floatRange(0f, 1f).fieldOf("blue").forGetter(RGBAColor::blue), + Codec.floatRange(0f, 1f).optionalFieldOf("alpha", 1f).forGetter(RGBAColor::alpha), + ).apply(it, ::RGBAColor) + } + + @JvmField + val CODECRGB: Codec = RecordCodecBuilder.create { + it.group( + Codec.floatRange(0f, 1f).fieldOf("red").forGetter(RGBAColor::red), + Codec.floatRange(0f, 1f).fieldOf("green").forGetter(RGBAColor::green), + Codec.floatRange(0f, 1f).fieldOf("blue").forGetter(RGBAColor::blue), + ).apply(it, ::RGBAColor) + } + + fun rgb(color: Int): RGBAColor { + val r = (color and 0xFF0000 ushr 16) / 255f + val g = (color and 0xFF00 ushr 8) / 255f + val b = (color and 0xFF) / 255f + return RGBAColor(r, g, b) + } + + fun rgb(color: Long): RGBAColor { + val r = (color and 0xFF0000 ushr 16) / 255f + val g = (color and 0xFF00 ushr 8) / 255f + val b = (color and 0xFF) / 255f + return RGBAColor(r, g, b) + } + + fun bgr(color: Int): RGBAColor { + val r = (color and 0xFF0000 ushr 16) / 255f + val g = (color and 0xFF00 ushr 8) / 255f + val b = (color and 0xFF) / 255f + return RGBAColor(b, g, r) + } + + fun bgr(color: Long): RGBAColor { + val r = (color and 0xFF0000 ushr 16) / 255f + val g = (color and 0xFF00 ushr 8) / 255f + val b = (color and 0xFF) / 255f + return RGBAColor(b, g, r) + } + + fun abgr(color: Int): RGBAColor { + val r = (color and -0x1000000 ushr 24) / 255f + val g = (color and 0xFF0000 ushr 16) / 255f + val b = (color and 0xFF00 ushr 8) / 255f + val a = (color and 0xFF) / 255f + return RGBAColor(a, b, g, r) + } + + fun argb(color: Int): RGBAColor { + val a = (color and -0x1000000 ushr 24) / 255f + val r = (color and 0xFF0000 ushr 16) / 255f + val g = (color and 0xFF00 ushr 8) / 255f + val b = (color and 0xFF) / 255f + return RGBAColor(r, g, b, a) + } + + private val hexChars = CharAVLTreeSet() + + init { + "#0123456789abcdefABCDEF".forEach { hexChars.add(it) } + } + + fun isHexCharacter(char: Char): Boolean { + return char in hexChars + } + + private val shorthandRGBHex = Regex("#?([0-9abcdef])([0-9abcdef])([0-9abcdef])", RegexOption.IGNORE_CASE) + private val longhandRGBHex = Regex("#?([0-9abcdef]{2})([0-9abcdef]{2})([0-9abcdef]{2})", RegexOption.IGNORE_CASE) + + private val shorthandRGBAHex = Regex("#?([0-9abcdef])([0-9abcdef])([0-9abcdef])([0-9abcdef])", RegexOption.IGNORE_CASE) + private val longhandRGBAHex = Regex("#?([0-9abcdef]{2})([0-9abcdef]{2})([0-9abcdef]{2})([0-9abcdef]{2})", RegexOption.IGNORE_CASE) + + fun fromHexStringRGB(value: String): RGBAColor? { + if (value.length == 3 || value.length == 4) { + val match = shorthandRGBHex.find(value) ?: return null + val red = match.groupValues[1].toIntOrNull(16) ?: return null + val green = match.groupValues[2].toIntOrNull(16) ?: return null + val blue = match.groupValues[3].toIntOrNull(16) ?: return null + return RGBAColor(red * 16, green * 16, blue * 16) + } else if (value.length == 6 || value.length == 7) { + val match = longhandRGBHex.find(value) ?: return null + val red = match.groupValues[1].toIntOrNull(16) ?: return null + val green = match.groupValues[2].toIntOrNull(16) ?: return null + val blue = match.groupValues[3].toIntOrNull(16) ?: return null + return RGBAColor(red, green, blue) + } else { + return null + } + } + + fun fromHexStringRGBA(value: String): RGBAColor? { + if (value.length == 4 || value.length == 5) { + val match = shorthandRGBAHex.find(value) ?: return null + val red = match.groupValues[1].toIntOrNull(16) ?: return null + val green = match.groupValues[2].toIntOrNull(16) ?: return null + val blue = match.groupValues[3].toIntOrNull(16) ?: return null + val alpha = match.groupValues[4].toIntOrNull(16) ?: return null + return RGBAColor(red * 16, green * 16, blue * 16, alpha * 16) + } else if (value.length == 8 || value.length == 9) { + val match = longhandRGBAHex.find(value) ?: return null + val red = match.groupValues[1].toIntOrNull(16) ?: return null + val green = match.groupValues[2].toIntOrNull(16) ?: return null + val blue = match.groupValues[3].toIntOrNull(16) ?: return null + val alpha = match.groupValues[4].toIntOrNull(16) ?: return null + return RGBAColor(red, green, blue, alpha) + } else { + return null + } + } + } +} + +fun linearInterpolation(t: Float, a: RGBAColor, b: RGBAColor): RGBAColor { + return a.linearInterpolation(t, b) +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/RGBAColor.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/RGBAColor.kt deleted file mode 100644 index 01eb2ba9c..000000000 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/RGBAColor.kt +++ /dev/null @@ -1,127 +0,0 @@ -package ru.dbotthepony.mc.otm.core.math - -import com.mojang.blaze3d.systems.RenderSystem -import net.minecraft.ChatFormatting -import kotlin.math.roundToInt - -data class RGBAColor(val red: Float, val green: Float, val blue: Float, val alpha: Float = 1f) { - constructor(r: Int, g: Int, b: Int) : this((r / 255f), (g / 255f), (b / 255f), 1f) - constructor(r: Int, g: Int, b: Int, a: Int) : this((r / 255f), (g / 255f), (b / 255f), (a / 255f)) - constructor(r: Int, g: Int, b: Int, a: Float) : this((r / 255f), (g / 255f), (b / 255f), a) - - constructor(color: Long) : this( - (((color and -0x1000000) ushr 24) / 255f), - (((color and 0xFF0000) ushr 16) / 255f), - (((color and 0xFF00) ushr 8) / 255f), - (((color and 0xFF)) / 255f) - ) - - val isWhite = red >= 1f && green >= 1f && blue >= 1f && alpha >= 1f - - val redInt = (red.coerceIn(0f, 1f) * 255f).roundToInt() - val greenInt = (green.coerceIn(0f, 1f) * 255f).roundToInt() - val blueInt = (blue.coerceIn(0f, 1f) * 255f).roundToInt() - val alphaInt = (alpha.coerceIn(0f, 1f) * 255f).roundToInt() - - fun toRGB(): Int { - return (redInt shl 16) or (greenInt shl 8) or blueInt - } - - fun toRGBA(): Int { - return (redInt shl 24) or (greenInt shl 16) or (blueInt shl 8) or alphaInt - } - - fun toARGB(): Int { - return (alphaInt shl 24) or (redInt shl 16) or (greenInt shl 8) or blueInt - } - - fun toBGRA(): Int { - return (blueInt shl 24) or (greenInt shl 16) or (redInt shl 8) or alphaInt - } - - fun toIntInv(): Int { - return (blueInt shl 16) or (greenInt shl 8) or redInt - } - - fun linearInterpolation(t: Float, other: RGBAColor, interpolateAlpha: Boolean = true): RGBAColor { - return RGBAColor( - linearInterpolation(t, red, other.red), - linearInterpolation(t, green, other.green), - linearInterpolation(t, blue, other.blue), - if (interpolateAlpha) linearInterpolation(t, alpha, other.alpha) else alpha, - ) - } - - operator fun times(other: RGBAColor): RGBAColor { - if (isWhite) - return other - else if (other.isWhite) - return this - - return RGBAColor(red * other.red, green * other.green, blue * other.blue, alpha * other.alpha) - } - - val isFullyTransparent get() = alpha <= 0f - - companion object { - @JvmField val TRANSPARENT_BLACK = RGBAColor(0f, 0f, 0f, 0f) - @JvmField val TRANSPARENT_WHITE = RGBAColor(1f, 1f, 1f, 0f) - - @JvmField val BLACK = RGBAColor(0f, 0f, 0f, 1f) - @JvmField val WHITE = RGBAColor(1f, 1f, 1f, 1f) - @JvmField val RED = RGBAColor(1f, 0f, 0f) - @JvmField val GREEN = RGBAColor(0f, 1f, 0f, 1f) - @JvmField val LIGHT_GREEN = RGBAColor(136, 255, 124) - @JvmField val SLATE_GRAY = RGBAColor(64, 64, 64) - @JvmField val GRAY = RGBAColor(0x2C2C2CFFL) - - @JvmField val DARK_BLUE = rgb(ChatFormatting.DARK_BLUE.color!!) - @JvmField val DARK_GREEN = rgb(ChatFormatting.DARK_GREEN.color!!) - @JvmField val DARK_AQUA = rgb(ChatFormatting.DARK_AQUA.color!!) - @JvmField val DARK_RED = rgb(ChatFormatting.DARK_RED.color!!) - @JvmField val DARK_PURPLE = rgb(ChatFormatting.DARK_PURPLE.color!!) - @JvmField val GOLD = rgb(ChatFormatting.GOLD.color!!) - @JvmField val DARK_GRAY = rgb(ChatFormatting.DARK_GRAY.color!!) - @JvmField val BLUE = rgb(ChatFormatting.BLUE.color!!) - @JvmField val AQUA = rgb(ChatFormatting.AQUA.color!!) - @JvmField val LIGHT_PURPLE = rgb(ChatFormatting.LIGHT_PURPLE.color!!) - @JvmField val YELLOW = rgb(ChatFormatting.YELLOW.color!!) - - @JvmField val LOW_POWER = RGBAColor(173, 41, 41) - @JvmField val FULL_POWER = RGBAColor(255, 242, 40) - @JvmField val LOW_MATTER = RGBAColor(0, 24, 148) - @JvmField val FULL_MATTER = RGBAColor(72, 90, 255) - @JvmField val LOW_PATTERNS = RGBAColor(44, 104, 57) - @JvmField val FULL_PATTERNS = RGBAColor(65, 255, 87) - - @JvmField val HALF_TRANSPARENT = RGBAColor(1f, 1f, 1f, 0.5f) - @JvmField val REDDISH = RGBAColor(1f, 0.4f, 0.4f) - - fun inv(color: Int): RGBAColor { - val r = (color and -0x1000000 ushr 24) / 255f - val g = (color and 0xFF0000 ushr 16) / 255f - val b = (color and 0xFF00 ushr 8) / 255f - val a = (color and 0xFF) / 255f - return RGBAColor(a, b, g, r) - } - - fun rgb(color: Int): RGBAColor { - val r = (color and 0xFF0000 ushr 16) / 255f - val g = (color and 0xFF00 ushr 8) / 255f - val b = (color and 0xFF) / 255f - return RGBAColor(r, g, b) - } - - fun argb(color: Int): RGBAColor { - val a = (color and -0x1000000 ushr 24) / 255f - val r = (color and 0xFF0000 ushr 16) / 255f - val g = (color and 0xFF00 ushr 8) / 255f - val b = (color and 0xFF) / 255f - return RGBAColor(r, g, b, a) - } - } -} - -fun linearInterpolation(t: Float, a: RGBAColor, b: RGBAColor): RGBAColor { - return a.linearInterpolation(t, b) -}