Skip to content

Commit

Permalink
Temp: wave start flicker reproducer
Browse files Browse the repository at this point in the history
  • Loading branch information
mahozad committed Oct 10, 2024
1 parent 549591a commit 4a52fb2
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ import kotlin.math.sin
/**
* The horizontal movement (shift) of the whole wave.
*/
enum class WaveDirection(internal inline val factor: (LayoutDirection) -> Float) {
enum class WaveDirection(internal val factor: (LayoutDirection) -> Float) {
/**
* Always shift toward left (regardless of layout direction).
*/
LEFT({ if (it == LayoutDirection.Ltr) 1f else -1f }),
LEFT(factor = { if (it == LayoutDirection.Ltr) 1f else -1f }),
/**
* Always shift toward right (regardless of layout direction).
*/
RIGHT({ if (it == LayoutDirection.Ltr) -1f else 1f }),
RIGHT(factor = { if (it == LayoutDirection.Ltr) -1f else 1f }),
/**
* Shift toward the start (depends on layout direction).
*/
TAIL({ 1f }),
TAIL(factor = { 1f }),
/**
* Shift toward the thumb (depends on layout direction).
*/
HEAD({ -1f })
HEAD(factor = { -1f })
}

/**
Expand Down Expand Up @@ -141,7 +141,7 @@ internal inline fun animateWaveShift(
val startTime = withFrameNanos { it }
while (true /* Android itself uses true instead of isActive */) {
val playTime = (withFrameNanos { it } - startTime) / 1_000_000_000f
shift.value = (startShift + (amount * playTime)).coerceAtLeast(-10.dp)
shift.value = startShift + (amount * playTime)
}
}
return shift
Expand Down Expand Up @@ -182,8 +182,8 @@ internal inline fun DrawScope.createWavyPath(
val waveShiftPx = waveShift.toPx()
val waveLengthPx = waveLength.toPx()
val waveHeightPx = waveHeight.toPx().absoluteValue
val startRadians = waveSpread * waveShiftPx / waveLengthPx * (2 * PI) //
val startHeightFactor = if (incremental) 0f else 1f //
val startRadians = waveSpread * waveShiftPx / waveLengthPx * (2 * PI)
val startHeightFactor = if (incremental) 0f else 1f
val startY = (sin(startRadians) * startHeightFactor * waveHeightPx + size.height) / 2
moveTo(startOffset.x, startY.toFloat())
val range = startOffset.x.toInt()..valueOffset.x.toInt()
Expand All @@ -192,9 +192,7 @@ internal inline fun DrawScope.createWavyPath(
val radians = waveSpread * (x - range.first + waveShiftPx) / waveLengthPx * (2 * PI)
val y = (sin(radians) * heightFactor * waveHeightPx + size.height) / 2
lineTo(x.toFloat(), y.toFloat())
println("${x.toFloat()} ${y.toFloat()}")
}
println("============================")
}

// Scale x1 from a1..b1 range to a2..b2 range
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,8 @@ private inline fun DrawScope.drawTrackActivePart(
} else {
drawPath(
path = createWavyPath(
startOffset.copy(x = startOffset.x + waveThickness.toPx() / 2),
// See the visual test #41 and its KDoc for more information.
startOffset.copy(x = (startOffset.x + waveThickness.toPx() / 2).roundToInt().toFloat()),
endOffset.copy(x = endOffset.x - waveThickness.toPx() / 2),
waveLength,
waveHeight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ir.mahozad.multiplatform.wavyslider
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.EaseOutBounce
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
Expand All @@ -11,6 +12,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
Expand Down Expand Up @@ -1017,9 +1020,68 @@ class VisualTest {
assert(isPassed)
}

// See https://github.com/JetBrains/compose-multiplatform/issues/4199
/**
* The start and end offset of wave for Material 3 variant are adjusted (± waveThickness.toPx() / 2)
* to account for the round cap of the path (cap = StrokeCap.Round) which fixes the excess length of
* the tips of the wave and also more importantly, prevents a sudden change in position of path tip
* at the moment the wave completely flattens and becomes a line (when toggling the waveHeight to zero).
*
* The above adjustment, in turn, results in a glitch visible on some fractional screen densities (like 1.25)
* which caused the start of the wave to constantly become a little longer and shorter.
* Here is the description of the reason:
* To draw the wave, the x is progressed and the y is calculated based on it.
* In a half a wavelength, the y should only change from increasing to decreasing or vice versa
* at most once (at the peek or at the trough) but when printing
* `lineTo(x.toFloat(), y.toFloat())` pairs, it was found that the y sometimes changed slope sign
* more than once and this probably messes with drawing.
*
* Calling roundToInt().toFloat() on the result of the above adjustment seems to have fixed the problem.
* Also, another less visually-perfect workaround would be to set the path join to StrokeJoin.Bevel for the wavyPath.
*/
@Test
fun `Test 41`() {
val isPassed = testApp(
name = object {}.javaClass.enclosingMethod.name,
given = "When the screen density is some fractional value",
expected = """
Should have no glitch at the start tip of the wave.
Also, when toggling the height, at the moment the wave completely flattens,
there should be almost no abrupt change in where the line starts (almost invisible).
"""
) { value, onChange ->
var density by remember { mutableStateOf(1.20f) }
var waveHeight by remember { mutableStateOf(10.dp) }
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
CompositionLocalProvider(LocalDensity provides Density(density)) {
WavySlider3(
value = 0.03f,
onValueChange = {},
waveLength = 24.dp,
waveHeight = waveHeight,
waveVelocity = 10.dp to HEAD,
animationSpecs = SliderDefaults.WaveAnimationSpecs.copy(
waveStartSpreadAnimationSpec = snap()
)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Column(modifier = Modifier.weight(1f)) {
Button(onClick = { waveHeight = if (waveHeight == 0.dp) 10.dp else 0.dp }) { Text("Toggle height") }
Text("Height: $waveHeight")
}
Column(modifier = Modifier.weight(4f)) {
Slider(value = density, onValueChange = { density = it }, valueRange = 1.20f..1.50f, steps = 9)
Text("Density: $density")
}
}
}
}
assert(isPassed)
}

// See https://github.com/JetBrains/compose-multiplatform/issues/4199
@Test
fun `Test 42`() {
val isPassed = testApp(
name = object {}.javaClass.enclosingMethod.name,
given = "Measure FPS of the app",
Expand Down Expand Up @@ -1047,7 +1109,7 @@ class VisualTest {
}

@Test
fun `Test 42`() {
fun `Test 43`() {
val isPassed = testApp(
name = object {}.javaClass.enclosingMethod.name,
given = "When there are many wavy sliders",
Expand Down Expand Up @@ -1080,7 +1142,7 @@ class VisualTest {
}

@Test
fun `Test 43`() {
fun `Test 44`() {
val isPassed = testApp(
name = object {}.javaClass.enclosingMethod.name,
given = "Start animation",
Expand Down
Binary file modified library/src/desktopTest/resources/reference-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 4a52fb2

Please sign in to comment.