Last active
December 29, 2022 15:53
-
-
Save Pooh3Mobi/4c11145227f7ab54bb98b30bb95436de to your computer and use it in GitHub Desktop.
Compose Christmas Tree inspired by https://twitter.com/jh3yy/status/1606705164152635393?s=20&t=4DD7g3e3Qwgkn13go5fW-Q
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
package com.example.composegraphicslayerplayground | |
import android.os.Bundle | |
import androidx.activity.ComponentActivity | |
import androidx.activity.compose.setContent | |
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.animateFloatAsState | |
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.clickable | |
import androidx.compose.foundation.layout.Box | |
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.foundation.shape.CircleShape | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.NonRestartableComposable | |
import androidx.compose.runtime.derivedStateOf | |
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.shadow | |
import androidx.compose.ui.geometry.Size | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.Outline | |
import androidx.compose.ui.graphics.Path | |
import androidx.compose.ui.graphics.Shape | |
import androidx.compose.ui.graphics.drawscope.Stroke | |
import androidx.compose.ui.graphics.graphicsLayer | |
import androidx.compose.ui.graphics.vector.ImageVector | |
import androidx.compose.ui.graphics.vector.rememberVectorPainter | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.res.vectorResource | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.Density | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.LayoutDirection | |
import androidx.compose.ui.unit.dp | |
import com.example.composegraphicslayerplayground.ui.theme.ComposeGraphicsLayerPlayGroundTheme | |
import kotlin.math.cos | |
import kotlin.math.sin | |
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
ComposeGraphicsLayerPlayGroundTheme { | |
var enabled by remember { mutableStateOf(false) } | |
Tree( | |
modifier = Modifier | |
.background(Color.Black) | |
.fillMaxSize() | |
.clickable { | |
enabled = !enabled | |
}, | |
enabled = enabled, | |
layerNum = 10, | |
) | |
} | |
} | |
} | |
} | |
const val CameraDistance = 100f | |
// レイヤー間の距離のスケール | |
const val LayerDistanceScale = 8f | |
val circleColor = Color(0x66999999) // LightGray | |
val baubleRed = Color(0xCCfeab9a) | |
val baubleGreen = Color(0xCC9afeab) | |
@Composable | |
@NonRestartableComposable | |
fun Tree( | |
modifier: Modifier = Modifier, | |
enabled: Boolean, | |
layerNum: Int = 3, | |
// 各レイヤーの傾き | |
layerDegree: Float = 65f | |
) { | |
val zIndexScale by animateFloatAsState( | |
targetValue = if (enabled) LayerDistanceScale else 0f, | |
animationSpec = tween( | |
durationMillis = 300, | |
easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f) | |
) | |
) | |
val infiniteTransition = rememberInfiniteTransition() | |
val degree by infiniteTransition.animateFloat( | |
initialValue = 0f, | |
targetValue = 360f, | |
animationSpec = infiniteRepeatable( | |
animation = tween(4_000, easing = LinearEasing), | |
repeatMode = RepeatMode.Restart | |
) | |
) | |
Box( | |
modifier = modifier | |
.fillMaxWidth(0.6f), | |
contentAlignment = Alignment.Center, | |
) { | |
(0 .. layerNum).forEach { | |
val translationY = -15f * (layerNum - it) * zIndexScale | |
if (it == 0) { | |
val starSize = with(LocalDensity.current) { 20.dp.toPx() } | |
Star( | |
modifier = Modifier | |
.size(20.dp) | |
.graphicsLayer( | |
translationY = translationY - starSize | |
), | |
degree = degree, | |
starSize = starSize, | |
) | |
} else { | |
TreeRing( | |
layerDegree = layerDegree, | |
translationY = translationY, | |
radius = (10 * it).dp, | |
degree = -degree + (it * 30), | |
) | |
} | |
} | |
} | |
} | |
@Composable | |
@NonRestartableComposable | |
fun TreeRing( | |
modifier: Modifier = Modifier, | |
layerDegree: Float, | |
translationY: Float, | |
radius: Dp, | |
degree: Float, | |
) { | |
val baubleAdjTransY = with(LocalDensity.current) { 5.dp.toPx() } | |
Box( | |
modifier, | |
) { | |
Ring( | |
modifier = Modifier | |
.graphicsLayer( | |
cameraDistance = CameraDistance, | |
rotationX = layerDegree, | |
translationY = translationY , | |
) | |
.size(radius * 2) | |
) | |
Baubles( | |
modifier = Modifier | |
.size(radius * 2) | |
.graphicsLayer( | |
cameraDistance = CameraDistance, | |
// translationY = translationY + baubleHalfSize, | |
translationY = translationY - baubleAdjTransY, | |
) | |
, | |
degree = degree, | |
layerDegree = layerDegree, | |
radius = radius | |
) | |
} | |
} | |
@Composable | |
fun Ring( | |
modifier: Modifier = Modifier, | |
) { | |
Canvas(modifier = modifier) { | |
drawCircle( | |
color = circleColor, | |
style = Stroke(5f), | |
radius = size.minDimension / 2, | |
) | |
} | |
} | |
@Composable | |
fun Baubles( | |
modifier: Modifier = Modifier, | |
degree: Float, | |
layerDegree: Float, | |
radius: Dp, | |
) { | |
val density = LocalDensity.current | |
val r by remember { | |
derivedStateOf { | |
with(density) { (radius + 10.dp).toPx() } | |
} | |
} | |
val radiusX by remember(r) { | |
mutableStateOf(r) | |
} | |
val radiusY by remember { | |
derivedStateOf { | |
val d = 90 - layerDegree | |
(r * sin(d * Math.PI / 180)).toFloat() | |
} | |
} | |
Box( | |
modifier = modifier, | |
contentAlignment = Alignment.Center, | |
) { | |
data class BaubleParams( | |
val x: Float, | |
val y: Float, | |
val degree: Float, | |
) | |
fun computeBaubleParams( | |
degree: Float, | |
): BaubleParams { | |
val x = (radiusX * sin(degree * Math.PI / 180)).toFloat() | |
val y = (radiusY * cos(degree * Math.PI / 180)).toFloat() | |
return BaubleParams(x, y, degree) | |
} | |
val bauble1 by remember(degree) { | |
derivedStateOf { computeBaubleParams(degree) } | |
} | |
val bauble2 by remember(degree) { | |
derivedStateOf { computeBaubleParams(degree + 180) } | |
} | |
val bauble3 by remember(degree) { | |
derivedStateOf { computeBaubleParams(degree + 90) } | |
} | |
val bauble4 by remember(degree) { | |
derivedStateOf { computeBaubleParams(degree + 270) } | |
} | |
Bauble( | |
modifier = Modifier | |
.graphicsLayer( | |
rotationY = bauble1.degree, | |
translationX = bauble1.x, | |
translationY = bauble1.y, | |
) | |
.size(16.dp), | |
type = BaubleType.Red, | |
) | |
Bauble( | |
modifier = Modifier | |
.graphicsLayer( | |
rotationY = bauble2.degree, | |
translationX = bauble2.x, | |
translationY = bauble2.y, | |
) | |
.size(16.dp), | |
type = BaubleType.Red, | |
) | |
Bauble( | |
modifier = Modifier | |
.graphicsLayer( | |
rotationY = bauble3.degree, | |
translationX = bauble3.x, | |
translationY = bauble3.y, | |
) | |
.size(16.dp), | |
type = BaubleType.Green, | |
) | |
Bauble( | |
modifier = Modifier | |
.graphicsLayer( | |
rotationY = bauble4.degree, | |
translationX = bauble4.x, | |
translationY = bauble4.y, | |
) | |
.size(16.dp), | |
type = BaubleType.Green, | |
) | |
} | |
} | |
@Composable | |
fun Bauble( | |
modifier: Modifier = Modifier, | |
type: BaubleType | |
) { | |
// circle bauble | |
// val color = when (type) { | |
// BaubleType.Red -> baubleRed | |
// BaubleType.Green -> baubleGreen | |
// } | |
// Canvas( | |
// modifier = modifier | |
// .shadow( | |
// ambientColor = color, | |
// spotColor = color, | |
// elevation = 10.5.dp, | |
// shape = CircleShape, | |
// clip = true, | |
// ) | |
// ) { | |
// drawCircle( | |
// color = color, | |
// radius = size.minDimension / 2, | |
// ) | |
// } | |
// droid icon | |
AndroidRobot(modifier = modifier, type = type) | |
} | |
enum class BaubleType { | |
Red, Green | |
} | |
@Composable | |
fun AndroidRobot( | |
modifier: Modifier = Modifier, | |
type: BaubleType, | |
) { | |
val vector = if (type == BaubleType.Green) { | |
ImageVector.vectorResource(id = R.drawable.robot_green) | |
} else { | |
ImageVector.vectorResource(id = R.drawable.robot_red) | |
} | |
val painter = rememberVectorPainter(image = vector) | |
// drop shadow | |
val color = when (type) { | |
BaubleType.Red -> baubleRed | |
BaubleType.Green -> baubleGreen | |
} | |
Canvas( | |
modifier = modifier | |
.size(15.dp) | |
.shadow( | |
ambientColor = color, | |
spotColor = color, | |
elevation = 20.dp, | |
shape = CircleShape, | |
clip = false, | |
) | |
) { | |
with(painter) { | |
draw(painter.intrinsicSize) | |
} | |
} | |
} | |
@Composable | |
fun Star( | |
modifier: Modifier = Modifier, | |
color: Color = Color.Yellow, | |
degree: Float, | |
starSize: Float, | |
) { | |
Canvas( | |
modifier = modifier | |
.graphicsLayer( | |
rotationY = -degree, | |
) | |
.shadow( | |
ambientColor = color, | |
spotColor = color, | |
elevation = 10.5.dp, | |
shape = object : Shape { | |
override fun createOutline( | |
size: Size, | |
layoutDirection: LayoutDirection, | |
density: Density | |
): Outline { | |
return Outline.Generic( | |
Path().starPath(starSize) | |
) | |
} | |
}, | |
clip = true, | |
) | |
) { | |
val path = Path().starPath(starSize) | |
drawPath( | |
path = path, | |
color = color, | |
style = Stroke(5f), | |
) | |
} | |
} | |
fun Path.starPath(radius: Float): Path { | |
val innerRadius = radius * 0.5f | |
val outerRadius = radius * 0.95f | |
val angle: Float = (2.0 * Math.PI / 5f).toFloat() | |
val halfAngle: Float = angle / 2.0f | |
val centerX = radius / 2.0f | |
val centerY = radius / 2.0f | |
moveTo( | |
(centerX + cos(0.0f) * outerRadius), | |
(centerY + sin(0.0f) * outerRadius) | |
) | |
for (i in 0 until 5) { | |
lineTo( | |
(centerX + cos((i * angle + halfAngle)) * innerRadius), | |
(centerY + sin((i * angle + halfAngle)) * innerRadius) | |
) | |
lineTo( | |
(centerX + cos((i * angle + angle)) * outerRadius), | |
(centerY + sin((i * angle + angle)) * outerRadius) | |
) | |
} | |
close() | |
return this | |
} | |
@Preview(showBackground = true) | |
@Composable | |
fun TreePreview() { | |
ComposeGraphicsLayerPlayGroundTheme { | |
Tree( | |
modifier = Modifier | |
.background(Color.Black) | |
.padding(top = 200.dp) | |
.fillMaxSize(), | |
enabled = true, | |
layerNum = 10, | |
) | |
} | |
} |
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
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |
android:width="30dp" | |
android:height="30dp" | |
android:viewportWidth="800" | |
android:viewportHeight="742.35"> | |
<path | |
android:pathData="M76.86,514.44c-5.18,17.1 4.48,35.15 21.57,40.31l40.31,12.23l-31.41,60.24c-8.24,15.81 -2.3,35.25 13.27,43.34l24.66,12.89c15.57,8.09 34.89,1.83 43.15,-14l39.35,-75.46l77.03,23.39l-22.31,73.5c-5.18,17.06 4.27,35.06 21.06,40.16l26.64,8.09c16.78,5.08 34.65,-4.65 39.83,-21.71l22.3,-73.47l27.05,8.21c17.1,5.2 35.15,-4.44 40.34,-21.54l77.46,-255.2l-382.81,-116.2L76.86,514.44zM107.46,245.67l21.75,-8.36c13.7,-5.28 19.65,-23.04 13.29,-39.69L76,24C69.6,7.36 53.29,-1.91 39.57,3.31l-21.72,8.37C4.14,16.92 -1.82,34.71 4.56,51.33L71.06,224.95C77.43,241.61 93.75,250.9 107.46,245.67zM544.96,116.8l-49.1,41.06c-20.37,-20.69 -45.42,-37.38 -74.57,-47.87c-29.88,-10.77 -60.61,-13.81 -90.16,-10.45l-12.33,-62.75c-0.78,-4.06 -4.7,-6.66 -8.69,-5.83c-4.01,0.77 -6.63,4.66 -5.85,8.66l12.23,62.17c-64.51,12.24 -121.42,55.76 -147.89,120.26l376.57,133.28c19.97,-66.69 3.18,-136.37 -39.31,-186.52l48.62,-40.65c3.12,-2.65 3.53,-7.33 0.92,-10.42C552.77,114.57 548.11,114.14 544.96,116.8zM311.06,179.85c-3.81,10.79 -15.67,16.43 -26.45,12.61c-10.78,-3.83 -16.47,-15.67 -12.64,-26.45c3.82,-10.81 15.69,-16.47 26.46,-12.64C309.22,157.19 314.89,169.06 311.06,179.85zM488.31,242.56c-3.82,10.8 -15.64,16.43 -26.46,12.63c-10.78,-3.83 -16.43,-15.67 -12.62,-26.45c3.82,-10.8 15.66,-16.45 26.45,-12.63C486.49,219.92 492.13,231.74 488.31,242.56zM793.66,281.73l-13.69,-18.82c-8.66,-11.88 -27.37,-12.99 -41.81,-2.49L587.9,369.85c-14.44,10.49 -19.15,28.66 -10.49,40.52l13.69,18.8c8.65,11.88 27.37,13.01 41.78,2.51l150.31,-109.4C797.61,311.77 802.32,293.6 793.66,281.73z" | |
android:fillColor="#CC9afeab"/> | |
</vector> |
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
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |
android:width="30dp" | |
android:height="30dp" | |
android:viewportWidth="800" | |
android:viewportHeight="742.35"> | |
<path | |
android:pathData="M76.86,514.44c-5.18,17.1 4.48,35.15 21.57,40.31l40.31,12.23l-31.41,60.24c-8.24,15.81 -2.3,35.25 13.27,43.34l24.66,12.89c15.57,8.09 34.89,1.83 43.15,-14l39.35,-75.46l77.03,23.39l-22.31,73.5c-5.18,17.06 4.27,35.06 21.06,40.16l26.64,8.09c16.78,5.08 34.65,-4.65 39.83,-21.71l22.3,-73.47l27.05,8.21c17.1,5.2 35.15,-4.44 40.34,-21.54l77.46,-255.2l-382.81,-116.2L76.86,514.44zM107.46,245.67l21.75,-8.36c13.7,-5.28 19.65,-23.04 13.29,-39.69L76,24C69.6,7.36 53.29,-1.91 39.57,3.31l-21.72,8.37C4.14,16.92 -1.82,34.71 4.56,51.33L71.06,224.95C77.43,241.61 93.75,250.9 107.46,245.67zM544.96,116.8l-49.1,41.06c-20.37,-20.69 -45.42,-37.38 -74.57,-47.87c-29.88,-10.77 -60.61,-13.81 -90.16,-10.45l-12.33,-62.75c-0.78,-4.06 -4.7,-6.66 -8.69,-5.83c-4.01,0.77 -6.63,4.66 -5.85,8.66l12.23,62.17c-64.51,12.24 -121.42,55.76 -147.89,120.26l376.57,133.28c19.97,-66.69 3.18,-136.37 -39.31,-186.52l48.62,-40.65c3.12,-2.65 3.53,-7.33 0.92,-10.42C552.77,114.57 548.11,114.14 544.96,116.8zM311.06,179.85c-3.81,10.79 -15.67,16.43 -26.45,12.61c-10.78,-3.83 -16.47,-15.67 -12.64,-26.45c3.82,-10.81 15.69,-16.47 26.46,-12.64C309.22,157.19 314.89,169.06 311.06,179.85zM488.31,242.56c-3.82,10.8 -15.64,16.43 -26.46,12.63c-10.78,-3.83 -16.43,-15.67 -12.62,-26.45c3.82,-10.8 15.66,-16.45 26.45,-12.63C486.49,219.92 492.13,231.74 488.31,242.56zM793.66,281.73l-13.69,-18.82c-8.66,-11.88 -27.37,-12.99 -41.81,-2.49L587.9,369.85c-14.44,10.49 -19.15,28.66 -10.49,40.52l13.69,18.8c8.65,11.88 27.37,13.01 41.78,2.51l150.31,-109.4C797.61,311.77 802.32,293.6 793.66,281.73z" | |
android:fillColor="#CCfeab9a"/> | |
</vector> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
no starting animation.