From 5b1941eceb38ce64612ee8656450efa8a3db4494 Mon Sep 17 00:00:00 2001 From: NoMathExpectation <85624722+NoMathExpectation@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:34:48 +0800 Subject: [PATCH] Added ColorOrPalette. --- .../rdlevel/rdkt/core/data/ColorOrPalette.kt | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/ColorOrPalette.kt diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/ColorOrPalette.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/ColorOrPalette.kt new file mode 100644 index 0000000..2fe7517 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/ColorOrPalette.kt @@ -0,0 +1,293 @@ +@file:OptIn(RDKTInternalAPI::class) +@file:JvmName("ColorOrPalettes") + +package cn.rdlevel.rdkt.core.data + +import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI +import cn.rdlevel.rdkt.core.serialization.PolymorphicDelegatedSerializer +import cn.rdlevel.rdkt.core.serialization.TransformSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlin.jvm.JvmName +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +/** + * A color that can be either a specific RGB color or a palette index. + */ +@Serializable(ColorOrPalette.Serializer::class) +public sealed interface ColorOrPalette { + public object Serializer : TransformSerializer("ColorOrPalette", String.serializer()) { + override fun toData(value: ColorOrPalette): String = when (value) { + is Palette -> "pal${value.index}" + is Color -> colorToHex(value.red, value.green, value.blue) + } + + override fun fromData(data: String): ColorOrPalette { + return if (data.startsWith("pal")) { + val index = data.substring(3).toIntOrNull() + ?: throw IllegalArgumentException("Invalid palette format: $data. Expected format is 'pal{index}'.") + Palette.of(index) + } else { + Color.of(data) + } + } + } +} + +/** + * A color that can be either a specific RGBA color or a palette index. + */ +@Serializable(ColorWithAlphaOrPalette.Serializer::class) +public sealed interface ColorWithAlphaOrPalette : ColorOrPalette { + public object Serializer : + TransformSerializer("ColorWithAlphaOrPalette", String.serializer()) { + override fun toData(value: ColorWithAlphaOrPalette): String = when (value) { + is Palette -> "pal${value.index}" + is ColorWithAlpha -> colorToHex(value.red, value.green, value.blue, value.alpha) + } + + override fun fromData(data: String): ColorWithAlphaOrPalette { + return if (data.startsWith("pal")) { + val index = data.substring(3).toIntOrNull() + ?: throw IllegalArgumentException("Invalid palette format: $data. Expected format is 'pal{index}'.") + Palette.of(index) + } else { + ColorWithAlpha.of(data) + } + } + } +} + +/** + * A color that is a palette index. + * + * @property index The index of the palette color, which must be between 0 and 20 inclusive. + */ +@Serializable(Palette.Serializer::class) +public sealed interface Palette : ColorWithAlphaOrPalette { + public val index: Int + + public object Serializer : PolymorphicDelegatedSerializer(ColorOrPalette.serializer()) + + public companion object { + /** + * Creates a [Palette] instance with the specified index. + */ + @JvmStatic + public fun of(index: Int): Palette = PaletteImpl(index) + } +} + +private fun Palette.checkIndex() { + require(index in 0..20) { "Palette index must be between 0 and 20, but was $index." } +} + +private data class PaletteImpl(override val index: Int) : Palette { + init { + checkIndex() + } + + override fun toString(): String = "pal$index" +} + +/** + * A color that is a specific RGB color. + * + * @property red The red component of the color, which must be between 0 and 255 inclusive. + * @property green The green component of the color, which must be between 0 and 255 inclusive. + * @property blue The blue component of the color, which must be between 0 and 255 inclusive. + */ +@Serializable(Color.Serializer::class) +public sealed interface Color : ColorOrPalette { + public val red: Int + public val green: Int + public val blue: Int + + /** + * Converts the color to a hexadecimal string representation. + * Depending on the implementation, this may return a 6-character hex string (for RGB) or an 8-character hex string (for RGBA). + */ + public fun toHex(): String + + public object Serializer : PolymorphicDelegatedSerializer(ColorOrPalette.serializer()) + + public companion object { + /** + * Creates a [Color] instance with the specified RGB components. + */ + @JvmStatic + public fun of(r: Int, g: Int, b: Int): Color = ColorImpl(r, g, b) + + /** + * Creates a [Color] instance from a hexadecimal string representation. The hex string must be 6 characters long, representing the RGB components in the format RRGGBB. + */ + @JvmStatic + public fun of(hex: String): Color { + require(hex.length == 6) { "Hex color must be 6 characters long, but was ${hex.length}." } + val r = hex.substring(0, 2).toInt(16) + val g = hex.substring(2, 4).toInt(16) + val b = hex.substring(4, 6).toInt(16) + return of(r, g, b) + } + } +} + +private fun Color.checkComponents() { + require(red in 0..255) { "Red component must be between 0 and 255, but was $red." } + require(green in 0..255) { "Green component must be between 0 and 255, but was $green." } + require(blue in 0..255) { "Blue component must be between 0 and 255, but was $blue." } +} + +/** + * A color that is a specific RGBA color. + * + * @property alpha The alpha component of the color, which must be between 0 and 255 inclusive. + */ +@Serializable(ColorWithAlpha.Serializer::class) +public sealed interface ColorWithAlpha : Color, ColorWithAlphaOrPalette { + public val alpha: Int + + public override fun toHex(): String + + public object Serializer : + PolymorphicDelegatedSerializer(ColorWithAlphaOrPalette.serializer()) + + public companion object { + /** + * Creates a [ColorWithAlpha] instance with the specified RGBA components. The alpha component is optional and defaults to 255 (fully opaque) if not provided. + */ + @JvmStatic + @JvmOverloads + public fun of(r: Int, g: Int, b: Int, a: Int = 255): ColorWithAlpha = ColorImpl(r, g, b, a, true) + + /** + * Creates a [ColorWithAlpha] instance from a hexadecimal string representation. The hex string must be 8 characters long, representing the RGBA components in the format RRGGBBAA. + */ + @JvmStatic + public fun of(hex: String): ColorWithAlpha { + require(hex.length == 8) { "Hex color with alpha must be 8 characters long, but was ${hex.length}." } + val r = hex.substring(0, 2).toInt(16) + val g = hex.substring(2, 4).toInt(16) + val b = hex.substring(4, 6).toInt(16) + val a = hex.substring(6, 8).toInt(16) + return of(r, g, b, a) + } + } +} + +private fun ColorWithAlpha.checkComponentsWithAlpha() { + checkComponents() + require(alpha in 0..255) { "Alpha component must be between 0 and 255, but was $alpha." } +} + +private data class ColorImpl( + override val red: Int, + override val green: Int, + override val blue: Int, + override val alpha: Int = 255, + private val withAlpha: Boolean = false, +) : ColorWithAlpha { + init { + checkComponentsWithAlpha() + } + + override fun toHex(): String = if (withAlpha) colorToHex(red, green, blue, alpha) else colorToHex(red, green, blue) + + override fun toString(): String = toHex() +} + +private fun colorToHex(r: Int, g: Int, b: Int): String { + val rHex = r.toString(16).padStart(2, '0') + val gHex = g.toString(16).padStart(2, '0') + val bHex = b.toString(16).padStart(2, '0') + return "$rHex$gHex$bHex" +} + +private fun colorToHex(r: Int, g: Int, b: Int, a: Int): String { + val rHex = r.toString(16).padStart(2, '0') + val gHex = g.toString(16).padStart(2, '0') + val bHex = b.toString(16).padStart(2, '0') + val aHex = a.toString(16).padStart(2, '0') + return "$rHex$gHex$bHex$aHex" +} + +/** + * A palette color that allows specifying RGBA components. + * Instances of this class is created by [LevelSettings][cn.rdlevel.rdkt.core.settings.LevelSettings], and can be assigned to events that need colors to specify palette colors. + */ +public sealed interface PaletteColor : ColorWithAlpha, Palette { + override var red: Int + override var green: Int + override var blue: Int + override var alpha: Int + + public var color: ColorWithAlpha + get() = this + set(value) { + red = value.red + green = value.green + blue = value.blue + alpha = value.alpha + } + + /** + * Detaches the palette color from the palette and returns a new [ColorWithAlpha] instance with the same RGBA components. + * This is useful when you want to use the color of a palette color without referencing the palette index. + */ + public fun asColorWithAlpha(): ColorWithAlpha = ColorWithAlpha.of(red, green, blue, alpha) +} + +private data class PaletteColorImpl( + override val index: Int, + override var red: Int, + override var green: Int, + override var blue: Int, + override var alpha: Int = 255, +) : PaletteColor { + init { + checkIndex() + checkComponentsWithAlpha() + } + + override fun toHex(): String = colorToHex(red, green, blue, alpha) + + override fun toString(): String = "pal$index=${toHex()}" +} + +public object PaletteColorListSerializer : TransformSerializer, List>( + "PaletteColorList", + ListSerializer(String.serializer()) +) { + override fun toData(value: List): List = value.map { it.toHex() } + + override fun fromData(data: List): List = data.mapIndexed { index, hexString -> + val color = ColorWithAlpha.of(hexString) + PaletteColorImpl(index, color.red, color.green, color.blue, color.alpha) + } +} + +/** + * A list of palette colors. This is used for serializing and deserializing the palette colors in [LevelSettings][cn.rdlevel.rdkt.core.settings.LevelSettings]. + * In [LevelSettings][cn.rdlevel.rdkt.core.settings.LevelSettings] this list must contain exactly 21 colors, which correspond to the palette indices from 0 to 20. + */ +public typealias PaletteColorList = @Serializable(PaletteColorListSerializer::class) List + +/** + * Creates a [Palette] instance with the specified index. + */ +@JvmName("ofPalette") +public fun colorOf(paletteIndex: Int): Palette = Palette.of(paletteIndex) + +/** + * Creates a [Color] instance with the specified RGB components. + */ +@JvmName("ofColor") +public fun colorOf(r: Int, g: Int, b: Int): Color = Color.of(r, g, b) + +/** + * Creates a [ColorWithAlpha] instance with the specified RGBA components. The alpha component is optional and defaults to 255 (fully opaque) if not provided. + */ +@JvmName("ofColor") +public fun colorOf(r: Int, g: Int, b: Int, a: Int = 255): ColorWithAlpha = ColorWithAlpha.of(r, g, b, a) \ No newline at end of file