Skip to content

Instantly share code, notes, and snippets.

@moshenskyi
Created June 7, 2026 08:56
Show Gist options
  • Select an option

  • Save moshenskyi/5f90cefb50ebce787bbba014b0e5c2aa to your computer and use it in GitHub Desktop.

Select an option

Save moshenskyi/5f90cefb50ebce787bbba014b0e5c2aa to your computer and use it in GitHub Desktop.
AGSL Lens Effect Shader
/*
* Copyright 2026 Nazarii Moshenskyi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.graphics.RenderEffect
import android.graphics.RuntimeShader
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.intellij.lang.annotations.Language
@Language("AGSL")
private const val MAGNIFIER_SHADER =
"""
uniform shader composable;
uniform float2 center;
uniform float radius;
const float LENS_BORDER_WIDTH = 36.0;
const float ANTI_ALIASING_DIST = 2.0;
half4 mixGlass(float2 fragCoord, half4 original) {
float2 direction = fragCoord - center;
float dist = length(direction);
if (dist > radius) {
return original;
}
float normalized = dist / radius;
float lens = 1.0 - normalized * normalized;
float2 uv = center + direction / (1.0 + 0.7 * lens);
half4 color = composable.eval(uv);
float edgeFade = 1.0 - smoothstep(radius - ANTI_ALIASING_DIST, radius, dist);
return mix(original, color, half(edgeFade));
}
float handleMask(float2 fragCoord, float2 start, float2 end, float radius) {
float2 pixelOffsetFromStart = fragCoord - start;
float2 handleVector = end - start;
float projectionOnHandle = dot(pixelOffsetFromStart, handleVector);
float handleLengthSquared = dot(handleVector, handleVector);
float normalizedPositionOnHandle = projectionOnHandle / handleLengthSquared;
float clampedPositionOnHandle = clamp(
normalizedPositionOnHandle,
0.0,
1.0
);
float dist = length(pixelOffsetFromStart - handleVector * clampedPositionOnHandle);
float projection = dot(pixelOffsetFromStart, normalize(handleVector));
float capsule = 1.0 - smoothstep(
radius,
radius + ANTI_ALIASING_DIST,
dist
);
capsule *= step(0.0, projection);
return capsule;
}
half4 blackHandleColor(float2 p, float2 a, float2 b, float width) {
float2 dir = normalize(b - a);
float2 normal = float2(-dir.y, dir.x);
float side = dot(p - a, normal) / width;
float t = clamp(dot(p - a, dir) / length(b - a), 0.0, 1.0);
half4 black = half4(0.06, 0.06, 0.07, 1.0);
half4 gray = half4(0.34, 0.34, 0.37, 1.0);
float highlight = smoothstep(-0.35, 0.75, side) * (1.0 - smoothstep(0.9, 1.0, t));
return mix(black, gray, half(highlight));
}
half4 main(float2 fragCoord) {
half4 original = composable.eval(fragCoord);
half4 lensColor = mixGlass(fragCoord, original);
float dist = length(fragCoord - center);
float border = smoothstep(radius - LENS_BORDER_WIDTH, radius, dist)
* (1.0 - smoothstep(radius, radius + ANTI_ALIASING_DIST, dist));
lensColor = mix(lensColor, half4(1.0), half(border));
float2 dir = normalize(float2(1.0, 1.0));
float2 handleStart = center + dir * radius;
float2 handleEnd = handleStart + float2(radius * 0.5, radius * 0.5);
float handle = handleMask(
fragCoord,
handleStart,
handleEnd,
14.0
);
half4 handleColor = blackHandleColor(fragCoord, handleStart, handleEnd, 14.0);
lensColor = mix(lensColor, handleColor, half(handle));
return lensColor;
}
"""
@Composable
fun LensEffectSample(modifier: Modifier) {
Box(
modifier = modifier.background(Color(0xFF2A2929)),
contentAlignment = Alignment.Center
) {
DraggableLensText()
}
}
@Composable
private fun DraggableLensText() {
val shader = remember { RuntimeShader(MAGNIFIER_SHADER) }
var lensCenter by remember { mutableStateOf(Offset.Unspecified) }
val density = LocalDensity.current
val radiusPx = remember(density) {
with(density) { 100.dp.toPx() }
}
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
lensCenter = offset
},
onDrag = { change, _ ->
lensCenter = change.position
}
)
}
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
val center = if (lensCenter == Offset.Unspecified) {
Offset(size.width / 2, size.height / 2)
} else lensCenter
shader.setFloatUniform("center", center.x, center.y)
shader.setFloatUniform("radius", radiusPx)
renderEffect = RenderEffect
.createRuntimeShaderEffect(shader, "composable")
.asComposeRenderEffect()
},
contentAlignment = Alignment.Center
) {
Text(
text = "Magnify this text",
color = Color.White,
fontSize = 36.sp,
fontWeight = FontWeight.Bold
)
}
}
@Preview
@Composable
fun LensEffectPreview() {
LensEffectSample(modifier = Modifier.fillMaxSize())
}
@moshenskyi

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment