CRT Effect

CRT Effect

Subscribe for Live Previews, in your browser

$3 / month

Code

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Brush
import androidx.compose.material.icons.rounded.Dangerous
import androidx.compose.material.icons.rounded.Egg
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
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.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.withSaveLayer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import theme.Colors
import kotlin.random.Random

@Composable
fun CRTBox(
    modifier: Modifier = Modifier,
    flickerDelay: Int = 32,
    content: @Composable () -> Unit,
) {
    var shake by remember { mutableStateOf(Offset.Zero) }

    LaunchedEffect(Unit) {
        while (flickerDelay > 0) {
            shake = Offset(
                Random.nextInt(-1, 1) * Random.nextFloat(),
                Random.nextInt(-1, 1) * Random.nextFloat(),
            )
            delay(flickerDelay.toLong())
        }
    }

    val graphicsLayer = rememberGraphicsLayer()

    Box(
        modifier = modifier
            .graphicsLayer {
                translationX = shake.x
                translationY = shake.y
            }
    ) {
        Box(Modifier.drawWithContent {
            graphicsLayer.record { this@drawWithContent.drawContent() }
        }) {
            content()
        }

        val blurLayers = remember {
            listOf(
                Triple(5.dp, .3f, 1.02f to 1.03f),
                Triple(0.dp, .8f, 1f to 1f),
                Triple(1.dp, .9f, 1f to 1f),
                Triple(10.dp, .6f, 1.001f to 1f),
                Triple(40.dp, .7f, 1f to 1f),
            )
        }

        blurLayers.forEach { (blur, alpha, scale) ->
            Box(
                Modifier
                    .matchParentSize()
                    .blur(blur, BlurredEdgeTreatment.Unbounded)
                    .graphicsLayer {
                        scaleX = scale.first
                        scaleY = scale.second
                        this.alpha = alpha
                    }
                    .drawBehind {
                        layer {
                            drawLayer(graphicsLayer)
                            drawScanLines(alpha = 1f, blendMode = BlendMode.DstOut)
                        }
                    }
            )
        }
    }
}

private fun DrawScope.layer(
    bounds: Rect = size.toRect(),
    block: DrawScope.() -> Unit
) =
    drawIntoCanvas { canvas ->
        canvas.withSaveLayer(
            bounds = bounds,
            paint = Paint(),
        ) { block() }
    }


private fun DrawScope.drawScanLines(alpha: Float, blendMode: BlendMode) {
    val color = Colors.Black.copy(alpha = alpha)
    drawRect(
        brush = Brush.verticalGradient(
            0f to color,
            0.4f to color,
            0.4f to Colors.Transparent,
            1f to Colors.Transparent,
            tileMode = TileMode.Repeated,
            startY = 0f,
            endY = 10f,
        ),
        blendMode = blendMode
    )
    drawRect(
        brush = Brush.horizontalGradient(
            0f to color,
            0.1f to color,
            0.1f to Colors.Transparent,
            1f to Colors.Transparent,
            tileMode = TileMode.Repeated,
            startX = 0f,
            endX = 10f,
        ),
        blendMode = blendMode
    )
}



/// Example Implementation

@Composable
fun CRTEffectImpl() {
    CRTBox {
        Column(
            modifier = Modifier.size(400.dp)
        ) {
            Row(
                Modifier
                    .weight(.5f)
                    .fillMaxWidth()
            ) {
                GreenBox { }
                GreenBox { }
            }
            Row(
                Modifier
                    .weight(1f)
                    .fillMaxWidth()
            ) {
                GreenBox { Text("CRT Effect") }
            }
            Row(
                Modifier
                    .weight(1f)
                    .fillMaxWidth()
            ) {
                GreenBox { GreenIcon(Icons.Rounded.Brush) }
                GreenBox { GreenIcon(Icons.Rounded.Egg) }
                GreenBox { GreenIcon(Icons.Rounded.Dangerous) }
            }
        }
    }
}

@Composable
fun RowScope.GreenBox(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {
    val interaction = remember { MutableInteractionSource() }
    val isHovered by interaction.collectIsHoveredAsState()
    Box(
        modifier
            .hoverable(interactionSource = interaction)
            .fillMaxHeight()
            .weight(1f)
            .padding(8.dp)
            .border(
                width = 10.dp,
                color = Color.Green
            )
            .background(
                color = if (isHovered) Color.Green else Color.Transparent
            ),
        contentAlignment = Alignment.Center
    ) {
        CompositionLocalProvider(
            LocalTextStyle provides TextStyle(
                color = if (isHovered) Color.Black else Color.Green,
                fontSize = 36.sp,
                fontFamily = FontFamily.Monospace,
                fontWeight = FontWeight.Bold,
            ),
            LocalContentColor provides if (isHovered) Color.Black else Color.Green,
            content = content
        )
    }
}

@Composable
fun GreenIcon(icon: ImageVector) {
    Icon(
        imageVector = icon, contentDescription = null,
        Modifier.fillMaxSize(.6f)
    )
}
Mastodon