Last active
January 14, 2026 21:50
-
-
Save brkckr/c72e7c9a69df9d6e0022489614856895 to your computer and use it in GitHub Desktop.
Rotating 3D sphere animation in Jetpack Compose using Canvas
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
| @Composable | |
| fun Sphere() { | |
| val infiniteTransition = rememberInfiniteTransition(label = "SphereRotation") | |
| // Smooth 30-second full rotation around Y-axis | |
| val rotationAngleDegrees by infiniteTransition.animateFloat( | |
| initialValue = 0f, | |
| targetValue = 360f, | |
| animationSpec = infiniteRepeatable( | |
| animation = tween(durationMillis = 15000, easing = LinearEasing), | |
| repeatMode = RepeatMode.Restart | |
| ), | |
| label = "RotationAngle" | |
| ) | |
| Box( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .background(Color(0xFF121212)) | |
| ) { | |
| Canvas(modifier = Modifier.fillMaxSize()) { | |
| val canvasWidth = size.width | |
| val canvasHeight = size.height | |
| val center = Offset(canvasWidth / 2f, canvasHeight / 2f) | |
| val sphereRadius = minOf(canvasWidth, canvasHeight) * 0.4f | |
| // Google-inspired vibrant colors for four quadrants | |
| val quadrantColors = listOf( | |
| Color(0xFFFF3B30), // Red | |
| Color(0xFF4285F4), // Blue | |
| Color(0xFFFFD60A), // Yellow | |
| Color(0xFF34A853) // Green | |
| ) | |
| // Precompute rotation values to avoid recalculation in tight loops | |
| val rotationRadians = Math.toRadians(rotationAngleDegrees.toDouble()).toFloat() | |
| val cosRotation = cos(rotationRadians) | |
| val sinRotation = sin(rotationRadians) | |
| // Configuration for latitude bands (phi) | |
| val numLatitudeBands = 12 | |
| val minPointsPerBand = 2 | |
| val maxPointsPerBand = 9 | |
| // Draw each of the four spherical quadrants | |
| for (quadrantIndex in 0 until 4) { | |
| val baseColor = quadrantColors[quadrantIndex] | |
| val thetaStart = quadrantIndex * (PI / 2f) // Start angle in radians | |
| val thetaEnd = (quadrantIndex + 1) * (PI / 2f) // End angle | |
| for (bandIndex in 0 until numLatitudeBands) { | |
| // Normalized position in latitude (0 at south pole, 1 at north pole) | |
| val latitudeFraction = (bandIndex + 0.5f) / numLatitudeBands | |
| val phi = latitudeFraction * PI.toFloat() // Latitude angle (0 to PI) | |
| // More points near equator for smoother appearance | |
| val latitudeDensityFactor = sin(phi) | |
| val pointsInBand = ( | |
| minPointsPerBand + (maxPointsPerBand - minPointsPerBand) * latitudeDensityFactor | |
| ).toInt().coerceAtLeast(2) | |
| val thetaStep = (thetaEnd - thetaStart) / pointsInBand | |
| for (pointIndex in 0 until pointsInBand) { | |
| val theta = thetaStart + (pointIndex + 0.5f) * thetaStep | |
| // Spherical coordinates → Cartesian (before rotation) | |
| val x = sphereRadius * sin(phi) * cos(theta) | |
| val y = sphereRadius * sin(phi) * sin(theta) | |
| val z = sphereRadius * cos(phi) | |
| // Apply Y-axis rotation | |
| val rotatedX = x * cosRotation + z * sinRotation | |
| val rotatedZ = -x * sinRotation + z * cosRotation | |
| // Back-face culling: only draw particles on the visible hemisphere | |
| if (rotatedZ >= 0f) { | |
| // Depth-based sizing and opacity for 3D illusion | |
| val depthFactor = (rotatedZ + sphereRadius) / (2f * sphereRadius) | |
| val particleRadius = 6f + depthFactor * 12f | |
| val alpha = 0.75f + depthFactor * 0.25f | |
| drawCircle( | |
| color = baseColor.copy(alpha = alpha.toFloat()), | |
| radius = particleRadius.toFloat(), | |
| center = center + Offset(rotatedX.toFloat(), y.toFloat()) | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
