mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-02 22:30:40 +00:00
Merge branch 'rgba-colors' into 'master'
RGBA colors. See merge request videostreaming/grayjay!131
This commit is contained in:
commit
fae77c1a63
9 changed files with 632 additions and 14 deletions
|
@ -0,0 +1,38 @@
|
|||
package com.futo.platformplayer
|
||||
|
||||
import android.graphics.Color
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import toAndroidColor
|
||||
|
||||
class CSSColorTests {
|
||||
@Test
|
||||
fun test1() {
|
||||
val androidHex = "#80336699"
|
||||
val androidColorInt = Color.parseColor(androidHex)
|
||||
|
||||
val cssHex = "#33669980"
|
||||
val cssColor = CSSColor.parseColor(cssHex)
|
||||
|
||||
assertEquals(
|
||||
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
|
||||
androidColorInt,
|
||||
cssColor.toAndroidColor(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test2() {
|
||||
val androidHex = "#123ABC"
|
||||
val androidColorInt = Color.parseColor(androidHex)
|
||||
|
||||
val cssHex = "#123ABCFF"
|
||||
val cssColor = CSSColor.parseColor(cssHex)
|
||||
|
||||
assertEquals(
|
||||
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
|
||||
androidColorInt,
|
||||
cssColor.toAndroidColor()
|
||||
)
|
||||
}
|
||||
}
|
319
app/src/main/java/com/futo/platformplayer/CSSColor.kt
Normal file
319
app/src/main/java/com/futo/platformplayer/CSSColor.kt
Normal file
|
@ -0,0 +1,319 @@
|
|||
import kotlin.math.*
|
||||
|
||||
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
|
||||
init {
|
||||
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
|
||||
"RGBA channels must be in [0,1]"
|
||||
}
|
||||
}
|
||||
|
||||
// -- RGB(A) channels stored 0–1 --
|
||||
var r: Float = r.coerceIn(0f, 1f)
|
||||
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
|
||||
var g: Float = g.coerceIn(0f, 1f)
|
||||
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
|
||||
var b: Float = b.coerceIn(0f, 1f)
|
||||
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
|
||||
var a: Float = a.coerceIn(0f, 1f)
|
||||
set(v) { field = v.coerceIn(0f, 1f) }
|
||||
|
||||
// -- Int views of RGBA 0–255 --
|
||||
var red: Int
|
||||
get() = (r * 255).roundToInt()
|
||||
set(v) { r = (v.coerceIn(0, 255) / 255f) }
|
||||
var green: Int
|
||||
get() = (g * 255).roundToInt()
|
||||
set(v) { g = (v.coerceIn(0, 255) / 255f) }
|
||||
var blue: Int
|
||||
get() = (b * 255).roundToInt()
|
||||
set(v) { b = (v.coerceIn(0, 255) / 255f) }
|
||||
var alpha: Int
|
||||
get() = (a * 255).roundToInt()
|
||||
set(v) { a = (v.coerceIn(0, 255) / 255f) }
|
||||
|
||||
// -- HSLA storage & lazy recompute flags --
|
||||
private var _h: Float = 0f
|
||||
private var _s: Float = 0f
|
||||
private var _l: Float = 0f
|
||||
private var _hslDirty = true
|
||||
|
||||
/** Hue [0...360) */
|
||||
var hue: Float
|
||||
get() { computeHslIfNeeded(); return _h }
|
||||
set(v) { setHsl(v, saturation, lightness) }
|
||||
|
||||
/** Saturation [0...1] */
|
||||
var saturation: Float
|
||||
get() { computeHslIfNeeded(); return _s }
|
||||
set(v) { setHsl(hue, v, lightness) }
|
||||
|
||||
/** Lightness [0...1] */
|
||||
var lightness: Float
|
||||
get() { computeHslIfNeeded(); return _l }
|
||||
set(v) { setHsl(hue, saturation, v) }
|
||||
|
||||
private fun computeHslIfNeeded() {
|
||||
if (!_hslDirty) return
|
||||
val max = max(max(r, g), b)
|
||||
val min = min(min(r, g), b)
|
||||
val d = max - min
|
||||
_l = (max + min) / 2f
|
||||
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
|
||||
_h = when {
|
||||
d == 0f -> 0f
|
||||
max == r -> ((g - b) / d % 6f) * 60f
|
||||
max == g -> (((b - r) / d) + 2f) * 60f
|
||||
else -> (((r - g) / d) + 4f) * 60f
|
||||
}.let { if (it < 0f) it + 360f else it }
|
||||
_hslDirty = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all three HSL channels at once.
|
||||
* Hue in degrees [0...360), s/l [0...1].
|
||||
*/
|
||||
fun setHsl(h: Float, s: Float, l: Float) {
|
||||
val hh = ((h % 360f) + 360f) % 360f
|
||||
val cc = (1f - abs(2f * l - 1f)) * s
|
||||
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
|
||||
val m = l - cc / 2f
|
||||
|
||||
val (rp, gp, bp) = when {
|
||||
hh < 60f -> Triple(cc, x, 0f)
|
||||
hh < 120f -> Triple(x, cc, 0f)
|
||||
hh < 180f -> Triple(0f, cc, x)
|
||||
hh < 240f -> Triple(0f, x, cc)
|
||||
hh < 300f -> Triple(x, 0f, cc)
|
||||
else -> Triple(cc, 0f, x)
|
||||
}
|
||||
|
||||
r = rp + m; g = gp + m; b = bp + m
|
||||
_h = hh; _s = s; _l = l; _hslDirty = false
|
||||
}
|
||||
|
||||
/** Return 0xRRGGBBAA int */
|
||||
fun toRgbaInt(): Int {
|
||||
val ai = (a * 255).roundToInt() and 0xFF
|
||||
val ri = (r * 255).roundToInt() and 0xFF
|
||||
val gi = (g * 255).roundToInt() and 0xFF
|
||||
val bi = (b * 255).roundToInt() and 0xFF
|
||||
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
|
||||
}
|
||||
|
||||
/** Return 0xAARRGGBB int */
|
||||
fun toArgbInt(): Int {
|
||||
val ai = (a * 255).roundToInt() and 0xFF
|
||||
val ri = (r * 255).roundToInt() and 0xFF
|
||||
val gi = (g * 255).roundToInt() and 0xFF
|
||||
val bi = (b * 255).roundToInt() and 0xFF
|
||||
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
|
||||
}
|
||||
|
||||
// — Convenience modifiers (chainable) —
|
||||
|
||||
/** Lighten by fraction [0...1] */
|
||||
fun lighten(fraction: Float): CSSColor = apply {
|
||||
lightness = (lightness + fraction).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
/** Darken by fraction [0...1] */
|
||||
fun darken(fraction: Float): CSSColor = apply {
|
||||
lightness = (lightness - fraction).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
/** Increase saturation by fraction [0...1] */
|
||||
fun saturate(fraction: Float): CSSColor = apply {
|
||||
saturation = (saturation + fraction).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
/** Decrease saturation by fraction [0...1] */
|
||||
fun desaturate(fraction: Float): CSSColor = apply {
|
||||
saturation = (saturation - fraction).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
/** Rotate hue by degrees (can be negative) */
|
||||
fun rotateHue(degrees: Float): CSSColor = apply {
|
||||
hue = (hue + degrees) % 360f
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create from Android 0xAARRGGBB */
|
||||
@JvmStatic fun fromArgb(color: Int): CSSColor {
|
||||
val a = ((color ushr 24) and 0xFF) / 255f
|
||||
val r = ((color ushr 16) and 0xFF) / 255f
|
||||
val g = ((color ushr 8) and 0xFF) / 255f
|
||||
val b = ( color and 0xFF) / 255f
|
||||
return CSSColor(r, g, b, a)
|
||||
}
|
||||
|
||||
/** Create from Android 0xRRGGBBAA */
|
||||
@JvmStatic fun fromRgba(color: Int): CSSColor {
|
||||
val r = ((color ushr 24) and 0xFF) / 255f
|
||||
val g = ((color ushr 16) and 0xFF) / 255f
|
||||
val b = ((color ushr 8) and 0xFF) / 255f
|
||||
val a = ( color and 0xFF) / 255f
|
||||
return CSSColor(r, g, b, a)
|
||||
}
|
||||
|
||||
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
|
||||
return fromArgb(color)
|
||||
}
|
||||
|
||||
private val NAMED_HEX = mapOf(
|
||||
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
|
||||
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
|
||||
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
|
||||
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
|
||||
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
|
||||
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
|
||||
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
|
||||
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
|
||||
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
|
||||
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
|
||||
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
|
||||
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
|
||||
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
|
||||
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
|
||||
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
|
||||
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
|
||||
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
|
||||
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
|
||||
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
|
||||
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
|
||||
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
|
||||
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
|
||||
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
|
||||
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
|
||||
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
|
||||
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
|
||||
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
|
||||
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
|
||||
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
|
||||
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
|
||||
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
|
||||
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
|
||||
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
|
||||
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
|
||||
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
|
||||
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
|
||||
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
|
||||
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
|
||||
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
|
||||
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
|
||||
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
|
||||
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
|
||||
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
|
||||
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
|
||||
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
|
||||
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
|
||||
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
|
||||
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
|
||||
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
|
||||
"yellowgreen" to "9ACD32"
|
||||
)
|
||||
private val NAMED: Map<String, Int> = NAMED_HEX
|
||||
.mapValues { (_, hexRgb) ->
|
||||
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
|
||||
val rgb = hexRgb.toInt(16)
|
||||
(rgb shl 8) or 0xFF
|
||||
} + ("transparent" to 0x00000000)
|
||||
|
||||
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
|
||||
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
|
||||
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
|
||||
|
||||
@JvmStatic
|
||||
fun parseColor(s: String): CSSColor {
|
||||
val str = s.trim()
|
||||
// named
|
||||
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
|
||||
|
||||
// hex
|
||||
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
|
||||
return parseHexPart(part)
|
||||
}
|
||||
|
||||
// rgb/rgba
|
||||
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
|
||||
return parseRgbParts(it.split(',').map(String::trim))
|
||||
}
|
||||
|
||||
// hsl/hsla
|
||||
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
|
||||
return parseHslParts(it.split(',').map(String::trim))
|
||||
}
|
||||
|
||||
error("Cannot parse color: \"$s\"")
|
||||
}
|
||||
|
||||
private fun parseHexPart(p: String): CSSColor {
|
||||
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
|
||||
val hex = when (p.length) {
|
||||
3 -> p.map { "$it$it" }.joinToString("") + "FF"
|
||||
4 -> p.map { "$it$it" }.joinToString("")
|
||||
6 -> p + "FF"
|
||||
8 -> p
|
||||
else -> error("Invalid hex color: #$p")
|
||||
}
|
||||
|
||||
val parsed = hex.toLong(16).toInt()
|
||||
val alpha = (parsed and 0xFF) shl 24
|
||||
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
|
||||
val argb = alpha or rgbOnly
|
||||
return fromArgb(argb)
|
||||
}
|
||||
|
||||
private fun parseRgbParts(parts: List<String>): CSSColor {
|
||||
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
|
||||
|
||||
// r/g/b: "128" → 128/255, "50%" → 0.5
|
||||
fun channel(ch: String): Float =
|
||||
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
|
||||
else ch.toFloat().coerceIn(0f, 255f) / 255f
|
||||
|
||||
// alpha: "0.5" → 0.5, "50%" → 0.5
|
||||
fun alpha(a: String): Float =
|
||||
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
|
||||
else a.toFloat().coerceIn(0f, 1f)
|
||||
|
||||
val r = channel(parts[0])
|
||||
val g = channel(parts[1])
|
||||
val b = channel(parts[2])
|
||||
val a = if (parts.size == 4) alpha(parts[3]) else 1f
|
||||
|
||||
return CSSColor(r, g, b, a)
|
||||
}
|
||||
|
||||
private fun parseHslParts(parts: List<String>): CSSColor {
|
||||
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
|
||||
|
||||
fun hueOf(h: String): Float = when {
|
||||
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
|
||||
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
|
||||
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
|
||||
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
|
||||
else -> h.toFloat()
|
||||
}
|
||||
|
||||
// for s and l you only ever see percentages
|
||||
fun pct(p: String): Float =
|
||||
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
|
||||
|
||||
// alpha: "0.5" → 0.5, "50%" → 0.5
|
||||
fun alpha(a: String): Float =
|
||||
if (a.endsWith("%")) pct(a)
|
||||
else a.toFloat().coerceIn(0f, 1f)
|
||||
|
||||
val h = hueOf(parts[0])
|
||||
val s = pct(parts[1])
|
||||
val l = pct(parts[2])
|
||||
val a = if (parts.size == 4) alpha(parts[3]) else 1f
|
||||
|
||||
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
|
||||
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
|
||||
fun CSSColor.toAndroidColor(): Int = toArgbInt()
|
|
@ -18,8 +18,7 @@ class LiveEventEmojis: IPlatformLiveEvent {
|
|||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventEmojis"
|
||||
return LiveEventEmojis(
|
||||
obj.getOrThrow(config, "emojis", contextName));
|
||||
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.futo.platformplayer.views.livechat
|
||||
|
||||
import CSSColor
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.LevelListDrawable
|
||||
import android.text.Spannable
|
||||
|
@ -24,6 +25,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter
|
|||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import toAndroidColor
|
||||
|
||||
class LiveChatDonationListItem(viewGroup: ViewGroup)
|
||||
: LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_donation, viewGroup, false)) {
|
||||
|
@ -55,10 +57,10 @@ class LiveChatDonationListItem(viewGroup: ViewGroup)
|
|||
_amount.text = event.amount.trim();
|
||||
|
||||
if(event.colorDonation != null && event.colorDonation.isHexColor()) {
|
||||
val color = Color.parseColor(event.colorDonation);
|
||||
_amountContainer.background.setTint(color);
|
||||
val color = CSSColor.parseColor(event.colorDonation);
|
||||
_amountContainer.background.setTint(color.toAndroidColor());
|
||||
|
||||
if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400)
|
||||
if(color.lightness > 0.5)
|
||||
_amount.setTextColor(Color.BLACK);
|
||||
else
|
||||
_amount.setTextColor(Color.WHITE);
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.bumptech.glide.Glide
|
|||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.models.live.LiveEventDonation
|
||||
import com.futo.platformplayer.isHexColor
|
||||
import toAndroidColor
|
||||
|
||||
class LiveChatDonationPill: LinearLayout {
|
||||
private val _imageAuthor: ImageView;
|
||||
|
@ -33,10 +34,10 @@ class LiveChatDonationPill: LinearLayout {
|
|||
|
||||
|
||||
if(donation.colorDonation != null && donation.colorDonation.isHexColor()) {
|
||||
val color = Color.parseColor(donation.colorDonation);
|
||||
root.background.setTint(color);
|
||||
val color = CSSColor.parseColor(donation.colorDonation);
|
||||
root.background.setTint(color.toAndroidColor());
|
||||
|
||||
if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400)
|
||||
if(color.lightness > 0.5)
|
||||
_textAmount.setTextColor(Color.BLACK);
|
||||
else
|
||||
_textAmount.setTextColor(Color.WHITE);
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter
|
|||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import toAndroidColor
|
||||
|
||||
class LiveChatMessageListItem(viewGroup: ViewGroup)
|
||||
: LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_message, viewGroup, false)) {
|
||||
|
@ -75,7 +76,7 @@ class LiveChatMessageListItem(viewGroup: ViewGroup)
|
|||
|
||||
if (!event.colorName.isNullOrEmpty()) {
|
||||
try {
|
||||
_authorName.setTextColor(Color.parseColor(event.colorName));
|
||||
_authorName.setTextColor(CSSColor.parseColor(event.colorName).toAndroidColor());
|
||||
} catch (ex: Throwable) {
|
||||
}
|
||||
} else
|
||||
|
|
|
@ -43,6 +43,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import toAndroidColor
|
||||
|
||||
|
||||
class LiveChatOverlay : LinearLayout {
|
||||
|
@ -291,10 +292,10 @@ class LiveChatOverlay : LinearLayout {
|
|||
_overlayDonation_Amount.text = donation.amount.trim();
|
||||
_overlayDonation.visibility = VISIBLE;
|
||||
if(donation.colorDonation != null && donation.colorDonation.isHexColor()) {
|
||||
val color = Color.parseColor(donation.colorDonation);
|
||||
_overlayDonation_AmountContainer.background.setTint(color);
|
||||
val color = CSSColor.parseColor(donation.colorDonation);
|
||||
_overlayDonation_AmountContainer.background.setTint(color.toAndroidColor());
|
||||
|
||||
if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400)
|
||||
if(color.lightness > 0.5)
|
||||
_overlayDonation_Amount.setTextColor(Color.BLACK)
|
||||
else
|
||||
_overlayDonation_Amount.setTextColor(Color.WHITE);
|
||||
|
|
257
app/src/test/java/com/futo/platformplayer/CSSColorTests.kt
Normal file
257
app/src/test/java/com/futo/platformplayer/CSSColorTests.kt
Normal file
|
@ -0,0 +1,257 @@
|
|||
package com.futo.platformplayer
|
||||
|
||||
import CSSColor
|
||||
import org.junit.Assert.assertEquals
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CSSColorTest {
|
||||
|
||||
private fun approxEq(expected: Float, actual: Float, eps: Float = 1e-5f) {
|
||||
assertTrue(abs(expected - actual) <= eps, "Expected $expected but got $actual")
|
||||
}
|
||||
|
||||
@Test fun `hex #RRGGBB parses correctly`() {
|
||||
val c = CSSColor.parseColor("#336699")
|
||||
assertEquals(0x33, c.red)
|
||||
assertEquals(0x66, c.green)
|
||||
assertEquals(0x99, c.blue)
|
||||
assertEquals(255, c.alpha)
|
||||
}
|
||||
|
||||
@Test fun `hex #RGB shorthand expands`() {
|
||||
val c = CSSColor.parseColor("#369")
|
||||
assertEquals(0x33, c.red)
|
||||
assertEquals(0x66, c.green)
|
||||
assertEquals(0x99, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `hex #RRGGBBAA parses alpha`() {
|
||||
val c = CSSColor.parseColor("#33669980")
|
||||
assertEquals(0x33, c.red)
|
||||
assertEquals(0x66, c.green)
|
||||
assertEquals(0x99, c.blue)
|
||||
approxEq(128 / 255f, c.a)
|
||||
assertEquals(128, c.alpha)
|
||||
}
|
||||
|
||||
@Test fun `hex #RGBA shorthand expands with alpha`() {
|
||||
val c = CSSColor.parseColor("#3698")
|
||||
assertEquals(0x33, c.red)
|
||||
assertEquals(0x66, c.green)
|
||||
assertEquals(0x99, c.blue)
|
||||
assertEquals(0x88, c.alpha)
|
||||
}
|
||||
|
||||
@Test fun `hex uppercase shorthand parses`() {
|
||||
val c = CSSColor.parseColor("#AbC")
|
||||
// expands to AABBCC
|
||||
assertEquals(0xAA, c.red)
|
||||
assertEquals(0xBB, c.green)
|
||||
assertEquals(0xCC, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `rgb(ints) functional parser`() {
|
||||
val c = CSSColor.parseColor("rgb(255,128,0)")
|
||||
assertEquals(255, c.red)
|
||||
assertEquals(128, c.green)
|
||||
assertEquals(0, c.blue)
|
||||
assertEquals(255, c.alpha)
|
||||
}
|
||||
|
||||
@Test fun `rgb(percent) functional parser`() {
|
||||
val c = CSSColor.parseColor("rgb(100%,50%,0%)")
|
||||
assertEquals(255, c.red)
|
||||
assertEquals(128, c.green)
|
||||
assertEquals(0, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `rgba raw‐float alpha functional parser`() {
|
||||
val c = CSSColor.parseColor("rgba(255,0,0,0.5)")
|
||||
assertEquals(255, c.red)
|
||||
assertEquals(0, c.green)
|
||||
assertEquals(0, c.blue)
|
||||
approxEq(0.5f, c.a)
|
||||
}
|
||||
|
||||
@Test fun `rgba percent alpha functional parser`() {
|
||||
val c = CSSColor.parseColor("rgba(100%,0%,0%,50%)")
|
||||
assertEquals(255, c.red)
|
||||
assertEquals(0, c.green)
|
||||
assertEquals(0, c.blue)
|
||||
approxEq(0.5f, c.a)
|
||||
}
|
||||
|
||||
@Test fun `hsl() functional parser yields correct RGB`() {
|
||||
// pure green: hue=120°, sat=100%, light=50%
|
||||
val c = CSSColor.parseColor("hsl(120,100%,50%)")
|
||||
assertEquals(0, c.red)
|
||||
assertEquals(255, c.green)
|
||||
assertEquals(0, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `hsla percent alpha functional parser`() {
|
||||
val c = CSSColor.parseColor("hsla(240,100%,50%,25%)")
|
||||
// pure blue, alpha 25%
|
||||
assertEquals(0, c.red)
|
||||
assertEquals(0, c.green)
|
||||
assertEquals(255, c.blue)
|
||||
approxEq(0.25f, c.a)
|
||||
}
|
||||
|
||||
@Test fun `hsla raw‐float alpha functional parser`() {
|
||||
val c = CSSColor.parseColor("hsla(240,100%,50%,0.25)")
|
||||
assertEquals(0, c.red)
|
||||
assertEquals(0, c.green)
|
||||
assertEquals(255, c.blue)
|
||||
approxEq(0.25f, c.a)
|
||||
}
|
||||
|
||||
@Test fun `hsl radian unit parsing`() {
|
||||
// 180° = π radians → cyan
|
||||
val c = CSSColor.parseColor("hsl(${PI}rad,100%,50%)")
|
||||
assertEquals(0, c.red)
|
||||
assertEquals(255, c.green)
|
||||
assertEquals(255, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `hsl turn unit parsing`() {
|
||||
// 0.5 turn = 180° → cyan
|
||||
val c = CSSColor.parseColor("hsl(0.5turn,100%,50%)")
|
||||
assertEquals(0, c.red)
|
||||
assertEquals(255, c.green)
|
||||
assertEquals(255, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `hsl grad unit parsing`() {
|
||||
// 200 grad = 180° → cyan
|
||||
val c = CSSColor.parseColor("hsl(200grad,100%,50%)")
|
||||
assertEquals(0, c.red)
|
||||
assertEquals(255, c.green)
|
||||
assertEquals(255, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `named colors parse`() {
|
||||
val red = CSSColor.parseColor("red")
|
||||
assertEquals(255, red.red)
|
||||
assertEquals(0, red.green)
|
||||
assertEquals(0, red.blue)
|
||||
|
||||
val rebecca = CSSColor.parseColor("rebeccapurple")
|
||||
assertEquals(0x66, rebecca.red)
|
||||
assertEquals(0x33, rebecca.green)
|
||||
assertEquals(0x99, rebecca.blue)
|
||||
|
||||
val transparent = CSSColor.parseColor("transparent")
|
||||
assertEquals(0, transparent.alpha)
|
||||
}
|
||||
|
||||
@Test fun `round-trip Android Int ↔ CSSColor`() {
|
||||
val original = CSSColor(0.2f, 0.4f, 0.6f, 0.8f)
|
||||
val colorInt = original.toRgbaInt()
|
||||
val back = CSSColor.fromRgba(colorInt)
|
||||
approxEq(original.r, back.r)
|
||||
approxEq(original.g, back.g)
|
||||
approxEq(original.b, back.b)
|
||||
approxEq(original.a, back.a)
|
||||
}
|
||||
|
||||
@Test fun `individual channel setters`() {
|
||||
val c = CSSColor(0f,0f,0f,1f)
|
||||
c.red = 128; assertEquals(128, c.red); approxEq(128/255f, c.r)
|
||||
c.green = 64; assertEquals(64, c.green); approxEq(64/255f, c.g)
|
||||
c.blue = 32; assertEquals(32, c.blue); approxEq(32/255f, c.b)
|
||||
c.alpha = 200; assertEquals(200, c.alpha); approxEq(200/255f, c.a)
|
||||
}
|
||||
|
||||
@Test fun `hsl channel setters update RGB`() {
|
||||
val c = CSSColor.parseColor("hsl(0,100%,50%)") // red
|
||||
c.hue = 120f // → green
|
||||
assertEquals(0, c.red)
|
||||
assertEquals(255, c.green)
|
||||
assertEquals(0, c.blue)
|
||||
|
||||
c.saturation = 0f // → gray
|
||||
assertTrue(c.red == c.green && c.green == c.blue)
|
||||
}
|
||||
|
||||
@Test fun `convenience modifiers chain as expected`() {
|
||||
val c = CSSColor.parseColor("#888888")
|
||||
.lighten(0.1f)
|
||||
.saturate(0.2f)
|
||||
.rotateHue(45f)
|
||||
|
||||
approxEq(0.633f, c.lightness, eps = 1e-3f)
|
||||
approxEq(0.2f, c.saturation, eps = 1e-3f)
|
||||
approxEq(45f, c.hue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid formats throw IllegalArgumentException`() {
|
||||
listOf("", "rgb()", "hsl(0,0)", "#12", "rgba(0,0,0,150%)", "hsla(0,0%,0%,2)").forEach { bad ->
|
||||
try {
|
||||
CSSColor.parseColor(bad)
|
||||
assert(false)
|
||||
} catch (e: Throwable) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun `out‐of‐range RGB ints clamp`() {
|
||||
val c = CSSColor.parseColor("rgb(300,-20, 260)")
|
||||
assertEquals(255, c.red)
|
||||
assertEquals(0, c.green)
|
||||
assertEquals(255, c.blue)
|
||||
}
|
||||
|
||||
@Test fun `parser is case- and whitespace-tolerant`() {
|
||||
val a = CSSColor.parseColor(" RgB( 10 ,20, 30 )")
|
||||
assertEquals(10, a.red)
|
||||
assertEquals(20, a.green)
|
||||
assertEquals(30, a.blue)
|
||||
|
||||
val b = CSSColor.parseColor(" ReBeCcaPURple ")
|
||||
assertEquals(0x66, b.red)
|
||||
assertEquals(0x33, b.green)
|
||||
assertEquals(0x99, b.blue)
|
||||
}
|
||||
|
||||
@Test fun `hsl lightness extremes`() {
|
||||
// lightness = 0 → black
|
||||
val black = CSSColor.parseColor("hsl(123,45%,0%)")
|
||||
assertEquals(0, black.red)
|
||||
assertEquals(0, black.green)
|
||||
assertEquals(0, black.blue)
|
||||
// lightness = 100% → white
|
||||
val white = CSSColor.parseColor("hsl(321,55%,100%)")
|
||||
assertEquals(255, white.red)
|
||||
assertEquals(255, white.green)
|
||||
assertEquals(255, white.blue)
|
||||
// saturation = 0 → gray (r==g==b)
|
||||
val gray = CSSColor.parseColor("hsl(50,0%,60%)")
|
||||
assertTrue(gray.red == gray.green && gray.green == gray.blue)
|
||||
}
|
||||
|
||||
@Test fun `hsl negative and large hues wrap`() {
|
||||
val c1 = CSSColor.parseColor("hsl(-120,100%,50%)") // → same as 240°
|
||||
assertEquals(0, c1.red)
|
||||
assertEquals(0, c1.green)
|
||||
assertEquals(255, c1.blue)
|
||||
|
||||
val c2 = CSSColor.parseColor("hsl(480,100%,50%)") // → same as 120°
|
||||
assertEquals(0, c2.red)
|
||||
assertEquals(255, c2.green)
|
||||
assertEquals(0, c2.blue)
|
||||
}
|
||||
|
||||
@Test fun `lighten then darken returns original`() {
|
||||
val base = CSSColor.parseColor("#123456")
|
||||
val round = base.lighten(0.2f).darken(0.2f)
|
||||
approxEq(base.r, round.r)
|
||||
approxEq(base.g, round.g)
|
||||
approxEq(base.b, round.b)
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ import java.util.Random
|
|||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
/*
|
||||
class NoiseProtocolTest {
|
||||
constructor() {
|
||||
Logger.setLogConsumers(listOf(
|
||||
|
@ -625,4 +625,4 @@ class NoiseProtocolTest {
|
|||
throw Exception("Byte mismatch at index ${i}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
Loading…
Add table
Add a link
Reference in a new issue