diff --git a/experimental/examples/visual-effects/.gitignore b/experimental/examples/visual-effects/.gitignore new file mode 100644 index 0000000000..ba8435b9c5 --- /dev/null +++ b/experimental/examples/visual-effects/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build/ +/captures +.externalNativeBuild +.cxx \ No newline at end of file diff --git a/experimental/examples/visual-effects/.run/desktop.run.xml b/experimental/examples/visual-effects/.run/desktop.run.xml new file mode 100644 index 0000000000..29d5ed2ecd --- /dev/null +++ b/experimental/examples/visual-effects/.run/desktop.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/experimental/examples/visual-effects/README.md b/experimental/examples/visual-effects/README.md new file mode 100644 index 0000000000..0943c31b12 --- /dev/null +++ b/experimental/examples/visual-effects/README.md @@ -0,0 +1,7 @@ +Several visual effects implmented with Compose Multiplatform, used in 1.0 release announce video. + +### Running desktop application +* To run, launch command: `./gradlew run` +* Or choose **desktop** configuration in IDE and run it. + ![desktop-run-configuration.png](screenshots/desktop-run-configuration.png) + diff --git a/experimental/examples/visual-effects/build.gradle.kts b/experimental/examples/visual-effects/build.gradle.kts new file mode 100644 index 0000000000..8fabdf370a --- /dev/null +++ b/experimental/examples/visual-effects/build.gradle.kts @@ -0,0 +1,62 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") +} + +group = "me.user" +version = "1.0" + +repositories { + mavenCentral() + google() + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } +} + +dependencies { + implementation(compose.desktop.currentOs) +} + +tasks.withType { + kotlinOptions.jvmTarget = "11" + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" +} + +compose.desktop { + application { + mainClass = "org.jetbrains.compose.demo.visuals.MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "compose-demo" + packageVersion = "1.0.0" + } + } +} + +afterEvaluate { + val additionalArguments = mutableListOf() + + val runTask = tasks.named("run") { + this.args = additionalArguments + } + + tasks.register("runWords") { + additionalArguments.add("words") + group = "compose desktop" + dependsOn(runTask) + } + + tasks.register("runWave") { + additionalArguments.add("wave") + group = "compose desktop" + dependsOn(runTask) + } + + tasks.register("runNewYear") { + additionalArguments.add("NY") + group = "compose desktop" + dependsOn(runTask) + } +} \ No newline at end of file diff --git a/experimental/examples/visual-effects/gradle.properties b/experimental/examples/visual-effects/gradle.properties new file mode 100644 index 0000000000..8e63016e66 --- /dev/null +++ b/experimental/examples/visual-effects/gradle.properties @@ -0,0 +1,3 @@ +kotlin.code.style=official +kotlin.version=1.7.20 +compose.version=1.2.2 diff --git a/experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.jar b/experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..e708b1c023 Binary files /dev/null and b/experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.jar differ diff --git a/experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.properties b/experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ae04661ee7 --- /dev/null +++ b/experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/experimental/examples/visual-effects/gradlew b/experimental/examples/visual-effects/gradlew new file mode 100755 index 0000000000..4f906e0c81 --- /dev/null +++ b/experimental/examples/visual-effects/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/experimental/examples/visual-effects/gradlew.bat b/experimental/examples/visual-effects/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/experimental/examples/visual-effects/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/experimental/examples/visual-effects/screenshots/desktop-run-configuration.png b/experimental/examples/visual-effects/screenshots/desktop-run-configuration.png new file mode 100644 index 0000000000..3688407c6f Binary files /dev/null and b/experimental/examples/visual-effects/screenshots/desktop-run-configuration.png differ diff --git a/experimental/examples/visual-effects/settings.gradle.kts b/experimental/examples/visual-effects/settings.gradle.kts new file mode 100644 index 0000000000..69d08df2c6 --- /dev/null +++ b/experimental/examples/visual-effects/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) + } +} + +rootProject.name = "visual-effects" diff --git a/experimental/examples/visual-effects/src/main/kotlin/HappyNY.kt b/experimental/examples/visual-effects/src/main/kotlin/HappyNY.kt new file mode 100644 index 0000000000..ca1d797055 --- /dev/null +++ b/experimental/examples/visual-effects/src/main/kotlin/HappyNY.kt @@ -0,0 +1,430 @@ +package org.jetbrains.compose.demo.visuals + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.* +import java.lang.Math.random +import kotlin.math.* +import kotlin.random.Random + +const val width = 1200 +const val height = 800 +const val snowCount = 80 +const val starCount = 60 +const val rocketPartsCount = 30 + +data class SnowFlake( + var x: Dp, + var y: Dp, + val scale: Float, + var v: Double, + var alpha: Float, + var angle: Float, + var rotate: Int, + var phase: Double +) + +data class Star(val x: Dp, val y: Dp, val color: Color, val size: Dp) + +const val HNYString = "Happy New Year!" + +class DoubleRocket(val particle: Particle) { + private val STATE_ROCKET = 0 + private val STATE_SMALL_ROCKETS = 1 + var state = STATE_ROCKET + var rockets: Array = emptyArray() + private fun checkState(time: Long) { + if (particle.vy > -3.0 && state == STATE_ROCKET) { + explode(time) + } + if (state == STATE_SMALL_ROCKETS) { + var done = true + rockets.forEach { + if (!it.exploded) { + it.checkExplode(time) + } + if (!it.checkDone()) { + done = false + } + } + if (done) { + reset() + } + } + } + + private fun reset() { + if (particle.vx < 0) return //to stop drawing after the second rocket. This could be commented out + state = STATE_ROCKET + particle.x = if (particle.vx > 0) width - 0.0 else 0.0 + particle.y = 1000.0 + particle.vx = -1 * particle.vx + particle.vy = -12.5 + } + + private fun explode(time: Long) { + val colors = arrayOf(Color(0xff, 0, 0), Color(192, 255, 192), Color(192, 212, 255)) + rockets = Array(7) { + val v = 1.2f + 1.0 * random() + val angle = 2 * PI * random() + Rocket( + Particle( + particle.x, + particle.y, + v * sin(angle) + particle.vx, + v * cos(angle) + particle.vy - 0.5f, + colors[it % colors.size] + ), colors[it % colors.size], time + ) + } + state = STATE_SMALL_ROCKETS + } + + fun move(time: Long, prevTime: Long) { + if (rocket.state == rocket.STATE_ROCKET) { + rocket.particle.move(time, prevTime) + rocket.particle.gravity(time, prevTime) + } else { + rocket.rockets.forEach { + it.move(time, prevTime) + } + } + rocket.checkState(time) + } + + @Composable + fun draw() { + if (state == rocket.STATE_ROCKET) { + particle.draw() + } else { + rockets.forEach { + it.draw() + } + } + } + +} + +class Rocket(val particle: Particle, val color: Color, val startTime: Long = 0) { + var exploded = false + var parts: Array = emptyArray() + + fun checkExplode(time: Long) { + if (time - startTime > 1200000000) { + explode() + } + } + + private fun explode() { + parts = Array(rocketPartsCount) { + val v = 0.5f + 1.5 * random() + val angle = 2 * PI * random() + Particle(particle.x, particle.y, v * sin(angle) + particle.vx, v * cos(angle) + particle.vy, color, 1) + } + exploded = true + } + + fun checkDone(): Boolean { + if (!exploded) return false + parts.forEach { + if (it.y < 800) return false + } + return true + } + + fun move(time: Long, prevTime: Long) { + if (!exploded) { + particle.move(time, prevTime) + particle.gravity(time, prevTime) + checkExplode(time) + } else { + parts.forEach { + it.move(time, prevTime) + it.gravity(time, prevTime) + } + } + } + + @Composable + fun draw() { + if (!exploded) { + particle.draw() + } else { + parts.forEach { + it.draw() + } + } + } +} + +class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double, val color: Color, val type: Int = 0) { + fun move(time: Long, prevTime: Long) { + x = (x + vx * (time - prevTime) / 30000000) + y = (y + vy * (time - prevTime) / 30000000) + } + + fun gravity(time: Long, prevTime: Long) { + vy = vy + 1.0f * (time - prevTime) / 300000000 + } + + @Composable + fun draw() { + val alphaFactor = if (type == 0) 1.0f else 1 / (1 + abs(vy / 5)).toFloat() + Box(Modifier.size(5.dp).offset(x.dp, y.dp).alpha(alphaFactor).clip(CircleShape).background(color)) + for (i in 1..5) { + Box( + Modifier.size(4.dp).offset((x - vx / 2 * i).dp, (y - vy / 2 * i).dp) + .alpha(alphaFactor * (1 - 0.18f * i)).clip(CircleShape).background(color) + ) + } + } +} + +val rocket = DoubleRocket(Particle(0.0, 1000.0, 2.1, -12.5, Color.White)) + + +@Composable +fun NYWindow(onCloseRequest: () -> Unit) { + val windowState = remember { WindowState(width = width.dp, height = height.dp) } + Window(onCloseRequest = onCloseRequest, undecorated = true, transparent = true, state = windowState) { + NYContent() + } +} + +fun prepareStarsAndSnowFlakes(stars: SnapshotStateList, snowFlakes: SnapshotStateList) { + for (i in 0..snowCount) { + snowFlakes.add( + SnowFlake( + (50 + (width - 50) * random()).dp, + (height * random()).dp, + 0.1f + 0.2f * random().toFloat(), + 1.5 + 3 * random(), + (0.4f + 0.4 * random()).toFloat(), + 60 * random().toFloat(), + Random.nextInt(1, 5) - 3, + random() * 2 * PI + ) + ) + } + val colors = arrayOf(Color.Red, Color.Yellow, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White) + for (i in 0..starCount) { + stars.add( + Star( + (width * random()).dp, + (height * random()).dp, + colors[Random.nextInt(colors.size)], + (3 + 5 * random()).dp + ) + ) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +@Preview +fun NYContent() { + var time by remember { mutableStateOf(System.nanoTime()) } + var started by remember { mutableStateOf(false) } + var startTime = remember { System.nanoTime() } + var prevTime by remember { mutableStateOf(System.nanoTime()) } + val snowFlakes = remember { mutableStateListOf() } + val stars = remember { mutableStateListOf() } + var flickering2 by remember { mutableStateOf(true) } + remember { prepareStarsAndSnowFlakes(stars, snowFlakes) } + + Surface( + modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)), + color = Color.Black, + shape = RoundedCornerShape(20.dp) + ) { + + LaunchedEffect(Unit) { + while (true) { + withFrameNanos { + prevTime = time + time = it + } + } + } + + if (!started) { //animation starts with delay, so there is some time to start recording + if (time - startTime in 7000000001..7099999999) println("ready!") + if (time - startTime > 10000000000) { + startTime = time //restarting timer + started = true + } + } + + if (flickering2) { + if (time - startTime > 15500000000) { //note, that startTime has been updated above + flickering2 = false + } + } + if (started) { + rocket.move(time, prevTime) + } + + with(LocalDensity.current) { + Box(Modifier.fillMaxSize()) { + + snow(time, prevTime, snowFlakes, startTime) + + starrySky(stars) + + Text( + "202", + Modifier.scale(10f).align(Alignment.Center).offset(-2.dp, 15.dp) + .alpha(if (flickering2) 0.8f else 1.0f), + color = Color.White + ) + + val alpha = if (flickering2) flickeringAlpha(time) else 1.0f + Text( + "2", + Modifier.scale(10f).align(Alignment.Center).offset(14.dp, 15.dp).alpha(alpha), + color = Color.White + ) + + if (started) { //delay to be able to start recording + //HNY + var i = 0 + val angle = (HNYString.length / 2 * 5) * -1.0f + val color = colorHNY(time, startTime) + HNYString.forEach { + val alpha = alphaHNY(i, time, startTime) + Text( + it.toString(), + color = color, + modifier = Modifier.scale(5f).align(Alignment.Center).offset(0.dp, 85.dp) + .rotate((angle + 5.0f * i)).offset(0.dp, -90.dp).alpha(alpha) + ) + i++ + } + + rocket.draw() + } + + Text( + "Powered by Compose Multiplatform", + modifier = Modifier.align(Alignment.BottomEnd).offset(-20.dp, 0.dp), + color = Color.White + ) + } + } + } +} + +fun colorHNY(time: Long, startTime: Long): Color { + val periodLength = 60 + val offset = ((time - startTime) / 80000000).toFloat() / periodLength + val color1 = Color.Red + val color2 = Color.Yellow + val color3 = Color.Magenta + if (offset < 1) return blend(color1, color2, offset) + if (offset < 2) return blend(color2, color3, offset - 1) + if (offset < 3) return blend(color3, color1, offset - 2) + return color1 +} + +fun blend(color1: Color, color2: Color, fraction: Float): Color { + if (fraction < 0) return color1 + if (fraction > 1) return color2 + return Color( + color2.red * fraction + color1.red * (1 - fraction), + color2.green * fraction + color1.green * (1 - fraction), + color2.blue * fraction + color1.blue * (1 - fraction) + ) +} + +fun alphaHNY(i: Int, time: Long, startTime: Long): Float { + val period = period(time, startTime, 200) - i + if (period < 0) return 0.0f + if (period > 10) return 1.0f + return 0.1f * period +} + +fun period(time: Long, startTime: Long, periodLength: Int, speed: Int = 1): Int { + val period = 200000000 / speed + return (((time - startTime) / period) % periodLength).toInt() +} + +fun flickeringAlpha(time: Long): Float { + val time = (time / 10000000) % 100 + var result = 0.2f + if (time > 75) { + result += 0.6f * ((time - 75) % 3) / 3 + } + return result +} + + +@Composable +fun starrySky(stars: SnapshotStateList) { + stars.forEach { + star(it.x, it.y, it.color, size = it.size) + } +} + +@Composable +fun star(x: Dp, y: Dp, color: Color = Color.White, size: Dp) { + Box(Modifier.offset(x, y).scale(1.0f, 0.2f).rotate(45f).size(size).background(color)) + Box(Modifier.offset(x, y).scale(0.2f, 1.0f).rotate(45f).size(size).background(color)) +} + +@Composable +fun snow(time: Long, prevTime: Long, snowFlakes: SnapshotStateList, startTime: Long) { + val deltaAngle = (time - startTime) / 100000000 + with(LocalDensity.current) { + snowFlakes.forEach { + var y = it.y + ((it.v * (time - prevTime)) / 300000000).dp + if (y > (height + 20).dp) { + y = -20.dp + } + it.y = y + val x = it.x + (15 * sin(time.toDouble() / 3000000000 + it.phase)).dp + snowFlake(Modifier.offset(x, y).scale(it.scale).rotate(it.angle + deltaAngle * it.rotate), it.alpha) + } + } +} + +@Composable +fun snowFlake(modifier: Modifier, alpha: Float = 0.8f) { + Box(modifier) { + snowFlakeInt(0, 0f, 30.dp, 0.dp, alpha) + snowFlakeInt(0, 60f, 15.dp, 25.dp, alpha) + snowFlakeInt(0, 120f, -15.dp, 25.dp, alpha) + snowFlakeInt(0, 180f, -30.dp, 0.dp, alpha) + snowFlakeInt(0, 240f, -15.dp, -25.dp, alpha) + snowFlakeInt(0, 300f, 15.dp, -25.dp, alpha) + } + +} + +@Composable +fun snowFlakeInt(level: Int, angle: Float, shiftX: Dp, shiftY: Dp, alpha: Float) { + if (level > 3) return + Box( + Modifier.offset(shiftX, shiftY).rotate(angle).width(100.dp).height(10.dp).scale(0.6f).alpha(1f) + .background(Color.White.copy(alpha = alpha)) + ) { + snowFlakeInt(level + 1, 30f, 12.dp, 20.dp, alpha * 0.8f) + snowFlakeInt(level + 1, -30f, 12.dp, -20.dp, alpha * 0.8f) + } +} + diff --git a/experimental/examples/visual-effects/src/main/kotlin/RotatingWords.kt b/experimental/examples/visual-effects/src/main/kotlin/RotatingWords.kt new file mode 100644 index 0000000000..2ccd610b5a --- /dev/null +++ b/experimental/examples/visual-effects/src/main/kotlin/RotatingWords.kt @@ -0,0 +1,162 @@ +package org.jetbrains.compose.demo.visuals + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.* +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.loadSvgPainter +import androidx.compose.ui.res.useResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication + +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class) +@Preview +@Composable +fun Words() { + val density = LocalDensity.current + val duration = 5000 + + val infiniteTransition = rememberInfiniteTransition() + val angle by infiniteTransition.animateFloat( + initialValue = -50f, + targetValue = 30f, + animationSpec = infiniteRepeatable( + animation = tween(duration, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ) + ) + val scale by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 7f, + animationSpec = infiniteRepeatable( + animation = tween(duration, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ) + ) + + val logoSvg = remember { + useResource("compose-community-primary.svg") { loadSvgPainter(it, density) } + } + + val baseLogo = DpOffset(350.dp, 270.dp) + + val baseText = DpOffset(350.dp, 270.dp) + + val baseRu = DpOffset(100.dp, 100.dp) + val baseEn = DpOffset(100.dp, 600.dp) + val baseCh = DpOffset(600.dp, 100.dp) + val baseJa = DpOffset(600.dp, 600.dp) + + val color1 = Color(0x6B, 0x57, 0xFF) + val color2 = Color(0xFE, 0x28, 0x57) + val color3 = Color(0xFD, 0xB6, 0x0D) + val color4 = Color(0xFC, 0xF8, 0x4A) + + Column(modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Word(position = baseRu, angle = angle, scale = scale, text = "Ваш", color = color1) + Word(position = baseEn, angle = angle, scale = scale, text = "Your", color = color2) + Word(position = baseCh, angle = angle, scale = scale, text = "您的", color = color3) + Word(position = baseJa, angle = angle, scale = scale, text = "あなたの", color = color4) + Word(position = baseText, angle = 0f, scale = 6f, text = " Compose\nMultiplatform", color = Color(52, 67, 235), + alpha = 0.4f) + + val size = 80.dp * scale + Image(logoSvg, contentDescription = "Logo", + modifier = Modifier + .offset(baseLogo.x - size / 2, baseLogo.y - size / 2) + .size(size) + .rotate(angle * 2f) + ) + } +} + +@Composable +fun Word(position: DpOffset, angle: Float, scale: Float, text: String, + color: Color, alpha: Float = 0.8f) { + Text( + modifier = Modifier + .offset(position.x, position.y) + .rotate(angle) + .scale(scale) + .alpha(alpha), + color = color, + fontWeight = FontWeight.Bold, + text = text, + ) +} + +@Composable +@Preview +fun FallingSnow() { + BoxWithConstraints(Modifier.fillMaxSize()) { + repeat(50) { + val size = remember { 20.dp + 10.dp * Math.random().toFloat() } + val alpha = remember { 0.10f + 0.15f * Math.random().toFloat() } + val sizePx = with(LocalDensity.current) { size.toPx() } + val x = remember { (constraints.maxWidth * Math.random()).toInt() } + + val infiniteTransition = rememberInfiniteTransition() + val t by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(16000 + (16000 * Math.random()).toInt(), easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) + val initialT = remember { Math.random().toFloat() } + val actualT = (initialT + t) % 1f + val y = (-sizePx + (constraints.maxHeight + sizePx) * actualT).toInt() + + Box( + Modifier + .offset { IntOffset(x, y) } + .clip(CircleShape) + .alpha(alpha) + .background(Color.White) + .size(size) + ) + + } + } +} + +@Composable +@Preview +fun Background() = Box( + Modifier + .fillMaxSize() + .background(Color(0xFF6F97FF)) +) + +@Composable +@Preview +fun RotatingWords() { + Background() + FallingSnow() + Words() +} diff --git a/experimental/examples/visual-effects/src/main/kotlin/WaveEffect.kt b/experimental/examples/visual-effects/src/main/kotlin/WaveEffect.kt new file mode 100644 index 0000000000..1f09d139e9 --- /dev/null +++ b/experimental/examples/visual-effects/src/main/kotlin/WaveEffect.kt @@ -0,0 +1,265 @@ +package org.jetbrains.compose.demo.visuals + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.WindowState +import kotlin.math.* + +@Composable +fun WaveEffect(onCloseRequest: () -> Unit, showControls: Boolean) { + val windowState = remember { WindowState(width = 1200.dp, height = 800.dp) } + Window(onCloseRequest = {}, undecorated = true, transparent = true, state = windowState) { + Grid() + } + + if (showControls) { + Window( + onCloseRequest = onCloseRequest, + state = WindowState(width = 200.dp, height = 400.dp, position = WindowPosition(1400.dp, 200.dp)) + ) { + Column { + SettingsPanel(State.red, "Red") + SettingsPanel(State.green, "Green") + SettingsPanel(State.blue, "Blue") + } + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +@Preview +fun Grid() { + var mouseX by remember { mutableStateOf(0) } + var mouseY by remember { mutableStateOf(0) } + var centerX by remember { mutableStateOf(1200) } + var centerY by remember { mutableStateOf(900) } + var vX by remember { mutableStateOf(0) } + var vY by remember { mutableStateOf(0) } + + var time by remember { mutableStateOf(System.nanoTime()) } + var prevTime by remember { mutableStateOf(System.nanoTime()) } + + if (State.mouseUsed) { + centerX = (centerX + vX * (time - prevTime) / 1000000000).toInt() + if (centerX < -100) centerX = -100 + if (centerX > 2600) centerX = 2600 + vX = + (vX * (1 - (time - prevTime).toDouble() / 500000000) + 10 * (mouseX - centerX) * (time - prevTime) / 1000000000).toInt() + centerY = (centerY + vY * (time - prevTime) / 1000000000).toInt() + if (centerY < -100) centerY = -100 + if (centerY > 1800) centerY = 1800 + vY = + (vY * (1 - (time - prevTime).toDouble() / 500000000) + 5 * (mouseY - centerY) * (time - prevTime) / 1000000000).toInt() + + prevTime = time + } + + Surface( + modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)) + .onPointerEvent(PointerEventType.Move) { + mouseX = it.changes.first().position.x.toInt() + mouseY = it.changes.first().position.y.toInt() + } + .onPointerEvent(PointerEventType.Enter) { + State.mouseUsed = true + } + .onPointerEvent(PointerEventType.Exit) { + State.mouseUsed = false + }, + color = Color(0, 0, 0), + shape = RoundedCornerShape(20.dp) + ) { + Box(Modifier.fillMaxSize()) { + var x = 10 // initial position + var y = 10 // initial position + val shift = 25 + var evenRow = false + val pointerOffsetX = (centerX / 2) + val pointerOffsety = (centerY / 2) + while (y < 790) { + x = if (evenRow) 10 + shift else 10 + while (x < 1190) { + val size: Int = size(x, y, pointerOffsetX, pointerOffsety) + val color = boxColor(x, y, time, pointerOffsetX, pointerOffsety) + Dot(size, Modifier.offset(x.dp, y.dp), color, time) + x += shift * 2 + } + y += shift + evenRow = !evenRow + } + HighPanel(pointerOffsetX, pointerOffsety) + } + + LaunchedEffect(Unit) { + while (true) { + withFrameNanos { + time = it + } + } + } + + } +} + +@Composable +fun HighPanel(mouseX: Int, mouseY: Int) { + Text( + "Compose", + Modifier.offset(270.dp, 600.dp).scale(7.0f).alpha(alpha(mouseX, mouseY, 270, 700)), + color = colorMouse(mouseX, mouseY, 270, 700), + fontWeight = FontWeight.Bold + ) + Text( + "Multiplatform", + Modifier.offset(350.dp, 700.dp).scale(7.0f).alpha(alpha(mouseX, mouseY, 550, 800)), + color = colorMouse(mouseX, mouseY, 550, 800), + fontWeight = FontWeight.Bold + ) + Text( + "1.0", + Modifier.offset(800.dp, 700.dp).scale(7.0f).alpha(alpha(mouseX, mouseY, 800, 800)), + color = colorMouse(mouseX, mouseY, 800, 800), + fontWeight = FontWeight.Bold + ) +} + +private fun alpha(mouseX: Int, mouseY: Int, x: Int, y: Int): Float { + var d = distance(mouseX, mouseY, x, y) + if (d > 450) return 0.0f + d = d / 450 - 0.1 + return (1 - d * d).toFloat() +} + +private fun colorMouse(mouseX: Int, mouseY: Int, x: Int, y: Int): Color { + val d = distance(mouseX, mouseY, x, y) / 450 + val color1 = Color(0x6B, 0x57, 0xFF) + val color2 = Color(0xFE, 0x28, 0x57) + val color3 = Color(0xFD, 0xB6, 0x0D) + val color4 = Color(0xFC, 0xF8, 0x4A) + if (d > 1) return color1 + if (d > 0.66) return balancedColor(3 * d - 2, color1, color2) + if (d > 0.33) return balancedColor(3 * d - 1, color2, color3) + return balancedColor(3 * d, color3, color4) +} + +private fun balancedColor(d: Double, color1: Color, color2: Color): Color { + if (d > 1) return color1 + if (d < 0) return color2 + val red = ((color1.red * d + color2.red * (1 - d)) * 255).toInt() + val green = ((color1.green * d + color2.green * (1 - d)) * 255).toInt() + val blue = ((color1.blue * d + color2.blue * (1 - d)) * 255).toInt() + return Color(red, green, blue) +} + + +private fun distance(x1: Int, y1: Int, x2: Int, y2: Int): Double { + return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2).toDouble()) +} + +@Composable +fun Dot(size: Int, modifier: Modifier, color: Color, time: Long) { + Box( + modifier.rotate(time.toFloat() / (15 * 10000000)).clip(RoundedCornerShape((3 + size / 20).dp)) + .size(width = size.dp, height = size.dp) + ) { + Box(modifier = Modifier.fillMaxSize().background(color)) { + } + } +} + +private fun size(x: Int, y: Int, mouseX: Int, mouseY: Int): Int { + val addSize = 3 + var result = 5 + if (y > 550 && x < 550) return result + if (y > 650 && x < 900) return result + val distance2 = sqrt((x - mouseX) * (x - mouseX) + (y - mouseY) * (y - mouseY).toDouble()) / 200 + val scale: Double = (if (distance2 < 1) { + addSize * (1 - distance2) + } else 0.toDouble()) + result += (if (State.mouseUsed) round(7.5 * scale).toInt() else 0) + return result +} + +private fun boxColor(x: Int, y: Int, time: Long, mouseX: Int, mouseY: Int): Color { + if (!State.mouseUsed) return Color.White + + val color1 = Color(0x6B, 0x57, 0xFF) + val color2 = Color(0xFE, 0x28, 0x57) + val color3 = Color(0xFC, 0xF8, 0x4A) + + val distance = sqrt(((x - mouseX) * (x - mouseX) + (y - mouseY) * (y - mouseY)).toDouble()) + val fade = exp(-1 * distance * distance / 150000) + + var c1 = sin(12 * distance / 450 - (time.toDouble() / (5 * 100000000))) + if (c1 < 0) c1 = 0.0 + var c2 = sin(2 + 12 * distance / 450 - (time.toDouble() / (5 * 100000000))) + if (c2 < 0) c2 = 0.0 + var c3 = sin(4 + 12 * distance / 450 - (time.toDouble() / (5 * 100000000))) + if (c3 < 0) c3 = 0.0 + var color = Color.White + + if (c1 <= 0) { + val d = c2 / (c2 + c3) + color = balancedColor(d, color2, color3) + } else if (c2 <= 0) { + val d = c3 / (c1 + c3) + color = balancedColor(d, color3, color1) + } else if (c3 <= 0) { + val d = c1 / (c1 + c2) + color = balancedColor(d, color1, color2) + } + + return balancedColor(fade, color, Color.White) +} + +internal class ColorSettings { + var enabled by mutableStateOf(true) + var waveLength by mutableStateOf(30.0) + var simple by mutableStateOf(true) + var period by mutableStateOf(80.0) +} + +private class State { + companion object { + var red by mutableStateOf(ColorSettings()) + var green by mutableStateOf(ColorSettings()) + var blue by mutableStateOf(ColorSettings()) + var mouseUsed by mutableStateOf(false) + } +} + +@Composable +internal fun SettingsPanel(settings: ColorSettings, name: String) { + Row { + Text(name) + Checkbox(settings.enabled, onCheckedChange = { settings.enabled = it }) + Checkbox(settings.simple, onCheckedChange = { settings.simple = it }) + Slider( + (settings.waveLength.toFloat() - 10) / 90, + { settings.waveLength = 10 + 90 * it.toDouble() }, + Modifier.width(100.dp) + ) + Slider( + (settings.period.toFloat() - 10) / 90, + { settings.period = 10 + 90 * it.toDouble() }, + Modifier.width(100.dp) + ) + } +} + diff --git a/experimental/examples/visual-effects/src/main/kotlin/main.kt b/experimental/examples/visual-effects/src/main/kotlin/main.kt new file mode 100644 index 0000000000..98d7d5c582 --- /dev/null +++ b/experimental/examples/visual-effects/src/main/kotlin/main.kt @@ -0,0 +1,32 @@ +package org.jetbrains.compose.demo.visuals + +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application +import androidx.compose.ui.window.singleWindowApplication + +fun mainWords() = singleWindowApplication( + title = "Compose Demo", state = WindowState(size = DpSize(830.dp, 830.dp)) +) { + RotatingWords() +} + +fun mainWave(controls: Boolean) = application { + WaveEffect(::exitApplication, controls) +} + +fun mainNY() = application { + NYWindow(::exitApplication) +} + +fun main(args: Array) { + if (args.isEmpty()) return mainWords() + when (val effect = args[0]) { + "words" -> mainWords() + "wave" -> mainWave(false) + "wave-controls" -> mainWave(true) + "NY" -> mainNY() + else -> throw Error("Unknown effect: $effect") + } +} diff --git a/experimental/examples/visual-effects/src/main/resources/compose-community-primary.png b/experimental/examples/visual-effects/src/main/resources/compose-community-primary.png new file mode 100644 index 0000000000..a73ddd7989 Binary files /dev/null and b/experimental/examples/visual-effects/src/main/resources/compose-community-primary.png differ diff --git a/experimental/examples/visual-effects/src/main/resources/compose-community-primary.svg b/experimental/examples/visual-effects/src/main/resources/compose-community-primary.svg new file mode 100644 index 0000000000..3e5ce57f72 --- /dev/null +++ b/experimental/examples/visual-effects/src/main/resources/compose-community-primary.svg @@ -0,0 +1,19 @@ + + + + + + + + + +