diff --git a/CHANGELOG.md b/CHANGELOG.md index 2136dc234e2..93a9d687b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 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..ba6963d2308 --- /dev/null +++ b/colors/common/build.gradle @@ -0,0 +1,40 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" + +kotlin { + sourceSets { + jvmMain { + dependencies { + api project(":micro_utils.coroutines") + } + } + androidMain { + dependencies { + api project(":micro_utils.coroutines") + api libs.android.fragment + } + dependsOn jvmMain + } + + linuxX64Main { + dependencies { + api libs.okio + } + } + mingwX64Main { + dependencies { + api libs.okio + } + } + linuxArm64Main { + dependencies { + api libs.okio + } + } + } +} diff --git a/colors/common/src/commonMain/kotlin/HEXAColor.kt b/colors/common/src/commonMain/kotlin/HEXAColor.kt new file mode 100644 index 00000000000..e145e2f16b2 --- /dev/null +++ b/colors/common/src/commonMain/kotlin/HEXAColor.kt @@ -0,0 +1,138 @@ +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 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 alphaOfOne: Float + get() = (uint and 0xffu).toFloat() / 256f + + 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, g, b, aOfOne) + fun copy( + r: Int = this.r, + g: Int = this.g, + b: Int = this.b, + a: Int + ) = HEXAColor(r, g, b, 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(",") + .map { it.toInt().toString(16) } + .joinToString("", postfix = "ff") + color.startsWith("rgba(") -> color + .removePrefix("rgba(") + .removeSuffix(")") + .replace(Regex("\\s"), "") + .split(",").let { + it.take(3).map { it.toInt().toString(16) } + (it.last().toFloat() * 0xff).toInt().toString(16) + } + .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..3c3cf668c9c --- /dev/null +++ b/colors/common/src/commonTest/kotlin/HexColorTests.kt @@ -0,0 +1,150 @@ +package dev.inmo.micro_utils.colors.common + +import kotlin.math.floor +import kotlin.math.round +import kotlin.test.Test +import kotlin.test.assertEquals + +class HexColorTests { + class TestColor( + val color: HEXAColor, + val shortHex: String?, + val shortHexa: String?, + val hex: String, + val hexa: String, + val r: Int, + val g: Int, + val b: Int, + val a: Int, + ) + val testColors: List + get() = listOf( + TestColor( + color = HEXAColor(uint = 0xff0000ffu), + shortHex = "#f00", + shortHexa = "#f00f", + hex = "#ff0000", + hexa = "#ff0000ff", + r = 0xff, + g = 0x00, + b = 0x00, + a = 0xff, + ), + TestColor( + color = HEXAColor(uint = 0x00ff00ffu), + shortHex = "#0f0", + shortHexa = "#0f0f", + hex = "#00ff00", + hexa = "#00ff00ff", + r = 0x00, + g = 0xff, + b = 0x00, + a = 0xff, + ), + TestColor( + color = HEXAColor(0x0000ffffu), + shortHex = "#00f", + shortHexa = "#00ff", + hex = "#0000ff", + hexa = "#0000ffff", + r = 0x00, + g = 0x00, + b = 0xff, + a = 0xff, + ), + TestColor( + color = HEXAColor(0xff000088u), + shortHex = "#f00", + shortHexa = "#f008", + hex = "#ff0000", + hexa = "#ff000088", + r = 0xff, + g = 0x00, + b = 0x00, + a = 0x88, + ), + TestColor( + color = HEXAColor(0x00ff0088u), + shortHex = "#0f0", + shortHexa = "#0f08", + hex = "#00ff00", + hexa = "#00ff0088", + r = 0x00, + g = 0xff, + b = 0x00, + a = 0x88, + ), + TestColor( + color = HEXAColor(0x0000ff88u), + shortHex = "#00f", + shortHexa = "#00f8", + hex = "#0000ff", + hexa = "#0000ff88", + r = 0x00, + g = 0x00, + b = 0xff, + a = 0x88, + ), + TestColor( + color = HEXAColor(0xff000022u), + shortHex = "#f00", + shortHexa = "#f002", + hex = "#ff0000", + hexa = "#ff000022", + r = 0xff, + g = 0x00, + b = 0x00, + a = 0x22, + ), + TestColor( + HEXAColor(0x00ff0022u), + "#0f0", + "#0f02", + "#00ff00", + "#00ff0022", + 0x00, + 0xff, + 0x00, + 0x22, + ), + TestColor( + color = HEXAColor(0x0000ff22u), + shortHex = "#00f", + shortHexa = "#00f2", + hex = "#0000ff", + hexa = "#0000ff22", + 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.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, HEXAColor.parseStringColor(it.hexa)) + assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.hex)) + it.shortHex ?.let { _ -> + assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.shortHex)) + } + it.shortHexa ?.let { _ -> + 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/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",