Last active
February 13, 2026 10:35
-
-
Save volodymyr-sch/12b5f43e880d33b0fc3023401f037018 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
| val ChaosDark = Color(0xFF0D0D0D) | |
| @RequiresApi(Build.VERSION_CODES.TIRAMISU) | |
| @Composable | |
| fun ChaosButton( | |
| text: String, | |
| modifier: Modifier = Modifier, | |
| useV2: Boolean = true, | |
| backgroundColor: Color = ChaosDark, | |
| textColor: Color = Color.White, | |
| buttonWidth: Dp = 300.dp, | |
| buttonHeight: Dp = 84.dp | |
| ) { | |
| val shader = remember { RuntimeShader(CHAOS_BUTTON_SHADER) } | |
| val interactionSource = remember { MutableInteractionSource() } | |
| val isPressed by interactionSource.collectIsPressedAsState() | |
| var pressSeed by remember { mutableIntStateOf(0) } | |
| var wasPressed by remember { mutableStateOf(false) } | |
| val scale by animateFloatAsState( | |
| targetValue = if (isPressed) 1.15f else 1.0f, | |
| animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) | |
| ) | |
| val infiniteTransition = rememberInfiniteTransition() | |
| val time by infiniteTransition.animateFloat( | |
| initialValue = 0f, | |
| targetValue = 100f, | |
| animationSpec = infiniteRepeatable( | |
| animation = tween(durationMillis = 100000, easing = LinearEasing), | |
| repeatMode = RepeatMode.Restart | |
| ) | |
| ) | |
| LaunchedEffect(isPressed) { | |
| if (isPressed && !wasPressed) { | |
| pressSeed += 1 | |
| } | |
| wasPressed = isPressed | |
| } | |
| Box( | |
| modifier = modifier | |
| .width(buttonWidth) | |
| .height(buttonHeight) | |
| .graphicsLayer { | |
| scaleX = scale | |
| scaleY = scale | |
| } | |
| .clickable( | |
| interactionSource = interactionSource, | |
| indication = null, | |
| onClick = {} | |
| ), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Canvas( | |
| modifier = Modifier | |
| .width(buttonWidth) | |
| .height(buttonHeight) | |
| .drawWithCache { | |
| shader.setFloatUniform("iResolution", size.width, size.height) | |
| // Set background color | |
| shader.setFloatUniform( | |
| "backgroundColor", | |
| backgroundColor.red, | |
| backgroundColor.green, | |
| backgroundColor.blue, | |
| backgroundColor.alpha | |
| ) | |
| shader.setFloatUniform("iTime", time) | |
| shader.setFloatUniform("iPress", if (isPressed) 1f else 0f) | |
| shader.setFloatUniform("iPressSeed", pressSeed.toFloat()) | |
| shader.setFloatUniform("iVersion", if (useV2) 1f else 0f) | |
| val paint = Paint().apply { | |
| this.shader = shader | |
| } | |
| onDrawBehind { | |
| drawIntoCanvas { canvas -> | |
| canvas.drawRect( | |
| 0f, | |
| 0f, | |
| size.width, | |
| size.height, | |
| paint | |
| ) | |
| } | |
| } | |
| } | |
| ) { | |
| // Canvas drawing happens in drawWithCache | |
| } | |
| // Text overlay | |
| Text( | |
| text = text, | |
| color = textColor, | |
| fontSize = 16.sp, | |
| fontWeight = FontWeight.SemiBold, | |
| textAlign = TextAlign.Center | |
| ) | |
| } | |
| } | |
| @Language("AGSL") | |
| internal const val CHAOS_BUTTON_SHADER = """ | |
| uniform float2 iResolution; | |
| uniform half4 backgroundColor; | |
| uniform float iTime; | |
| uniform float iPress; | |
| uniform float iPressSeed; | |
| uniform float iVersion; | |
| float3 hash3(float3 p) { | |
| p = float3(dot(p, float3(127.1, 311.7, 74.7)), | |
| dot(p, float3(269.5, 183.3, 246.1)), | |
| dot(p, float3(113.5, 271.9, 124.6))); | |
| return -1.0 + 2.0 * fract(sin(p) * 43758.5453123); | |
| } | |
| float segmentDistance(float2 p, float2 a, float2 b) { | |
| float2 pa = p - a; | |
| float2 ba = b - a; | |
| float denom = max(dot(ba, ba), 1e-4); | |
| float h = clamp(dot(pa, ba) / denom, 0.0, 1.0); | |
| return length(pa - ba * h); | |
| } | |
| float2 bezier2(float2 a, float2 c, float2 b, float t) { | |
| float u = 1.0 - t; | |
| return u * u * a + 2.0 * u * t * c + t * t * b; | |
| } | |
| float curveDistanceWobble( | |
| float2 p, | |
| float2 a, | |
| float2 c, | |
| float2 b, | |
| float2 perp, | |
| float wobbleAmp, | |
| float wobbleFreq, | |
| float wobblePhase | |
| ) { | |
| float2 p0 = a; | |
| float2 p1 = bezier2(a, c, b, 0.125); | |
| float2 p2 = bezier2(a, c, b, 0.25); | |
| float2 p3 = bezier2(a, c, b, 0.375); | |
| float2 p4 = bezier2(a, c, b, 0.5); | |
| float2 p5 = bezier2(a, c, b, 0.625); | |
| float2 p6 = bezier2(a, c, b, 0.75); | |
| float2 p7 = bezier2(a, c, b, 0.875); | |
| float2 p8 = b; | |
| float phaseScale = 6.2831; | |
| float t1 = wobblePhase + wobbleFreq * 0.125 * phaseScale; | |
| float t2 = wobblePhase + wobbleFreq * 0.25 * phaseScale; | |
| float t3 = wobblePhase + wobbleFreq * 0.375 * phaseScale; | |
| float t4 = wobblePhase + wobbleFreq * 0.5 * phaseScale; | |
| float t5 = wobblePhase + wobbleFreq * 0.625 * phaseScale; | |
| float t6 = wobblePhase + wobbleFreq * 0.75 * phaseScale; | |
| float t7 = wobblePhase + wobbleFreq * 0.875 * phaseScale; | |
| p1 += perp * (sin(t1) * wobbleAmp); | |
| p2 += perp * (sin(t2) * wobbleAmp); | |
| p3 += perp * (sin(t3) * wobbleAmp); | |
| p4 += perp * (sin(t4) * wobbleAmp); | |
| p5 += perp * (sin(t5) * wobbleAmp); | |
| p6 += perp * (sin(t6) * wobbleAmp); | |
| p7 += perp * (sin(t7) * wobbleAmp); | |
| float d0 = segmentDistance(p, p0, p1); | |
| float d1 = segmentDistance(p, p1, p2); | |
| float d2 = segmentDistance(p, p2, p3); | |
| float d3 = segmentDistance(p, p3, p4); | |
| float d4 = segmentDistance(p, p4, p5); | |
| float d5 = segmentDistance(p, p5, p6); | |
| float d6 = segmentDistance(p, p6, p7); | |
| float d7 = segmentDistance(p, p7, p8); | |
| return min(min(min(d0, d1), min(d2, d3)), min(min(d4, d5), min(d6, d7))); | |
| } | |
| float3 neonPalette(float t) { | |
| float3 red = float3(1.0, 0.2, 0.2); | |
| float3 green = float3(0.2, 1.0, 0.45); | |
| float3 navy = float3(0.05, 0.12, 0.45); | |
| float3 deepBlue = float3(0.08, 0.18, 0.7); | |
| float3 indigo = float3(0.18, 0.12, 0.85); | |
| float3 violet = float3(0.65, 0.25, 0.95); | |
| float3 pink = float3(1.0, 0.3, 0.75); | |
| float3 c = red; | |
| c = mix(c, green, step(0.16, t)); | |
| c = mix(c, navy, step(0.33, t)); | |
| c = mix(c, deepBlue, step(0.5, t)); | |
| c = mix(c, indigo, step(0.66, t)); | |
| c = mix(c, violet, step(0.78, t)); | |
| c = mix(c, pink, step(0.9, t)); | |
| return c; | |
| } | |
| float3 coolPalette(float t) { | |
| float3 cyan = float3(0.1, 0.9, 1.0); | |
| float3 teal = float3(0.05, 0.7, 0.9); | |
| float3 blue = float3(0.1, 0.35, 0.95); | |
| float3 deepBlue = float3(0.05, 0.18, 0.55); | |
| float3 sky = float3(0.4, 0.85, 1.0); | |
| float3 c = cyan; | |
| c = mix(c, teal, step(0.2, t)); | |
| c = mix(c, blue, step(0.45, t)); | |
| c = mix(c, deepBlue, step(0.7, t)); | |
| c = mix(c, sky, step(0.88, t)); | |
| return c; | |
| } | |
| float3 warmPalette(float t) { | |
| float3 gold = float3(1.0, 0.75, 0.2); | |
| float3 orange = float3(1.0, 0.45, 0.15); | |
| float3 coral = float3(1.0, 0.25, 0.35); | |
| float3 magenta = float3(0.95, 0.15, 0.65); | |
| float3 pink = float3(1.0, 0.35, 0.75); | |
| float3 c = gold; | |
| c = mix(c, orange, step(0.18, t)); | |
| c = mix(c, coral, step(0.42, t)); | |
| c = mix(c, magenta, step(0.68, t)); | |
| c = mix(c, pink, step(0.85, t)); | |
| return c; | |
| } | |
| float3 pickPalette(float t, float palettePick) { | |
| if (palettePick < 0.5) { | |
| return neonPalette(t); | |
| } | |
| if (palettePick < 1.5) { | |
| return coolPalette(t); | |
| } | |
| return warmPalette(t); | |
| } | |
| // SDF | |
| float stadiumSDF(float2 p, float2 size) { | |
| float radius = size.y; | |
| float halfWidth = size.x - radius; | |
| p.x = abs(p.x) - halfWidth; | |
| p.x = max(p.x, 0.0); | |
| return length(p) - radius; | |
| } | |
| float starfield(float2 uv, float radius) { | |
| float2 cellCoord = fract(uv) - 0.5; | |
| float2 cellID = floor(uv); | |
| float3 cellHashValue = hash3(float3(cellID, 1.0)); | |
| float2 starPosition = cellHashValue.xy * (0.5 - radius * 16.0); | |
| float baseBrightness = clamp(cellHashValue.z, 0.0, 1.0); | |
| float brightMask = smoothstep(0.85, 0.95, baseBrightness); | |
| float starBrightness = pow(baseBrightness, 0.5) * mix(1.0, 2.0, brightMask); | |
| float d = length(cellCoord - starPosition); | |
| float radiusBoost = mix(1.0, 1.8, brightMask); | |
| float glow = exp(-1.2 * d / (radius * radiusBoost)); | |
| float coreGlow = exp(-3.0 * d / (radius * 0.6)) * brightMask * 0.8; | |
| return clamp((glow + coreGlow) * starBrightness, 0.0, 1.0); | |
| } | |
| float3 curveOverlay(float2 uv, float time, float aspectRatio, float pressSeed) { | |
| float timeScaled = time * 4.0; | |
| float frame = floor(timeScaled); | |
| float frameNext = frame + 1.0; | |
| float tBlend = fract(timeScaled); | |
| float blend = tBlend * tBlend * (3.0 - 2.0 * tBlend); | |
| float3 pressSeedHash = hash3(float3(pressSeed, 91.0, 7.0)); | |
| float countRand = clamp(pressSeedHash.x * 0.5 + 0.5, 0.0, 1.0); | |
| float count = floor(12.0 + 8.0 * countRand); | |
| count = min(count, 20.0); | |
| float paletteRand = clamp(pressSeedHash.y * 0.5 + 0.5, 0.0, 1.0); | |
| float palettePick = min(floor(paletteRand * 3.0), 2.0); | |
| float2 uvAspect = float2((uv.x - 0.5) * aspectRatio + 0.5, uv.y); | |
| float2 center = float2(0.5, 0.5); | |
| float2 box = float2(0.5 * aspectRatio, 0.5); | |
| float3 acc = float3(0.0); | |
| for (int i = 0; i < 20; ++i) { | |
| float fi = float(i); | |
| float active = step(fi, count - 0.5); | |
| if (active <= 0.0) { | |
| continue; | |
| } | |
| float3 seedA = hash3(float3(frame, fi, 11.0)); | |
| float3 seedB = hash3(float3(frameNext, fi, 11.0)); | |
| float3 seed = mix(seedA, seedB, blend); | |
| float3 seed2A = hash3(float3(frame, fi, 23.0)); | |
| float3 seed2B = hash3(float3(frameNext, fi, 23.0)); | |
| float3 seed2 = mix(seed2A, seed2B, blend); | |
| float3 seed3A = hash3(float3(frame, fi, 37.0)); | |
| float3 seed3B = hash3(float3(frameNext, fi, 37.0)); | |
| float3 seed3 = mix(seed3A, seed3B, blend); | |
| float rAngle = clamp(seed.x * 0.5 + 0.5, 0.0, 1.0); | |
| float rCurve = clamp(seed.y * 0.5 + 0.5, 0.0, 1.0); | |
| float rWidth = clamp(seed.z * 0.5 + 0.5, 0.0, 1.0); | |
| float rOffsetX = clamp(seed2.x * 0.5 + 0.5, 0.0, 1.0); | |
| float rOffsetY = clamp(seed2.y * 0.5 + 0.5, 0.0, 1.0); | |
| float rPalette = clamp(seed2.z * 0.5 + 0.5, 0.0, 1.0); | |
| float rWobbleFreq = clamp(seed3.x * 0.5 + 0.5, 0.0, 1.0); | |
| float rWobbleAmp = clamp(seed3.y * 0.5 + 0.5, 0.0, 1.0); | |
| float rCircle = clamp(seed3.z * 0.5 + 0.5, 0.0, 1.0); | |
| float angle = rAngle * 6.2831; | |
| float2 dir = float2(cos(angle), sin(angle)); | |
| float2 perp = float2(-dir.y, dir.x); | |
| float2 baseOffset = float2(rOffsetX - 0.5, rOffsetY - 0.5); | |
| float2 offsetDir = normalize(baseOffset + float2(1e-4, 1e-4)); | |
| float outward = mix(0.08, 0.22, rWobbleFreq); | |
| float2 offset = baseOffset * float2(0.35 * aspectRatio, 0.35) | |
| + offsetDir * outward * float2(aspectRatio, 1.0); | |
| float2 c0 = center + offset; | |
| float2 dirAbs = max(abs(dir), float2(1e-3)); | |
| float tMax = min(box.x / dirAbs.x, box.y / dirAbs.y); | |
| float2 a = c0 - dir * tMax * 1.5; | |
| float2 b = c0 + dir * tMax * 1.5; | |
| float curveBias = mix(rCurve, 0.5, 0.2); | |
| float curveRand = mix(0.8, 1.2, rWobbleAmp); | |
| float curvature = (curveBias - 0.5) * 3.2 * curveRand; | |
| float2 c = c0 + perp * curvature * min(box.x, box.y); | |
| float wobbleAmp = mix(0.02, 0.09, rWobbleAmp) * min(box.x, box.y); | |
| float wobbleFreq = mix(0.8, 2.8, rWobbleFreq); | |
| float wobblePhase = rOffsetX * 6.2831 + rOffsetY * 3.1415; | |
| float d = 0.0; | |
| if (rCircle > 0.78) { | |
| float radius = mix(min(box.x, box.y) * 0.8, max(box.x, box.y) * 1.8, curveBias); | |
| float2 circleJitter = float2(rOffsetX - 0.5, rOffsetY - 0.5) * float2(0.18 * aspectRatio, 0.18); | |
| d = abs(length(uvAspect - (c0 + circleJitter)) - radius); | |
| } else { | |
| d = curveDistanceWobble(uvAspect, a, c, b, perp, wobbleAmp, wobbleFreq, wobblePhase); | |
| } | |
| float width = mix(0.0035, 0.012, rWidth); | |
| float glowStrength = 0.2; | |
| float bloomStrength = 0.4; | |
| float coreStrength = 1.0; | |
| float glow = exp(-2.0 * d / width) * 1.6 * glowStrength; | |
| float halo = exp(-4.5 * d / (width * 2.4)) * 1.8 * glowStrength; | |
| float core = exp(-7.0 * d / width) * 2.0 * coreStrength; | |
| float bloom = exp(-2.0 * d / (width * 4.8)) * 1.4 * bloomStrength; | |
| float3 neon = pickPalette(rPalette, palettePick); | |
| acc += neon * (core + glow + halo + bloom) * 2.0 * active; | |
| } | |
| return acc; | |
| } | |
| half4 main(float2 fragCoord) { | |
| float2 uv = fragCoord / iResolution; | |
| float2 center = float2(0.5); | |
| float aspectRatio = iResolution.x / iResolution.y; | |
| float2 p = uv - center; | |
| p.x *= aspectRatio; | |
| float margin = 0.08; | |
| float2 size = float2((0.5 - margin) * aspectRatio, 0.5 - margin * 2.0); | |
| float dist = stadiumSDF(p, size); | |
| // Anti-aliasing | |
| float pixelSize = 1.0 / min(iResolution.x, iResolution.y); | |
| float aa = pixelSize * 2.0; | |
| // Inside: backgroundColor + stars, Outside: transparent | |
| float alpha = smoothstep(aa, -aa, dist); | |
| float3 color = backgroundColor.rgb; | |
| if (dist < 0.0) { | |
| // Scale UV for star density (more = denser stars) | |
| float2 starUV = (uv + iTime * float2(0.1, 0.05)) * float2(45.0, 22.5); | |
| float stars = starfield(starUV, 0.05); | |
| color = mix(backgroundColor.rgb, float3(1.0), stars); | |
| } | |
| // Add white glow border at the edge (-0.02 to +0.02 from SDF=0) | |
| float borderGlow = 1.0 - smoothstep(0.0, 0.02, abs(dist)); | |
| color = mix(color, float3(1.0), borderGlow * 0.5); | |
| if (dist < 0.0) { | |
| float3 lines = float3(0.0); | |
| lines = curveOverlay(uv, iTime, aspectRatio, iPressSeed); | |
| color = clamp(color + lines * iPress, 0.0, 1.0); | |
| } | |
| return half4(color, alpha); | |
| } | |
| """ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment