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 @@
+
+
+