Created
August 20, 2025 06:14
-
-
Save Kyriakos-Georgiopoulos/044c05a7c13c3e42d4a78ead238c0b99 to your computer and use it in GitHub Desktop.
YinYang
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
import android.graphics.RuntimeShader | |
import android.os.Build | |
import androidx.annotation.RequiresApi | |
import androidx.compose.animation.core.FastOutSlowInEasing | |
import androidx.compose.animation.core.RepeatMode | |
import androidx.compose.animation.core.animateFloat | |
import androidx.compose.animation.core.infiniteRepeatable | |
import androidx.compose.animation.core.keyframes | |
import androidx.compose.animation.core.rememberInfiniteTransition | |
import androidx.compose.foundation.Canvas | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.remember | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.geometry.Size | |
import androidx.compose.ui.graphics.ShaderBrush | |
import androidx.compose.ui.tooling.preview.Preview | |
private const val YY_SHADER = """ | |
uniform float2 resolution; | |
uniform float progress; // 0..1 | |
uniform float rotation; // degrees | |
float sdCircle(vec2 p, float r){ return length(p) - r; } | |
float sdRightHalf(vec2 p){ return -p.x; } // <0 when x>=0 (right side) | |
// rotate around origin by degrees | |
vec2 rot(vec2 p, float deg){ | |
float a = radians(deg); | |
float c = cos(a), s = sin(a); | |
return vec2(c*p.x - s*p.y, s*p.x + c*p.y); | |
} | |
half4 main(float2 fragCoord){ | |
float2 res = resolution; | |
float s = min(res.x, res.y); | |
// center at (0,0); units so that LEFT/RIGHT edges are ~±0.5 | |
vec2 p = (fragCoord - res*0.5) / s; | |
p = rot(p, rotation); | |
// target taijitu geometry at radius R = 0.5 (fits on any aspect) | |
float R = 0.45; | |
float rSmall = R * 0.5; | |
float dOuter = sdCircle(p, R); // <0 inside big circle | |
float dRight = sdRightHalf(p); | |
float dTopSm = sdCircle(p - vec2(0.0, -rSmall), rSmall); | |
float dBotSm = sdCircle(p - vec2(0.0, +rSmall), rSmall); | |
// right semicircle = outer ∩ right half-plane | |
float dRightSemi = max(dOuter, dRight); | |
// union with bottom small circle | |
float dUnion = min(dRightSemi, dBotSm); | |
// subtract top small circle -> black region of final taijitu | |
float dBlackYY = max(dUnion, -dTopSm); | |
// start state: plain vertical split (black on right) | |
float dSplit = sdRightHalf(p); | |
// morph boundary: interpolate SDFs (keeps edges crisp) | |
float d = mix(dSplit, dBlackYY, progress); | |
// base color from SDF (1 = black, 0 = white) | |
float black = step(d, 0.0); | |
// eyes grow in as we approach the circle | |
float eyeR = mix(0.0, R * 0.18, progress); // final eye size ~ R*0.18 | |
float dEyeTop = sdCircle(p - vec2(0.0, -rSmall), eyeR); | |
float dEyeBot = sdCircle(p - vec2(0.0, +rSmall), eyeR); | |
float inTopEye = 1.0 - step(0.0, dEyeTop); | |
float inBotEye = 1.0 - step(0.0, dEyeBot); | |
// top eye is black, bottom eye is white | |
black = max(black, inTopEye); | |
black = black * (1.0 - inBotEye); | |
// fade outside of the big circle to white only near the end, | |
// so earlier frames still use the full-screen split. | |
float outside = step(0.0, dOuter); // 1 outside circle | |
float outFade = smoothstep(1.0, 1.0, progress); | |
black = mix(black, 0.0, outside * outFade); | |
return half4(black, black, black, 1.0); | |
} | |
""" | |
@RequiresApi(Build.VERSION_CODES.TIRAMISU) | |
@Composable | |
fun YinYang( | |
modifier: Modifier = Modifier, | |
cycleMs: Int = 4200, | |
holdMs: Int = 900, | |
rotatePerCycle: Float = 180f | |
) { | |
val phase by rememberInfiniteTransition(label = "yyPhase").animateFloat( | |
initialValue = 0f, targetValue = 1f, | |
animationSpec = infiniteRepeatable( | |
animation = keyframes { | |
durationMillis = cycleMs | |
0f at 0 with FastOutSlowInEasing | |
1f at (cycleMs - holdMs) with FastOutSlowInEasing | |
1f at cycleMs | |
}, | |
repeatMode = RepeatMode.Reverse | |
), | |
label = "phase" | |
) | |
val progress = FastOutSlowInEasing.transform(phase).coerceIn(0f, 1f) | |
val rotation = rotatePerCycle * phase | |
val shader = remember { RuntimeShader(YY_SHADER) } | |
Canvas(modifier.fillMaxSize()) { | |
shader.setFloatUniform("resolution", size.width, size.height) | |
shader.setFloatUniform("progress", progress) | |
shader.setFloatUniform("rotation", rotation) | |
drawRect(brush = ShaderBrush(shader), size = Size(size.width, size.height)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment