package com.vgmlr.grain
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
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.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.ceil
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.random.Random
private val W = 320.dp
private val H = 540.dp
private val DROP = 60.dp
private val FONT = 56.sp
private const val CX = 160f
private const val GY = 270f
private const val LIFT = 6f
private const val TOP = 24f
private const val STEP = 3f
private const val PI_F = PI.toFloat()
private const val CAP = .711f
private const val TEXT_Y = GY + 58f
private const val BASE = 4f
private const val LEAF = .13f
private const val LEAF_GAP = .02f
private const val TRUNK_A = .10f
private const val TRUNK_B = .48f
private val DIALOG_COLOR = Color(0xFFFFFFFF)
// canopy outline
private val CANOPY = floatArrayOf(
217.3f, 190.7f, 207.6f, 190.4f, 204.7f, 189.5f, 200.3f, 187.3f, 197.5f, 184.8f, 195.9f, 185.4f,
193.4f, 184.8f, 186.8f, 180.0f, 186.0f, 180.5f, 185.7f, 182.4f, 184.9f, 182.9f, 183.0f, 181.6f,
181.1f, 181.6f, 179.8f, 182.2f, 174.5f, 177.2f, 174.0f, 178.0f, 174.0f, 181.1f, 174.3f, 181.5f,
174.0f, 183.7f, 173.2f, 184.4f, 172.0f, 184.4f, 169.6f, 181.8f, 166.1f, 174.5f, 165.4f, 173.7f,
164.6f, 174.5f, 164.4f, 178.1f, 156.8f, 178.1f, 153.1f, 181.3f, 151.0f, 181.1f, 150.7f, 180.5f,
151.0f, 180.2f, 151.0f, 176.1f, 150.2f, 175.0f, 149.6f, 175.0f, 145.2f, 179.1f, 141.7f, 181.6f,
136.1f, 184.4f, 129.1f, 186.3f, 126.3f, 186.7f, 125.0f, 185.7f, 122.8f, 185.7f, 121.9f, 186.3f,
116.2f, 187.3f, 115.3f, 187.0f, 113.4f, 184.8f, 107.4f, 183.8f, 106.9f, 183.0f, 107.2f, 180.2f,
102.4f, 177.5f, 99.7f, 174.8f, 99.0f, 173.6f, 98.7f, 171.7f, 99.2f, 171.2f, 99.8f, 170.9f,
102.7f, 171.2f, 103.5f, 170.7f, 103.9f, 169.0f, 108.7f, 168.7f, 110.2f, 168.1f, 111.8f, 166.5f,
116.2f, 166.2f, 120.3f, 165.2f, 126.6f, 162.7f, 135.1f, 157.7f, 139.7f, 153.4f, 140.6f, 151.5f,
140.5f, 151.1f, 139.2f, 151.4f, 138.6f, 151.1f, 137.6f, 149.2f, 133.5f, 150.4f, 130.7f, 150.4f,
129.6f, 149.6f, 129.6f, 149.0f, 128.8f, 148.2f, 125.3f, 148.5f, 124.2f, 147.8f, 123.8f, 146.6f,
117.8f, 147.9f, 114.0f, 147.9f, 111.5f, 147.0f, 110.5f, 146.0f, 106.6f, 145.5f, 106.8f, 144.1f,
113.1f, 141.3f, 116.5f, 138.8f, 118.1f, 139.4f, 123.5f, 137.2f, 124.6f, 136.1f, 123.8f, 135.0f,
118.1f, 135.0f, 115.9f, 134.4f, 114.0f, 132.5f, 112.3f, 131.7f, 112.0f, 129.8f, 112.8f, 129.0f,
116.5f, 129.0f, 117.8f, 127.7f, 125.3f, 126.5f, 128.2f, 125.5f, 133.9f, 122.4f, 136.1f, 121.8f,
140.2f, 117.4f, 145.8f, 113.9f, 148.5f, 110.9f, 149.4f, 108.4f, 148.3f, 107.6f, 145.5f, 111.4f,
139.8f, 115.5f, 137.9f, 115.8f, 137.0f, 115.1f, 134.8f, 116.4f, 130.4f, 117.4f, 120.9f, 116.4f,
120.2f, 115.3f, 120.2f, 114.0f, 120.9f, 113.3f, 128.5f, 111.1f, 130.2f, 109.3f, 128.5f, 108.8f,
125.7f, 109.5f, 124.7f, 110.1f, 118.4f, 110.1f, 116.5f, 109.5f, 115.1f, 108.4f, 115.1f, 106.2f,
115.6f, 105.7f, 120.6f, 105.1f, 125.0f, 103.5f, 131.7f, 99.7f, 137.6f, 95.0f, 140.0f, 92.6f,
141.9f, 88.5f, 141.9f, 87.9f, 140.3f, 86.3f, 142.2f, 83.5f, 142.2f, 82.5f, 138.6f, 84.0f,
135.4f, 86.5f, 132.6f, 88.1f, 129.4f, 88.7f, 129.0f, 87.3f, 128.2f, 86.5f, 125.3f, 86.2f,
123.5f, 84.3f, 122.2f, 83.6f, 116.2f, 82.4f, 111.8f, 80.2f, 109.8f, 78.5f, 109.4f, 76.2f,
111.2f, 75.1f, 113.4f, 74.8f, 115.6f, 75.1f, 116.8f, 74.8f, 117.2f, 75.1f, 119.4f, 74.5f,
120.6f, 73.3f, 127.9f, 72.9f, 133.5f, 71.4f, 147.1f, 64.7f, 150.2f, 62.5f, 153.9f, 58.9f,
157.0f, 54.2f, 158.3f, 51.0f, 158.1f, 50.6f, 150.6f, 56.6f, 148.0f, 57.5f, 145.8f, 57.5f,
144.1f, 56.7f, 144.1f, 54.5f, 143.6f, 54.0f, 141.7f, 53.7f, 137.9f, 54.4f, 136.7f, 55.0f,
133.5f, 55.0f, 132.3f, 54.4f, 129.8f, 51.2f, 125.7f, 49.6f, 124.9f, 47.6f, 125.7f, 46.5f,
126.6f, 46.2f, 127.6f, 46.5f, 127.9f, 46.2f, 128.2f, 46.5f, 131.7f, 46.2f, 132.9f, 44.0f,
133.2f, 44.3f, 136.1f, 44.0f, 136.4f, 44.3f, 143.6f, 42.7f, 148.7f, 40.2f, 152.6f, 37.2f,
159.2f, 30.3f, 159.2f, 28.4f, 158.3f, 27.4f, 160.8f, 22.1f, 160.6f, 21.3f, 158.9f, 22.7f,
157.3f, 25.5f, 155.9f, 26.9f, 155.0f, 27.3f, 153.4f, 26.9f, 149.6f, 30.4f, 148.3f, 31.0f,
146.8f, 31.0f, 144.6f, 29.5f, 143.3f, 29.5f, 139.8f, 30.7f, 138.7f, 29.0f, 142.0f, 26.9f,
146.8f, 25.1f, 150.9f, 22.2f, 156.1f, 16.7f, 157.3f, 14.5f, 163.8f, 8.0f, 166.6f, 5.8f,
167.9f, 6.2f, 171.8f, 10.1f, 173.1f, 12.6f, 176.4f, 15.6f, 180.5f, 17.8f, 184.9f, 19.1f,
185.0f, 20.2f, 183.5f, 21.4f, 183.5f, 22.4f, 188.7f, 26.3f, 191.5f, 27.9f, 194.3f, 28.8f,
195.1f, 30.3f, 194.8f, 31.2f, 194.0f, 31.7f, 191.8f, 31.0f, 189.6f, 31.4f, 189.1f, 32.1f,
189.1f, 34.0f, 188.3f, 34.8f, 187.1f, 34.2f, 183.6f, 31.0f, 182.4f, 30.4f, 179.5f, 30.7f,
177.6f, 29.5f, 176.5f, 29.9f, 176.5f, 32.8f, 175.4f, 33.2f, 172.8f, 30.9f, 170.1f, 27.3f,
169.6f, 28.4f, 169.9f, 30.6f, 172.1f, 35.0f, 178.0f, 40.8f, 182.4f, 43.6f, 183.8f, 45.1f,
183.8f, 46.3f, 181.4f, 47.4f, 178.3f, 47.1f, 176.4f, 48.1f, 175.4f, 48.1f, 174.2f, 47.1f,
173.4f, 47.6f, 174.0f, 48.8f, 179.2f, 52.1f, 181.3f, 54.2f, 181.9f, 57.0f, 182.4f, 57.5f,
191.8f, 62.2f, 197.2f, 63.8f, 203.5f, 64.7f, 209.1f, 64.1f, 211.3f, 63.2f, 212.9f, 63.2f,
213.7f, 64.3f, 213.7f, 65.8f, 211.3f, 67.9f, 209.8f, 68.2f, 208.0f, 69.9f, 207.7f, 71.2f,
206.9f, 71.7f, 200.9f, 71.4f, 199.1f, 72.0f, 197.5f, 73.6f, 195.6f, 73.6f, 189.9f, 71.4f,
186.8f, 71.7f, 186.3f, 72.2f, 186.6f, 76.6f, 186.1f, 77.0f, 183.9f, 76.4f, 181.7f, 74.8f,
178.3f, 73.3f, 170.4f, 65.4f, 169.9f, 65.8f, 173.7f, 73.4f, 179.5f, 79.9f, 185.2f, 84.3f,
194.7f, 89.6f, 196.1f, 91.0f, 196.1f, 91.7f, 194.7f, 92.8f, 189.3f, 92.5f, 189.1f, 93.9f,
190.9f, 95.6f, 201.3f, 103.2f, 215.1f, 110.4f, 219.2f, 111.7f, 221.0f, 113.7f, 220.5f, 114.8f,
218.6f, 115.5f, 216.4f, 115.1f, 214.2f, 114.2f, 212.0f, 114.2f, 211.3f, 113.6f, 208.2f, 114.5f,
203.8f, 112.6f, 196.5f, 107.9f, 195.4f, 108.7f, 195.1f, 110.0f, 198.3f, 114.7f, 198.0f, 116.2f,
196.9f, 116.7f, 192.1f, 114.2f, 189.9f, 112.6f, 188.7f, 111.1f, 186.8f, 111.4f, 185.2f, 111.1f,
179.5f, 105.7f, 177.8f, 104.9f, 178.1f, 106.5f, 179.7f, 109.6f, 182.2f, 113.1f, 185.8f, 117.0f,
190.1f, 120.7f, 190.1f, 121.9f, 189.6f, 122.4f, 187.1f, 122.4f, 185.8f, 121.8f, 185.4f, 122.5f,
189.5f, 128.5f, 195.0f, 134.4f, 201.3f, 139.4f, 211.3f, 145.4f, 212.3f, 145.4f, 216.1f, 147.0f,
218.0f, 146.6f, 218.6f, 146.0f, 223.0f, 147.6f, 224.3f, 147.6f, 225.0f, 148.4f, 224.7f, 148.7f,
225.0f, 149.6f, 223.6f, 150.1f, 216.1f, 150.1f, 213.2f, 151.1f, 212.0f, 151.1f, 210.7f, 150.4f,
208.2f, 150.4f, 207.2f, 151.1f, 204.1f, 151.1f, 203.9f, 153.7f, 203.5f, 154.5f, 196.5f, 151.4f,
195.6f, 151.4f, 194.3f, 152.6f, 193.1f, 152.3f, 190.9f, 151.1f, 187.4f, 147.9f, 184.7f, 149.6f,
185.0f, 150.9f, 187.9f, 154.4f, 187.9f, 155.6f, 186.3f, 156.9f, 186.3f, 157.5f, 187.2f, 158.8f,
187.2f, 160.3f, 188.3f, 162.1f, 190.6f, 163.0f, 199.7f, 171.5f, 209.8f, 178.8f, 218.6f, 183.5f,
224.9f, 185.7f, 225.7f, 186.8f, 225.7f, 187.4f, 224.9f, 188.2f, 223.3f, 188.9f, 223.0f, 188.5f,
222.7f, 188.9f, 222.4f, 188.5f, 218.9f, 188.5f, 217.8f, 189.3f
)
// canopy flecks
private val CANOPY_HOLES = arrayOf(
floatArrayOf(172.1f, 88.5f, 172.4f, 87.9f, 170.9f, 83.8f, 170.9f, 81.9f, 170.1f, 81.1f, 169.9f, 84.1f, 170.2f, 84.4f, 170.6f, 86.6f, 170.9f, 87.0f, 170.9f, 87.6f, 172.0f, 88.7f),
floatArrayOf(180.6f, 92.0f, 180.9f, 91.4f, 180.3f, 90.1f, 179.2f, 89.0f, 177.6f, 88.4f, 177.5f, 89.8f, 177.8f, 90.4f, 179.8f, 91.8f),
floatArrayOf(169.3f, 47.3f, 169.6f, 46.6f, 169.0f, 45.1f, 168.2f, 44.3f, 167.6f, 44.3f, 167.4f, 46.0f, 167.7f, 46.6f, 168.5f, 47.4f),
floatArrayOf(159.2f, 157.2f, 159.8f, 156.6f, 159.8f, 155.9f, 159.5f, 155.3f, 158.7f, 154.8f, 157.6f, 155.9f, 157.6f, 156.9f, 158.1f, 157.4f),
floatArrayOf(178.4f, 158.5f, 178.7f, 157.8f, 177.3f, 156.1f, 175.9f, 156.9f, 175.9f, 157.2f, 177.6f, 158.6f),
floatArrayOf(160.2f, 163.5f, 161.7f, 162.2f, 160.9f, 161.1f, 160.6f, 161.1f, 159.8f, 162.2f, 159.5f, 163.5f),
floatArrayOf(192.6f, 172.0f, 192.9f, 171.4f, 191.5f, 170.0f, 191.2f, 170.0f, 190.7f, 170.4f, 190.7f, 171.1f, 191.8f, 172.2f),
floatArrayOf(163.6f, 82.5f, 163.9f, 82.2f, 163.9f, 81.3f, 163.6f, 80.7f, 162.8f, 80.2f, 162.7f, 81.6f, 163.0f, 82.2f, 163.5f, 82.7f),
floatArrayOf(186.6f, 98.6f, 186.9f, 98.3f, 186.9f, 97.7f, 186.3f, 97.0f, 185.8f, 95.9f, 185.7f, 96.7f, 186.3f, 97.7f, 185.7f, 98.6f),
floatArrayOf(185.4f, 133.6f, 185.7f, 133.3f, 185.7f, 132.6f, 184.6f, 132.2f, 184.4f, 133.6f),
floatArrayOf(169.0f, 80.0f, 169.0f, 79.4f, 167.9f, 78.6f, 167.7f, 79.4f, 168.5f, 80.2f),
floatArrayOf(182.5f, 132.3f, 182.2f, 132.0f, 182.2f, 130.7f, 181.7f, 130.0f, 181.6f, 132.0f, 182.1f, 132.5f)
)
// limb skeletons
private val LIMBS = floatArrayOf(
160.0f, 26.0f, 165.0f, 22.0f, 169.0f, 8.0f,
160.0f, 20.0f, 180.0f, 25.0f, 194.0f, 31.0f,
160.0f, 38.0f, 173.0f, 43.0f, 183.0f, 46.0f,
160.0f, 56.0f, 190.0f, 68.0f, 213.0f, 66.0f,
160.0f, 78.0f, 190.0f, 99.0f, 218.5f, 115.0f,
160.0f, 106.0f, 178.0f, 115.0f, 189.0f, 122.0f,
160.0f, 124.0f, 196.0f, 149.0f, 223.5f, 149.0f,
160.0f, 158.0f, 195.0f, 180.0f, 225.0f, 187.0f,
160.0f, 21.0f, 148.0f, 24.0f, 146.0f, 29.5f,
160.0f, 43.0f, 142.0f, 48.0f, 126.0f, 49.0f,
160.0f, 66.0f, 135.0f, 76.0f, 111.0f, 79.0f,
160.0f, 96.0f, 137.0f, 104.0f, 116.0f, 108.0f,
160.0f, 128.0f, 132.0f, 138.0f, 107.0f, 145.0f,
160.0f, 164.0f, 128.0f, 174.0f, 99.0f, 172.0f
)
// leaf cells
private val CELLS = arrayOf(
floatArrayOf( // C
162.5f, 32.6f, 154.8f, 32.6f, 154.1f, 32.0f, 155.6f, 26.8f, 160.9f, 20.2f, 157.2f, 15.6f,
150.5f, 11.1f, 138.9f, 0.0f, 193.8f, -0.1f, 188.0f, 5.4f, 181.5f, 10.1f, 162.1f, 20.5f,
167.9f, 31.0f, 167.0f, 31.9f
),
floatArrayOf( // R1
201.8f, 47.1f, 187.5f, 38.1f, 167.8f, 31.9f, 161.4f, 21.0f, 158.5f, 20.6f, 150.9f, 11.5f,
151.8f, 10.9f, 157.2f, 14.6f, 161.8f, 19.9f, 181.5f, 9.1f, 193.0f, -0.1f, 234.8f, -0.1f,
234.9f, 31.0f, 209.8f, 44.6f
),
floatArrayOf( // R2
186.8f, 54.6f, 181.2f, 54.4f, 165.8f, 49.6f, 164.1f, 45.0f, 162.0f, 41.9f, 155.0f, 39.6f,
150.4f, 35.2f, 154.2f, 31.6f, 162.5f, 31.6f, 167.5f, 30.6f, 187.5f, 37.1f, 201.4f, 46.5f,
199.5f, 48.6f, 194.2f, 52.1f
),
floatArrayOf( // R3
230.8f, 87.9f, 216.5f, 87.1f, 189.0f, 78.9f, 170.5f, 71.6f, 166.4f, 65.2f, 162.8f, 62.4f,
154.5f, 61.1f, 147.9f, 57.8f, 153.0f, 52.1f, 159.2f, 49.1f, 166.0f, 48.6f, 181.2f, 53.4f,
189.2f, 53.1f, 194.2f, 51.1f, 201.0f, 46.1f, 209.8f, 43.6f, 234.8f, 30.1f, 234.9f, 87.2f
),
floatArrayOf( // R4
227.2f, 131.4f, 216.2f, 130.4f, 205.8f, 125.9f, 201.4f, 118.2f, 197.0f, 113.6f, 172.0f, 99.6f,
168.1f, 93.5f, 165.2f, 90.6f, 159.2f, 87.9f, 148.2f, 85.9f, 147.4f, 84.8f, 151.1f, 77.8f,
155.2f, 73.6f, 158.8f, 71.9f, 166.5f, 71.4f, 170.2f, 70.4f, 194.0f, 79.6f, 214.2f, 85.6f,
222.2f, 86.9f, 234.8f, 86.4f, 234.9f, 130.2f
),
floatArrayOf( // R5
194.2f, 129.6f, 187.0f, 128.9f, 160.5f, 116.1f, 148.5f, 115.6f, 147.9f, 115.0f, 148.6f, 112.8f,
151.6f, 106.8f, 156.5f, 101.9f, 159.5f, 100.6f, 165.2f, 100.4f, 171.8f, 98.4f, 195.2f, 111.4f,
202.4f, 118.2f, 206.1f, 125.2f, 200.5f, 128.1f
),
floatArrayOf( // R6
234.8f, 168.6f, 224.0f, 168.1f, 207.2f, 163.1f, 187.0f, 154.9f, 164.0f, 143.6f, 163.4f, 143.0f,
163.6f, 133.8f, 162.4f, 128.5f, 160.5f, 126.6f, 157.2f, 126.1f, 154.5f, 124.6f, 146.4f, 116.2f,
148.2f, 114.6f, 160.5f, 115.1f, 183.5f, 126.4f, 190.5f, 128.6f, 197.0f, 128.1f, 206.0f, 124.9f,
213.2f, 128.4f, 219.8f, 130.1f, 227.2f, 130.4f, 234.8f, 129.4f
),
floatArrayOf( // R7
234.8f, 199.9f, 162.2f, 199.9f, 164.6f, 176.0f, 164.6f, 170.5f, 163.6f, 165.2f, 161.2f, 162.1f,
156.2f, 161.1f, 152.2f, 158.9f, 144.1f, 150.8f, 148.2f, 147.4f, 154.0f, 144.4f, 159.2f, 142.9f,
164.2f, 142.6f, 187.0f, 153.9f, 207.2f, 162.1f, 221.5f, 166.6f, 234.8f, 167.6f
),
floatArrayOf( // L1
140.0f, 38.9f, 133.8f, 38.1f, 126.8f, 34.6f, 85.5f, 3.4f, 84.9f, 2.8f, 84.9f, 0.0f,
139.8f, -0.1f, 151.9f, 11.2f, 158.5f, 19.6f, 161.1f, 20.8f, 156.6f, 26.8f, 155.1f, 32.0f,
150.8f, 35.9f
),
floatArrayOf( // L2
124.5f, 62.4f, 118.8f, 61.9f, 113.0f, 60.1f, 84.9f, 45.5f, 85.0f, 1.9f, 128.8f, 34.9f,
132.2f, 36.6f, 137.8f, 37.9f, 142.0f, 37.6f, 150.8f, 34.9f, 155.0f, 38.6f, 162.0f, 40.9f,
165.1f, 45.0f, 166.1f, 48.0f, 165.5f, 49.6f, 159.2f, 50.1f, 154.8f, 51.9f, 148.2f, 58.4f,
138.2f, 60.6f
),
floatArrayOf( // L3
85.0f, 97.4f, 85.0f, 44.6f, 113.0f, 59.1f, 118.8f, 60.9f, 130.0f, 60.9f, 147.5f, 57.4f,
154.5f, 60.1f, 161.5f, 60.9f, 167.4f, 65.2f, 170.6f, 70.5f, 169.0f, 71.9f, 160.0f, 72.6f,
156.0f, 74.1f, 152.1f, 77.8f, 148.4f, 85.0f, 146.5f, 86.1f, 126.5f, 90.9f
),
floatArrayOf( // L4
112.5f, 125.1f, 102.2f, 124.4f, 84.9f, 120.0f, 85.2f, 96.1f, 126.5f, 89.9f, 148.0f, 84.6f,
151.2f, 85.9f, 159.2f, 86.9f, 163.2f, 88.4f, 167.9f, 92.0f, 172.1f, 98.8f, 167.5f, 100.9f,
159.5f, 101.6f, 155.8f, 103.4f, 152.6f, 106.8f, 149.1f, 114.8f, 147.0f, 116.6f, 122.2f, 123.4f
),
floatArrayOf( // L5
108.0f, 159.6f, 101.5f, 159.1f, 84.9f, 154.2f, 85.0f, 119.1f, 102.2f, 123.4f, 112.5f, 124.1f,
122.2f, 122.4f, 146.8f, 115.6f, 154.5f, 123.6f, 156.5f, 124.9f, 160.5f, 125.6f, 163.4f, 128.5f,
164.6f, 133.8f, 164.4f, 143.0f, 163.8f, 143.6f, 159.2f, 143.9f, 154.0f, 145.4f, 143.8f, 151.6f,
125.2f, 156.4f
),
floatArrayOf( // L6
163.0f, 199.9f, 85.0f, 199.9f, 84.9f, 153.5f, 101.5f, 158.1f, 108.0f, 158.6f, 125.2f, 155.4f,
144.8f, 150.4f, 152.2f, 157.9f, 156.2f, 160.1f, 161.2f, 161.1f, 164.6f, 165.2f, 165.6f, 170.5f,
165.6f, 176.0f
)
)
// hang points
private val HANGS = floatArrayOf(
211.3f, 67.9f, 108.4f,
189.9f, 71.4f, 86.9f,
200.9f, 71.4f, 102.9f,
109.8f, 78.5f, 142.8f,
116.2f, 82.4f, 105.7f,
123.5f, 84.3f, 104.0f,
135.4f, 86.5f, 101.0f,
194.7f, 92.8f, 107.3f,
196.5f, 107.9f, 135.7f,
115.1f, 108.4f, 129.2f,
185.2f, 111.1f, 125.6f,
203.8f, 112.6f, 140.8f,
211.3f, 113.6f, 145.3f,
192.1f, 114.2f, 131.2f,
220.5f, 114.8f, 146.8f,
137.0f, 115.1f, 129.6f,
120.9f, 116.4f, 130.9f,
130.4f, 117.4f, 131.9f,
189.6f, 122.4f, 136.9f,
112.3f, 131.7f, 146.2f,
110.5f, 146.0f, 167.8f,
123.8f, 146.6f, 163.8f,
187.4f, 147.9f, 162.4f,
137.6f, 149.2f, 163.7f,
216.1f, 150.1f, 182.3f,
223.6f, 150.1f, 185.3f,
130.7f, 150.4f, 164.9f,
208.2f, 150.4f, 177.9f,
195.6f, 151.4f, 167.9f,
99.0f, 173.6f, 272.0f,
186.8f, 180.0f, 272.0f,
107.4f, 183.8f, 272.0f,
136.1f, 184.4f, 272.0f,
193.4f, 184.8f, 272.0f,
122.8f, 185.7f, 272.0f,
129.1f, 186.3f, 272.0f,
115.3f, 187.0f, 272.0f,
200.3f, 187.3f, 272.0f,
224.9f, 188.2f, 272.0f,
217.8f, 189.3f, 272.0f,
207.6f, 190.4f, 272.0f
)
// letter sticks
private val STICKS = arrayOf(
arrayOf( // M
floatArrayOf(.14f, 0f, .5f, .85f), floatArrayOf(.86f, 0f, .5f, .85f),
floatArrayOf(.12f, -.05f, .12f, 1.05f), floatArrayOf(.88f, -.05f, .88f, 1.05f)
),
arrayOf( // L
floatArrayOf(.2f, -.05f, .2f, 1.05f), floatArrayOf(.1f, .88f, .95f, .88f)
),
arrayOf( // W
floatArrayOf(.07f, 0f, .29f, 1f), floatArrayOf(.29f, 1f, .5f, .05f),
floatArrayOf(.5f, .05f, .71f, 1f), floatArrayOf(.71f, 1f, .93f, 0f)
),
arrayOf( // R
floatArrayOf(.18f, -.05f, .18f, 1.05f), floatArrayOf(.1f, .1f, .7f, .1f),
floatArrayOf(.74f, .08f, .74f, .48f), floatArrayOf(.1f, .52f, .7f, .52f),
floatArrayOf(.45f, .5f, .88f, 1.02f)
),
arrayOf( // K
floatArrayOf(.18f, -.05f, .18f, 1.05f), floatArrayOf(.9f, 0f, .28f, .62f),
floatArrayOf(.42f, .45f, .9f, 1.02f)
)
)
// nail joints
private val JOINTS = arrayOf(
arrayOf( // M
floatArrayOf(0f, 2f, .13f, .07f), floatArrayOf(1f, 3f, .87f, .07f),
floatArrayOf(0f, 1f, .5f, .78f)
),
arrayOf( // L
floatArrayOf(0f, 1f, .2f, .88f)
),
arrayOf( // W
floatArrayOf(0f, 1f, .29f, .92f), floatArrayOf(1f, 2f, .5f, .12f),
floatArrayOf(2f, 3f, .71f, .92f)
),
arrayOf( // R
floatArrayOf(0f, 1f, .18f, .1f), floatArrayOf(1f, 2f, .74f, .1f),
floatArrayOf(2f, 3f, .74f, .5f), floatArrayOf(0f, 3f, .18f, .52f),
floatArrayOf(3f, 4f, .5f, .53f)
),
arrayOf( // K
floatArrayOf(0f, 1f, .26f, .6f), floatArrayOf(1f, 2f, .45f, .48f)
)
)
@Composable
fun VersionDialog(show: Boolean, onDismiss: () -> Unit, uriHandler: UriHandler) {
if (show) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
val p = remember { Animatable(0f) }
val cone = remember { Animatable(0f) }
val seed = remember { Random.nextInt() }
val tm = rememberTextMeasurer()
LaunchedEffect(Unit) {
p.animateTo(1f, tween(3000, easing = LinearEasing))
cone.animateTo(1f, tween(2400, easing = LinearEasing))
}
val ink = DIALOG_COLOR
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismiss
)
) {
Box(Modifier.layout { m, c ->
val pl = m.measure(Constraints.fixed(W.roundToPx(), H.roundToPx()))
val fit = minOf(1f, c.maxWidth / pl.width.toFloat(), c.maxHeight / pl.height.toFloat())
layout(c.maxWidth, c.maxHeight) {
pl.placeWithLayer(
(c.maxWidth - pl.width) / 2,
(c.maxHeight - pl.height) / 2 + DROP.roundToPx()
) {
scaleX = fit
scaleY = fit
}
}
}) {
// scene
Spacer(
Modifier
.matchParentSize()
.drawWithCache {
fun d(v: Float) = v.dp.toPx()
val g0 = Offset(d(CX), d(GY))
// letters
val style = TextStyle(
fontSize = FONT,
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Black,
letterSpacing = (-0.07).em
)
val lay = tm.measure("MLWRK", style)
val capPx = FONT.toPx() * CAP
val wb = lay.getBoundingBox(2)
val tl = Offset(
g0.x - wb.center.x,
d(GY + LIFT) - (lay.firstBaseline - capPx)
)
// letter heave
val lr = Random(seed + 1)
val glyphs = List(5) { tm.measure("MLWRK"[it].toString(), style) }
val spots = Array(5) { Offset(tl.x + lay.getBoundingBox(it).left, tl.y) }
val pivots = Array(5) {
val b = lay.getBoundingBox(it)
Offset(tl.x + b.center.x, if (it == 2) d(GY + LIFT) else tl.y + lay.firstBaseline)
}
val tilt = FloatArray(5) {
val s = if (lr.nextBoolean()) 1f else -1f
s * (if (it == 2) 1f + lr.nextFloat() * 2f else 3f + lr.nextFloat() * 6f)
}
val nudge = Array(5) {
if (it == 2) Offset.Zero
else Offset(d(lr.nextFloat() * 4f - 2f), d(lr.nextFloat() * 3f - 1f))
}
val heave = FloatArray(5) { .12f + .125f * abs(it - 2) }
// wood
val gr = Random(seed + 2)
val capY = d(GY + LIFT)
val stickW = capPx / CAP * .2f
val boxes = Array(5) {
val m = d(10f)
Rect(
spots[it].x - m, spots[it].y - m,
spots[it].x + glyphs[it].size.width + m,
spots[it].y + glyphs[it].size.height + m
)
}
fun pt(i: Int, u: Float, v: Float): Offset {
val w = glyphs[i].size.width.toFloat()
val half = (w + capPx / CAP * .07f) * .45f
return Offset(spots[i].x + w / 2f + (u - .5f) * 2f * half, capY + v * capPx)
}
fun grain(
dst: Path, sx: Float, sy: Float, ux: Float, uy: Float,
len: Float, off: Float, ks: Float, ko: Float, kr: Float
) {
val nx = -uy
val ny = ux
val amp = d(.3f + gr.nextFloat() * .8f)
val fq = .2f + gr.nextFloat() * .2f
val ph = gr.nextFloat() * 6.28f
var s = 0f
var first = true
while (s <= len) {
var o = off + amp * sin(s / density * fq + ph)
if (kr > 0f) {
val ds = s - ks
val dO = off - ko
o += (if (dO >= 0f) 1f else -1f) * kr *
exp(-(ds * ds / 4f + dO * dO) / (kr * kr))
}
val x = sx + ux * s + nx * o
val y = sy + uy * s + ny * o
if (first) dst.moveTo(x, y) else dst.lineTo(x, y)
first = false
s += d(2f)
}
}
fun ring(
dst: Path, sx: Float, sy: Float, ux: Float, uy: Float,
ks: Float, ko: Float, rx: Float, ry: Float
) {
for (k in 0..16) {
val a = k / 16f * 2f * PI_F
val s = ks + cos(a) * rx
val o = ko + sin(a) * ry
val x = sx + ux * s - uy * o
val y = sy + uy * s + ux * o
if (k == 0) dst.moveTo(x, y) else dst.lineTo(x, y)
}
dst.close()
}
fun bake(bnd: Rect, block: DrawScope.() -> Unit): ImageBitmap {
val img = ImageBitmap(
max(1, ceil(bnd.width).toInt()),
max(1, ceil(bnd.height).toInt())
)
CanvasDrawScope().draw(this, layoutDirection, Canvas(img), Size(bnd.width, bnd.height)) {
translate(-bnd.left, -bnd.top) { block() }
}
return img
}
val baseC = Color.Black.copy(alpha = .09f)
val grainC = Color.Black.copy(alpha = .13f)
val seamC = Color.Black.copy(alpha = .22f)
val nailC = Color.Black.copy(alpha = .5f)
val nailHi = Color.White.copy(alpha = .8f)
val hair = Stroke(d(.7f))
val seam = Stroke(d(.8f))
val nailR = d(1.7f)
val kb = Random(seed + 4)
val letters = List(5) { i ->
val bx = boxes[i]
// sticks
val pieces = STICKS[i].map { (u0, v0, u1, v1) ->
val a = pt(i, u0, v0)
val b = pt(i, u1, v1)
val dx = b.x - a.x
val dy = b.y - a.y
val l = sqrt(dx * dx + dy * dy)
val ux = dx / l
val uy = dy / l
val nx = -uy
val ny = ux
val h = stickW / 2f
val sx = a.x - ux * h
val sy = a.y - uy * h
val len = l + stickW
val poly = Path().apply {
moveTo(sx + nx * h, sy + ny * h)
lineTo(sx + ux * len + nx * h, sy + uy * len + ny * h)
lineTo(sx + ux * len - nx * h, sy + uy * len - ny * h)
lineTo(sx - nx * h, sy - ny * h)
close()
}
val lines = Path()
val knot = gr.nextFloat() < .4f
val ks = len * (.2f + .6f * gr.nextFloat())
val ko = (gr.nextFloat() - .5f) * stickW * .6f
val kr = if (knot) d(2f + 2f * gr.nextFloat()) else 0f
var o = -h
while (o <= h) {
grain(lines, sx, sy, ux, uy, len, o, ks, ko, kr)
o += d(1.6f + gr.nextFloat() * 1.6f)
}
if (knot) for (rr in floatArrayOf(.4f, .8f)) {
ring(lines, sx, sy, ux, uy, ks, ko, kr * rr * 1.6f, kr * rr * .8f)
}
val bnd = poly.getBounds().inflate(d(2f)).intersect(bx)
val img = bake(bnd) {
clipPath(poly) {
drawText(
textLayoutResult = glyphs[i],
color = Color.White,
topLeft = spots[i]
)
}
drawPath(lines, grainC, style = hair, blendMode = BlendMode.SrcAtop)
drawPath(poly, seamC, style = seam, blendMode = BlendMode.SrcAtop)
}
Piece(img, bnd.topLeft, poly, a, b)
}
// leftover ink
val base = Path()
var x = bx.left
while (x < bx.right) {
grain(base, x, bx.top, 0f, 1f, bx.height, 0f, 0f, 0f, 0f)
x += d(2.2f + gr.nextFloat() * 2.8f)
}
val body = bake(bx) {
drawText(
textLayoutResult = glyphs[i],
color = Color.White,
topLeft = spots[i]
)
drawPath(base, baseC, style = hair, blendMode = BlendMode.SrcAtop)
pieces.forEach { drawPath(it.poly, Color.Black, blendMode = BlendMode.DstOut) }
}
// nails
val js = JOINTS[i]
val nails = js.map { j ->
pt(i, j[2] + (gr.nextFloat() - .5f) * .03f, j[3] + (gr.nextFloat() - .5f) * .03f)
}
// joint breaks
val moves = arrayOfNulls<Move>(pieces.size)
val broken = BooleanArray(js.size)
var count = 0
val want = if (i == 2) kb.nextInt(0, 2) else kb.nextInt(1, 3)
for (ji in js.indices.shuffled(kb)) {
if (count >= want) break
if (i == 2 && ji == 1) continue
val j = js[ji]
val opts = listOf(j[0].toInt(), j[1].toInt())
.filter { moves[it] == null }
.shuffled(kb)
if (opts.isEmpty()) continue
val mover = opts[0]
broken[ji] = true
count++
val jp = pt(i, j[2], j[3])
val ang = (if (kb.nextBoolean()) 1f else -1f) * (5f + kb.nextFloat() * 7f)
val holds = js.indices.filter {
!broken[it] && (js[it][0].toInt() == mover || js[it][1].toInt() == mover)
}
val pc = pieces[mover]
moves[mover] = if (holds.isNotEmpty()) {
val hj = js[holds.maxByOrNull { (pt(i, js[it][2], js[it][3]) - jp).getDistance() }!!]
Move(pt(i, hj[2], hj[3]), ang, Offset.Zero)
} else {
val far = if ((pc.a - jp).getDistance() > (pc.b - jp).getDistance()) pc.a else pc.b
Move(far, ang, Offset(0f, d(1f + kb.nextFloat() * 2f)))
}
}
Letter(body, bx.topLeft, pieces, moves, nails, broken)
}
// roots
val rnd = Random(seed)
fun r(a: Float, b: Float) = a + rnd.nextFloat() * (b - a)
val roots = ArrayList<Root>()
fun spawn(
x0: Float, y0: Float, a0: Float, len: Float,
w: Float, t0: Float, t1: Float, kids: Boolean
) {
val n = max(4, (len / STEP).toInt())
val xs = FloatArray(n + 1)
val ys = FloatArray(n + 1)
xs[0] = x0
ys[0] = y0
var a = a0
for (i in 1..n) {
a += r(-.5f, .5f)
a += (a0 - a) * .25f
xs[i] = (xs[i - 1] + cos(a) * STEP).coerceIn(-150f, 150f)
ys[i] = max(ys[i - 1] + sin(a) * STEP, 1f)
}
roots += Root(
FloatArray(n + 1) { d(CX + xs[it]) },
FloatArray(n + 1) { d(GY + ys[it]) },
d(w), t0, t1
)
if (kids) repeat(rnd.nextInt(1, 3)) {
val i = rnd.nextInt(n / 4, n * 2 / 3 + 1)
val f = i.toFloat() / n
val s = if (rnd.nextBoolean()) 1f else -1f
spawn(
xs[i], ys[i],
(a0 + s * r(.5f, 1.1f)).coerceIn(.1f, PI_F - .1f),
len * r(.3f, .5f),
w * (1f - f) * .8f,
t0 + (t1 - t0) * f,
(t1 + r(.05f, .15f)).coerceAtMost(.85f),
false
)
}
}
spawn(0f, 0f, PI_F / 2 + r(-.1f, .1f), r(60f, 75f), 12f, .07f, .55f, true)
for (s in floatArrayOf(-1f, 1f)) {
fun dir(a: Float) = if (s > 0) a else PI_F - a
spawn(s * 4f, 2f, dir(r(.05f, .18f)), r(110f, 135f), 10f, .08f, .70f, true)
spawn(s * 4f, 3f, dir(r(.45f, .65f)), r(70f, 90f), 8f, .10f, .64f, true)
spawn(s * 3f, 4f, dir(r(.9f, 1.1f)), r(45f, 60f), 6f, .12f, .60f, true)
}
// trunk
val trunk = Path().apply {
moveTo(d(CX - 18f), d(GY + 4f))
quadraticTo(d(CX - 10f), d(GY + 1f), d(CX - 10f), d(GY - 20f))
lineTo(d(CX - 3f), d(TOP))
lineTo(d(CX + 3f), d(TOP))
lineTo(d(CX + 10f), d(GY - 20f))
quadraticTo(d(CX + 10f), d(GY + 1f), d(CX + 18f), d(GY + 4f))
close()
}
val trunkH = d(GY - TOP)
val trunkBox = Rect(d(CX - 20f), d(TOP - 2f), d(CX + 20f), d(GY + 6f))
val plain = Paint()
// bark
val br = Random(seed + 3)
val bark = Path()
for (k in 0 until 7) {
val f = -.85f + 1.7f * (k + br.nextFloat() * .6f - .3f) / 6f
val ph = br.nextFloat() * 6.28f
val fq = .12f + br.nextFloat() * .12f
var y = TOP + br.nextFloat() * 6f
var pen = false
while (y <= GY) {
val hw = 3f + 7f * ((y - TOP) / (GY - 20f - TOP)).coerceIn(0f, 1f)
val x = CX + f * hw + .5f * sin(y * fq + ph)
if (pen) bark.lineTo(d(x), d(y)) else bark.moveTo(d(x), d(y))
pen = true
if (br.nextFloat() < .04f) {
pen = false
y += 2f + br.nextFloat() * 4f
}
y += 2f
}
}
val barkBrush = Brush.verticalGradient(
0f to Color.Black.copy(alpha = .22f),
.85f to Color.Black.copy(alpha = .22f),
1f to Color.Transparent,
startY = d(TOP),
endY = d(GY)
)
val barkS = Stroke(d(.9f), cap = StrokeCap.Round)
// canopy warp
val tr = Random(seed + 6)
val widthK = .89f + tr.nextFloat() * .22f
val sideK = FloatArray(2) { .95f + tr.nextFloat() * .1f }
val ph = FloatArray(8) { tr.nextFloat() * 2f * PI_F }
val stretchA = FloatArray(2) { .15f + tr.nextFloat() * .12f }
val droopA = FloatArray(2) { 4f + tr.nextFloat() * 4.5f }
val k1 = 2f * PI_F / (29f + tr.nextFloat() * 14f)
val k2 = 2f * PI_F / (13.5f + tr.nextFloat() * 6.5f)
val k3 = 2f * PI_F / (24f + tr.nextFloat() * 13f)
fun warp(x: Float, y: Float): Offset {
val dx = x - CX
val si = if (dx >= 0f) 0 else 1
val rr = (abs(dx) / 66f).coerceIn(0f, 1f)
val w = rr * rr
val side = 1f + w * (sideK[si] - 1f)
val stretch = widthK * side * (1f + w * stretchA[si] * (
sin(y * k1 + ph[si * 4]) + .5f * sin(y * k2 + ph[si * 4 + 1])
))
val yy = y + w * droopA[si] * (
sin(y * k3 + ph[si * 4 + 2]) * .8f + .2f * sin(y * k2 + ph[si * 4 + 3])
)
return Offset(CX + dx * stretch, yy)
}
// canopy
fun Path.poly(a: FloatArray) {
val m = a.size / 2
for (i in 0 until m) {
val j = (i + 1) % m
val x0 = a[i * 2]
val y0 = a[i * 2 + 1]
val x1 = a[j * 2]
val y1 = a[j * 2 + 1]
val n = max(1, ceil(sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)) / 2f).toInt())
for (k in 0 until n) {
val u = k.toFloat() / n
val o = warp(x0 + (x1 - x0) * u, y0 + (y1 - y0) * u)
if (i == 0 && k == 0) moveTo(d(o.x), d(o.y)) else lineTo(d(o.x), d(o.y))
}
}
close()
}
val canopy = Path().apply {
fillType = PathFillType.EvenOdd
poly(CANOPY)
CANOPY_HOLES.forEach { poly(it) }
}
val cb = canopy.getBounds().inflate(d(2f))
val cImg = bake(cb) { drawPath(canopy, Color.White) }
// trunk reach
val sTop = -60
val sTab = FloatArray(GY.toInt() - sTop + 1) { k ->
val need = ((GY - (sTop + k)) / (GY - TOP)).coerceIn(0f, 1f)
var lo = 0f
var hi = 1f
repeat(16) {
val m = (lo + hi) * .5f
if (FastOutSlowInEasing.transform(m) < need) lo = m else hi = m
}
TRUNK_A + (TRUNK_B - TRUNK_A) * hi
}
fun reach(y: Float): Float {
val f = (y - sTop).coerceIn(0f, (sTab.size - 1).toFloat())
val i = min(f.toInt(), sTab.size - 2)
return sTab[i] + (sTab[i + 1] - sTab[i]) * (f - i)
}
// canopy hit
val hx = FloatArray(CANOPY.size / 2)
val hy = FloatArray(CANOPY.size / 2)
for (i in hx.indices) {
val o = warp(CANOPY[i * 2], CANOPY[i * 2 + 1])
hx[i] = o.x
hy[i] = o.y
}
fun inside(x: Float, y: Float): Boolean {
var c = false
var j = hx.size - 1
for (i in hx.indices) {
if ((hy[i] > y) != (hy[j] > y) &&
x < (hx[j] - hx[i]) * (y - hy[i]) / (hy[j] - hy[i]) + hx[i]
) c = !c
j = i
}
return c
}
// skeleton
val sk = Random(seed + 7)
fun rs(a: Float, b: Float) = a + sk.nextFloat() * (b - a)
fun limb(
ox: Float, oy: Float, cx: Float, cy: Float,
tx: Float, ty: Float, amp: Float
): Pair<FloatArray, FloatArray>? {
val len = sqrt((tx - ox) * (tx - ox) + (ty - oy) * (ty - oy))
val n = max(4, (len / 2.5f).toInt())
val wph = rs(0f, 2f * PI_F)
val wv = rs(1.5f, 3f)
val xs = ArrayList<Float>(n + 1)
val ys = ArrayList<Float>(n + 1)
for (i in 0..n) {
val u = i.toFloat() / n
val v = 1f - u
val x = v * v * ox + 2f * v * u * cx + u * u * tx
val y = v * v * oy + 2f * v * u * cy + u * u * ty
val gx = 2f * v * (cx - ox) + 2f * u * (tx - cx)
val gy = 2f * v * (cy - oy) + 2f * u * (ty - cy)
val gl = sqrt(gx * gx + gy * gy).coerceAtLeast(1e-3f)
val o = amp * sin(u * PI_F * wv + wph) * sin(u * PI_F)
val w = warp(x - gy / gl * o, y + gx / gl * o)
if (i > 0 && !inside(w.x, w.y)) break
xs += d(w.x)
ys += d(w.y)
}
return if (xs.size < 2) null else xs.toFloatArray() to ys.toFloatArray()
}
val nTier = CELLS.size
val rank = IntArray(nTier)
(0 until nTier).sortedByDescending { LIMBS[it * 6 + 1] }
.forEachIndexed { r, i -> rank[i] = r }
val tiers = List(nTier) { i ->
val b = i * 6
val ox = LIMBS[b]
val oy = LIMBS[b + 1]
val cx = LIMBS[b + 2] + rs(-1.5f, 1.5f)
val cy = LIMBS[b + 3] + rs(-1.5f, 1.5f)
val tx = LIMBS[b + 4] + rs(-1f, 1f)
val ty = LIMBS[b + 5] + rs(-1f, 1f)
val len = sqrt((tx - ox) * (tx - ox) + (ty - oy) * (ty - oy))
val lk = (len / 70f).coerceAtMost(1f)
val t0 = reach(oy + BASE)
val t1 = t0 + .06f + .1f * lk
val w = 2f + 2.2f * lk
val limbs = ArrayList<Root>()
limb(ox, oy, cx, cy, tx, ty, rs(.2f, .6f))?.let { (xs, ys) ->
limbs += Root(xs, ys, d(w), t0, t1)
}
// twigs
var flip = sk.nextBoolean()
repeat(if (len < 25f) sk.nextInt(0, 2) else sk.nextInt(1, 4)) {
val f = rs(.3f, .8f)
val v = 1f - f
val px = v * v * ox + 2f * v * f * cx + f * f * tx
val py = v * v * oy + 2f * v * f * cy + f * f * ty
val gx = 2f * v * (cx - ox) + 2f * f * (tx - cx)
val gy = 2f * v * (cy - oy) + 2f * f * (ty - cy)
val a = atan2(gy, gx) + (if (flip) 1f else -1f) * rs(.45f, .9f)
flip = !flip
val tl = len * rs(.2f, .38f)
val twig = limb(
px, py,
px + cos(a) * tl * .5f + rs(-1.5f, 1.5f),
py + sin(a) * tl * .5f + rs(-1.5f, 1.5f),
px + cos(a) * tl, py + sin(a) * tl,
rs(.2f, .5f)
)
val s0 = t0 + (t1 - t0) * f
val s1 = s0 + rs(.05f, .09f)
if (twig != null) limbs += Root(twig.first, twig.second, d(w * v * .7f + .6f), s0, s1)
}
// leaves
val cell = Path().apply { poly(CELLS[i]) }
val bnd = cell.getBounds().intersect(cb)
val img = bake(bnd) {
clipPath(cell) { drawPath(canopy, Color.White) }
}
val tip = warp(tx, ty)
val ang = atan2(tip.y - oy, tip.x - ox) * 180f / PI_F
val leaf = TRUNK_B - .02f + rank[i] * LEAF_GAP + rs(-.008f, .008f)
Tier(img, bnd.topLeft, Offset(d(ox), d(oy)), ang, leaf, limbs)
}
val leafStart = tiers.minOf { it.leaf }
val leafDone = tiers.maxOf { it.leaf } + LEAF
val shadowA = .06f
val shadowTint = ColorFilter.tint(Color.Black)
val shadowOffs = floatArrayOf(d(3f), d(6f))
val lettersBox = Rect(0f, d(GY - 20f), size.width, d(GY + 120f))
val rootShadowC = Color.Black.copy(alpha = .06f)
val rootOffs = arrayOf(Offset(d(.7f), d(1.2f)), Offset(d(1.4f), d(2.4f)), Offset(d(2.1f), d(3.6f)))
// cone
val coneHdp = 11f
val coneWdp = 3.6f
val stemDp = 3f
val coneH = d(coneHdp)
val coneW = d(coneWdp)
val conePath = Path().apply {
moveTo(0f, -coneH / 2f)
cubicTo(coneW * 1.5f, -coneH / 2f, coneW * 1.3f, coneH * .15f, 0f, coneH / 2f)
cubicTo(-coneW * 1.3f, coneH * .15f, -coneW * 1.5f, -coneH / 2f, 0f, -coneH / 2f)
close()
}
val coneScales = Path().apply {
for (k in 1..5) {
val y = -coneH / 2f + coneH * k / 6f
var x = -coneW * 1.4f + (k % 2) * coneW * .35f
moveTo(x, y)
while (x < coneW * 1.4f) {
quadraticTo(x + coneW * .35f, y + d(1.6f), x + coneW * .7f, y)
x += coneW * .7f
}
}
}
val coneScaleC = Color.Black.copy(alpha = .3f)
val coneScaleS = Stroke(d(.6f))
val stemW = d(1f)
fun DrawScope.drawCone() {
drawPath(conePath, Color.White)
clipPath(conePath) {
drawPath(coneScales, coneScaleC, style = coneScaleS)
}
}
// cone origin
val kc = Random(seed + 5)
val bottoms = (0 until HANGS.size / 3).filter { HANGS[it * 3 + 2] >= GY }
val hk = bottoms[kc.nextInt(bottoms.size)] * 3
val hp = warp(HANGS[hk], HANGS[hk + 1] - .5f)
val coneA = Offset(d(hp.x), d(hp.y))
val coneHC = Offset(coneA.x, coneA.y + d(stemDp + coneHdp / 2f))
val coneSurf = d(HANGS[hk + 2])
val coneSide = if (hp.x >= CX) 1f else -1f
onDrawBehind {
val t = p.value
roots.forEach {
val g = phase(t, it.t0, it.t1)
if (g != it.g) {
it.g = g
if (g > 0f) it.build(g)
}
}
// letters
drawContext.canvas.saveLayer(lettersBox, plain)
val la = phase(t, 0f, .05f)
if (la > 0f) for (i in 0..4) {
val g = phase(t, heave[i], heave[i] + .35f)
val gb = phase(t, heave[i] + .08f, heave[i] + .45f)
val lt = letters[i]
translate(nudge[i].x * g, nudge[i].y * g) {
rotate(tilt[i] * g, pivot = pivots[i]) {
drawImage(lt.body, lt.at, alpha = la)
lt.pieces.forEachIndexed { si, pc ->
val mv = lt.moves[si]
if (mv != null && gb > 0f) {
rotate(mv.angle * gb, pivot = mv.pivot) {
translate(mv.shift.x * gb, mv.shift.y * gb) {
drawImage(pc.img, pc.at, alpha = la)
}
}
} else {
drawImage(pc.img, pc.at, alpha = la)
}
}
lt.nails.forEachIndexed { ni, n ->
if (lt.popped[ni] && gb > .3f) {
drawCircle(nailC, nailR * .45f, n, alpha = la)
} else {
drawCircle(nailC, nailR, n, alpha = la)
drawCircle(
nailHi, nailR * .35f,
n - Offset(nailR * .3f, nailR * .3f),
alpha = la
)
}
}
}
}
}
// root shadows
roots.forEach { rt ->
if (rt.g > 0f) for (o in rootOffs) translate(o.x, o.y) {
drawPath(rt.path, rootShadowC, blendMode = BlendMode.SrcAtop)
}
}
drawContext.canvas.restore()
// roots
roots.forEach {
if (it.g > 0f) drawPath(it.path, Color.White)
}
// limbs
val shown = t > leafStart
val done = t >= leafDone
if (!done) tiers.forEach { tier ->
if (t < tier.leaf + LEAF) tier.limbs.forEach {
val g = phase(t, it.t0, it.t1)
if (g != it.g) {
it.g = g
if (g > 0f) it.build(g)
}
if (it.g > 0f) drawPath(it.path, Color.White)
}
}
fun DrawScope.leaves(a: Float, tint: ColorFilter?, mode: BlendMode, dy: Float) {
if (done) {
drawImage(cImg, Offset(cb.left, cb.top + dy), alpha = a, colorFilter = tint, blendMode = mode)
return
}
tiers.forEach { tier ->
val g = phase(t, tier.leaf, tier.leaf + LEAF)
if (g > 0f) translate(0f, dy) {
rotate(tier.angle, pivot = tier.origin) {
scale(1f, .06f + .94f * g, pivot = tier.origin) {
rotate(-tier.angle, pivot = tier.origin) {
drawImage(
tier.img, tier.at,
alpha = a * min(1f, g * 2.5f),
colorFilter = tint, blendMode = mode
)
}
}
}
}
}
}
// trunk
val rv = phase(t, TRUNK_A, TRUNK_B)
if (rv > 0f) clipRect(top = g0.y - rv * trunkH) {
drawContext.canvas.saveLayer(trunkBox, plain)
scale(.35f + .65f * phase(t, .10f, .80f), 1f, pivot = g0) {
drawPath(trunk, Color.White)
drawPath(bark, barkBrush, style = barkS, blendMode = BlendMode.SrcAtop)
}
// trunk shadow
if (shown) for (o in shadowOffs) {
leaves(shadowA, shadowTint, BlendMode.SrcAtop, o)
}
drawContext.canvas.restore()
}
// canopy
if (shown) leaves(1f, null, BlendMode.SrcOver, 0f)
// cone
val c = cone.value
if (c > 0f) {
if (c < .5f) {
val sw = 14f * sin(c / .5f * 6f * PI_F) * (1f - c / .5f)
val grow = phase(c, 0f, .12f)
rotate(sw, pivot = coneA) {
drawLine(Color.White, coneA, coneA + Offset(0f, d(stemDp)), strokeWidth = stemW)
translate(coneHC.x, coneHC.y) {
scale(grow, pivot = Offset.Zero) { drawCone() }
}
}
} else {
drawLine(Color.White, coneA, coneA + Offset(0f, d(stemDp * .5f)), strokeWidth = stemW)
val land = coneSurf - coneW * .9f
val dist = max(land - coneHC.y, 0f)
val ff = (.3f * sqrt(dist / d(200f))).coerceIn(.1f, .3f)
val ft = ((c - .5f) / ff).coerceIn(0f, 1f)
var y = coneHC.y + dist * ft * ft
var rot = coneSide * 95f * ft
val bs = .5f + ff
if (c > bs) {
val b = ((c - bs) / .15f).coerceIn(0f, 1f)
y -= d(4f) * abs(sin(b * 2f * PI_F)) * (1f - b) * (1f - b)
rot = coneSide * (95f - 15f * phase(c, bs, bs + .15f))
}
translate(coneHC.x, y) {
rotate(rot, pivot = Offset.Zero) { drawCone() }
}
}
}
}
}
)
// version info
Column(
modifier = Modifier
.fillMaxWidth()
.offset(y = TEXT_Y.dp)
.graphicsLayer {
val a = phase(p.value, .84f, 1f)
alpha = a
translationY = (1f - a) * 6.dp.toPx()
},
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"com.vgmlr.grain",
color = ink,
fontSize = 14.sp,
lineHeight = 18.sp
)
Text(
"215.026.1",
color = ink,
fontSize = 14.sp,
lineHeight = 18.sp,
modifier = Modifier.clickable {
uriHandler.openUri("https://vgmlr.com/?tenon=Grain")
}
)
}
}
}
}
}
}
private class Root(
val xs: FloatArray, val ys: FloatArray,
val w: Float, val t0: Float, val t1: Float
) {
val path = Path()
var g = 0f
private val n = xs.size - 1
private val ex = FloatArray((n + 1) * 2)
private val ey = FloatArray((n + 1) * 2)
fun build(g: Float) {
path.reset()
val e = g * n
val k = ceil(e).toInt().coerceIn(1, n)
val f = e - (k - 1)
for (i in 0..k) {
val x = if (i < k) xs[i] else xs[k - 1] + (xs[k] - xs[k - 1]) * f
val y = if (i < k) ys[i] else ys[k - 1] + (ys[k] - ys[k - 1]) * f
val tx = xs[min(i + 1, n)] - xs[max(i - 1, 0)]
val ty = ys[min(i + 1, n)] - ys[max(i - 1, 0)]
val len = sqrt(tx * tx + ty * ty).coerceAtLeast(1e-3f)
val hw = w * .5f * (1f - i.toFloat() / k) * (.5f + .5f * g)
val nx = -ty / len * hw
val ny = tx / len * hw
ex[i * 2] = x + nx
ey[i * 2] = y + ny
ex[i * 2 + 1] = x - nx
ey[i * 2 + 1] = y - ny
}
path.moveTo(ex[0], ey[0])
for (i in 1..k) path.lineTo(ex[i * 2], ey[i * 2])
for (i in k downTo 0) path.lineTo(ex[i * 2 + 1], ey[i * 2 + 1])
path.close()
}
}
private class Tier(
val img: ImageBitmap,
val at: Offset,
val origin: Offset,
val angle: Float,
val leaf: Float,
val limbs: List<Root>
)
private class Piece(val img: ImageBitmap, val at: Offset, val poly: Path, val a: Offset, val b: Offset)
private class Move(val pivot: Offset, val angle: Float, val shift: Offset)
private class Letter(
val body: ImageBitmap,
val at: Offset,
val pieces: List<Piece>,
val moves: Array<Move?>,
val nails: List<Offset>,
val popped: BooleanArray
)
private fun phase(t: Float, a: Float, b: Float) =
FastOutSlowInEasing.transform(((t - a) / (b - a)).coerceIn(0f, 1f))