Skip to content

Commit

Permalink
Add LightLinesWatchFace
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisCAD committed Jun 21, 2024
1 parent ef6bccb commit e5b0883
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 0 deletions.
158 changes: 158 additions & 0 deletions shared/src/main/kotlin/elements/FatHourDigits.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package org.splitties.compose.oclock.sample.elements

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.Font
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.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.louiscad.composeoclockplayground.shared.R
import org.splitties.compose.oclock.LocalIsAmbient
import org.splitties.compose.oclock.LocalTime
import org.splitties.compose.oclock.OClockCanvas
import org.splitties.compose.oclock.sample.extensions.SizeDependentState
import org.splitties.compose.oclock.sample.extensions.blurOffset
import org.splitties.compose.oclock.sample.extensions.center
import org.splitties.compose.oclock.sample.extensions.rememberGraphicsLayerAsState
import org.splitties.compose.oclock.sample.extensions.sizeForLayer

@Composable
fun FatHourDigits(
interactiveColor: Color,
interactiveShadowColor: Color = interactiveColor,
ambientShadowColor: Color = interactiveShadowColor,
interactiveShadowRepeat: Int = 1,
ambientShadowRepeat: Int = interactiveShadowRepeat.takeIf { it > 1 } ?: 2,
fontSize: TextUnit = 70.sp,
interactiveBlurRadius: Dp = 10.dp,
ambientBlurRadius: Dp = 10.dp
) {
val interactive = fatHourDigitsLayer(
textColor = interactiveColor,
shadowColor = interactiveShadowColor,
fontSize = fontSize,
blurRadius = interactiveBlurRadius
)
val ambient = fatHourDigitsLayer(
textColor = Color.Black,
shadowColor = ambientShadowColor,
fontSize = fontSize,
blurRadius = ambientBlurRadius
)
val isAmbient by LocalIsAmbient.current
OClockCanvas {
val count = if (isAmbient) ambientShadowRepeat else interactiveShadowRepeat
val layer = (if (isAmbient) ambient else interactive).get()
repeat(count) {
drawLayer(layer)
}
}
}

@Composable
fun fatHourDigitsLayer(
textColor: Color = Color.White,
shadowColor: Color = Color.White,
blendMode: BlendMode = BlendMode.SrcOver,
fontSize: TextUnit = 70.sp,
blurRadius: Dp = 10.dp
): SizeDependentState<GraphicsLayer> {
val style = rememberFatHourDigitsTextStyle(
textColor = textColor,
shadowColor = shadowColor,
blendMode = blendMode,
fontSize = fontSize,
blurRadius = blurRadius
)
val hourDigits = rememberHourDigits(style)
return rememberGraphicsLayerAsState { layer ->
layer.blendMode = blendMode
layer.record(size = hourDigits.sizeForLayer()) {
drawText(hourDigits, topLeft = hourDigits.blurOffset())
}
layer.center()
}
}

@Composable
fun rememberFatHourDigitsTextStyle(
textColor: Color = Color.White,
shadowColor: Color = Color.White,
blendMode: BlendMode = BlendMode.SrcOver,
fontSize: TextUnit = 70.sp,
drawStyle: DrawStyle = Fill,
blurRadius: Dp = 10.dp
): TextStyle {
val density = LocalDensity.current
val fontFamily = remember { FontFamily(Font(R.font.outfit_extrabold)) }
@Suppress("name_shadowing")
val blurRadius = with(density) { blurRadius.toPx() }
return remember(textColor, shadowColor) {
TextStyle(
color = textColor,
fontSize = fontSize,
// fontWeight = FontWeight.ExtraBold,
drawStyle = drawStyle,
fontFamily = fontFamily,
shadow = if (blurRadius > 0f) Shadow(
color = shadowColor,
blurRadius = blurRadius
) else null
)
}
}

@Composable
fun HourDigitsUnCached(
textColor: Color = Color.White,
shadowColor: Color = Color.White,
blendMode: BlendMode = BlendMode.SrcOver,
fontSize: TextUnit = 70.sp,
) {
val textMeasurer = rememberTextMeasurer()
val density = LocalDensity.current
val style = remember(textColor, shadowColor) {
val fontFamily = FontFamily(
Font(R.font.outfit_extrabold)
)
TextStyle(
color = textColor,
fontSize = fontSize,
fontWeight = FontWeight.ExtraBold,
fontFamily = fontFamily,
shadow = Shadow(
color = shadowColor,
blurRadius = with(density) { 10.dp.toPx() }
)
)
}
val time = LocalTime.current
val measuredText = remember(time.hours) { textMeasurer.measure("${time.hours}", style) }
OClockCanvas {
drawText(
measuredText,
topLeft = center.run {
copy(
x = x - measuredText.size.width / 2f,
y = y - measuredText.size.height / 2f
)
},
blendMode = blendMode
)
}
}
18 changes: 18 additions & 0 deletions shared/src/main/kotlin/elements/HourDigitsHelpers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.splitties.compose.oclock.sample.elements

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import org.splitties.compose.oclock.LocalTime

@Composable
fun rememberHourDigits(textStyle: TextStyle): TextLayoutResult {
val time = LocalTime.current
val textMeasurer = rememberTextMeasurer()
val measuredText = remember(time.hours, textStyle) {
textMeasurer.measure(text = "${time.hours}", style = textStyle)
}
return measuredText
}
6 changes: 6 additions & 0 deletions shared/src/main/kotlin/extensions/Size.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.splitties.compose.oclock.sample.extensions

import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.IntSize
import kotlin.math.floor
import kotlin.math.min


Expand All @@ -25,3 +27,7 @@ fun Size.fitIn(other: Size): Size {
val factor = min(maxFactor, minFactor)
return this * factor
}

fun Size.toFlooredIntSize(): IntSize {
return IntSize(floor(width).toInt(), floor(height).toInt())
}
16 changes: 16 additions & 0 deletions shared/src/main/kotlin/extensions/TextLayoutResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.splitties.compose.oclock.sample.extensions

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toIntSize
import androidx.compose.ui.unit.toSize

fun TextLayoutResult.sizeForLayer(): IntSize {
val spaceForBlur = (layoutInput.style.shadow?.blurRadius ?: 0f) * 2
return (size.toSize() + spaceForBlur).toIntSize()
}

fun TextLayoutResult.blurOffset(): Offset = layoutInput.style.shadow?.blurRadius?.let {
Offset(it, it)
} ?: Offset.Zero
4 changes: 4 additions & 0 deletions shared/src/main/kotlin/utils/Bits.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.splitties.compose.oclock.sample.utils

@PublishedApi
internal fun Int.getBitAt(position: Int, last: Int = 32): Boolean = this shr (last - position) and 1 > 0
34 changes: 34 additions & 0 deletions shared/src/main/kotlin/utils/FiveMinutesLayoutOrder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.splitties.compose.oclock.sample.utils

import androidx.annotation.IntRange

@JvmInline
value class FiveMinutesLayoutOrder(
private val storage: Int
) {
init {
require((storage and 0b11111_00000_00000_00000).countOneBits() == 1)
require((storage and 0b00000_11111_00000_00000).countOneBits() == 2)
require((storage and 0b00000_00000_11111_00000).countOneBits() == 3)
require((storage and 0b00000_00000_00000_11111).countOneBits() == 4)
}

companion object {
val linear = FiveMinutesLayoutOrder(0b10000_11000_11100_11110)
val symmetricalSpread = FiveMinutesLayoutOrder(0b00100_01010_10101_11011)
val symmetricalPacked = FiveMinutesLayoutOrder(0b00100_01010_01110_11011)
val symmetricalEdges = FiveMinutesLayoutOrder(0b00100_10001_10101_11011)
}

fun showFor(
@IntRange(0, 59) minute: Int,
@IntRange(0, 4) minuteMarkIndex: Int,
): Boolean {
require(minute in 0..59)
require(minuteMarkIndex in 0..4)
return storage.getBitAt(
position = ((minute - 1) % 5) * 5 + minuteMarkIndex,
last = 19
)
}
}
94 changes: 94 additions & 0 deletions shared/src/main/kotlin/utils/FiveMinutesSlicePattern.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.splitties.compose.oclock.sample.utils

import androidx.annotation.IntRange
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.graphics.layer.drawLayer
import org.splitties.compose.oclock.LocalTime
import org.splitties.compose.oclock.OClockCanvas
import org.splitties.compose.oclock.sample.extensions.SizeDependentState
import org.splitties.compose.oclock.sample.extensions.rememberGraphicsLayerAsState
import org.splitties.compose.oclock.sample.extensions.rememberStateWithSize
import org.splitties.compose.oclock.sample.extensions.toFlooredIntSize

@Composable
fun <T> FiveMinutesSlicePattern(
layoutOrder: FiveMinutesLayoutOrder = FiveMinutesLayoutOrder.symmetricalSpread,
mirrored: Boolean = true,
createSliceData: SizeDependentState.Scope.(slicePath: Path, bounds: Rect) -> T,
drawSliceContent: DrawScope.(fiveMinutesIndex: Int, sliceData: T) -> Unit
) {
val slicePathAndBounds = rememberStateWithSize {
val path = Path().also {
it.setToPieSlice(size, 30f, centered = false)
}
path to path.getBounds()
}
val sliceData = rememberStateWithSize {
val (path, bounds) = slicePathAndBounds.get()
createSliceData(path, bounds)
}
val minuteLayers = List(5) { i ->
minuteSliceLayer(
slicePath = slicePathAndBounds,
sliceData = sliceData,
fiveMinutesIndex = i,
drawSliceContent = drawSliceContent
)
}
val time = LocalTime.current
OClockCanvas {
val max = 12
val minutes = time.minutes
for (i in 0..<max) {
val isLast = i == (minutes / 5)
if (isLast && minutes % 5 == 0) break
val normalRotation = mirrored.not() || i % 2 == 0
val angle = 30f * if (normalRotation) i else (i + 1)
rotate(angle) {
scale(
scaleX = if (normalRotation) 1f else -1f,
scaleY = 1f
) {
for (j in 0..4) {
val index = if (normalRotation) j else 4 - j
if (isLast) {
val shouldShowIt = layoutOrder.showFor(
minute = minutes,
minuteMarkIndex = j
)
if (shouldShowIt.not()) continue
}
val layer = minuteLayers[index].get()
drawLayer(layer)
}
}
}
if (isLast) break
}
}
}

@Composable
private fun <T> minuteSliceLayer(
slicePath: SizeDependentState<Pair<Path, Rect>>,
sliceData: SizeDependentState<T>,
@IntRange(0, 4) fiveMinutesIndex: Int,
drawSliceContent: DrawScope.(fiveMinutesIndex: Int, sliceData: T) -> Unit
) = rememberGraphicsLayerAsState { layer ->
require(fiveMinutesIndex in 0..4)
val (path, bounds) = slicePath.get()
layer.compositingStrategy = CompositingStrategy.Offscreen
layer.setPathOutline(path)
layer.clip = true
layer.translationX = center.x
@Suppress("name_shadowing") val sliceData = sliceData.get()
layer.record(size = bounds.size.toFlooredIntSize()) {
drawSliceContent(fiveMinutesIndex, sliceData)
}
}
19 changes: 19 additions & 0 deletions shared/src/main/kotlin/utils/PieSlicePath.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.splitties.compose.oclock.sample.utils

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Path
import org.splitties.compose.oclock.sample.extensions.lineTo
import org.splitties.compose.oclock.sample.extensions.moveTo

fun Path.setToPieSlice(size: Size, degrees: Float, centered: Boolean = true) {
reset()
moveTo(size.center)
lineTo(size.center.copy(y = 0f))
arcTo(size.toRect(), startAngleDegrees = -90f, sweepAngleDegrees = degrees, forceMoveTo = false)
lineTo(size.center)
close()
if (centered.not()) translate(Offset(x = -size.center.x, y = 0f))
}
1 change: 1 addition & 0 deletions shared/src/main/kotlin/watchfaces/AllWatchFaces.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.splitties.compose.oclock.sample.elements.LoopingSeconds

val allWatchFaces: List<@Composable () -> Unit> = listOf(
{ LoopingSeconds() },
{ LightLinesWatchFace() },
{ KotlinFanClock() },
{ ComposeFanClock() },
{ BasicAnalogClock() },
Expand Down
Loading

0 comments on commit e5b0883

Please sign in to comment.