Created
July 20, 2026 20:03
-
-
Save Syedwaheed/39126565f50cfc2011d8ebb98f916ffa 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
| import androidx.compose.animation.core.Animatable | |
| import androidx.compose.animation.core.CubicBezierEasing | |
| import androidx.compose.animation.core.LinearEasing | |
| 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.background | |
| import androidx.compose.foundation.gestures.detectDragGestures | |
| import androidx.compose.foundation.gestures.detectTapGestures | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.Column | |
| import androidx.compose.foundation.layout.Spacer | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableFloatStateOf | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.runtime.withFrameNanos | |
| 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.geometry.Size | |
| import androidx.compose.ui.graphics.BlendMode | |
| import androidx.compose.ui.graphics.Brush | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.graphics.CompositingStrategy | |
| import androidx.compose.ui.graphics.lerp as lerpColor | |
| import androidx.compose.ui.graphics.Path | |
| import androidx.compose.ui.graphics.StrokeCap | |
| import androidx.compose.ui.graphics.drawscope.DrawScope | |
| import androidx.compose.ui.graphics.drawscope.Stroke | |
| import androidx.compose.ui.graphics.graphicsLayer | |
| import androidx.compose.ui.input.pointer.pointerInput | |
| import androidx.compose.ui.text.style.TextAlign | |
| import androidx.compose.ui.tooling.preview.Preview | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.unit.sp | |
| import androidx.compose.ui.util.lerp | |
| import kotlinx.coroutines.channels.Channel | |
| import kotlinx.coroutines.delay | |
| import kotlinx.coroutines.withTimeoutOrNull | |
| import kotlin.math.PI | |
| import kotlin.math.abs | |
| import kotlin.math.cos | |
| import kotlin.math.min | |
| import kotlin.math.pow | |
| import kotlin.math.sin | |
| import kotlin.math.sqrt | |
| import kotlin.random.Random | |
| private val NightTop = Color(0xFF04060E) | |
| private val NightHorizon = Color(0xFF101528) | |
| private val TowerGold = Color(0xFFFFC66B) | |
| private val TowerChampagne = Color(0xFFFFE9C4) | |
| private val BeamWarm = Color(0xFFFFE3B0) | |
| private val SparkleWhite = Color(0xFFFFF6E0) | |
| @Composable | |
| fun EiffelTowerTorch() { | |
| val sweep = remember { Animatable(0f) } | |
| val reveal = remember { Animatable(0f) } | |
| val shatter = remember { Animatable(0f) } | |
| var shatterOrigin by remember { mutableStateOf<Offset?>(null) } | |
| val taps = remember { Channel<Offset>(Channel.CONFLATED) } | |
| var dragPos by remember { mutableStateOf<Offset?>(null) } | |
| var hasDragged by remember { mutableStateOf(false) } | |
| var time by remember { mutableFloatStateOf(0f) } | |
| LaunchedEffect(Unit) { | |
| val start = withFrameNanos { it } | |
| while (true) withFrameNanos { time = (it - start) / 1e9f } | |
| } | |
| // One show cycle: sweep the torch down, reveal the whole tower, then hold. | |
| // A tap during the hold shatters the tower into its lattice pieces; either | |
| // way the lights go out and the torch comes back to rebuild it. | |
| LaunchedEffect(Unit) { | |
| val sweepEasing = CubicBezierEasing(0.45f, 0f, 0.55f, 1f) | |
| while (true) { | |
| shatter.snapTo(0f) | |
| reveal.snapTo(0f) | |
| sweep.snapTo(0f) | |
| sweep.animateTo(1f, tween(6000, easing = sweepEasing)) | |
| reveal.animateTo(1f, tween(500)) | |
| while (taps.tryReceive().isSuccess) Unit // drop taps landed before the hold | |
| val tap = withTimeoutOrNull(2800) { taps.receive() } | |
| if (tap == null) { | |
| reveal.animateTo(0f, tween(1400)) | |
| } else { | |
| shatterOrigin = tap | |
| reveal.snapTo(0f) | |
| shatter.animateTo(1f, tween(1150, easing = LinearEasing)) | |
| } | |
| delay(400) | |
| } | |
| } | |
| val stars = remember { List(90) { NightStar.random() } } | |
| val sparkles = remember { List(160) { Sparkle.random() } } | |
| val artCache = remember { TowerArtCache() } | |
| val hintAlpha by rememberInfiniteTransition(label = "hint").animateFloat( | |
| initialValue = 0.25f, targetValue = 0.7f, | |
| animationSpec = infiniteRepeatable(tween(900), RepeatMode.Reverse), | |
| label = "hintAlpha", | |
| ) | |
| Box( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .background(NightTop) | |
| .pointerInput(Unit) { | |
| detectDragGestures( | |
| onDragStart = { dragPos = it; hasDragged = true }, | |
| onDragEnd = { dragPos = null }, | |
| onDragCancel = { dragPos = null }, | |
| ) { change, _ -> | |
| change.consume() | |
| dragPos = change.position | |
| } | |
| } | |
| .pointerInput(Unit) { | |
| detectTapGestures { pos -> | |
| if (reveal.value > 0.99f && shatter.value == 0f) taps.trySend(pos) | |
| } | |
| }, | |
| ) { | |
| // Ambient layer: sky, stars, moon and a barely-visible tower silhouette. | |
| Canvas(modifier = Modifier.fillMaxSize()) { | |
| val art = artCache.get(size) | |
| val torch = dragPos ?: autoTorchPosition(sweep.value, art) | |
| val beamRadius = min(size.width, size.height) * 0.36f | |
| drawRect(Brush.verticalGradient(0f to NightTop, 1f to NightHorizon)) | |
| drawStars(stars, time) | |
| drawMoon() | |
| // Faint city glow behind the tower base. | |
| drawCircle( | |
| brush = Brush.radialGradient( | |
| 0f to TowerGold.copy(alpha = 0.06f), 1f to Color.Transparent, | |
| center = Offset(size.width / 2f, art.groundY), radius = size.width * 0.8f, | |
| ), | |
| center = Offset(size.width / 2f, art.groundY), | |
| radius = size.width * 0.8f, | |
| ) | |
| drawTower(art, TowerGold, alpha = 0.055f) | |
| // The torch light itself, spilling past the tower. | |
| val glowAlpha = 0.12f * (1f - reveal.value) | |
| if (glowAlpha > 0.005f) { | |
| drawCircle( | |
| brush = Brush.radialGradient( | |
| 0f to BeamWarm.copy(alpha = glowAlpha), | |
| 1f to Color.Transparent, | |
| center = torch, radius = beamRadius * 1.3f, | |
| ), | |
| center = torch, | |
| radius = beamRadius * 1.3f, | |
| ) | |
| } | |
| } | |
| // Lit layer: the golden tower, masked to the torch beam (or fully shown during the finale). | |
| Canvas( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }, | |
| ) { | |
| // While the tower is in pieces the debris layer owns the pixels. | |
| if (shatter.value > 0f) return@Canvas | |
| val art = artCache.get(size) | |
| val torch = dragPos ?: autoTorchPosition(sweep.value, art) | |
| val beamRadius = min(size.width, size.height) * 0.36f | |
| // Brightness surge: as the full tower reveals, the gold warms up toward | |
| // champagne white and the glow pass intensifies — "lights at full power". | |
| val surge = reveal.value | |
| drawTower( | |
| art = art, | |
| color = lerpColor(TowerGold, TowerChampagne, surge), | |
| alpha = 1f, | |
| glow = true, | |
| glowStrength = 1f + surge * 0.9f, | |
| ) | |
| if (surge > 0.01f) drawSparkles(sparkles, art, time, surge) | |
| // Keep the layer where the beam is; outside it, keep only `reveal` worth of alpha. | |
| drawRect( | |
| brush = Brush.radialGradient( | |
| 0f to Color.White, | |
| 0.55f to Color.White, | |
| 1f to Color.White.copy(alpha = reveal.value), | |
| center = torch, radius = beamRadius, | |
| ), | |
| blendMode = BlendMode.DstIn, | |
| ) | |
| } | |
| // Shatter layer: the tower broken into its lattice pieces, thrown from | |
| // the tap point and tumbling under gravity. | |
| if (shatter.value > 0f && shatter.value < 1f) { | |
| Canvas(modifier = Modifier.fillMaxSize()) { | |
| val art = artCache.get(size) | |
| val origin = shatterOrigin ?: Offset(art.cx, art.yAt(0.4f)) | |
| drawShatteredTower(art, origin, shatter.value) | |
| } | |
| } | |
| Column( | |
| modifier = Modifier | |
| .align(Alignment.BottomCenter) | |
| .padding(bottom = 40.dp), | |
| horizontalAlignment = Alignment.CenterHorizontally, | |
| ) { | |
| val towerFull = reveal.value > 0.99f && shatter.value == 0f | |
| if (towerFull) { | |
| Text( | |
| text = "✦ tap the tower ✦", | |
| color = BeamWarm.copy(alpha = hintAlpha), | |
| fontSize = 12.sp, | |
| textAlign = TextAlign.Center, | |
| ) | |
| Spacer(Modifier.height(14.dp)) | |
| } else if (!hasDragged) { | |
| Text( | |
| text = "✦ drag the light ✦", | |
| color = BeamWarm.copy(alpha = hintAlpha), | |
| fontSize = 12.sp, | |
| textAlign = TextAlign.Center, | |
| ) | |
| Spacer(Modifier.height(14.dp)) | |
| } | |
| Text( | |
| text = "LA TOUR EIFFEL", | |
| color = TowerGold.copy(alpha = 0.75f), | |
| fontSize = 14.sp, | |
| letterSpacing = 6.sp, | |
| textAlign = TextAlign.Center, | |
| ) | |
| Spacer(Modifier.height(4.dp)) | |
| Text( | |
| text = "Paris · Jetpack Compose Canvas", | |
| color = Color.White.copy(alpha = 0.35f), | |
| fontSize = 10.sp, | |
| letterSpacing = 2.sp, | |
| textAlign = TextAlign.Center, | |
| ) | |
| } | |
| } | |
| } | |
| /* ---------------------------- torch path ---------------------------- */ | |
| private fun autoTorchPosition(sweep: Float, art: TowerArt): Offset { | |
| val overshoot = art.towerH * 0.12f | |
| val y = art.topY - overshoot + sweep * (art.groundY - art.topY + 2 * overshoot) | |
| // Slight hand-held sway while descending. | |
| val x = art.cx + sin(sweep * 7f) * art.baseHalf * 0.45f | |
| return Offset(x, y) | |
| } | |
| /* ---------------------------- tower geometry ---------------------------- */ | |
| private class TowerArt( | |
| val size: Size, | |
| val strokes: Path, | |
| val decks: Path, | |
| val pieces: List<TowerPiece>, | |
| val cx: Float, | |
| val topY: Float, | |
| val groundY: Float, | |
| val towerH: Float, | |
| val baseHalf: Float, | |
| private val flare: Float, | |
| val archInnerHalf: Float, | |
| val archTopY: Float, | |
| ) { | |
| fun halfWidthAt(t: Float) = towerH * (0.012f + flare * t.pow(2.6f)) | |
| fun yAt(t: Float) = topY + t * towerH | |
| } | |
| private class TowerArtCache { | |
| private var art: TowerArt? = null | |
| fun get(size: Size): TowerArt = | |
| art?.takeIf { it.size == size } ?: buildTowerArt(size).also { art = it } | |
| } | |
| private fun buildTowerArt(size: Size): TowerArt { | |
| val w = size.width | |
| val h = size.height | |
| val cx = w / 2f | |
| val groundY = h * 0.865f | |
| val topY = h * 0.175f | |
| val towerH = groundY - topY | |
| val baseHalf = min(0.26f * towerH, 0.42f * w) | |
| val flare = baseHalf / towerH - 0.012f | |
| fun half(t: Float) = towerH * (0.012f + flare * t.pow(2.6f)) | |
| fun y(t: Float) = topY + t * towerH | |
| val strokes = Path() | |
| val decks = Path() | |
| // Every drawn segment is also recorded as a piece, so a shatter can throw | |
| // the exact same geometry the intact tower is made of. | |
| val pieces = mutableListOf<TowerPiece>() | |
| val rnd = Random(7) | |
| fun piece(x1: Float, y1: Float, x2: Float, y2: Float, width: Float = 0f) { | |
| pieces += TowerPiece( | |
| a = Offset(x1, y1), b = Offset(x2, y2), width = width, | |
| jx = rnd.nextFloat() * 2f - 1f, | |
| jy = rnd.nextFloat() * 2f - 1f, | |
| spin = rnd.nextFloat() * 2f - 1f, | |
| ) | |
| } | |
| // Outer edges, sampled so the power-curve flare stays smooth. | |
| val steps = 72 | |
| val edgeChunk = 4 | |
| for (side in intArrayOf(-1, 1)) { | |
| for (i in 0..steps) { | |
| val t = i / steps.toFloat() | |
| val x = cx + side * half(t) | |
| if (i == 0) strokes.moveTo(x, y(t)) else strokes.lineTo(x, y(t)) | |
| } | |
| for (i in 0 until steps step edgeChunk) { | |
| val ta = i / steps.toFloat() | |
| val tb = (i + edgeChunk) / steps.toFloat() | |
| piece(cx + side * half(ta), y(ta), cx + side * half(tb), y(tb)) | |
| } | |
| } | |
| // Antenna spire. | |
| strokes.moveTo(cx, topY) | |
| strokes.lineTo(cx, h * 0.125f) | |
| val antennaMidY = (topY + h * 0.125f) / 2f | |
| piece(cx, topY, cx, antennaMidY) | |
| piece(cx, antennaMidY, cx, h * 0.125f) | |
| // Observation decks: top, second and first floors. | |
| for ((t, widthFactor, thickness) in listOf( | |
| Triple(0.055f, 1.9f, towerH * 0.016f), | |
| Triple(0.385f, 1.45f, towerH * 0.022f), | |
| Triple(0.685f, 1.32f, towerH * 0.026f), | |
| )) { | |
| val hw = half(t) * widthFactor | |
| decks.addRect(Rect(cx - hw, y(t) - thickness / 2f, cx + hw, y(t) + thickness / 2f)) | |
| val deckChunks = 4 | |
| for (k in 0 until deckChunks) { | |
| val x0 = cx - hw + 2f * hw * k / deckChunks | |
| val x1 = cx - hw + 2f * hw * (k + 1) / deckChunks | |
| piece(x0, y(t), x1, y(t), width = thickness) | |
| } | |
| } | |
| // Lattice X-bracing between the outer edges. | |
| fun braceBand(t0: Float, t1: Float, bands: Int) { | |
| for (k in 0 until bands) { | |
| val ta = t0 + (t1 - t0) * k / bands | |
| val tb = t0 + (t1 - t0) * (k + 1) / bands | |
| val ya = y(ta) | |
| val yb = y(tb) | |
| strokes.moveTo(cx - half(ta), ya); strokes.lineTo(cx + half(tb), yb) | |
| strokes.moveTo(cx + half(ta), ya); strokes.lineTo(cx - half(tb), yb) | |
| strokes.moveTo(cx - half(tb), yb); strokes.lineTo(cx + half(tb), yb) | |
| piece(cx - half(ta), ya, cx + half(tb), yb) | |
| piece(cx + half(ta), ya, cx - half(tb), yb) | |
| piece(cx - half(tb), yb, cx + half(tb), yb) | |
| } | |
| } | |
| braceBand(0.075f, 0.365f, 6) | |
| braceBand(0.405f, 0.662f, 4) | |
| braceBand(0.71f, 0.80f, 1) | |
| // The arch between the legs: top half of an ellipse resting on the ground line. | |
| val archInnerHalf = baseHalf * 0.52f | |
| val archTopY = y(0.80f) | |
| val archRy = groundY - archTopY | |
| strokes.arcTo( | |
| rect = Rect(cx - archInnerHalf, groundY - archRy, cx + archInnerHalf, groundY + archRy), | |
| startAngleDegrees = 180f, | |
| sweepAngleDegrees = 180f, | |
| forceMoveTo = true, | |
| ) | |
| val archSegs = 16 | |
| var prevArchX = cx - archInnerHalf | |
| var prevArchY = groundY | |
| for (i in 1..archSegs) { | |
| val ang = PI.toFloat() * (1f + i / archSegs.toFloat()) | |
| val ax = cx + archInnerHalf * cos(ang) | |
| val ay = groundY + archRy * sin(ang) | |
| piece(prevArchX, prevArchY, ax, ay) | |
| prevArchX = ax | |
| prevArchY = ay | |
| } | |
| fun archInnerX(yy: Float): Float { | |
| val n = 1f - ((groundY - yy) / archRy).pow(2) | |
| return archInnerHalf * sqrt(n.coerceAtLeast(0f)) | |
| } | |
| // Leg lattice between each outer edge and the arch. | |
| val legBands = 3 | |
| for (k in 0 until legBands) { | |
| val ta = 0.82f + 0.18f * k / legBands | |
| val tb = 0.82f + 0.18f * (k + 1) / legBands | |
| val ya = y(ta) | |
| val yb = y(tb) | |
| for (side in intArrayOf(-1, 1)) { | |
| strokes.moveTo(cx + side * half(ta), ya); strokes.lineTo(cx + side * archInnerX(yb), yb) | |
| strokes.moveTo(cx + side * archInnerX(ya), ya); strokes.lineTo(cx + side * half(tb), yb) | |
| piece(cx + side * half(ta), ya, cx + side * archInnerX(yb), yb) | |
| piece(cx + side * archInnerX(ya), ya, cx + side * half(tb), yb) | |
| } | |
| } | |
| // Ground line. | |
| strokes.moveTo(cx - baseHalf * 1.35f, groundY) | |
| strokes.lineTo(cx + baseHalf * 1.35f, groundY) | |
| val groundChunks = 8 | |
| val groundX0 = cx - baseHalf * 1.35f | |
| val groundSpan = baseHalf * 2.7f | |
| for (k in 0 until groundChunks) { | |
| piece( | |
| groundX0 + groundSpan * k / groundChunks, groundY, | |
| groundX0 + groundSpan * (k + 1) / groundChunks, groundY, | |
| ) | |
| } | |
| return TowerArt(size, strokes, decks, pieces, cx, topY, groundY, towerH, baseHalf, flare, archInnerHalf, archTopY) | |
| } | |
| private fun DrawScope.drawTower( | |
| art: TowerArt, | |
| color: Color, | |
| alpha: Float, | |
| glow: Boolean = false, | |
| glowStrength: Float = 1f, | |
| ) { | |
| val strokeWidth = (art.towerH * 0.0035f).coerceAtLeast(1.5f) | |
| if (glow) { | |
| drawPath( | |
| art.strokes, | |
| color, | |
| alpha = (alpha * 0.25f * glowStrength).coerceAtMost(0.5f), | |
| style = Stroke(strokeWidth * 4f, cap = StrokeCap.Round), | |
| ) | |
| } | |
| drawPath(art.strokes, color, alpha = alpha, style = Stroke(strokeWidth, cap = StrokeCap.Round)) | |
| drawPath(art.decks, color, alpha = alpha) | |
| } | |
| /* ---------------------------- shatter ---------------------------- */ | |
| private class TowerPiece( | |
| val a: Offset, | |
| val b: Offset, | |
| val width: Float, // 0 = use the tower stroke width | |
| val jx: Float, | |
| val jy: Float, | |
| val spin: Float, | |
| ) | |
| private fun smoothstep(edge0: Float, edge1: Float, x: Float): Float { | |
| val t = ((x - edge0) / (edge1 - edge0)).coerceIn(0f, 1f) | |
| return t * t * (3f - 2f * t) | |
| } | |
| private fun DrawScope.drawShatteredTower(art: TowerArt, origin: Offset, progress: Float) { | |
| val t = progress * 1.15f // virtual seconds since the break | |
| val gravity = art.towerH * 2.4f | |
| val falloff = art.towerH * 0.35f | |
| val jitter = art.towerH * 0.18f | |
| val strokeWidth = (art.towerH * 0.0035f).coerceAtLeast(1.5f) | |
| // Pieces cool from champagne white back down to gold as they fall. | |
| val color = lerpColor(TowerChampagne, TowerGold, progress) | |
| // Impact flash at the tap point. | |
| if (progress < 0.3f) { | |
| val k = progress / 0.3f | |
| val flashRadius = art.towerH * (0.06f + 0.28f * k) | |
| drawCircle( | |
| brush = Brush.radialGradient( | |
| 0f to SparkleWhite.copy(alpha = 0.45f * (1f - k)), 1f to Color.Transparent, | |
| center = origin, radius = flashRadius, | |
| ), | |
| center = origin, | |
| radius = flashRadius, | |
| ) | |
| } | |
| for (p in art.pieces) { | |
| val fade = 1f - smoothstep(0.5f + 0.25f * abs(p.jy), 1f, progress) | |
| if (fade <= 0.01f) continue | |
| val mid = Offset((p.a.x + p.b.x) / 2f, (p.a.y + p.b.y) / 2f) | |
| // Radial impulse from the tap point: nearby pieces fly hard, distant | |
| // ones mostly just crumble and drop. | |
| val dx = mid.x - origin.x | |
| val dy = mid.y - origin.y | |
| val dist = sqrt(dx * dx + dy * dy).coerceAtLeast(1f) | |
| val burst = art.towerH * 1.2f / (1f + dist / falloff) | |
| val vx = dx / dist * burst + p.jx * jitter | |
| val vy = dy / dist * burst + p.jy * jitter | |
| val shiftX = vx * t | |
| val shiftY = vy * t + 0.5f * gravity * t * t | |
| val angle = p.spin * 6f * t | |
| val ca = cos(angle) | |
| val sa = sin(angle) | |
| fun place(pt: Offset): Offset { | |
| val rx = pt.x - mid.x | |
| val ry = pt.y - mid.y | |
| return Offset( | |
| mid.x + shiftX + rx * ca - ry * sa, | |
| mid.y + shiftY + rx * sa + ry * ca, | |
| ) | |
| } | |
| val a = place(p.a) | |
| val b = place(p.b) | |
| val w = if (p.width > 0f) p.width else strokeWidth | |
| drawLine(color, a, b, strokeWidth = w * 3.5f, cap = StrokeCap.Round, alpha = fade * 0.18f) | |
| drawLine(color, a, b, strokeWidth = w, cap = StrokeCap.Round, alpha = fade) | |
| } | |
| // Hot sparks thrown from the impact point. | |
| if (progress < 0.5f) { | |
| val sparkFade = 1f - progress / 0.5f | |
| for (i in 0 until 14) { | |
| val ang = i * 2.4f // golden-angle spacing scatters directions evenly | |
| val speed = art.towerH * (0.5f + 0.45f * ((i * 37) % 10) / 10f) | |
| val sx = origin.x + cos(ang) * speed * t | |
| val sy = origin.y + sin(ang) * speed * t + 0.35f * gravity * t * t | |
| drawCircle(SparkleWhite.copy(alpha = sparkFade * 0.5f), radius = 3.5f, center = Offset(sx, sy)) | |
| drawCircle(SparkleWhite.copy(alpha = sparkFade), radius = 1.5f, center = Offset(sx, sy)) | |
| } | |
| } | |
| } | |
| /* ---------------------------- night sky ---------------------------- */ | |
| private class NightStar(val x: Float, val y: Float, val radius: Float, val phase: Float, val speed: Float) { | |
| companion object { | |
| fun random() = NightStar( | |
| x = Random.nextFloat(), | |
| y = Random.nextFloat() * 0.55f, | |
| radius = 0.6f + Random.nextFloat() * 1.3f, | |
| phase = Random.nextFloat() * 6.28f, | |
| speed = 0.4f + Random.nextFloat() * 1.4f, | |
| ) | |
| } | |
| } | |
| private fun DrawScope.drawStars(stars: List<NightStar>, time: Float) { | |
| for (star in stars) { | |
| val twinkle = 0.35f + 0.65f * (0.5f + 0.5f * sin(time * star.speed + star.phase)) | |
| drawCircle( | |
| color = Color.White.copy(alpha = 0.35f * twinkle), | |
| radius = star.radius, | |
| center = Offset(star.x * size.width, star.y * size.height), | |
| ) | |
| } | |
| } | |
| private fun DrawScope.drawMoon() { | |
| val center = Offset(size.width * 0.82f, size.height * 0.10f) | |
| val radius = size.width * 0.045f | |
| drawCircle( | |
| brush = Brush.radialGradient( | |
| 0f to Color(0xFFE8ECF5).copy(alpha = 0.25f), 1f to Color.Transparent, | |
| center = center, radius = radius * 3.5f, | |
| ), | |
| center = center, | |
| radius = radius * 3.5f, | |
| ) | |
| drawCircle(Color(0xFFE8ECF5).copy(alpha = 0.8f), radius, center) | |
| } | |
| /* ---------------------------- sparkle finale ---------------------------- */ | |
| private class Sparkle(val t: Float, val u: Float, val phase: Float, val speed: Float) { | |
| companion object { | |
| fun random() = Sparkle( | |
| t = Random.nextFloat(), | |
| u = Random.nextFloat() * 2f - 1f, | |
| phase = Random.nextFloat() * 6.28f, | |
| speed = 5f + Random.nextFloat() * 7f, | |
| ) | |
| } | |
| } | |
| private fun DrawScope.drawSparkles(sparkles: List<Sparkle>, art: TowerArt, time: Float, reveal: Float) { | |
| for (sparkle in sparkles) { | |
| // Below the arch, push sparkles out onto the legs so none float in the empty archway. | |
| val u = if (sparkle.t > 0.80f) { | |
| val sign = if (sparkle.u < 0f) -1f else 1f | |
| sign * lerp(0.6f, 1f, kotlin.math.abs(sparkle.u)) | |
| } else { | |
| sparkle.u | |
| } | |
| val x = art.cx + u * art.halfWidthAt(sparkle.t) * 0.95f | |
| val y = art.yAt(sparkle.t) | |
| val flicker = (0.5f + 0.5f * sin(time * sparkle.speed + sparkle.phase)).pow(6) | |
| val alpha = flicker * reveal | |
| if (alpha < 0.02f) continue | |
| drawCircle(SparkleWhite.copy(alpha = alpha * 0.5f), radius = 4f, center = Offset(x, y)) | |
| drawCircle(SparkleWhite.copy(alpha = alpha), radius = 1.6f, center = Offset(x, y)) | |
| } | |
| } | |
| @Preview(showBackground = true, backgroundColor = 0xFF04060E, widthDp = 400, heightDp = 860) | |
| @Composable | |
| private fun EiffelTowerTorchPreview() { | |
| EiffelTowerTorch() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment