Skip to content

Instantly share code, notes, and snippets.

@wiomoc
Created March 2, 2026 19:52
Show Gist options
  • Select an option

  • Save wiomoc/f07b3e8c1894cbf0c749cc4570e6ae6d to your computer and use it in GitHub Desktop.

Select an option

Save wiomoc/f07b3e8c1894cbf0c749cc4570e6ae6d to your computer and use it in GitHub Desktop.
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