Pith - grain
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))