From febd32b964cb2dfb46a1f1c68b791dfb182d9868 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 17:59:33 +0300 Subject: [PATCH] Updated on 2026-08-14 --- core/ui/build.gradle.kts | 4 +- .../components/background/ShaderBackground.kt | 70 +++++++ .../MovingColorfulBlubsBackground.kt | 169 +++++++++++++++ .../NorthernLightsBackground.kt | 151 ++++++++++++++ .../tangem/core/ui/screen/ComposeScreen.kt | 5 +- .../com/tangem/core/ui/shader/GlossyShader.kt | 30 +++ .../NorthernLightsMeshGradientShader.kt | 193 ++++++++++++++++++ .../com/tangem/core/ui/shader/TangemShader.kt | 16 ++ .../shader/runtime/FallbackRuntimeEffect.kt | 13 ++ .../core/ui/shader/runtime/RuntimeEffect.kt | 35 ++++ .../ui/shader/runtime/RuntimeShaderEffect.kt | 41 ++++ .../presentation/wallet/ui/WalletScreen2.kt | 4 + 12 files changed, 728 insertions(+), 3 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index db9bc30c60..482fb4ddd7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -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") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt new file mode 100644 index 0000000000..4b7090b987 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt @@ -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 +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt new file mode 100644 index 0000000000..f4e9988a2b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt new file mode 100644 index 0000000000..a59ce0f0e6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt @@ -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), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index d1c1e10b32..cff7ef913b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -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) + } } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt new file mode 100644 index 0000000000..600ad9af39 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt @@ -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); +} + """ +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt new file mode 100644 index 0000000000..05ade74d32 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt @@ -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, + 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) { + 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], + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt new file mode 100644 index 0000000000..388914a11b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt @@ -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) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt new file mode 100644 index 0000000000..899e3c8a78 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt @@ -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)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt new file mode 100644 index 0000000000..035495aad8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt @@ -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() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt new file mode 100644 index 0000000000..78c50e2e7c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt @@ -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) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index 8a878d9111..cd10d61f4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -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,