diff --git a/app/src/main/java/com/darkrockstudios/apps/fasttrack/screens/fasting/TimeLine.kt b/app/src/main/java/com/darkrockstudios/apps/fasttrack/screens/fasting/TimeLine.kt index 56e0a39..f74a93f 100644 --- a/app/src/main/java/com/darkrockstudios/apps/fasttrack/screens/fasting/TimeLine.kt +++ b/app/src/main/java/com/darkrockstudios/apps/fasttrack/screens/fasting/TimeLine.kt @@ -1,15 +1,25 @@ package com.darkrockstudios.apps.fasttrack.screens.fasting +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import com.darkrockstudios.apps.fasttrack.data.Phase @@ -36,30 +46,49 @@ fun TimeLine( onPhaseClick: (Phase) -> Unit = {} ) { val padding = 16.dp - val spacing = 18.dp + val spacing = 4.dp val barSize = 16.dp val needleSize = 3.dp val needleRadius = 4.dp + val slantOffset = 8.dp val outlineColor = MaterialTheme.colorScheme.onBackground + + val curPhase = Stages.getCurrentPhase(elapsedHours.hours) + + // Continuous blink animation for current phase + val infiniteTransition = rememberInfiniteTransition(label = "phase_blink") + val blinkProgress by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 500), + repeatMode = RepeatMode.Reverse + ), + label = "blink_progress" + ) Canvas( modifier = modifier .fillMaxWidth() .height(padding + barSize) - .pointerInput(Stages.phases) { + .pointerInput(curPhase) { detectTapGestures { offset -> val paddingPx = padding.toPx() val spacingPx = spacing.toPx() val barSizePx = barSize.toPx() - val availableWidth = size.width - paddingPx - val phaseWidth = (availableWidth / Stages.phases.size) - spacingPx + val slantOffsetPx = slantOffset.toPx() + + val phaseWidth = (size.width - (2 * paddingPx) - (Stages.phases.size - 1) * spacingPx) / Stages.phases.size + val totalWidth = (Stages.phases.size * phaseWidth) + ((Stages.phases.size - 1) * spacingPx) + slantOffsetPx + val startOffset = (size.width - totalWidth) / 2f + val startY = paddingPx val yOk = abs(offset.y - startY) <= (barSizePx / 2f) if (yOk) { Stages.phases.forEachIndexed { index, phase -> - val startX = (index * phaseWidth) + (index * spacingPx) + paddingPx - val endX = startX + phaseWidth + val startX = startOffset + (index * phaseWidth) + (index * spacingPx) + val endX = startX + phaseWidth + slantOffsetPx if (offset.x in startX..endX) { onPhaseClick(phase) return@detectTapGestures @@ -72,54 +101,54 @@ fun TimeLine( val lastPhase = Stages.phases.last() val lastPhaseHoursWeighted = lastPhase.hours * 1.5f - val availableWidth = size.width - padding.toPx() - val phaseWidth = (availableWidth / Stages.phases.size) - spacing.toPx() + val slantOffsetPx = slantOffset.toPx() + val barSizePx = barSize.toPx() + + val phaseWidth = (size.width - (2 * padding.toPx()) - (Stages.phases.size - 1) * spacing.toPx()) / Stages.phases.size + val totalWidth = (Stages.phases.size * phaseWidth) + ((Stages.phases.size - 1) * spacing.toPx()) + slantOffsetPx + + val startOffset = (size.width - totalWidth) / 2f - val curPhase = Stages.getCurrentPhase(elapsedHours.hours) - - // Draw the bubbles (phases) Stages.phases.forEachIndexed { index, phase -> - val startX = (index * phaseWidth) + (index * spacing.toPx()) + padding.toPx() + val startX = startOffset + (index * phaseWidth) + (index * spacing.toPx()) val startY = padding.toPx() - // Current phase, thicket orange outline - if (curPhase == phase) { - // Outline - drawLine( - color = Color(0xFFE67E22), - start = Offset(startX, startY), - end = Offset(startX + phaseWidth, startY), - strokeWidth = barSize.toPx(), - cap = StrokeCap.Round - ) + val rhombusPath = Path().apply { + moveTo(startX + slantOffsetPx, startY - barSizePx / 2) + lineTo(startX + phaseWidth + slantOffsetPx, startY - barSizePx / 2) + lineTo(startX + phaseWidth, startY + barSizePx / 2) + lineTo(startX, startY + barSizePx / 2) + close() + } - // Current phase - filled - drawLine( + if (curPhase == phase) { + drawPath( + path = rhombusPath, color = gaugeColors[index], - start = Offset(startX, startY), - end = Offset(startX + phaseWidth, startY), - strokeWidth = barSize.toPx() * 0.7f, - cap = StrokeCap.Round + style = Fill ) - } else { - // Other phases - thinner "onBackground" outline - - // Thinner outline - drawLine( - color = outlineColor, - start = Offset(startX, startY), - end = Offset(startX + phaseWidth, startY), - strokeWidth = barSize.toPx(), - cap = StrokeCap.Round + + // Continuously animate between orange and yellow + val baseOutlineColor = Color(0xFFE67E22) // Orange + val blinkColor = Color.Yellow + val currentOutlineColor = lerp(baseOutlineColor, blinkColor, blinkProgress) + + drawPath( + path = rhombusPath, + color = currentOutlineColor, + style = Stroke(width = 3.dp.toPx()) ) - - // Phase color - drawLine( + } else { + drawPath( + path = rhombusPath, color = gaugeColors[index], - start = Offset(startX, startY), - end = Offset(startX + phaseWidth, startY), - strokeWidth = barSize.toPx() * 0.8f, // Slightly thinner - cap = StrokeCap.Round + style = Fill + ) + + drawPath( + path = rhombusPath, + color = outlineColor, + style = Stroke(width = 2.dp.toPx()) ) } } @@ -140,10 +169,9 @@ fun TimeLine( val halfPadding = padding.toPx() / 2f - val startX = (curPhaseIndex * phaseWidth) + (curPhaseIndex * spacing.toPx()) + padding.toPx() + val startX = startOffset + (curPhaseIndex * phaseWidth) + (curPhaseIndex * spacing.toPx()) val x = (startX + (phaseWidth * percent)).toFloat() - // Draw needle line drawLine( color = Color.DarkGray, start = Offset(x, halfPadding), @@ -152,7 +180,6 @@ fun TimeLine( cap = StrokeCap.Square ) - // Draw needle circle drawCircle( color = Color.DarkGray, radius = needleRadius.toPx(), @@ -160,4 +187,4 @@ fun TimeLine( ) } } -} +} \ No newline at end of file