Updated on 2026-08-14
This commit is contained in:
parent
3edb222a84
commit
febd32b964
12 changed files with 728 additions and 3 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
|
@ -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],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue