Created
March 2, 2026 19:52
-
-
Save wiomoc/f07b3e8c1894cbf0c749cc4570e6ae6d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package de.wiomoc.loop | |
| import android.graphics.RuntimeShader | |
| import androidx.compose.animation.core.RepeatMode | |
| import androidx.compose.animation.core.animateFloat | |
| import androidx.compose.animation.core.infiniteRepeatable | |
| import androidx.compose.animation.core.rememberInfiniteTransition | |
| import androidx.compose.animation.core.tween | |
| import androidx.compose.foundation.Canvas | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.graphics.ShaderBrush | |
| import androidx.compose.ui.tooling.preview.Preview | |
| import de.wiomoc.loop.ui.theme.LoopTheme | |
| import kotlin.math.PI | |
| const val shaderString = """ | |
| uniform half2 iResolution; | |
| uniform half iValue; | |
| const half PI = 3.14159265359; | |
| const half TWO_PI = PI * 2.0; | |
| const half INV_TWO_PI = 1.0 / TWO_PI; | |
| const half RADIUS = 135.0; | |
| const half RING_WIDTH = 30.0; | |
| const half RING_WIDTH_SOFT = RING_WIDTH + 3.0; | |
| const half RING_INNER = RADIUS - RING_WIDTH; | |
| const half RING_INNER_SOFT = RING_INNER - 2.0; | |
| const half RING_OUTER = RADIUS + RING_WIDTH; | |
| const half RING_OUTER_SOFT = RING_OUTER + 2.0; | |
| const half TOTAL_SIZE = 2.0 * RING_OUTER_SOFT; | |
| // --- colors --- | |
| const half4 COLOR_END = half4(42.0/256.0, 123.0/256.0, 155.0/256.0,1.0); | |
| const half4 COLOR_MID = half4(87.0/256.0, 199.0/256.0, 133.0/256.0,1.0); | |
| const half4 COLOR_START = half4(237.0/256.0,221.0/256.0, 83.0/256.0,1.0); | |
| half4 gradientColor(half angle) | |
| { | |
| const half INV_MAX = 1.0 / (3.0 * PI); | |
| half t = angle * INV_MAX; | |
| half midPoint = 0.5; | |
| half4 left = mix(COLOR_START, COLOR_MID, t / midPoint); | |
| half4 right = mix(COLOR_MID, COLOR_END, (t - midPoint) / (1.0 - midPoint)); | |
| return mix(left, right, step(midPoint, t)); | |
| } | |
| half4 layerColor(half sweepTime, half angle) | |
| { | |
| half delta = sweepTime - angle; | |
| if (delta < 0.0) | |
| return half4(0.0); | |
| half layer = floor(delta * INV_TWO_PI); | |
| half phase = angle + layer * TWO_PI; | |
| return gradientColor(phase); | |
| } | |
| half4 shadowColor(half sweepTime, half angle, half sweepDist) | |
| { | |
| half r1 = (1.5 * PI + sweepTime - angle) * INV_TWO_PI; | |
| half r2 = (TWO_PI + sweepTime - angle) * INV_TWO_PI; | |
| half mask = step(floor(r2), floor(r1)); | |
| mask *= smoothstep(PI * 1.5, PI * 2.5, sweepTime); | |
| half fadeOuter = smoothstep(10.0, 80.0, sweepDist); | |
| half edgeSoft = smoothstep(RING_WIDTH, RING_WIDTH_SOFT, sweepDist); | |
| return half4(0.3,0.3,0.3,0.8) * edgeSoft * (1.0 - fadeOuter) * mask; | |
| } | |
| half4 main(float2 fragCoord) | |
| { | |
| half maxSize = min(iResolution.x, iResolution.y); | |
| half2 center = iResolution * 0.5; | |
| half2 pos = half2(fragCoord) - center; | |
| pos *= TOTAL_SIZE / maxSize; | |
| pos.y *= -1.0; | |
| half radialDist = length(pos); | |
| // ring mask | |
| half innerMask = smoothstep(RING_INNER_SOFT, RING_INNER, radialDist); | |
| half outerMask = smoothstep(RING_OUTER_SOFT, RING_OUTER, radialDist); | |
| half ringMask = innerMask * outerMask; | |
| if (ringMask == 0.0) { | |
| return half4(0.0); | |
| } | |
| // sweep position | |
| half sweepAngle = mod(iValue, TWO_PI); | |
| half2 sweepPos = half2(sin(sweepAngle), cos(sweepAngle)) * RADIUS; | |
| half sweepDist = distance(sweepPos, pos); | |
| // angle computation (normalized) | |
| half angle = (-atan(pos.x / radialDist, -pos.y / radialDist)) + PI; | |
| // base marker | |
| half northDist = distance(half2(0.0, RADIUS), pos); | |
| half northMask = smoothstep(RING_WIDTH, RING_WIDTH_SOFT, northDist); | |
| half4 baseColor = | |
| mix(layerColor(1.0, 0.1), half4(0.0), northMask); | |
| // sweep blend | |
| half sweepEdge = smoothstep(RING_WIDTH, RING_WIDTH_SOFT, sweepDist); | |
| half4 color = | |
| mix(layerColor(iValue + PI, angle), | |
| layerColor(iValue, angle), | |
| sweepEdge); | |
| half4 shadow = shadowColor(iValue, angle, sweepDist); | |
| color = mix(baseColor, color, color.a); | |
| color = mix(color, half4(shadow.rgb, 1.0), shadow.a); | |
| return mix(half4(0.0), color, ringMask); | |
| } | |
| """ | |
| val shader = RuntimeShader(shaderString) | |
| val shaderBrush = ShaderBrush(shader) | |
| @Composable | |
| fun Loop(value: Float) { | |
| Canvas( | |
| modifier = Modifier.fillMaxSize() | |
| ) { | |
| shader.setFloatUniform( | |
| "iResolution", | |
| size.width, size.height | |
| ) | |
| shader.setFloatUniform("iValue", value); | |
| drawRect(brush = shaderBrush) | |
| } | |
| } | |
| @Preview(showBackground = true) | |
| @Composable | |
| fun LoopPreview() { | |
| val infiniteTransition = rememberInfiniteTransition() | |
| val value by infiniteTransition.animateFloat( | |
| initialValue = 0f, | |
| targetValue = (4.2 * PI).toFloat(), | |
| animationSpec = infiniteRepeatable(tween(6000), RepeatMode.Reverse), | |
| label = "scale" | |
| ) | |
| LoopTheme { | |
| Loop(value) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment