Updated on 2026-08-14
This commit is contained in:
parent
6796e868c6
commit
ef389a6735
22 changed files with 867 additions and 36 deletions
|
|
@ -0,0 +1,115 @@
|
|||
package com.tangem.core.ui.components.icons.identicon
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Ident icon. Blockies algorithm.
|
||||
* @see [Blockies](https://github.com/ethereum/blockies)
|
||||
*
|
||||
* @param address Address to generate the ident icon
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
internal data class Blockies(
|
||||
val address: String,
|
||||
) {
|
||||
|
||||
val primaryColor: Int
|
||||
val backgroundColor: Int
|
||||
val spotColor: Int
|
||||
val data: MutableList<Float>
|
||||
|
||||
init {
|
||||
val seed = seedFromAddress(address)
|
||||
// IMPORTANT! Color generation is based on the seed, thus ORDER is important.
|
||||
primaryColor = colorFromSeed(seed)
|
||||
backgroundColor = colorFromSeed(seed)
|
||||
spotColor = colorFromSeed(seed)
|
||||
data = dataFromSeed(seed)
|
||||
}
|
||||
|
||||
private fun seedFromAddress(address: String): MutableList<Long> {
|
||||
val seed = MutableList(SIZE) { DEFAULT_VALUE_L }
|
||||
address.indices.forEach { index ->
|
||||
seed[index % HALF_SIZE] =
|
||||
(seed[index % HALF_SIZE] shl 5) - seed[index % HALF_SIZE] + Character.codePointAt(address, index)
|
||||
}
|
||||
|
||||
// Important cast to preserve JavaScript type behavior
|
||||
seed.indices.map { seed[it] = seed[it].toInt().toLong() }
|
||||
return seed
|
||||
}
|
||||
|
||||
private fun colorFromSeed(seed: MutableList<Long>): Int {
|
||||
val h = floor(nextSeed(seed) * DEGREE_CIRCLE_F)
|
||||
val s = nextSeed(seed) * SATURATION_MAX + SATURATION_MIN
|
||||
val l = (nextSeed(seed) + nextSeed(seed) + nextSeed(seed) + nextSeed(seed)) * PROBABILITY_LIGHTNESS
|
||||
return toRgb(h, s, l)
|
||||
}
|
||||
|
||||
private fun nextSeed(seed: MutableList<Long>): Float {
|
||||
val t = (seed[0] xor (seed[0] shl 11)).toInt()
|
||||
seed[0] = seed[1]
|
||||
seed[1] = seed[2]
|
||||
seed[2] = seed[3]
|
||||
seed[3] = seed[3] xor (seed[3] shr 19) xor t.toLong() xor (t shr 8).toLong()
|
||||
|
||||
return abs(seed[3]).toFloat() / Integer.MAX_VALUE
|
||||
}
|
||||
|
||||
private fun dataFromSeed(seed: MutableList<Long>) = MutableList(SIZE * SIZE) { DEFAULT_VALUE_F }.apply {
|
||||
(0 until SIZE).forEach { row ->
|
||||
(0 until HALF_SIZE).forEach { column ->
|
||||
val value = floor(nextSeed(seed) * PROBABILITY_COLOR)
|
||||
this[row * SIZE + column] = value
|
||||
this[(row + 1) * SIZE - column - 1] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toRgb(paramH: Float, paramS: Float, paramL: Float): Int {
|
||||
val h = paramH % DEGREE_CIRCLE_F / DEGREE_CIRCLE_F
|
||||
val s = paramS / PERCENT_MAX
|
||||
val l = paramL / PERCENT_MAX
|
||||
|
||||
val q = if (l < 0.5) l * (1 + s) else l + s - s * l
|
||||
val p = 2 * l - q
|
||||
|
||||
val r = hueToRGB(p, q, h + 1f / 3f).coerceIn(0f, 1f)
|
||||
val g = hueToRGB(p, q, h).coerceIn(0f, 1f)
|
||||
val b = hueToRGB(p, q, h - 1f / 3f).coerceIn(0f, 1f)
|
||||
|
||||
val red = (r * HEX_COLOR_MAX).toInt()
|
||||
val green = (g * HEX_COLOR_MAX).toInt()
|
||||
val blue = (b * HEX_COLOR_MAX).toInt()
|
||||
return RGB_ALPHA_PART shl RGB_R_PART or (red shl RGB_G_PART) or (green shl RGB_B_PART) or blue
|
||||
}
|
||||
|
||||
private fun hueToRGB(p: Float, q: Float, h: Float): Float {
|
||||
var hue = h
|
||||
if (hue < 0) hue += 1f
|
||||
if (hue > 1) hue -= 1f
|
||||
if (6 * hue < 1) return p + (q - p) * 6f * hue
|
||||
if (2 * hue < 1) return q
|
||||
|
||||
return if (3 * hue < 2) p + (q - p) * 6f * (2f / 3f - hue) else p
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val SIZE = 8
|
||||
private const val HALF_SIZE = SIZE / 2
|
||||
private const val DEFAULT_VALUE_L = 0L
|
||||
private const val DEFAULT_VALUE_F = 0f
|
||||
private const val DEGREE_CIRCLE_F = 360.0f
|
||||
private const val HEX_COLOR_MAX = 255
|
||||
private const val PERCENT_MAX = 100f
|
||||
private const val SATURATION_MIN = 40f
|
||||
private const val SATURATION_MAX = 60f
|
||||
private const val PROBABILITY_LIGHTNESS = 25f
|
||||
private const val PROBABILITY_COLOR = 2.3f
|
||||
private const val RGB_ALPHA_PART = 0xFF
|
||||
private const val RGB_R_PART = 24
|
||||
private const val RGB_G_PART = 16
|
||||
private const val RGB_B_PART = 8
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.core.ui.components.icons.identicon
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
private const val GAP_WIDTH = 1f
|
||||
private const val ALPHA = 0.9f
|
||||
|
||||
/**
|
||||
* Ident icon
|
||||
*
|
||||
* @param address to generate ident icon from
|
||||
* @param modifier to apply to ident icon
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun IdentIcon(address: String, modifier: Modifier = Modifier) {
|
||||
if (address.isBlank()) {
|
||||
Box(modifier = modifier)
|
||||
} else {
|
||||
val blockies = Blockies(address.toLowerCase(Locale.current))
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (LocalIsInDarkTheme.current) {
|
||||
Modifier.alpha(ALPHA)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
val cellWidth = size.width / Blockies.SIZE
|
||||
blockies.data.forEachIndexed { index, item ->
|
||||
val y = index / Blockies.SIZE
|
||||
val x = index % Blockies.SIZE
|
||||
val colorInt = when (item) {
|
||||
1f -> blockies.primaryColor
|
||||
2f -> blockies.spotColor
|
||||
else -> blockies.backgroundColor
|
||||
}
|
||||
drawRect(
|
||||
color = Color(colorInt),
|
||||
topLeft = Offset(x * cellWidth, y * cellWidth),
|
||||
size = Size(cellWidth + GAP_WIDTH, cellWidth + GAP_WIDTH),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IdentIconPreview_Light() {
|
||||
TangemTheme {
|
||||
IdentIcon(
|
||||
address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359",
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size40),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IdentIconPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
IdentIcon(
|
||||
address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359",
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size40),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
|
@ -31,6 +31,7 @@ data class TangemDimens internal constructor(
|
|||
val radius14: Dp = 14.dp,
|
||||
val radius16: Dp = 16.dp,
|
||||
val radius18: Dp = 18.dp,
|
||||
val radius20: Dp = 20.dp,
|
||||
val radius24: Dp = 24.dp,
|
||||
val radius26: Dp = 26.dp,
|
||||
val radius28: Dp = 28.dp,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue