chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:31 +08:00
commit f06de36198
4585 changed files with 588418 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+23
View File
@@ -0,0 +1,23 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
plugins {
alias(libs.plugins.image.toolbox.library)
alias(libs.plugins.image.toolbox.compose)
}
android.namespace = "com.t8rin.snowfall"
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ ImageToolbox is an image editor for android
~ Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~
~ 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.
~
~ You should have received a copy of the Apache License
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
-->
<manifest />
@@ -0,0 +1,31 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.snowfall
internal object Constants {
internal const val angleRange = 0.1f
internal const val angleDivisor = 10000.0f
internal const val angleSeed = 100.0f
internal const val baseFrameDurationMillis = 16
internal const val snowflakeDensity = 0.05
internal const val defaultAlpha = 0.65f
internal val incrementRange = 0.2f..0.8f
internal val sizeRange = 20f..40f
internal val angleSeedRange = -angleSeed..angleSeed
}
@@ -0,0 +1,162 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.snowfall
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.IntSize
import com.t8rin.snowfall.Constants.angleRange
import com.t8rin.snowfall.Constants.angleSeed
import com.t8rin.snowfall.Constants.incrementRange
import com.t8rin.snowfall.Constants.sizeRange
import com.t8rin.snowfall.types.AnimType
import kotlin.math.PI
import kotlin.math.roundToInt
internal data class SnowAnimState(
var tickNanos: Long,
val snowflakes: List<Snowflake>,
val painters: List<Painter>,
val animType: AnimType,
val colors: List<Color>,
val density: Double,
val alpha: Float,
) {
constructor(
tick: Long,
canvasSize: IntSize,
painters: List<Painter>,
animType: AnimType,
colors: List<Color>,
density: Double,
alpha: Float
) : this(
tickNanos = tick,
snowflakes = createSnowFlakes(
flakesProvider = painters,
canvasSize = canvasSize,
animType = animType,
colors = colors,
density = density,
alpha = alpha
),
painters = painters,
animType = animType,
colors = colors,
density = density,
alpha = alpha
)
fun draw(contentDrawScope: ContentDrawScope) {
snowflakes.forEach {
it.draw(contentDrawScope)
}
}
fun resize(newSize: IntSize) = copy(
snowflakes = createSnowFlakes(
flakesProvider = painters,
canvasSize = newSize,
animType = animType,
colors = colors,
density = density,
alpha = alpha
)
)
companion object {
fun createSnowFlakes(
flakesProvider: List<Painter>,
canvasSize: IntSize,
animType: AnimType,
colors: List<Color>,
density: Double,
alpha: Float,
): List<Snowflake> =
when (animType) {
AnimType.Falling -> createFallingSnowflakes(
canvasSize = canvasSize,
painters = flakesProvider,
colors = colors,
snowflakeDensity = density,
alpha = alpha
)
AnimType.Melting -> createMeltingSnowflakes(
canvasSize = canvasSize,
painters = flakesProvider,
colors = colors,
snowflakeDensity = density,
)
}
private fun createMeltingSnowflakes(
canvasSize: IntSize,
painters: List<Painter>,
colors: List<Color>,
snowflakeDensity: Double,
): List<MeltingSnowflake> {
if (canvasSize.height == 0 || canvasSize.width == 0 || snowflakeDensity == 0.0) {
return emptyList()
}
val canvasArea = canvasSize.width * canvasSize.height
val normalizedDensity = snowflakeDensity.coerceIn(0.0..1.0) / 2000.0
val count = (canvasArea * normalizedDensity).roundToInt()
val snowflakesCount = count.coerceIn(painters.size.coerceAtMost(count), count)
return List(snowflakesCount) {
MeltingSnowflake(
incrementFactor = incrementRange.random(),
canvasSize = canvasSize,
maxAlpha = (0.1f..0.7f).random(),
painter = painters[it % painters.size],
initialPosition = canvasSize.randomPosition(),
color = colors.random(),
)
}
}
private fun createFallingSnowflakes(
canvasSize: IntSize,
painters: List<Painter>,
colors: List<Color>,
snowflakeDensity: Double,
alpha: Float,
): List<FallingSnowflake> {
val canvasArea = canvasSize.width * canvasSize.height
val normalizedDensity = snowflakeDensity.coerceIn(0.0..1.0) / 1000.0
val snowflakesCount = (canvasArea * normalizedDensity).roundToInt()
return List(snowflakesCount) {
FallingSnowflake(
incrementFactor = incrementRange.random(),
size = sizeRange.random(),
canvasSize = canvasSize,
initialPosition = canvasSize.randomPosition(),
angle = angleSeed.random() / angleSeed * angleRange + (PI / 2.0) - (angleRange / 2.0),
painter = painters[it % painters.size],
color = colors.random(),
alpha = alpha,
)
}
}
}
}
@@ -0,0 +1,168 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.snowfall
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.Lifecycle
import com.t8rin.snowfall.Constants.defaultAlpha
import com.t8rin.snowfall.Constants.snowflakeDensity
import com.t8rin.snowfall.types.AnimType
import com.t8rin.snowfall.types.FlakeType
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlin.time.Duration.Companion.nanoseconds
/**
* Creates flakes falling animation base on `FlakeType` param.
* @param type type of flake.
* @param colors list of Colors for Flakes
* @param density density of Flakes must be between 0.0 (no Flakes) or 1.0 (a lot of Flakes). Default is 0.05
* @param alpha transparency of the falling flakes
*
*/
fun Modifier.snowfall(
type: FlakeType,
colors: List<Color>,
density: Double = snowflakeDensity,
alpha: Float = defaultAlpha,
): Modifier =
letItSnow(
flakeType = type,
animType = AnimType.Falling, colors = colors,
density = density,
alpha = alpha
)
/**
* Creates flakes falling animation base on `FlakeType` param.
* @param type type of flake.
* @param color single Color for Flakes
* @param density density of Flakes must be between 0.0 (no Flakes) or 1.0 (a lot of Flakes). Default is 0.05
* @param alpha transparency of the falling flakes
*
*/
fun Modifier.snowfall(
type: FlakeType,
color: Color = Color.Unspecified,
density: Double = snowflakeDensity,
alpha: Float = defaultAlpha,
): Modifier =
letItSnow(
flakeType = type,
animType = AnimType.Falling, colors = listOf(color),
density = density,
alpha = alpha
)
/**
* Creates flakes melting animation base on `FlakeType` param.
* @param type - type of flake.
* @param colors - list of Colors for Flakes
* @param density - density of Flakes must be between 0.0 (no Flakes) or 1.0 (a lot of Flakes). Default is 0.05
*
*/
fun Modifier.snowmelt(
type: FlakeType,
colors: List<Color>,
density: Double = snowflakeDensity,
): Modifier =
letItSnow(type, AnimType.Melting, colors = colors, density = density)
/**
* Creates flakes melting animation base on `FlakeType` param.
* @param type - type of flake.
* @param color - single Color for Flakes
* @param density - density of Flakes must be between 0.0 (no Flakes) or 1.0 (a lot of Flakes). Default is 0.05
*
*/
fun Modifier.snowmelt(
type: FlakeType,
color: Color = Color.Unspecified,
density: Double = snowflakeDensity,
): Modifier =
letItSnow(type, AnimType.Melting, colors = listOf(color), density = density)
private fun Modifier.letItSnow(
flakeType: FlakeType,
animType: AnimType,
colors: List<Color>,
density: Double,
alpha: Float = 1f,
) = composed {
val flakes = when (flakeType) {
is FlakeType.Custom -> flakeType.data
}
var snowAnimState by remember {
mutableStateOf(
SnowAnimState(
tick = -1,
canvasSize = IntSize(0, 0),
painters = flakes,
animType = animType,
colors = colors,
density = density,
alpha = alpha,
)
)
}
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
LaunchedEffect(Unit) {
while (isActive) {
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
withFrameNanos { frameTimeNanos ->
val elapsedMillis =
(frameTimeNanos - snowAnimState.tickNanos).nanoseconds.inWholeMilliseconds
val isFirstRun = snowAnimState.tickNanos < 0
snowAnimState.tickNanos = frameTimeNanos
if (isFirstRun) return@withFrameNanos 0
snowAnimState.snowflakes.forEach {
it.update(elapsedMillis)
}
return@withFrameNanos frameTimeNanos
}
} else {
withFrameNanos { frameTimeNanos ->
snowAnimState.tickNanos = frameTimeNanos
}
}
delay(8L)
}
}
onSizeChanged { newSize ->
snowAnimState = snowAnimState.resize(newSize)
}
.clipToBounds()
.drawWithContent {
drawContent()
snowAnimState.draw(this)
}
}
@@ -0,0 +1,145 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.snowfall
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.IntSize
import com.t8rin.snowfall.Constants.angleDivisor
import com.t8rin.snowfall.Constants.angleSeedRange
import com.t8rin.snowfall.Constants.baseFrameDurationMillis
import kotlin.math.cos
import kotlin.math.sin
internal interface Snowflake {
fun update(elapsedMillis: Long)
fun draw(contentDrawScope: ContentDrawScope)
}
internal class MeltingSnowflake(
private val initialPosition: Offset,
private val incrementFactor: Float,
private val canvasSize: IntSize,
private val maxAlpha: Float,
private val painter: Painter,
private val color: Color,
) : Snowflake {
init {
require(maxAlpha in 0.1..1.0)
}
private var position by mutableStateOf(initialPosition)
private var alpha by mutableFloatStateOf(0.001f)
private var isIncreasing by mutableStateOf(true)
override fun update(elapsedMillis: Long) {
val increment = incrementFactor * (elapsedMillis / baseFrameDurationMillis) / 100f
alpha = (if (isIncreasing) {
alpha + increment
} else {
alpha - increment
}).coerceIn(0f, maxAlpha)
if (alpha == maxAlpha) {
isIncreasing = false
}
if (alpha == 0f) {
isIncreasing = true
alpha = 0.001f
position = canvasSize.randomPosition()
}
}
override fun draw(contentDrawScope: ContentDrawScope) {
with(contentDrawScope) {
translate(
left = position.x,
top = position.y
) {
with(painter) {
draw(
size = intrinsicSize,
alpha = alpha,
colorFilter = if (color == Color.Unspecified) null else ColorFilter.tint(
color
)
)
}
}
}
}
}
internal class FallingSnowflake(
private val incrementFactor: Float,
private val size: Float,
private val canvasSize: IntSize,
initialPosition: Offset,
angle: Double,
private val painter: Painter,
private val color: Color,
private val alpha: Float,
) : Snowflake {
private val baseSpeedPxAt60Fps = 5
private var position by mutableStateOf(initialPosition)
private var angle by mutableDoubleStateOf(angle)
override fun update(elapsedMillis: Long) {
val increment =
incrementFactor * (elapsedMillis / baseFrameDurationMillis) * baseSpeedPxAt60Fps
val xDelta = (increment * cos(angle)).toFloat()
val yDelta = (increment * sin(angle)).toFloat()
position = Offset(position.x + xDelta, position.y + yDelta)
angle += angleSeedRange.random() / angleDivisor
if (position.y > canvasSize.height + size) {
position =
Offset(canvasSize.width.random().toFloat(), -size - painter.intrinsicSize.height)
}
}
override fun draw(contentDrawScope: ContentDrawScope) {
with(contentDrawScope) {
translate(
position.x,
position.y
) {
with(painter) {
draw(
size = intrinsicSize,
alpha = alpha,
colorFilter = if (color == Color.Unspecified) null else ColorFilter.tint(
color
)
)
}
}
}
}
}
@@ -0,0 +1,35 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.snowfall
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
import java.util.concurrent.ThreadLocalRandom
internal fun ClosedRange<Float>.random() =
ThreadLocalRandom.current().nextFloat() * (endInclusive - start) + start
internal fun Float.random() =
ThreadLocalRandom.current().nextFloat() * this
internal fun Int.random() =
ThreadLocalRandom.current().nextInt(this)
internal fun IntSize.randomPosition() =
Offset(width.random().toFloat(), height.random().toFloat())
@@ -0,0 +1,23 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.snowfall.types
internal enum class AnimType {
Falling,
Melting
}
@@ -0,0 +1,27 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* 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.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.snowfall.types
import androidx.compose.ui.graphics.painter.Painter
/**
* Type of flake used for animation.
*/
sealed interface FlakeType {
class Custom(val data: List<Painter>) : FlakeType
}