diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d200d8902..93a9d687b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.20.25 + +* `Colors`: + * `Common`: + * Module inited + ## 0.20.24 **Since this version depdendencies of klock and krypto replaced with `com.soywiz.korge:korlibs-time` and `com.soywiz.korge:korlibs-crypto`** diff --git a/colors/common/build.gradle b/colors/common/build.gradle new file mode 100644 index 00000000000..d425197852e --- /dev/null +++ b/colors/common/build.gradle @@ -0,0 +1,7 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" diff --git a/colors/common/src/commonMain/kotlin/HEXAColor.kt b/colors/common/src/commonMain/kotlin/HEXAColor.kt new file mode 100644 index 00000000000..f40e547121f --- /dev/null +++ b/colors/common/src/commonMain/kotlin/HEXAColor.kt @@ -0,0 +1,141 @@ +package dev.inmo.micro_utils.colors.common + +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline +import kotlin.math.floor + +/** + * Wrapper for RGBA colors. Receiving [UInt] in main constructor. Each part in main constructor + * configured with `0x00 - 0xff` range. Examples: + * + * * Red: `0xff0000ffu` + * * Red (0.5 capacity): `0xff000088u` + * + * Anyway it is recommended to use + */ +@Serializable +@JvmInline +value class HEXAColor ( + val uint: UInt +) : Comparable { + val hexa: String + get() = "#${uint.toString(16).padStart(8, '0')}" + val hex: String + get() = hexa.take(7) + val rgba: String + get() = "rgba($r,$g,$b,${aOfOne.toString().take(5)})" + val rgb: String + get() = "rgb($r,$g,$b)" + val shortHex: String + get() = "#${r.shortPart()}${g.shortPart()}${b.shortPart()}" + val shortHexa: String + get() = "$shortHex${a.shortPart()}" + val rgbInt: Int + get() = (uint shr 2).toInt() + + val r: Int + get() = ((uint and 0xff000000u) / 0x1000000u).toInt() + val g: Int + get() = ((uint and 0x00ff0000u) / 0x10000u).toInt() + val b: Int + get() = ((uint and 0x0000ff00u) / 0x100u).toInt() + val a: Int + get() = ((uint and 0x000000ffu)).toInt() + val aOfOne: Float + get() = a.toFloat() / (0xff) + init { + require(uint in 0u ..0xffffffffu) + } + + constructor(r: Int, g: Int, b: Int, a: Int) : this( + ((r * 0x1000000).toLong() + g * 0x10000 + b * 0x100 + a).toUInt() + ) { + require(r in 0 ..0xff) + require(g in 0 ..0xff) + require(b in 0 ..0xff) + require(a in 0 ..0xff) + } + + constructor(r: Int, g: Int, b: Int, aOfOne: Float = 1f) : this( + r = r, g = g, b = b, a = (aOfOne * 0xff).toInt() + ) + + override fun toString(): String { + return hexa + } + + override fun compareTo(other: HEXAColor): Int = (uint - other.uint).coerceIn(Int.MIN_VALUE.toUInt(), Int.MAX_VALUE.toLong().toUInt()).toInt() + + fun copy( + r: Int = this.r, + g: Int = this.g, + b: Int = this.b, + aOfOne: Float = this.aOfOne + ) = HEXAColor(r = r, g = g, b = b, aOfOne = aOfOne) + fun copy( + r: Int = this.r, + g: Int = this.g, + b: Int = this.b, + a: Int + ) = HEXAColor(r = r, g = g, b = b, a = a) + + companion object { + /** + * Parsing color from [color] + * + * Supported formats samples (on Red color based): + * + * * `#f00` + * * `#f00f` + * * `#ff0000` + * * `#ff0000ff` + * * `rgb(255, 0, 0)` + * * `rgba(255, 0, 0, 1)` + */ + fun parseStringColor(color: String): HEXAColor = when { + color.startsWith("#") -> color.removePrefix("#").let { color -> + when (color.length) { + 3 -> color.map { "$it$it" }.joinToString(separator = "", postfix = "ff") + 4 -> color.take(3).map { "$it$it" }.joinToString(separator = "", postfix = color.takeLast(1).let { "${it}0" }) + 6 -> "${color}ff" + 8 -> color + else -> error("Malfurmed color string: $color. It is expected that color started with # will contains 3, 6 or 8 valuable parts") + } + } + color.startsWith("rgb(") -> color + .removePrefix("rgb(") + .removeSuffix(")") + .replace(Regex("\\s"), "") + .split(",") + .joinToString("", postfix = "ff") { + it.toInt().toString(16).padStart(2, '0') + } + color.startsWith("rgba(") -> color + .removePrefix("rgba(") + .removeSuffix(")") + .replace(Regex("\\s"), "") + .split(",").let { + it.take(3).map { it.toInt().toString(16).padStart(2, '0') } + (it.last().toFloat() * 0xff).toInt().toString(16).padStart(2, '0') + } + .joinToString("") + else -> color + }.lowercase().toUInt(16).let(::HEXAColor) + + /** + * Parsing color from [color] + * + * Supported formats samples (on Red color based): + * + * * `#f00` + * * `#ff0000` + * * `#ff0000ff` + * * `rgb(255, 0, 0)` + * * `rgba(255, 0, 0, 1)` + */ + operator fun invoke(color: String) = parseStringColor(color) + + private fun Int.shortPart(): String { + return (floor(toFloat() / 16)).toInt().toString(16) + } + } +} diff --git a/colors/common/src/commonTest/kotlin/HexColorTests.kt b/colors/common/src/commonTest/kotlin/HexColorTests.kt new file mode 100644 index 00000000000..8d710c82a53 --- /dev/null +++ b/colors/common/src/commonTest/kotlin/HexColorTests.kt @@ -0,0 +1,175 @@ +package dev.inmo.micro_utils.colors.common + +import kotlin.math.floor +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class HexColorTests { + val alphaRgbaPrecision = 5 + class TestColor( + val color: HEXAColor, + val shortHex: String, + val shortHexa: String, + val hex: String, + val hexa: String, + val rgb: String, + val rgba: String, + val r: Int, + val g: Int, + val b: Int, + val a: Int, + vararg val additionalRGBAVariants: String + ) + val testColors: List + get() = listOf( + TestColor( + color = HEXAColor(uint = 0xff0000ffu), + shortHex = "#f00", + shortHexa = "#f00f", + hex = "#ff0000", + hexa = "#ff0000ff", + rgb = "rgb(255,0,0)", + rgba = "rgba(255,0,0,1.0)", + r = 0xff, + g = 0x00, + b = 0x00, + a = 0xff, + "rgba(255,0,0,1)", + ), + TestColor( + color = HEXAColor(uint = 0x00ff00ffu), + shortHex = "#0f0", + shortHexa = "#0f0f", + hex = "#00ff00", + hexa = "#00ff00ff", + rgb = "rgb(0,255,0)", + rgba = "rgba(0,255,0,1.0)", + r = 0x00, + g = 0xff, + b = 0x00, + a = 0xff, + "rgba(0,255,0,1)" + ), + TestColor( + color = HEXAColor(0x0000ffffu), + shortHex = "#00f", + shortHexa = "#00ff", + hex = "#0000ff", + hexa = "#0000ffff", + rgb = "rgb(0,0,255)", + rgba = "rgba(0,0,255,1.0)", + r = 0x00, + g = 0x00, + b = 0xff, + a = 0xff, + "rgba(0,0,255,1)" + ), + TestColor( + color = HEXAColor(0xff000088u), + shortHex = "#f00", + shortHexa = "#f008", + hex = "#ff0000", + hexa = "#ff000088", + rgb = "rgb(255,0,0)", + rgba = "rgba(255,0,0,0.533)", + r = 0xff, + g = 0x00, + b = 0x00, + a = 0x88, + ), + TestColor( + color = HEXAColor(0x00ff0088u), + shortHex = "#0f0", + shortHexa = "#0f08", + hex = "#00ff00", + hexa = "#00ff0088", + rgb = "rgb(0,255,0)", + rgba = "rgba(0,255,0,0.533)", + r = 0x00, + g = 0xff, + b = 0x00, + a = 0x88, + ), + TestColor( + color = HEXAColor(0x0000ff88u), + shortHex = "#00f", + shortHexa = "#00f8", + hex = "#0000ff", + hexa = "#0000ff88", + rgb = "rgb(0,0,255)", + rgba = "rgba(0,0,255,0.533)", + r = 0x00, + g = 0x00, + b = 0xff, + a = 0x88, + ), + TestColor( + color = HEXAColor(0xff000022u), + shortHex = "#f00", + shortHexa = "#f002", + hex = "#ff0000", + hexa = "#ff000022", + rgb = "rgb(255,0,0)", + rgba = "rgba(255,0,0,0.133)", + r = 0xff, + g = 0x00, + b = 0x00, + a = 0x22, + ), + TestColor( + color = HEXAColor(0x00ff0022u), + shortHex = "#0f0", + shortHexa = "#0f02", + hex = "#00ff00", + hexa = "#00ff0022", + rgb = "rgb(0,255,0)", + rgba = "rgba(0,255,0,0.133)", + r = 0x00, + g = 0xff, + b = 0x00, + a = 0x22, + ), + TestColor( + color = HEXAColor(0x0000ff22u), + shortHex = "#00f", + shortHexa = "#00f2", + hex = "#0000ff", + hexa = "#0000ff22", + rgb = "rgb(0,0,255)", + rgba = "rgba(0,0,255,0.133)", + r = 0x00, + g = 0x00, + b = 0xff, + a = 0x22, + ), + ) + + @Test + fun baseTest() { + testColors.forEach { + assertEquals(it.hex, it.color.hex) + assertEquals(it.hexa, it.color.hexa) + assertEquals(it.shortHex, it.color.shortHex) + assertEquals(it.shortHexa, it.color.shortHexa) + assertEquals(it.rgb, it.color.rgb) + assertTrue(it.rgba == it.color.rgba || it.color.rgba in it.additionalRGBAVariants) + assertEquals(it.r, it.color.r) + assertEquals(it.g, it.color.g) + assertEquals(it.b, it.color.b) + assertEquals(it.a, it.color.a) + } + } + + @Test + fun testHexParseColor() { + testColors.forEach { + assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.hex)) + assertEquals(it.color, HEXAColor.parseStringColor(it.hexa)) + assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.rgb)) + assertTrue(it.color.uint.toInt() - HEXAColor.parseStringColor(it.rgba).uint.toInt() in -0x1 .. 0x1, ) + assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.shortHex)) + assertEquals(it.color.copy(a = floor(it.color.a.toFloat() / 16).toInt() * 0x10), HEXAColor.parseStringColor(it.shortHexa)) + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index d93d99cf483..8dc3c326a3a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,5 +15,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.20.24 -android_code_version=230 +version=0.20.25 +android_code_version=231 diff --git a/settings.gradle b/settings.gradle index d9d6508c329..79702a0bc1e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -42,6 +42,7 @@ String[] includes = [ ":serialization:mapper", ":startup:plugin", ":startup:launcher", + ":colors:common", ":resources",