Created
September 9, 2026 15:07
-
-
Save Kyriakos-Georgiopoulos/7c872b0a85167f0483ad8d8299da6612 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Copyright 2026 Kyriakos Georgiopoulos | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| import android.graphics.Bitmap | |
| import androidx.compose.animation.core.Animatable | |
| import androidx.compose.animation.core.VectorConverter | |
| import androidx.compose.animation.core.spring | |
| import androidx.compose.foundation.Canvas | |
| import androidx.compose.foundation.gestures.detectDragGestures | |
| import androidx.compose.foundation.gestures.detectTapGestures | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.mutableFloatStateOf | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.withFrameNanos | |
| import androidx.compose.runtime.rememberCoroutineScope | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.geometry.Offset | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.graphics.drawscope.drawIntoCanvas | |
| import androidx.compose.ui.graphics.nativeCanvas | |
| import androidx.compose.ui.input.pointer.pointerInput | |
| import androidx.compose.ui.layout.onSizeChanged | |
| import androidx.compose.ui.tooling.preview.Preview | |
| import androidx.compose.ui.unit.IntSize | |
| import kotlinx.coroutines.Dispatchers | |
| import kotlinx.coroutines.async | |
| import kotlinx.coroutines.awaitAll | |
| import kotlinx.coroutines.coroutineScope | |
| import kotlinx.coroutines.delay | |
| import kotlinx.coroutines.ensureActive | |
| import kotlin.coroutines.coroutineContext | |
| import kotlinx.coroutines.launch | |
| import kotlinx.coroutines.withContext | |
| import kotlin.math.abs | |
| import kotlin.math.cos | |
| import kotlin.math.exp | |
| import kotlin.math.min | |
| import kotlin.math.roundToInt | |
| import kotlin.math.sin | |
| import kotlin.math.sqrt | |
| /* | |
| * A rusted, half-painted steel tear plate: the industrial floor you have walked on | |
| * in every car park and every fire escape. | |
| * | |
| * Nothing here is a traced bitmap. The plate is a pure function of position, and | |
| * every constant in it was measured off one photograph rather than tuned by eye. | |
| * The lattice came out of the image's own autocorrelation (76.35 x 72.51 px, two | |
| * studs per cell, half a cell apart, tilting +-44.6 degrees). The stud came out of | |
| * two hundred studs stacked after being re-aligned to each other. The rust came | |
| * out of the photograph's band energies and tonal quantiles, so the texture is | |
| * generated but its statistics are the photograph's. | |
| * | |
| * What that buys is a plate with no repeat. The reference is 992 x 662 px of | |
| * floor; this is however much floor the screen asks for, at the same density, with | |
| * no tile seam anywhere in it. | |
| */ | |
| private const val PX = 76.35f | |
| private const val PY = 72.51f | |
| private const val INV_PX = 1f / PX | |
| private const val INV_PY = 1f / PY | |
| private const val PHASE_X = 2.83f | |
| private const val PHASE_Y = 18.94f | |
| private val COS_A = cos(Math.toRadians(44.89).toFloat()) | |
| private val SIN_A = sin(Math.toRadians(44.89).toFloat()) | |
| private val COS_B = cos(Math.toRadians(-44.28).toFloat()) | |
| private val SIN_B = sin(Math.toRadians(-44.28).toFloat()) | |
| private const val CAP_A = 20.4f | |
| private const val CAP_B = 6f | |
| private const val RIM_A = 23.657f | |
| private const val RIM_B = 7.8881f | |
| private const val CAP_GRAIN = 0.235f | |
| private const val EDGE_AA = 1.5f | |
| private const val INV_EDGE_AA = 1f / EDGE_AA | |
| private const val INV_CAP_A = 1f / CAP_A | |
| private const val INV_CAP_B = 1f / CAP_B | |
| /** | |
| * Radius of the two arcs that make the boss outline, and the offset of their | |
| * centres from the stud's own centre. | |
| */ | |
| private val RIM_R = (RIM_A * RIM_A + RIM_B * RIM_B) / (2f * RIM_B) | |
| private val RIM_CY = RIM_R - RIM_B | |
| private val CAP_R = (CAP_A * CAP_A + CAP_B * CAP_B) / (2f * CAP_B) | |
| private val CAP_CY = CAP_R - CAP_B | |
| private const val LIGHT_X = -0.32048f | |
| private const val LIGHT_Y = -0.65463f | |
| private const val LIGHT_Z = 0.37003f | |
| private val LIGHT_LEN = sqrt(LIGHT_X * LIGHT_X + LIGHT_Y * LIGHT_Y + LIGHT_Z * LIGHT_Z) | |
| private val LIGHT_XY = sqrt(LIGHT_X * LIGHT_X + LIGHT_Y * LIGHT_Y) | |
| private val INV_LIGHT_LEN = 1f / LIGHT_LEN | |
| private val INV_LIGHT_XY = 1f / LIGHT_XY | |
| private const val DOME_H = 6.6818f | |
| private const val DOME_U = DOME_H / (CAP_A * CAP_A) | |
| private const val DOME_V = DOME_H / (CAP_B * CAP_B) | |
| private const val AMB_R = 52.911f | |
| private const val AMB_G = 123.53f | |
| private const val AMB_B = 139.07f | |
| private const val DIF_R = 42.515f | |
| private const val DIF_G = 43.408f | |
| private const val DIF_B = 43.612f | |
| private const val RUST_AMB_R = 70f | |
| private const val RUST_AMB_G = 62f | |
| private const val RUST_AMB_B = 50f | |
| private const val RUST_DIF_R = 76f | |
| private const val RUST_DIF_G = 66f | |
| private const val RUST_DIF_B = 55f | |
| private const val EDGE_W = 2.9922f | |
| private const val INV_EDGE_W = 1f / EDGE_W | |
| private const val EDGE_P = 1.809f | |
| private const val DARK_R = 14.199f | |
| private const val DARK_G = 0.00061179f | |
| private const val DARK_B = 0.00039027f | |
| private const val RUST_DARK_R = 25f | |
| private const val RUST_DARK_G = 14f | |
| private const val RUST_DARK_B = 8f | |
| private const val RIM_LO = 8f | |
| private const val RIM_HI = 170.91f | |
| private const val RIM_P = 1.6226f | |
| private const val CAST_SHADOW = 0.47512f | |
| private const val SPEC_AMP = 96f | |
| private const val SPEC_K = SPEC_AMP / 120f | |
| private const val SPEC_W = 1f | |
| private const val SPEC_R = 153.57f | |
| private const val SPEC_G = 203.95f | |
| private const val SPEC_B = 202.38f | |
| private val STAIN = floatArrayOf( | |
| 99.5151f, -4.32356f, -4.51962f, -23.3768f, 10.0501f, -13.0407f, -0.453534f, | |
| 3.89637f, -8.5532f, -36.1819f | |
| ) | |
| private val OCT_LAMBDA = floatArrayOf( | |
| 7.6f, 12.5f, 20.6f, 34f, 56f, 92f, 152f | |
| ) | |
| private val OCT_AMP = floatArrayOf( | |
| 38.35f, 12.64f, 11.72f, 10.9f, 11.65f, 23.37f, 29.86f | |
| ) | |
| private const val COARSE_FROM = 2 | |
| private const val FINE_GAIN = 19.844f | |
| private val CHROMA_LAMBDA = floatArrayOf( | |
| 2.977f, 7.814f, 18.49f, 64.71f | |
| ) | |
| private val CHROMA_AMP = floatArrayOf( | |
| 0.9235f, 0.63f, 0.4195f, 0.3413f | |
| ) | |
| private const val CHROMA_MID = 0.63673f | |
| private const val CHROMA_K = 1.5737f | |
| private const val PAINT_LAM1 = 34.587f | |
| private const val PAINT_LAM2 = 15.228f | |
| private const val PAINT_W1 = 0.75415f | |
| private const val PAINT_LO = 0.69658f | |
| private const val PAINT_WIDTH = 0.026746f | |
| private const val INV_PAINT_WIDTH = 1f / PAINT_WIDTH | |
| private const val PAINT_NEAR_KEEP = 0.2191f | |
| private const val PAINT_NEAR = 8.8849f | |
| private const val PAINT_FAR = 18.832f | |
| private const val PAINT_TONE = 0.36043f | |
| private const val FLECK_P = 0.00040f | |
| private const val PIT_P = 0.00022f | |
| private const val CLUSTER_LO = 0.25f | |
| private const val CLUSTER_HI = 1.5f | |
| /** | |
| * The loosest threshold either population can ask for, at the clustering field's | |
| * ceiling. A pixel that fails this cannot pass the real test at any clustering, so | |
| * the field itself is only evaluated for the one pixel in a thousand that gets | |
| * that far. | |
| */ | |
| private const val FLECK_GATE = 1f - FLECK_P * (CLUSTER_LO + CLUSTER_HI) | |
| private const val PIT_GATE = 1f - PIT_P * (CLUSTER_LO + CLUSTER_HI) | |
| private const val CHROMA_FINE = 2 | |
| private const val FINE_WARP = 1.347f | |
| private const val FINE_CONTRAST = 0.24f | |
| private const val FINE_S1 = 0.4719f | |
| private const val FINE_S2 = 1.1522f | |
| private const val FINE_S3 = 1.0288f | |
| private const val INV_FINE_S1 = 1f / FINE_S1 | |
| private const val INV_FINE_S2 = 1f / FINE_S2 | |
| private const val INV_FINE_S3 = 1f / FINE_S3 | |
| private const val FINE_CONTRAST_K = FINE_CONTRAST / 0.29f | |
| private const val FINE_GAIN_S3 = FINE_GAIN / FINE_S3 | |
| /** | |
| * |g| raised to the warp exponent, tabulated. The warp runs on every pixel of the | |
| * plate, and Math.pow there costs more than every octave of noise put together. | |
| * The field is normalised to unit sd, so eight sigma covers the whole domain. | |
| */ | |
| private val WARP_LUT = FloatArray(1025) { i -> | |
| Math.pow((i * 8f / 1024f).toDouble(), FINE_WARP.toDouble()).toFloat() | |
| } | |
| private fun warp(g: Float): Float { | |
| val a = if (g < 0f) -g else g | |
| val x = a * 128f | |
| val i = x.toInt() | |
| if (i >= 1024) return if (g < 0f) -WARP_LUT[1024] else WARP_LUT[1024] | |
| val w = WARP_LUT[i] + (WARP_LUT[i + 1] - WARP_LUT[i]) * (x - i) | |
| return if (g < 0f) -w else w | |
| } | |
| /** exp for |x| < 0.5, which is the only range the contrast term ever reaches. */ | |
| private fun expSmall(x: Float): Float = 1f + x * (1f + x * (0.5f + x * 0.1666667f)) | |
| /** | |
| * Four more tabulated curves. exp and pow measure 135 and 163 ns a call on the | |
| * test device, an order of magnitude past a table read, and between them they run | |
| * on every pixel anywhere near a stud, which is most of the plate. The first two | |
| * are indexed by the SQUARED distance, so a pixel off the boss never pays for a | |
| * square root either. | |
| */ | |
| private val SHADOW_Q0 = RIM_R * RIM_R | |
| private val SHADOW_Q1 = (RIM_R + 12f) * (RIM_R + 12f) | |
| private val INV_SHADOW_SPAN = 1f / (SHADOW_Q1 - SHADOW_Q0) | |
| private val SHADOW_LUT = FloatArray(257) { | |
| val d = sqrt(SHADOW_Q0 + (SHADOW_Q1 - SHADOW_Q0) * it / 256f) - RIM_R | |
| exp(-(if (d > 0f) d else 0f) / 2.5f) | |
| } | |
| private val GATE_Q0 = (RIM_R + PAINT_NEAR) * (RIM_R + PAINT_NEAR) | |
| private val GATE_Q1 = (RIM_R + PAINT_FAR) * (RIM_R + PAINT_FAR) | |
| private val INV_GATE_SPAN = 1f / (GATE_Q1 - GATE_Q0) | |
| private val GATE_LUT = FloatArray(257) { | |
| val d = sqrt(GATE_Q0 + (GATE_Q1 - GATE_Q0) * it / 256f) - RIM_R | |
| clamp01((d - PAINT_NEAR) / (PAINT_FAR - PAINT_NEAR)) | |
| } | |
| private val BOSS_Q = (RIM_R + EDGE_AA * 0.5f) * (RIM_R + EDGE_AA * 0.5f) | |
| private val RIM_LUT = FloatArray(257) { Math.pow(it / 256.0, RIM_P.toDouble()).toFloat() } | |
| private val EDGE_LUT = FloatArray(257) { Math.pow(it / 256.0, EDGE_P.toDouble()).toFloat() } | |
| private val SPEC_LUT = FloatArray(257) { | |
| val d = it * 8f / 256f | |
| exp(-(d / SPEC_W) * (d / SPEC_W)) | |
| } | |
| /** Reads a 257-entry table over 0..1 with linear interpolation. */ | |
| private fun lut01(t: FloatArray, x: Float): Float { | |
| val p = clamp01(x) * 256f | |
| val i = p.toInt() | |
| if (i >= 256) return t[256] | |
| return t[i] + (t[i + 1] - t[i]) * (p - i) | |
| } | |
| private const val LUT_N = 1024 | |
| private const val T_LO = -69.5467f | |
| private const val T_HI = 237.646f | |
| private const val LUT_K = (LUT_N - 1f) / (T_HI - T_LO) | |
| /** | |
| * The tone quantiles the three colour ramps are indexed by. They are non-uniform, | |
| * dense where the plate spends its time, so the ramps are resampled onto a uniform | |
| * lookup once at load rather than binary-searched per pixel. | |
| */ | |
| private val TONE_Q = floatArrayOf( | |
| -69.5467f, 15.7951f, 25.4389f, 31.4504f, 36.0867f, 39.725f, 42.9351f, 45.6747f, | |
| 48.1273f, 50.36f, 52.4905f, 54.4382f, 56.2221f, 57.9629f, 59.5674f, 61.1434f, | |
| 62.6654f, 64.1216f, 65.5534f, 66.9279f, 68.2747f, 69.5971f, 70.8873f, 72.1526f, | |
| 73.4146f, 74.6483f, 75.8554f, 77.0348f, 78.2107f, 79.3847f, 80.5766f, 81.7359f, | |
| 82.9096f, 84.0611f, 85.2143f, 86.4067f, 87.5652f, 88.731f, 89.9261f, 91.0977f, | |
| 92.2644f, 93.4915f, 94.7503f, 96.019f, 97.3387f, 98.6748f, 100.046f, 101.424f, | |
| 102.884f, 104.344f, 105.881f, 107.463f, 109.177f, 110.948f, 112.827f, 114.858f, | |
| 117.041f, 119.397f, 122.047f, 125.036f, 128.51f, 132.953f, 138.722f, 147.899f, | |
| 237.646f | |
| ) | |
| private val PAINT_TONE_MID = TONE_Q[32] | |
| private val PAINT_TONE_K = PAINT_TONE / (TONE_Q[56] - TONE_Q[8]) | |
| /** | |
| * Three ramps, all 65 stops against [TONE_Q]. Each takes its luminance from the | |
| * photograph's quantile at that stop and its hue from the mean colour there. Mean | |
| * RGB alone averages the ends away (its top is 168 where the plate reaches 186), | |
| * and per-channel quantiles alone bleach them, because they put the maximum red | |
| * and the maximum blue on the same pixel and the photograph never does. | |
| */ | |
| private val RAMP_SATURATED = intArrayOf( | |
| 0x000000, 0x220400, 0x2E0800, 0x370C01, 0x3F1002, 0x451303, 0x4B1604, 0x501905, | |
| 0x551C07, 0x5A1F08, 0x5F2209, 0x64250B, 0x68270C, 0x6C2A0D, 0x6F2C0F, 0x722E10, | |
| 0x753011, 0x773212, 0x7A3413, 0x7C3614, 0x7E3716, 0x803916, 0x833A17, 0x843C18, | |
| 0x863D19, 0x873F1A, 0x89401B, 0x8A411C, 0x8B431D, 0x8D441D, 0x8E451E, 0x8F471F, | |
| 0x904820, 0x914921, 0x924A22, 0x934C23, 0x954D23, 0x964E24, 0x974F25, 0x985126, | |
| 0x995227, 0x9A5328, 0x9B5428, 0x9C562A, 0x9E572B, 0x9F582C, 0x9F5A2D, 0xA05B2E, | |
| 0xA25C2F, 0xA35E30, 0xA45F31, 0xA66132, 0xA76333, 0xA86435, 0xA96637, 0xAB6838, | |
| 0xAD6A3A, 0xAE6D3C, 0xB06F3F, 0xB27242, 0xB57645, 0xB97A49, 0xBE8150, 0xC78C5C, | |
| 0xFFE5A2 | |
| ) | |
| private val RAMP_DULL = intArrayOf( | |
| 0x000000, 0x140704, 0x1D0D07, 0x24110A, 0x29150C, 0x2E180E, 0x331C10, 0x381F12, | |
| 0x3C2314, 0x402616, 0x442918, 0x482C1A, 0x4B2E1C, 0x4E311D, 0x51331F, 0x543521, | |
| 0x563722, 0x583924, 0x5B3B25, 0x5C3D27, 0x5E3F28, 0x604029, 0x61422A, 0x63442C, | |
| 0x65452C, 0x66462E, 0x68482F, 0x694930, 0x6A4A31, 0x6B4C32, 0x6C4D34, 0x6E4E34, | |
| 0x6F5036, 0x705137, 0x715238, 0x725339, 0x73553A, 0x74563B, 0x75573C, 0x76583D, | |
| 0x775A3E, 0x785B3F, 0x795C41, 0x7B5D41, 0x7C5F42, 0x7C6044, 0x7D6145, 0x7E6346, | |
| 0x806448, 0x816549, 0x82674A, 0x83694C, 0x846A4D, 0x866C4F, 0x876E50, 0x897052, | |
| 0x8A7254, 0x8C7456, 0x8E7759, 0x91795C, 0x947D5F, 0x978264, 0x9C886A, 0xA79476, | |
| 0xFFEFC7 | |
| ) | |
| private val RAMP_PAINT = intArrayOf( | |
| 0x000000, 0x040B0C, 0x081212, 0x0B1718, 0x0D1C1D, 0x0F2021, 0x122425, 0x142829, | |
| 0x152C2D, 0x172F32, 0x183336, 0x1A3639, 0x1B393D, 0x1D3C40, 0x1F3E42, 0x224043, | |
| 0x254344, 0x244548, 0x25474A, 0x27494C, 0x2A4B4C, 0x294D4F, 0x2B4E50, 0x2E5050, | |
| 0x2D5254, 0x2E5355, 0x2F5557, 0x2F5659, 0x30585A, 0x33595A, 0x345A5C, 0x355B5D, | |
| 0x375C5E, 0x375E5F, 0x395F60, 0x3B6060, 0x3C6162, 0x3C6364, 0x3E6364, 0x3F6565, | |
| 0x416666, 0x426767, 0x426869, 0x44696A, 0x446B6C, 0x446D6D, 0x476D6D, 0x4A6E6D, | |
| 0x4B706F, 0x4B7171, 0x4D7372, 0x507473, 0x517674, 0x527776, 0x547978, 0x567B7A, | |
| 0x597D7C, 0x5B7F7E, 0x5E8180, 0x628383, 0x668786, 0x6B8B8C, 0x739191, 0x819B9C, | |
| 0xD5F9FA | |
| ) | |
| private val SAT_LUT = buildLut(RAMP_SATURATED) | |
| private val DULL_LUT = buildLut(RAMP_DULL) | |
| private val PAINT_LUT = buildLut(RAMP_PAINT) | |
| /** | |
| * How much paint each stud kept, as 101 quantiles: 6% of them are down to a fifth | |
| * of their cap, 57% still have four fifths. Drawing every stud as the average is | |
| * exactly what makes a procedural plate read as wallpaper. | |
| */ | |
| private val SURVIVAL = floatArrayOf( | |
| 0.000f, 0.047f, 0.060f, 0.085f, 0.119f, 0.169f, 0.195f, 0.227f, 0.247f, 0.268f, | |
| 0.300f, 0.360f, 0.373f, 0.394f, 0.401f, 0.403f, 0.425f, 0.454f, 0.491f, 0.519f, | |
| 0.530f, 0.534f, 0.541f, 0.560f, 0.578f, 0.610f, 0.642f, 0.656f, 0.674f, 0.686f, | |
| 0.710f, 0.725f, 0.742f, 0.748f, 0.756f, 0.763f, 0.770f, 0.771f, 0.774f, 0.785f, | |
| 0.787f, 0.798f, 0.800f, 0.800f, 0.807f, 0.809f, 0.809f, 0.812f, 0.818f, 0.822f, | |
| 0.829f, 0.836f, 0.848f, 0.858f, 0.858f, 0.859f, 0.863f, 0.864f, 0.868f, 0.870f, | |
| 0.876f, 0.879f, 0.886f, 0.890f, 0.896f, 0.896f, 0.899f, 0.903f, 0.907f, 0.907f, | |
| 0.910f, 0.912f, 0.917f, 0.926f, 0.930f, 0.932f, 0.936f, 0.937f, 0.944f, 0.945f, | |
| 0.947f, 0.952f, 0.956f, 0.959f, 0.961f, 0.965f, 0.965f, 0.971f, 0.975f, 0.980f, | |
| 0.984f, 0.986f, 0.986f, 0.988f, 0.991f, 0.993f, 1.000f, 1.000f, 1.000f, 1.000f, | |
| 1.000f | |
| ) | |
| /** | |
| * The reference frame is 992 px across; matching that keeps the plate at the | |
| * density it was photographed at whatever the screen is. | |
| */ | |
| private const val ART_W = 992f | |
| /** | |
| * The row and seed half of [hash2]. Int overflow wraps, which is the arithmetic | |
| * this wants, and it is also what makes the split exact: the three terms are added | |
| * in a ring, so folding two of them early cannot change the result. | |
| */ | |
| private fun hashRow(iy: Int, seed: Int) = iy * 668265263 + seed * 1442695041 | |
| /** | |
| * [hash2] with the row already folded in. A scanline holds iy and the seed fixed | |
| * for its whole length, so two of the hash's three multiplies belong to the row | |
| * rather than to the pixel. | |
| */ | |
| private fun hashAt(ix: Int, row: Int): Float { | |
| var h = ix * 374761393 + row | |
| h = (h xor (h ushr 13)) * 1274126177 | |
| h = h xor (h ushr 16) | |
| return ((h ushr 8) and 0xFFFF) * (1f / 65535f) | |
| } | |
| /** A 32-bit integer hash of a lattice site. */ | |
| private fun hash2(ix: Int, iy: Int, seed: Int) = hashAt(ix, hashRow(iy, seed)) | |
| /** | |
| * Bilinear value noise on a unit lattice, walked along a scanline. | |
| * | |
| * A bake samples about eight octaves a pixel, and four fifths of the lattice | |
| * corners they ask for belong to a cell the previous pixel already visited: an | |
| * octave with a 12 px wavelength changes cell once in thirteen. Holding the row's | |
| * four corner hashes and shifting them when the cell does advance turns thirty-two | |
| * hashes a pixel into nine, and that is the difference between a bake you wait for | |
| * and one you do not. | |
| * | |
| * Art coordinates are never negative here, so toInt() is floor(). | |
| */ | |
| private class Octave(lambda: Float, private val seed: Int) { | |
| private val invLambda = 1f / lambda | |
| private var row0 = 0 | |
| private var row1 = 0 | |
| private var sy = 0f | |
| private var ix = Int.MIN_VALUE | |
| private var h00 = 0f | |
| private var h10 = 0f | |
| private var h01 = 0f | |
| private var h11 = 0f | |
| fun row(ay: Float) { | |
| val yy = ay * invLambda | |
| val iy = yy.toInt() | |
| val t = yy - iy | |
| sy = t * t * (3f - 2f * t) | |
| row0 = hashRow(iy, seed) | |
| row1 = hashRow(iy + 1, seed) | |
| ix = Int.MIN_VALUE | |
| } | |
| fun at(ax: Float): Float { | |
| val xx = ax * invLambda | |
| val cx = xx.toInt() | |
| if (cx != ix) { | |
| if (cx == ix + 1) { | |
| h00 = h10; h01 = h11 | |
| h10 = hashAt(cx + 1, row0); h11 = hashAt(cx + 1, row1) | |
| } else { | |
| h00 = hashAt(cx, row0); h10 = hashAt(cx + 1, row0) | |
| h01 = hashAt(cx, row1); h11 = hashAt(cx + 1, row1) | |
| } | |
| ix = cx | |
| } | |
| val t = xx - cx | |
| val sx = t * t * (3f - 2f * t) | |
| val top = h00 + (h10 - h00) * sx | |
| return top + (h01 + (h11 - h01) * sx - top) * sy | |
| } | |
| } | |
| /** | |
| * How much of a stud's paint has blistered off at this pixel. Two octaves, walked | |
| * along the row like every other one, and read at most twice a pixel because both | |
| * stud families share the field. | |
| */ | |
| private class Blot(seed: Int) { | |
| private val fine = Octave(7f, seed + 9001) | |
| private val coarse = Octave(17f, seed + 9203) | |
| fun row(ay: Float) { | |
| fine.row(ay) | |
| coarse.row(ay) | |
| } | |
| fun at(ax: Float): Float = | |
| 0.5f + 1.9f * ((fine.at(ax) * 0.72f + coarse.at(ax) * 0.28f) - 0.5f) | |
| } | |
| /** | |
| * Marquise field: negative inside, in pixels, so it doubles as antialiasing | |
| * coverage. [out] receives the outward normal, which is what the lighting needs. | |
| * The bevel and the white edge are both functions of which way the outline faces, | |
| * not of where the pixel sits. | |
| * | |
| * The shape is max() of two circles, and for cy > 0 the sign of v alone decides | |
| * which arc is nearer. Choosing the arc first is one square root instead of two, | |
| * and a square root here costs 139 ns, more than an octave of noise, so the branch | |
| * pays for itself many times over. | |
| */ | |
| private fun lens(u: Float, v: Float, r: Float, cy: Float, out: FloatArray): Float { | |
| val vv = if (v >= 0f) v + cy else v - cy | |
| val len = sqrt(u * u + vv * vv) | |
| val inv = if (len > 1e-6f) 1f / len else 0f | |
| out[0] = u * inv | |
| out[1] = vv * inv | |
| return len - r | |
| } | |
| /** | |
| * The squared distance to the active arc centre, the same field before the square | |
| * root. Everything a pixel off the boss needs (the cast shadow, the paint gate) is | |
| * a monotone function of it, so those are tabulated against it and only the one | |
| * pixel in eight that actually lands on a stud pays for the root. | |
| */ | |
| private fun lensQ(u: Float, v: Float, cy: Float): Float { | |
| val vv = if (v >= 0f) v + cy else v - cy | |
| return u * u + vv * vv | |
| } | |
| /** | |
| * Toward the bottom edge the paint does not merely darken, it stops existing. A | |
| * brightness multiply cannot fake that, so coverage carries it. | |
| */ | |
| private fun tealSurvival(ny: Float): Float = when { | |
| ny < 0.635f -> 1f | |
| ny < 0.816f -> 1f - 0.28f * (ny - 0.635f) / 0.181f | |
| ny < 0.907f -> 0.72f - 0.28f * (ny - 0.816f) / 0.091f | |
| ny < 0.975f -> 0.44f * (1f - (ny - 0.907f) / 0.068f) | |
| else -> 0f | |
| } | |
| private fun clamp01(v: Float) = if (v < 0f) 0f else if (v > 1f) 1f else v | |
| /** | |
| * Resample a 65-stop ramp onto a uniform lookup over the tone range, so the | |
| * per-pixel cost is an index and a lerp instead of a search through [TONE_Q]. | |
| */ | |
| private fun buildLut(ramp: IntArray): FloatArray { | |
| val lut = FloatArray(LUT_N * 3) | |
| var stop = 0 | |
| for (i in 0 until LUT_N) { | |
| val t = T_LO + (T_HI - T_LO) * i / (LUT_N - 1f) | |
| while (stop < TONE_Q.size - 2 && TONE_Q[stop + 1] < t) stop++ | |
| val span = TONE_Q[stop + 1] - TONE_Q[stop] | |
| val f = clamp01(if (span > 1e-4f) (t - TONE_Q[stop]) / span else 0f) | |
| val c0 = ramp[stop] | |
| val c1 = ramp[stop + 1] | |
| lut[i * 3] = ((c0 shr 16) and 0xFF) + (((c1 shr 16) and 0xFF) - ((c0 shr 16) and 0xFF)) * f | |
| lut[i * 3 + 1] = ((c0 shr 8) and 0xFF) + (((c1 shr 8) and 0xFF) - ((c0 shr 8) and 0xFF)) * f | |
| lut[i * 3 + 2] = (c0 and 0xFF) + ((c1 and 0xFF) - (c0 and 0xFF)) * f | |
| } | |
| return lut | |
| } | |
| private const val COARSE = 4 | |
| private const val INV_COARSE = 1f / COARSE | |
| /** | |
| * Everything slower than about 12 px of wavelength, evaluated on a quarter | |
| * resolution grid and read back bilinearly. Nine octaves of value noise per pixel | |
| * is most of the bake's cost and none of its detail: at this scale a quarter-res | |
| * grid is indistinguishable, and it is sixteen times less work. | |
| * | |
| * The three fields share a grid, so a pixel resolves one cell index and one pair | |
| * of weights and spends them three times. | |
| */ | |
| private class SlowFields(val w: Int, val h: Int) { | |
| val tone = FloatArray(w * h) | |
| val chroma = FloatArray(w * h) | |
| val paint = FloatArray(w * h) | |
| fun sample(field: FloatArray, i: Int, fx: Float, fy: Float): Float { | |
| val top = field[i] + (field[i + 1] - field[i]) * fx | |
| val bot = field[i + w] + (field[i + w + 1] - field[i + w]) * fx | |
| return top + (bot - top) * fy | |
| } | |
| } | |
| /** | |
| * Only octaves longer than about 15 art px go on the coarse grid. Its step is 3.7 | |
| * art px, so anything finer would be sampled below Nyquist, and the chroma field's | |
| * shortest octave, at 2.96 px, carries its largest amplitude. Putting that one on | |
| * the grid would quietly throw the plate's chroma variation away. | |
| */ | |
| private suspend fun buildSlow(w: Int, h: Int, scale: Float, seed: Int): SlowFields { | |
| val cw = w / COARSE + 2 | |
| val ch = h / COARSE + 2 | |
| val s = SlowFields(cw, ch) | |
| val tone = Array(OCT_LAMBDA.size - COARSE_FROM) { | |
| Octave(OCT_LAMBDA[it + COARSE_FROM], seed + 1000 + (it + COARSE_FROM) * 13) | |
| } | |
| val chroma = Array(CHROMA_LAMBDA.size - CHROMA_FINE) { | |
| Octave(CHROMA_LAMBDA[it + CHROMA_FINE], seed + 8101 + (it + CHROMA_FINE) * 211) | |
| } | |
| val paint1 = Octave(PAINT_LAM1, seed + 7001) | |
| val paint2 = Octave(PAINT_LAM2, seed + 7307) | |
| var i = 0 | |
| for (cy in 0 until ch) { | |
| coroutineContext.ensureActive() | |
| val ay = cy * COARSE / scale | |
| for (o in tone) o.row(ay) | |
| for (o in chroma) o.row(ay) | |
| paint1.row(ay); paint2.row(ay) | |
| for (cx in 0 until cw) { | |
| val ax = cx * COARSE / scale | |
| var t = 0f | |
| for (k in tone.indices) t += OCT_AMP[k + COARSE_FROM] * (tone[k].at(ax) - 0.5f) | |
| s.tone[i] = t | |
| var c = 0f | |
| for (k in chroma.indices) c += CHROMA_AMP[k + CHROMA_FINE] * (chroma[k].at(ax) - 0.5f) | |
| s.chroma[i] = c | |
| s.paint[i] = PAINT_W1 * paint1.at(ax) + (1f - PAINT_W1) * paint2.at(ax) | |
| i++ | |
| } | |
| } | |
| return s | |
| } | |
| /** | |
| * Where the nearest stud of one family is, in that stud's own frame, written into | |
| * [q] as u, v and the lattice site. Returns the squared distance to the arc | |
| * centre, so the caller can reject a pixel without a square root. | |
| * | |
| * Only the rounded lattice site is tested, not its eight neighbours, and that is | |
| * exact rather than an approximation: a stud reaches 23.8 px along its axis and | |
| * the half cell is 38.2 x 36.3, so no other site's lens can cover this pixel. | |
| * Checking all nine would cost eighteen lens evaluations a pixel for nothing. | |
| * | |
| * A scanline sits in one lattice row, so the row index and the offset to it are | |
| * resolved once in [row] and only the column moves per pixel. | |
| */ | |
| private class Lattice( | |
| private val phx: Float, private val phy: Float, | |
| private val ct: Float, private val st: Float, | |
| ) { | |
| private var gj = 0 | |
| private var dy = 0f | |
| fun row(ay: Float) { | |
| gj = ((ay - phy) * INV_PY).roundToInt() | |
| dy = ay - (gj * PY + phy) | |
| } | |
| fun at(q: FloatArray, ax: Float): Float { | |
| val gi = ((ax - phx) * INV_PX).roundToInt() | |
| val dx = ax - (gi * PX + phx) | |
| q[0] = dx * ct + dy * st | |
| q[1] = -dx * st + dy * ct | |
| q[2] = gi.toFloat() | |
| q[3] = gj.toFloat() | |
| return lensQ(q[0], q[1], RIM_CY) | |
| } | |
| } | |
| /** Lays one stud over the ground: bevel, cap, dark edge, white edge, mottle. */ | |
| private fun shadeStud( | |
| rgb: FloatArray, n: FloatArray, q: FloatArray, ax: Float, ct: Float, st: Float, | |
| capSeed: Int, survival: Float, grain: Float, blot: Blot, | |
| ) { | |
| val u = q[0] | |
| val v = q[1] | |
| val dRim = lens(u, v, RIM_R, RIM_CY, n) | |
| val mRim = clamp01(0.5f - dRim * INV_EDGE_AA) | |
| if (mRim <= 0f) return | |
| run { | |
| val rL = ((n[0] * ct - n[1] * st) * LIGHT_X + (n[0] * st + n[1] * ct) * LIGHT_Y) * INV_LIGHT_XY | |
| val rim = RIM_LO + (RIM_HI - RIM_LO) * lut01(RIM_LUT, rL) | |
| rgb[0] += (rim * 0.92f - rgb[0]) * mRim | |
| rgb[1] += (rim * 0.95f - rgb[1]) * mRim | |
| rgb[2] += (rim - rgb[2]) * mRim | |
| } | |
| val dCap = lens(u, v, CAP_R, CAP_CY, n) | |
| val mCap = clamp01(0.5f - dCap * INV_EDGE_AA) | |
| if (mCap <= 0f) return | |
| val nL = ((n[0] * ct - n[1] * st) * LIGHT_X + (n[0] * st + n[1] * ct) * LIGHT_Y) * INV_LIGHT_XY | |
| val ua = u * INV_CAP_A | |
| val va = v * INV_CAP_B | |
| val hh = sqrt((1f - ua * ua - va * va).coerceIn(1e-3f, 1f)) | |
| val invHh = 1f / hh | |
| val gu = DOME_U * u * invHh | |
| val gv = DOME_V * v * invHh | |
| val sx = gu * ct - gv * st | |
| val sy = gu * st + gv * ct | |
| val diff = clamp01((sx * LIGHT_X + sy * LIGHT_Y + LIGHT_Z) * INV_LIGHT_LEN / | |
| sqrt(sx * sx + sy * sy + 1f)) | |
| val keptIdx = (hash2(q[2].toInt(), q[3].toInt(), capSeed) * 100f).toInt() | |
| val kept = SURVIVAL[keptIdx.coerceIn(0, 100)] * survival | |
| val eu = clamp01(abs(ua)) | |
| val keep = clamp01((kept - blot.at(ax) - 0.22f * eu * eu) * 11f + 0.5f) | |
| var cr = (RUST_AMB_R + (AMB_R - RUST_AMB_R) * keep) + (RUST_DIF_R + (DIF_R - RUST_DIF_R) * keep) * diff | |
| var cg = (RUST_AMB_G + (AMB_G - RUST_AMB_G) * keep) + (RUST_DIF_G + (DIF_G - RUST_DIF_G) * keep) * diff | |
| var cb = (RUST_AMB_B + (AMB_B - RUST_AMB_B) * keep) + (RUST_DIF_B + (DIF_B - RUST_DIF_B) * keep) * diff | |
| val e = lut01(EDGE_LUT, 1f + dCap * INV_EDGE_W) * clamp01(-nL) | |
| if (e > 0f) { | |
| cr += ((RUST_DARK_R + (DARK_R - RUST_DARK_R) * keep) - cr) * e | |
| cg += ((RUST_DARK_G + (DARK_G - RUST_DARK_G) * keep) - cg) * e | |
| cb += ((RUST_DARK_B + (DARK_B - RUST_DARK_B) * keep) - cb) * e | |
| } | |
| if (nL > 0f) { | |
| val din = if (dCap < 0f) -dCap else 0f | |
| val nl5 = nL * nL * nL * nL * nL | |
| val spec = clamp01(SPEC_K * lut01(SPEC_LUT, din * 0.125f) * nl5) * keep | |
| cr += (SPEC_R - cr) * spec | |
| cg += (SPEC_G - cg) * spec | |
| cb += (SPEC_B - cb) * spec | |
| } | |
| val mot = (1f + CAP_GRAIN * grain).coerceIn(0.12f, 2.4f) | |
| cr *= mot; cg *= mot; cb *= mot | |
| rgb[0] += (cr - rgb[0]) * mCap | |
| rgb[1] += (cg - rgb[1]) * mCap | |
| rgb[2] += (cb - rgb[2]) * mCap | |
| } | |
| /** | |
| * Bakes the plate into a bitmap. Called once per size change, off the UI thread | |
| * and split across cores by row band; after that a frame is one drawBitmap. | |
| * | |
| * The row loop hoists everything that only depends on y: the stain polynomial | |
| * collapses to four coefficients a row and is then evaluated in nx by Horner, and | |
| * the coarse grid's row index and vertical weight are resolved once for all three | |
| * slow fields. | |
| */ | |
| private suspend fun bakePlate(w: Int, h: Int, seed: Int): Bitmap = coroutineScope { | |
| val scale = w / ART_W | |
| val invScale = 1f / scale | |
| val invW2 = 2f / w | |
| val invH = 1f / h | |
| val pixels = IntArray(w * h) | |
| val slow = buildSlow(w, h, scale, seed) | |
| val workers = min(Runtime.getRuntime().availableProcessors(), 8).coerceAtLeast(1) | |
| val band = (h + workers - 1) / workers | |
| (0 until workers).map { worker -> | |
| async(Dispatchers.Default) { | |
| val rgb = FloatArray(3) | |
| val nrm = FloatArray(2) | |
| val qa = FloatArray(4) | |
| val qb = FloatArray(4) | |
| val ground = Array(COARSE_FROM) { Octave(OCT_LAMBDA[it], seed + 1000 + it * 13) } | |
| val fine = Array(4) { Octave(1f * (1 shl it), seed + 3001 + it * 97) } | |
| val contrast = Octave(24f, seed + 3907) | |
| val cluster = Octave(16f, seed + 9601) | |
| val chromaFine = Array(CHROMA_FINE) { Octave(CHROMA_LAMBDA[it], seed + 8101 + it * 211) } | |
| val blot = Blot(seed) | |
| val latA = Lattice(PHASE_X, PHASE_Y, COS_A, SIN_A) | |
| val latB = Lattice(PHASE_X + PX * 0.5f, PHASE_Y + PY * 0.5f, COS_B, SIN_B) | |
| val capSeedA = seed * 7919 + 104729 | |
| val capSeedB = seed * 7919 + 209458 | |
| val y1 = min(h, (worker + 1) * band) | |
| for (y in worker * band until y1) { | |
| val ay = y * invScale | |
| val iy = ay.toInt() | |
| val ny = y * invH | |
| val nyc = ny * 2f - 1f | |
| val nyc2 = nyc * nyc | |
| val survival = tealSurvival(ny) | |
| val painted = survival > 0f | |
| val stain0 = STAIN[0] + STAIN[2] * nyc + STAIN[5] * nyc2 + STAIN[9] * nyc2 * nyc | |
| val stain1 = STAIN[1] + STAIN[4] * nyc + STAIN[8] * nyc2 | |
| val stain2 = STAIN[3] + STAIN[7] * nyc | |
| val fleckRow = hashRow(iy, seed + 9707) | |
| val pitRow = hashRow(iy, seed + 9811) | |
| val sy0 = y / COARSE | |
| val sfy = (y - sy0 * COARSE) * INV_COARSE | |
| val srow = sy0 * slow.w | |
| for (o in ground) o.row(ay) | |
| for (o in fine) o.row(ay) | |
| contrast.row(ay) | |
| cluster.row(ay) | |
| for (o in chromaFine) o.row(ay) | |
| blot.row(ay) | |
| latA.row(ay); latB.row(ay) | |
| ensureActive() | |
| var idx = y * w | |
| for (x in 0 until w) { | |
| val ax = x * invScale | |
| val nx = x * invW2 - 1f | |
| val sx0 = x / COARSE | |
| val sfx = (x - sx0 * COARSE) * INV_COARSE | |
| val si = srow + sx0 | |
| var t = stain0 + nx * (stain1 + nx * (stain2 + nx * STAIN[6])) | |
| t += slow.sample(slow.tone, si, sfx, sfy) | |
| for (k in 0 until COARSE_FROM) t += OCT_AMP[k] * (ground[k].at(ax) - 0.5f) | |
| var g = 0f | |
| for (o in fine) g += o.at(ax) - 0.5f | |
| g = warp(g * INV_FINE_S1) * INV_FINE_S2 | |
| g *= expSmall(FINE_CONTRAST_K * (contrast.at(ax) - 0.5f)) | |
| t += FINE_GAIN_S3 * g | |
| g *= INV_FINE_S3 | |
| val li = ((t - T_LO) * LUT_K).toInt().coerceIn(0, LUT_N - 1) * 3 | |
| var cr0 = slow.sample(slow.chroma, si, sfx, sfy) | |
| for (k in 0 until CHROMA_FINE) cr0 += CHROMA_AMP[k] * (chromaFine[k].at(ax) - 0.5f) | |
| val c = clamp01(CHROMA_MID + CHROMA_K * cr0) | |
| rgb[0] = DULL_LUT[li] + (SAT_LUT[li] - DULL_LUT[li]) * c | |
| rgb[1] = DULL_LUT[li + 1] + (SAT_LUT[li + 1] - DULL_LUT[li + 1]) * c | |
| rgb[2] = DULL_LUT[li + 2] + (SAT_LUT[li + 2] - DULL_LUT[li + 2]) * c | |
| val qA = latA.at(qa, ax) | |
| val qB = latB.at(qb, ax) | |
| val qMin = if (qA < qB) qA else qB | |
| if (qMin > BOSS_Q && painted) { | |
| val gate = PAINT_NEAR_KEEP + (1f - PAINT_NEAR_KEEP) * | |
| lut01(GATE_LUT, (qMin - GATE_Q0) * INV_GATE_SPAN) | |
| val pn = slow.sample(slow.paint, si, sfx, sfy) + | |
| PAINT_TONE_K * (t - PAINT_TONE_MID) | |
| val p = clamp01((pn - PAINT_LO) * INV_PAINT_WIDTH) * gate * survival | |
| if (p > 0f) { | |
| rgb[0] += (PAINT_LUT[li] - rgb[0]) * p | |
| rgb[1] += (PAINT_LUT[li + 1] - rgb[1]) * p | |
| rgb[2] += (PAINT_LUT[li + 2] - rgb[2]) * p | |
| } | |
| } | |
| if (qA < SHADOW_Q1) { | |
| val sh = 1f - CAST_SHADOW * lut01(SHADOW_LUT, (qA - SHADOW_Q0) * INV_SHADOW_SPAN) | |
| rgb[0] *= sh; rgb[1] *= sh; rgb[2] *= sh | |
| } | |
| if (qB < SHADOW_Q1) { | |
| val sh = 1f - CAST_SHADOW * lut01(SHADOW_LUT, (qB - SHADOW_Q0) * INV_SHADOW_SPAN) | |
| rgb[0] *= sh; rgb[1] *= sh; rgb[2] *= sh | |
| } | |
| if (qA < BOSS_Q) shadeStud(rgb, nrm, qa, ax, COS_A, SIN_A, capSeedA, survival, g, blot) | |
| if (qB < BOSS_Q) shadeStud(rgb, nrm, qb, ax, COS_B, SIN_B, capSeedB, survival, g, blot) | |
| val ix = ax.toInt() | |
| val fleck = hashAt(ix, fleckRow) | |
| val pit = hashAt(ix, pitRow) | |
| if (fleck > FLECK_GATE || pit > PIT_GATE) { | |
| val cl = CLUSTER_LO + CLUSTER_HI * cluster.at(ax) | |
| if (fleck > 1f - FLECK_P * cl) { | |
| rgb[0] += 73f; rgb[1] += 77f; rgb[2] += 77f | |
| } else if (pit > 1f - PIT_P * cl) { | |
| rgb[0] -= 73f; rgb[1] -= 72f; rgb[2] -= 62f | |
| } | |
| } | |
| pixels[idx++] = (0xFF shl 24) or | |
| (rgb[0].toInt().coerceIn(0, 255) shl 16) or | |
| (rgb[1].toInt().coerceIn(0, 255) shl 8) or | |
| rgb[2].toInt().coerceIn(0, 255) | |
| } | |
| } | |
| } | |
| }.awaitAll() | |
| Bitmap.createBitmap(pixels, w, h, Bitmap.Config.ARGB_8888).apply { | |
| setHasAlpha(false) | |
| prepareToDraw() | |
| } | |
| } | |
| /** | |
| * How much bigger than the viewport the plate is baked, so a drag has somewhere to | |
| * go. 8% costs 17% of the bake and buys the whole gesture. | |
| */ | |
| private const val SLACK = 0.08f | |
| /** The plate's own mean, so the frame before the bake lands is not a flash. */ | |
| private val PLATE_MEAN = Color(0xFF71452B) | |
| /** | |
| * The viewport settles a frame or two after the first layout. Waiting this long | |
| * before baking keeps the first bake from still running when the second starts, | |
| * where the two halve each other on a four-core device. | |
| */ | |
| private const val VIEWPORT_SETTLE_MS = 48L | |
| @Composable | |
| fun DiamondPlateCanvas(modifier: Modifier = Modifier) { | |
| MatchStatusBarToPage(PLATE_MEAN) | |
| var plate by remember { mutableStateOf<Bitmap?>(null) } | |
| var viewport by remember { mutableStateOf(IntSize.Zero) } | |
| val pan = remember { Animatable(Offset.Zero, Offset.VectorConverter) } | |
| var rig by remember { mutableStateOf<WeldRig?>(null) } | |
| val weld = remember { WeldPaints() } | |
| val weldClock = remember { mutableFloatStateOf(0f) } | |
| val scope = rememberCoroutineScope() | |
| LaunchedEffect(viewport) { | |
| if (viewport.width > 0 && viewport.height > 0) { | |
| delay(VIEWPORT_SETTLE_MS) | |
| plate = withContext(Dispatchers.Default) { | |
| bakePlate( | |
| (viewport.width * (1 + SLACK)).toInt(), | |
| (viewport.height * (1 + SLACK)).toInt(), | |
| seed = 1 | |
| ) | |
| } | |
| } | |
| } | |
| LaunchedEffect(rig) { | |
| val r = rig ?: return@LaunchedEffect | |
| weld.reset() | |
| weldClock.floatValue = 0f | |
| var last = 0L | |
| while (true) { | |
| withFrameNanos { now -> | |
| val dt = if (last == 0L) 0.016f else ((now - last) / 1e9f).coerceIn(0.001f, 0.05f) | |
| last = now | |
| r.advance(dt) | |
| weldClock.floatValue = r.t | |
| } | |
| if (r.finished && r.doneAt > 7f && r.sparkCount() == 0) break | |
| } | |
| } | |
| Canvas( | |
| modifier | |
| .fillMaxSize() | |
| .onSizeChanged { viewport = it } | |
| .pointerInput(Unit) { | |
| detectTapGestures { rig = WeldRig(0f, 0f) } | |
| } | |
| .pointerInput(Unit) { | |
| val mx = size.width * SLACK * 0.5f | |
| val my = size.height * SLACK * 0.5f | |
| detectDragGestures( | |
| onDragEnd = { scope.launch { pan.animateTo(Offset.Zero, spring(0.9f, 90f)) } } | |
| ) { change, drag -> | |
| change.consume() | |
| scope.launch { | |
| pan.snapTo( | |
| Offset( | |
| (pan.value.x + drag.x * 0.35f).coerceIn(-mx, mx), | |
| (pan.value.y + drag.y * 0.35f).coerceIn(-my, my) | |
| ) | |
| ) | |
| } | |
| } | |
| } | |
| ) { | |
| val bmp = plate | |
| if (bmp == null) { | |
| drawRect(PLATE_MEAN) | |
| return@Canvas | |
| } | |
| val ox = pan.value.x - size.width * SLACK * 0.5f | |
| val oy = pan.value.y - size.height * SLACK * 0.5f | |
| val r = rig | |
| val elapsed = weldClock.floatValue | |
| drawIntoCanvas { | |
| val c = it.nativeCanvas | |
| c.drawBitmap(bmp, ox, oy, null) | |
| if (r != null && elapsed > 0f) { | |
| val s = bmp.width / ART_W | |
| c.save() | |
| c.translate(ox, oy) | |
| c.scale(s, s) | |
| c.translate(bmp.width / (2f * s), bmp.height / (2f * s)) | |
| drawWeld(r, weld) | |
| c.restore() | |
| } | |
| } | |
| } | |
| } | |
| @Composable | |
| fun DiamondPlateScreen() { | |
| DiamondPlateCanvas() | |
| } | |
| @Preview(showSystemUi = true) | |
| @Composable | |
| fun DiamondPlatePreview() { | |
| DiamondPlateCanvas() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Classes are missing
WeldRig
MatchStatusBarToPage