Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-24 17:59:33 +03:00
parent 3edb222a84
commit febd32b964
12 changed files with 728 additions and 3 deletions

View file

@ -61,12 +61,12 @@ dependencies {
api(deps.jodatime)
implementation(deps.timber)
implementation(deps.markdown)
implementation(deps.haze) {
api(deps.haze) {
exclude(module = "activity-compose")
exclude(module = "activity")
exclude(module = "activity-ktx")
}
implementation(deps.haze.materials) {
api(deps.haze.materials) {
exclude(module = "activity-compose")
exclude(module = "activity")
exclude(module = "activity-ktx")

View file

@ -0,0 +1,70 @@
@file:Suppress("MagicNumber", "UnnecessaryParentheses")
package com.tangem.core.ui.components.background
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import com.tangem.core.ui.shader.TangemShader
import com.tangem.core.ui.shader.runtime.buildEffect
import kotlin.math.round
@Composable
fun Modifier.shaderBackground(
shader: TangemShader,
speed: Float = 1f,
fallback: () -> Brush = {
Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
},
): Modifier {
val runtimeEffect = remember(shader) { buildEffect(shader) }
var size: Size by remember { mutableStateOf(Size(-1f, -1f)) }
val speedModifier = shader.speedModifier
val time by if (runtimeEffect.isSupported) {
var startMillis = remember(shader) { -1L }
produceState(0f, speedModifier) {
while (true) {
withInfiniteAnimationFrameMillis { frameTimeMillis ->
if (startMillis < 0) startMillis = frameTimeMillis
value = ((frameTimeMillis - startMillis) / 16.6f) / 10f
}
}
}
} else {
remember { mutableFloatStateOf(-1f) }
}
return this then Modifier.onGloballyPositioned {
size = Size(it.size.width.toFloat(), it.size.height.toFloat())
}.drawBehind {
runtimeEffect.update(
shader = shader,
time = (time * speed * speedModifier).round(3),
width = size.width,
height = size.height,
) // set uniforms for the shaders
if (runtimeEffect.isReady) {
drawRect(brush = runtimeEffect.build())
} else {
drawRect(brush = fallback())
}
}
}
private fun Float.round(decimals: Int): Float {
var multiplier = 1.0f
repeat(decimals) { multiplier *= 10 }
return round(this * multiplier) / multiplier
}

View file

@ -0,0 +1,169 @@
@file:Suppress("MagicNumber")
package com.tangem.core.ui.components.background.northernlights
import androidx.compose.runtime.Composable
import android.graphics.BlurMaskFilter
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
@Suppress("LongMethod")
@Composable
internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
val transition = rememberInfiniteTransition(label = "FluidMeshGradient")
// ── Circle 1 (left) ──────────────────────────────────────────────────────
val color1 by transition.animateColor(
initialValue = Color(0xFF3355EE),
targetValue = Color(0xFF5577FF),
animationSpec = infiniteRepeatable(
animation = tween(4_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
),
label = "color1",
)
val x1 by transition.animateFloat(
initialValue = 0.05f,
targetValue = 0.28f,
animationSpec = infiniteRepeatable(
animation = tween(5_500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
),
label = "x1",
)
val y1 by transition.animateFloat(
initialValue = 0.0f,
targetValue = 0.18f,
animationSpec = infiniteRepeatable(
animation = tween(6_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
),
label = "y1",
)
// ── Circle 2 (right) ─────────────────────────────────────────────────────
val color2 by transition.animateColor(
initialValue = Color(0xFF7733CC),
targetValue = Color(0xFF4455EE),
animationSpec = infiniteRepeatable(
animation = tween(5_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(1_500),
),
label = "color2",
)
val x2 by transition.animateFloat(
initialValue = 0.68f,
targetValue = 0.92f,
animationSpec = infiniteRepeatable(
animation = tween(7_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
),
label = "x2",
)
val y2 by transition.animateFloat(
initialValue = 0.02f,
targetValue = 0.20f,
animationSpec = infiniteRepeatable(
animation = tween(5_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(2_000),
),
label = "y2",
)
// ── Oval (center) ────────────────────────────────────────────────────────
val ovalColor by transition.animateColor(
initialValue = Color(0xFF5533CC),
targetValue = Color(0xFF8844EE),
animationSpec = infiniteRepeatable(
animation = tween(7_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(2_500),
),
label = "ovalColor",
)
// ── Circle 3 (center) ────────────────────────────────────────────────────
val color3 by transition.animateColor(
initialValue = Color(0xFF9933BB),
targetValue = Color(0xFFBB44DD),
animationSpec = infiniteRepeatable(
animation = tween(6_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(3_000),
),
label = "color3",
)
val x3 by transition.animateFloat(
initialValue = 0.35f,
targetValue = 0.58f,
animationSpec = infiniteRepeatable(
animation = tween(6_500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(1_000),
),
label = "x3",
)
val y3 by transition.animateFloat(
initialValue = 0.0f,
targetValue = 0.15f,
animationSpec = infiniteRepeatable(
animation = tween(4_500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(500),
),
label = "y3",
)
var blurRadiusState by remember { mutableFloatStateOf(0f) }
val circlePaint1 = remember { Paint() }
val circlePaint2 = remember { Paint() }
val circlePaint3 = remember { Paint() }
val ovalPaint = remember { Paint() }
Canvas(modifier = modifier) {
val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f)
val circleRadius = size.width * 0.52f
// Update maskFilter only when blur radius changes meaningfully
if (blurRadiusState != blurRadius) {
blurRadiusState = blurRadius
val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
circlePaint1.asFrameworkPaint().maskFilter = mf
circlePaint2.asFrameworkPaint().maskFilter = mf
circlePaint3.asFrameworkPaint().maskFilter = mf
ovalPaint.asFrameworkPaint().maskFilter = mf
}
circlePaint1.color = color1.copy(alpha = 0.85f)
circlePaint2.color = color2.copy(alpha = 0.85f)
circlePaint3.color = color3.copy(alpha = 0.85f)
ovalPaint.color = ovalColor.copy(alpha = 0.80f)
drawIntoCanvas { canvas ->
canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1)
canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2)
canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3)
val halfW = size.width * 0.68f
val halfH = size.width * 0.24f
val ovalCx = size.width * 0.50f
val ovalCy = 0f
canvas.drawOval(
Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH),
ovalPaint,
)
}
}
}

View file

@ -0,0 +1,151 @@
@file:Suppress("MagicNumber")
package com.tangem.core.ui.components.background.northernlights
import android.os.Build
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.background.shaderBackground
import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
/**
* Animated northern lights background.
* Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode.
*/
@Composable
fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: Boolean = false) {
val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) {
NorthernLightsBackgroundWithShader(modifier)
} else {
MovingColorfulBlubsBackground(modifier)
}
}
@Suppress("LongMethod")
@Composable
private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) {
val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
val backgroundColor = TangemTheme.colors2.surface.level1
// Each track cycles through 4 states (matching the screenshot frames):
// deep/dark → saturated+bright → light/pastel → vibrant/vivid → back
// 16 s total per track, staggered so no two tracks peak simultaneously.
// ── Color 1 indigo → bright blue → lavender → hot violet ──────────────
val color1 by transition.animateColor(
initialValue = Color(0xFF2A1480),
targetValue = Color(0xFF2A1480),
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 16_000
Color(0xFF2A1480) at 0 using FastOutSlowInEasing
Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing
Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing
Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
),
label = "color1",
)
// ── Color 2 dark blue → cyan-blue → sky → teal ─────────────────────────
val color2 by transition.animateColor(
initialValue = Color(0xFF1444AA),
targetValue = Color(0xFF1444AA),
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 16_000
Color(0xFF1444AA) at 0 using FastOutSlowInEasing
Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing
Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing
Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(4_000),
),
label = "color2",
)
// ── Color 3 dark purple → medium purple → rose pink → magenta ──────────
val color3 by transition.animateColor(
initialValue = Color(0xFF4422BB),
targetValue = Color(0xFF4422BB),
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 16_000
Color(0xFF4422BB) at 0 using FastOutSlowInEasing
Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing
Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing
Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(8_000),
),
label = "color3",
)
// ── Color 4 dark violet → medium violet → light pink → hot pink ────────
val color4 by transition.animateColor(
initialValue = Color(0xFF331199),
targetValue = Color(0xFF331199),
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 16_000
Color(0xFF331199) at 0 using FastOutSlowInEasing
Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing
Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing
Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(2_000),
),
label = "color4",
)
// Keep a stable shader instance so the RuntimeShader is never recreated.
// Colors are pushed each recomposition via updateColors().
val shader = remember {
NorthernLightsMeshGradientShader(
colors = arrayOf(
Color(0xFF2A1480),
Color(0xFF1444AA),
Color(0xFF4422BB),
Color(0xFF331199),
backgroundColor,
),
speed = 0.5f,
scale = 4f,
)
}
val colorsArray = remember { Array(5) { Color.Unspecified } }
colorsArray[0] = color1
colorsArray[1] = color2
colorsArray[2] = color3
colorsArray[3] = color4
colorsArray[4] = backgroundColor
shader.updateColors(colorsArray)
Box(
modifier = modifier
.background(backgroundColor)
.fillMaxSize()
.shaderBackground(shader),
)
}

View file

@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemeRedesign
/**
* Interface representing a Compose screen with common theming and content composition properties.
@ -61,7 +62,9 @@ internal fun ComposeScreen.createComposeView(
uiDependencies = uiDependencies,
overrideSystemBarColors = overrideSystemBarColors,
) {
ScreenContent(modifier = screenModifier)
TangemThemeRedesign {
ScreenContent(modifier = screenModifier)
}
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.core.ui.shader
class GlossyShader : TangemShader {
override val sksl: String =
"""
// The MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
uniform float uTime;
uniform vec3 uResolution;
vec4 main( vec2 fragCoord )
{
float mr = min(uResolution.x, uResolution.y);
vec2 uv = (fragCoord * 2.0 - uResolution.xy) / mr;
float d = -uTime * 0.5;
float a = 0.0;
for (float i = 0.0; i < 8.0; ++i) {
a += cos(i - d - a * uv.x);
d += sin(uv.y * i + a);
}
d += uTime * 0.5;
vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);
col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5);
return vec4(col,1.0);
}
"""
}

View file

@ -0,0 +1,193 @@
@file:Suppress("MagicNumber")
package com.tangem.core.ui.shader
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.shader.runtime.RuntimeEffect
/**
* A shader that creates a colorful, flowing "northern lights" effect.
* @param colors The colors to display. The last provided color acts like a "background"
* @param speed Adjust the speed of the movement
* @param scale Adjusts the scale of the board. Higher number -> larger billboard -> smaller color blobs
*
[REDACTED_AUTHOR]
*/
class NorthernLightsMeshGradientShader(
colors: Array<Color>,
speed: Float = 1f,
scale: Float = 2f,
) : TangemShader {
private val colorCount = colors.size
private val colorUniforms = colors.flatMap {
listOf(it.red, it.green, it.blue)
}.toTypedArray().toFloatArray()
private val ambientUniform = FloatArray(3)
init {
recomputeAmbient()
}
override val sksl = """
uniform float uTime;
uniform vec3 uResolution;
uniform vec3 uAmbient;
const int MAX_COLORS = $colorCount;
uniform vec3 uColor[MAX_COLORS];
// Simplex 3D Noise
// by Ian McEwan, Ashima Arts
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
//
vec4 permute(vec4 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
float snoise(vec3 v) {
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
// First corner
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
// Other corners
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
// x0 = x0 - 0. + 0.0 * C
vec3 x1 = x0 - i1 + 1.0 * C.xxx;
vec3 x2 = x0 - i2 + 2.0 * C.xxx;
vec3 x3 = x0 - 1. + 3.0 * C.xxx;
// Permutations
i = mod(i, 289.0);
vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
// Gradients
// ( N*N points uniformly over a square, mapped onto an octahedron.)
float n_ = 1.0 / 7.0; // N=7
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N)
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_); // mod(j,N)
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
//Normalise gradients
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
// Mix final noise value
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
vec4 main( vec2 fragCoord ) {
float mr = min(uResolution.x, uResolution.y);
vec2 uv = (fragCoord * $scale - uResolution.xy) / mr;
vec2 base = uv / 2;
vec3 vColor = uColor[MAX_COLORS - 1];
const vec2 frequency = vec2(0.7, 0.3);
const float noiseFloor = 0.00001;
float t = uTime * 0.005;
for(int i = 0; i < MAX_COLORS - 1; i++) {
float fi = float(i);
float flow = 5. + fi * 0.3;
float speed = 6. * $speed + fi * 0.3;
float seed = 1. + fi * 4.;
float noiseCeil = 0.6 + fi * 0.07;
float noise = smoothstep(noiseFloor, noiseCeil, snoise(vec3(base.x * frequency.x, base.y * frequency.y - t * flow, t * speed + seed)));
vColor = mix(vColor, uColor[i], noise);
}
vColor = max(vColor, uAmbient);
// Elliptical falloff centred at the very top of the screen.
// Using fragCoord directly (pixels) and uResolution for screen size.
// Horizontal radius ~ 80 % of screen width → wide enough to cover corners.
// Vertical radius ~ 45 % of screen height → controls how far down the glow reaches.
vec2 topCenter = vec2(uResolution.x * 0.5, 0.0);
vec2 delta = fragCoord - topCenter;
vec2 radii = vec2(uResolution.x * 0.9, uResolution.y * 0.65);
float normDist = length(delta / radii);
float alpha = pow(1.0 - smoothstep(0.0, 1.0, normDist), 1.5);
// Pre-multiplied alpha so the shader composites correctly over the dark background.
return vec4(vColor * alpha, alpha);
}
"""
/** Updates the animated colors in-place without recreating the shader. */
fun updateColors(colors: Array<Color>) {
colors.forEachIndexed { i, color ->
colorUniforms[i * 3 + 0] = color.red
colorUniforms[i * 3 + 1] = color.green
colorUniforms[i * 3 + 2] = color.blue
}
recomputeAmbient()
}
private fun recomputeAmbient() {
val count = colorCount - 1
var r = 0f
var g = 0f
var b = 0f
for (i in 0 until count) {
r += colorUniforms[i * 3]
g += colorUniforms[i * 3 + 1]
b += colorUniforms[i * 3 + 2]
}
val scale = 0.5f / count
ambientUniform[0] = r * scale
ambientUniform[1] = g * scale
ambientUniform[2] = b * scale
}
override fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
super.applyUniforms(runtimeEffect = runtimeEffect, time = time, width = width, height = height)
runtimeEffect.setFloatUniform(name = "uColor", values = colorUniforms)
runtimeEffect.setFloatUniform(
name = "uAmbient",
value1 = ambientUniform[0],
value2 = ambientUniform[1],
value3 = ambientUniform[2],
)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.ui.shader
import com.tangem.core.ui.shader.runtime.RuntimeEffect
interface TangemShader {
val speedModifier: Float
get() = 0.5f
val sksl: String
/** Applies the uniforms required for this shader to the effect */
fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) {
runtimeEffect.setFloatUniform(name = "uResolution", value1 = width, value2 = height, value3 = width / height)
runtimeEffect.setFloatUniform(name = "uTime", value1 = time)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.ui.shader.runtime
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
internal class FallbackRuntimeEffect : RuntimeEffect {
override val isSupported: Boolean = false
override val isReady: Boolean = false
override fun build(): Brush {
return Brush.horizontalGradient(listOf(Color.White, Color.White))
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.core.ui.shader.runtime
import android.os.Build
import androidx.compose.ui.graphics.Brush
import com.tangem.core.ui.shader.TangemShader
interface RuntimeEffect {
val isSupported: Boolean
val isReady: Boolean
/** Sets a float array uniform for this shader */
fun setFloatUniform(name: String, value1: Float) {}
/** Sets a float array uniform for this shader */
fun setFloatUniform(name: String, value1: Float, value2: Float) {}
/** Sets a float array uniform for this shader */
fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {}
/** Sets a float array uniform for this shader */
fun setFloatUniform(name: String, values: FloatArray) {}
fun update(shader: TangemShader, time: Float, width: Float, height: Float) {}
fun build(): Brush
}
internal fun buildEffect(shader: TangemShader): RuntimeEffect {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RuntimeShaderEffect(shader)
} else {
FallbackRuntimeEffect()
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.core.ui.shader.runtime
import android.graphics.RuntimeShader
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ShaderBrush
import com.tangem.core.ui.shader.TangemShader
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
internal class RuntimeShaderEffect(tangemShader: TangemShader) : RuntimeEffect {
private val compositeRuntimeEffect = RuntimeShader(tangemShader.sksl)
override val isSupported: Boolean = true
override var isReady: Boolean = false
override fun setFloatUniform(name: String, value1: Float) {
compositeRuntimeEffect.setFloatUniform(name, value1)
}
override fun setFloatUniform(name: String, value1: Float, value2: Float) {
compositeRuntimeEffect.setFloatUniform(name, value1, value2)
}
override fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {
compositeRuntimeEffect.setFloatUniform(name, value1, value2, value3)
}
override fun setFloatUniform(name: String, values: FloatArray) {
compositeRuntimeEffect.setFloatUniform(name, values)
}
override fun update(shader: TangemShader, time: Float, width: Float, height: Float) {
shader.applyUniforms(runtimeEffect = this, time = time, width = width, height = height)
isReady = width > 0 && height > 0
}
override fun build(): Brush {
return ShaderBrush(compositeRuntimeEffect)
}
}

View file

@ -35,6 +35,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
@ -118,6 +119,9 @@ private fun WalletContent2(
val partialCollapsedHeight = 64.dp + statusBarHeight
val scaffoldContent: @Composable (PaddingValues?) -> Unit = { _ ->
Box(Modifier.fillMaxSize()) {
NorthernLightsBackground(Modifier.matchParentSize())
}
val pagerState = rememberPagerState(
initialPage = state.selectedWalletIndex,