Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-30 18:31:14 +03:00
parent 6796e868c6
commit ef389a6735
22 changed files with 867 additions and 36 deletions

View file

@ -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
}
}

View file

@ -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

View file

@ -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,