├── .gitignore ├── README.MD ├── build.gradle.kts ├── composeApp ├── build.gradle.kts ├── composeApp.podspec └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── com │ │ └── nezihyilmaz │ │ └── multiplatform │ │ └── particles │ │ └── App.android.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── nezihyilmaz │ │ └── multiplatform │ │ └── particles │ │ ├── ResourceProvider.kt │ │ ├── SVGCoordinates.kt │ │ ├── controller │ │ └── Engine.kt │ │ ├── physics │ │ ├── ForceField.kt │ │ ├── Particle.kt │ │ └── Vector2.kt │ │ └── ui │ │ ├── Color.kt │ │ ├── Theme.kt │ │ ├── composable │ │ ├── App.kt │ │ ├── Fabric.kt │ │ ├── Renderer.kt │ │ ├── Space.kt │ │ └── Time.kt │ │ └── modifier │ │ └── GestureHandler.kt │ ├── desktopMain │ └── kotlin │ │ ├── com │ │ └── nezihyilmaz │ │ │ └── multiplatform │ │ │ └── particles │ │ │ └── App.jvm.kt │ │ └── main.kt │ ├── iosMain │ └── kotlin │ │ ├── com │ │ └── nezihyilmaz │ │ │ └── multiplatform │ │ │ └── particles │ │ │ └── App.ios.kt │ │ └── main.kt │ └── jsMain │ ├── kotlin │ ├── BrowserViewportWindow.kt │ ├── com │ │ └── nezihyilmaz │ │ │ └── multiplatform │ │ │ └── particles │ │ │ └── App.js.kt │ └── main.kt │ └── resources │ └── index.html ├── gradle.bat ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── iosApp ├── Podfile ├── Podfile.lock ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── iosApp.xcworkspace │ └── contents.xcworkspacedata └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iosApp.swift ├── screenshot ├── ss_desktop_action.png └── ss_desktop_idle.png └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.iml 3 | .gradle 4 | .idea 5 | .DS_Store 6 | build 7 | */build 8 | captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | xcuserdata/ 13 | Pods/ 14 | *.jks 15 | *yarn.lock 16 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 | *This projects skeleton was created by [Compose Multiplatform Template Wizard](https://terrakok.github.io/Compose-Multiplatform-Wizard/).* 3 | 4 | # Particle Physics Project | Compose Multiplatform 5 | 6 | This project is a basic physics simulation built on Multiplatform Compose Canvas. It has the same capabilities as any other vector driven particle system. 7 | 8 | Showcase example is a text/image rendering using small particles and a radial repelling force (created by pointer). 9 | 10 | ![image](screenshot/ss_desktop_idle.png) 11 | 12 | ![image](screenshot/ss_desktop_action.png) 13 | 14 | ## Before running! 15 | - check your system with (KDoctor)[https://github.com/Kotlin/kdoctor] 16 | - install JDK 8 on your machine 17 | - add `local.properties` file to the project root and set a path to Android SDK there 18 | - run `./gradlew podInstall` in the project root 19 | 20 | ### Android 21 | To run the application on android device/emulator: 22 | - open project in Android Studio and run imported android run configuration 23 | 24 | To build the application bundle: 25 | - run `./gradlew :composeApp:assembleDebug` 26 | - find `.apk` file in `composeApp/build/outputs/apk/debug/composeApp-debug.apk` 27 | 28 | ### Desktop 29 | Run the desktop application: `./gradlew :composeApp:run` 30 | 31 | ### iOS 32 | To run the application on iPhone device/simulator: 33 | - Open `iosApp/iosApp.xcworkspace` in Xcode and run standard configuration 34 | - Or use (Kotlin Multiplatform Mobile plugin)[https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile] for Android Studio 35 | 36 | ### Browser 37 | Run the browser application: `./gradlew :composeApp:jsBrowserDevelopmentRun` 38 | 39 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.multiplatform).apply(false) 3 | alias(libs.plugins.compose).apply(false) 4 | alias(libs.plugins.cocoapods).apply(false) 5 | alias(libs.plugins.android.application).apply(false) 6 | alias(libs.plugins.libres).apply(false) 7 | alias(libs.plugins.serialization).apply(false) 8 | } 9 | -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.compose) 6 | alias(libs.plugins.cocoapods) 7 | alias(libs.plugins.android.application) 8 | alias(libs.plugins.libres) 9 | alias(libs.plugins.serialization) 10 | } 11 | 12 | kotlin { 13 | android { 14 | compilations.all { 15 | kotlinOptions { 16 | jvmTarget = "1.8" 17 | } 18 | } 19 | } 20 | 21 | jvm("desktop") 22 | 23 | js { 24 | browser() 25 | binaries.executable() 26 | } 27 | 28 | iosX64() 29 | iosArm64() 30 | iosSimulatorArm64() 31 | 32 | cocoapods { 33 | version = "1.0.0" 34 | summary = "Compose application framework" 35 | homepage = "empty" 36 | ios.deploymentTarget = "11.0" 37 | podfile = project.file("../iosApp/Podfile") 38 | framework { 39 | baseName = "ComposeApp" 40 | isStatic = true 41 | } 42 | } 43 | 44 | sourceSets { 45 | val commonMain by getting { 46 | dependencies { 47 | implementation(compose.runtime) 48 | implementation(compose.foundation) 49 | implementation(compose.material) 50 | implementation(libs.libres) 51 | implementation(libs.kotlinx.coroutines.core) 52 | implementation(libs.kotlinx.serialization.json) 53 | } 54 | } 55 | 56 | val commonTest by getting { 57 | dependencies { 58 | implementation(kotlin("test")) 59 | } 60 | } 61 | 62 | val androidMain by getting { 63 | dependencies { 64 | implementation(libs.androidx.appcompat) 65 | implementation(libs.androidx.activityCompose) 66 | implementation(libs.compose.uitooling) 67 | implementation(libs.kotlinx.coroutines.android) 68 | } 69 | } 70 | 71 | val desktopMain by getting { 72 | dependencies { 73 | implementation(compose.desktop.common) 74 | implementation(compose.desktop.currentOs) 75 | } 76 | } 77 | 78 | val jsMain by getting { 79 | dependencies { 80 | implementation(compose.web.core) 81 | } 82 | } 83 | 84 | val iosX64Main by getting 85 | val iosArm64Main by getting 86 | val iosSimulatorArm64Main by getting 87 | val iosMain by creating { 88 | dependsOn(commonMain) 89 | iosX64Main.dependsOn(this) 90 | iosArm64Main.dependsOn(this) 91 | iosSimulatorArm64Main.dependsOn(this) 92 | dependencies { 93 | } 94 | } 95 | 96 | val iosX64Test by getting 97 | val iosArm64Test by getting 98 | val iosSimulatorArm64Test by getting 99 | val iosTest by creating { 100 | dependsOn(commonTest) 101 | iosX64Test.dependsOn(this) 102 | iosArm64Test.dependsOn(this) 103 | iosSimulatorArm64Test.dependsOn(this) 104 | } 105 | } 106 | } 107 | 108 | android { 109 | namespace = "com.nezihyilmaz.multiplatform.particles" 110 | compileSdk = 33 111 | 112 | defaultConfig { 113 | minSdk = 21 114 | targetSdk = 33 115 | 116 | applicationId = "com.nezihyilmaz.multiplatform.particles.androidApp" 117 | versionCode = 1 118 | versionName = "1.0.0" 119 | } 120 | sourceSets["main"].apply { 121 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 122 | res.srcDirs("src/androidMain/resources") 123 | } 124 | compileOptions { 125 | sourceCompatibility = JavaVersion.VERSION_1_8 126 | targetCompatibility = JavaVersion.VERSION_1_8 127 | } 128 | packagingOptions { 129 | resources.excludes.add("META-INF/**") 130 | } 131 | buildTypes { 132 | getByName("release") { 133 | signingConfig = signingConfigs.getByName("debug") 134 | } 135 | } 136 | } 137 | 138 | compose.desktop { 139 | application { 140 | mainClass = "MainKt" 141 | 142 | nativeDistributions { 143 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 144 | packageName = "com.nezihyilmaz.multiplatform.particles.desktopApp" 145 | packageVersion = "1.0.0" 146 | 147 | } 148 | } 149 | kotlin { 150 | this.jvmToolchain(8) 151 | } 152 | } 153 | 154 | compose.experimental { 155 | web.application {} 156 | } 157 | 158 | libres { 159 | generatedClassName = "MainRes" 160 | generateNamedArguments = true 161 | camelCaseNamesForAppleFramework = false 162 | } 163 | 164 | tasks.getByPath("desktopProcessResources").dependsOn("libresGenerateResources") 165 | tasks.getByPath("desktopSourcesJar").dependsOn("libresGenerateResources") 166 | tasks.getByPath("jsProcessResources").dependsOn("libresGenerateResources") 167 | -------------------------------------------------------------------------------- /composeApp/composeApp.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'composeApp' 3 | spec.version = '1.0.0' 4 | spec.homepage = 'empty' 5 | spec.source = { :http=> ''} 6 | spec.authors = '' 7 | spec.license = '' 8 | spec.summary = 'Compose application framework' 9 | spec.vendored_frameworks = 'build/cocoapods/framework/ComposeApp.framework' 10 | spec.libraries = 'c++' 11 | spec.ios.deployment_target = '11.0' 12 | 13 | 14 | spec.pod_target_xcconfig = { 15 | 'KOTLIN_PROJECT_PATH' => ':composeApp', 16 | 'PRODUCT_MODULE_NAME' => 'ComposeApp', 17 | } 18 | 19 | spec.script_phases = [ 20 | { 21 | :name => 'Build composeApp', 22 | :execution_position => :before_compile, 23 | :shell_path => '/bin/sh', 24 | :script => <<-SCRIPT 25 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then 26 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" 27 | exit 0 28 | fi 29 | set -ev 30 | REPO_ROOT="$PODS_TARGET_SRCROOT" 31 | "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ 32 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ 33 | -Pkotlin.native.cocoapods.archs="$ARCHS" \ 34 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" 35 | SCRIPT 36 | } 37 | ] 38 | spec.resource_bundles = { 39 | 'LibresComposeApp' => ['build/generated/libres/apple/resources/images/LibresComposeApp.xcassets'] 40 | } 41 | end -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/nezihyilmaz/multiplatform/particles/App.android.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles 2 | 3 | import android.app.Application 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import com.nezihyilmaz.multiplatform.particles.ui.composable.App 9 | 10 | class AndroidApp : Application() { 11 | companion object { 12 | lateinit var INSTANCE: AndroidApp 13 | } 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | INSTANCE = this 18 | } 19 | } 20 | 21 | class AppActivity : ComponentActivity() { 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | window.navigationBarColor = Color.BLACK 24 | super.onCreate(savedInstanceState) 25 | setContent { App() } 26 | } 27 | } 28 | 29 | actual val DEFAULT_RESOURCE = FormattedSVGResource.ANDROID_ICON_AND_TEXT -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.nezihyilmaz.multiplatform.particles.physics.ParticleProperties 5 | import kotlinx.serialization.decodeFromString 6 | import kotlinx.serialization.json.Json 7 | 8 | data class FormattedSVGResource( 9 | val viewportWidth: Float, 10 | val viewportHeight: Float, 11 | val data: String, 12 | val additionalOffsetX: Float = 0f, 13 | val additionalOffsetY: Float = 0f, 14 | val particleProperties: ParticleProperties? = null 15 | ) { 16 | companion object { 17 | val ANDROID_ICON_AND_TEXT = FormattedSVGResource( 18 | 60f, 19 | 60f, 20 | SVG_COORD_ANDROID_LOGO_AND_TEXT, 21 | particleProperties = ParticleProperties(color = Color(28,121,33), radius = 6f) 22 | ) 23 | val IOS_ICON_AND_TEXT = FormattedSVGResource( 24 | 60f, 25 | 60f, 26 | SVG_COORDS_IOS_LOGO_AND_TEXT, 27 | particleProperties = ParticleProperties(color = Color.Black, radius = 6f) 28 | ) 29 | 30 | val TEXT_PLAY = FormattedSVGResource( 31 | 60f, 32 | 60f, 33 | SVG_COORDS_TEXT_PLAY, 34 | particleProperties = ParticleProperties(color = Color.Magenta, radius = 6f) 35 | ) 36 | } 37 | } 38 | 39 | expect val DEFAULT_RESOURCE : FormattedSVGResource 40 | 41 | fun getUIData( 42 | canvasWidth: Float, 43 | canvasHeight: Float, 44 | formattedSVGResource: FormattedSVGResource = DEFAULT_RESOURCE 45 | ): UIData { 46 | val data = Json.decodeFromString>>(formattedSVGResource.data) 47 | val resourceWidth = formattedSVGResource.viewportWidth 48 | val resourceHeight = formattedSVGResource.viewportHeight 49 | val canvasAspectRatio = canvasWidth / canvasHeight 50 | val resourceAspectRatio = resourceWidth / resourceHeight 51 | 52 | val scaleX: Float 53 | val scaleY: Float 54 | 55 | val paddingRatio = 0.1f 56 | 57 | if (resourceAspectRatio > canvasAspectRatio) { 58 | scaleX = (canvasWidth / resourceWidth) * (1 - paddingRatio) 59 | scaleY = scaleX 60 | } else { 61 | scaleY = (canvasHeight / resourceHeight) * (1 - paddingRatio) 62 | scaleX = scaleY 63 | } 64 | 65 | val offsetX = 66 | (canvasWidth - resourceWidth * scaleX) / 2 + formattedSVGResource.additionalOffsetX 67 | val offsetY = 68 | ((canvasHeight - resourceHeight * scaleY) / 2) + formattedSVGResource.additionalOffsetY 69 | val mappedList = data.map { 70 | val x = it[0] 71 | val y = it[1] 72 | val transformedX = x * scaleX + offsetX 73 | val transformedY = y * scaleY + offsetY 74 | Pair(transformedX, transformedY) 75 | } 76 | return UIData(mappedList, formattedSVGResource.particleProperties) 77 | } 78 | 79 | data class UIData( 80 | val coordinates: List>, 81 | val particleProperties: ParticleProperties? 82 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/controller/Engine.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.controller 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.nezihyilmaz.multiplatform.particles.getUIData 5 | import com.nezihyilmaz.multiplatform.particles.physics.ForceField 6 | import com.nezihyilmaz.multiplatform.particles.physics.Particle 7 | import com.nezihyilmaz.multiplatform.particles.physics.ParticleProperties 8 | import com.nezihyilmaz.multiplatform.particles.physics.Vector2 9 | import kotlin.math.PI 10 | import kotlin.math.pow 11 | 12 | class Engine { 13 | 14 | val pointerForceRadius = 300f 15 | val pointerForceFieldBaseStrength = 30f 16 | val pointerForceFieldStrengthCurveSwapDistanceFractionThreshold = 0.8f 17 | val pointerForceFieldStrengthWaveScaling = 4f 18 | 19 | val instinctForceRadius = 10000f 20 | val instinctForceBaseStrength = 1.5f 21 | 22 | val maxLimit : Float? = 12f 23 | 24 | var particles: MutableList = mutableListOf() 25 | var pointerForceField: ForceField? = null 26 | 27 | fun create(width: Float, height: Float) { 28 | 29 | val uiData = getUIData(width, height) 30 | val particleProperties = uiData.particleProperties ?: ParticleProperties(Color.Red, 6f) 31 | val particleList = uiData.coordinates.map { 32 | val position = Vector2(it.first, it.second) 33 | Particle(position = position, 34 | instinctForce = ForceField(center = position.copy(), 35 | radius = instinctForceRadius, 36 | forceGenerator = { directionUnitVector, distanceFraction, distance -> 37 | val gravity = directionUnitVector.rotate(PI.toFloat()) * (distanceFraction) * instinctForceBaseStrength 38 | gravity 39 | }), particleProperties = particleProperties) 40 | } 41 | particles = particleList.toMutableList() 42 | } 43 | 44 | fun update(time: Float) { 45 | 46 | fun getIdleLimit(distance: Float) : Float? { 47 | if (maxLimit == null) return null 48 | val limit = (distance / pointerForceRadius) * maxLimit 49 | return if (distance > pointerForceRadius) maxLimit else limit 50 | } 51 | 52 | particles.forEach { 53 | val instinctInteraction = it.instinctForce.interact(it) 54 | val fingerInteraction = pointerForceField?.interact(it) 55 | val distanceToCenter = it.position.distanceTo(it.instinctForce.center) 56 | var limit : Float? = if (fingerInteraction?.valid == true) maxLimit else getIdleLimit(distanceToCenter) 57 | it.update(limit) 58 | } 59 | 60 | 61 | } 62 | 63 | fun createForceField(x: Float, y: Float) { 64 | pointerForceField = ForceField(Vector2(x, y), 65 | radius = pointerForceRadius, 66 | forceGenerator = { directionUnitVector, distanceFraction, distance -> 67 | val strength = pointerForceFieldBaseStrength 68 | val swapThreshold = pointerForceFieldStrengthCurveSwapDistanceFractionThreshold 69 | val unitVector = directionUnitVector.copy() 70 | val multiplier = if (distanceFraction <= swapThreshold) { 71 | (pointerForceFieldStrengthWaveScaling * distanceFraction).pow(strength) / pointerForceFieldStrengthWaveScaling 72 | } else { 73 | ((pointerForceFieldStrengthWaveScaling - pointerForceFieldStrengthWaveScaling * distanceFraction).pow(strength)) / pointerForceFieldStrengthWaveScaling + (1 - swapThreshold) 74 | } 75 | unitVector * (multiplier) * strength 76 | }) 77 | } 78 | 79 | fun moveForceFieldBy(x: Float, y: Float) { 80 | if (pointerForceField == null) return 81 | pointerForceField!!.center += Vector2(x, y) 82 | } 83 | 84 | fun removeForceField() { 85 | pointerForceField = null 86 | } 87 | 88 | 89 | 90 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/physics/ForceField.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.physics 2 | 3 | /** 4 | * A [ForceField] is a region of space that exerts a force on any particle that is inside it. 5 | */ 6 | class ForceField( 7 | val center: Vector2, 8 | val radius: Float, 9 | val forceGenerator: (directionUnitVector: Vector2, distanceFraction: Float, distance: Float) -> Vector2, 10 | ) { 11 | 12 | fun interact(particle: Particle) : InteractionResult{ 13 | //get the distance vector between the particle and the center of the force field 14 | val distanceVector = particle.position.distanceVector(center) 15 | val distance = distanceVector.magnitude() 16 | if (distance > radius) return InteractionResult.invalidInteraction 17 | //get the fraction of the distance between the particle and the center of the force field relative to the radius 18 | val distanceFraction = 1 - (distance / radius) 19 | //generate the force vector and apply it to the particle 20 | val force = forceGenerator(distanceVector.normalized(), distanceFraction, distance) 21 | particle.applyForce(force) 22 | return InteractionResult(true, force) 23 | } 24 | 25 | } 26 | 27 | data class InteractionResult(val valid: Boolean, val force: Vector2?){ 28 | companion object{ 29 | val invalidInteraction = InteractionResult(false, null) 30 | } 31 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/physics/Particle.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.physics 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | data class Particle( 6 | val position: Vector2, 7 | val velocity: Vector2 = Vector2(0f, 0f), 8 | val acceleration: Vector2 = Vector2(0f, 0f), 9 | val instinctForce: ForceField, 10 | val particleProperties: ParticleProperties 11 | ) { 12 | 13 | fun applyForce(force: Vector2) { 14 | acceleration += force 15 | } 16 | 17 | fun update(limit: Float? = null) { 18 | velocity += acceleration 19 | limit?.let { velocity.limit(it) } 20 | position += velocity 21 | acceleration *= 0f 22 | } 23 | } 24 | 25 | data class ParticleProperties( 26 | val color: Color, 27 | val radius: Float 28 | ) 29 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/physics/Vector2.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.physics 2 | 3 | import kotlin.math.cos 4 | import kotlin.math.sin 5 | import kotlin.math.sqrt 6 | import kotlin.random.Random 7 | 8 | /** 9 | * A class representing a 2D vector. 10 | * @property x The x component of the vector. 11 | * @property y The y component of the vector. 12 | */ 13 | data class Vector2(var x: Float, var y: Float) { 14 | 15 | /** 16 | * Adds the given [vector] to this vector. 17 | */ 18 | operator fun plus(vector: Vector2): Vector2 { 19 | x += vector.x 20 | y += vector.y 21 | return this 22 | } 23 | 24 | operator fun plusAssign(vector: Vector2) { 25 | this + vector 26 | } 27 | 28 | /** 29 | * Subtracts the given [vector] from this vector. 30 | */ 31 | operator fun minus(vector: Vector2): Vector2{ 32 | x -= vector.x 33 | y -= vector.y 34 | return this 35 | } 36 | 37 | operator fun minusAssign(vector: Vector2) { 38 | this - vector 39 | } 40 | 41 | /** 42 | * Multiplies this vector by the given [scalar]. 43 | */ 44 | operator fun times(scalar: Float): Vector2{ 45 | x *= scalar 46 | y *= scalar 47 | return this 48 | } 49 | 50 | operator fun timesAssign(scalar: Float) { 51 | this * scalar 52 | } 53 | 54 | /** 55 | * Divides this vector by the given [scalar]. 56 | */ 57 | operator fun div(scalar: Float): Vector2{ 58 | x /= scalar 59 | y /= scalar 60 | return this 61 | } 62 | 63 | operator fun divAssign(scalar: Float) { 64 | this / scalar 65 | } 66 | 67 | /** 68 | * Returns the magnitude of this vector. 69 | */ 70 | fun magnitude(): Float = sqrt(magnitudeSquared()) 71 | 72 | /** 73 | * Returns the squared magnitude of this vector. 74 | */ 75 | fun magnitudeSquared(): Float = x * x + y * y 76 | 77 | 78 | fun rotate(angle: Float): Vector2 { 79 | val cosAngle = cos(angle) 80 | val sinAngle = sin(angle) 81 | val newX = x * cosAngle - y * sinAngle 82 | val newY = x * sinAngle + y * cosAngle 83 | x = newX 84 | y = newY 85 | return this 86 | } 87 | 88 | 89 | /** 90 | * Returns a normalized copy of this vector. 91 | */ 92 | fun normalized(): Vector2 { 93 | if (isZero()) return this.copy() 94 | val mag = magnitude() 95 | return this.copy() / mag 96 | } 97 | 98 | /** 99 | * Normalizes this vector in place. 100 | */ 101 | fun normalize() : Vector2{ 102 | if (isZero()) return this 103 | val mag = magnitude() 104 | this /= mag 105 | return this 106 | } 107 | 108 | /** 109 | * Limits the magnitude of this vector to the given value. 110 | */ 111 | fun limit(max: Float) { 112 | if (magnitudeSquared() > max * max) { 113 | this *= (max / magnitude()) 114 | } 115 | } 116 | 117 | /** 118 | * Returns the dot product of this vector and the given vector. 119 | * @param vector The vector to dot product with. 120 | */ 121 | fun dot(vector: Vector2): Float = x * vector.x + y * vector.y 122 | 123 | /** 124 | * Returns the distance between this vector and the given vector. 125 | * @param vector The vector to get the distance to. 126 | */ 127 | fun distanceTo(vector: Vector2): Float { 128 | val dx = x - vector.x 129 | val dy = y - vector.y 130 | return sqrt((dx * dx + dy * dy)) 131 | } 132 | 133 | /** 134 | * Returns the distance vector between this vector and the given vector. 135 | */ 136 | fun distanceVector(vector: Vector2): Vector2 { 137 | return this.copy() - vector 138 | } 139 | 140 | fun isZero(): Boolean { 141 | return x == 0f && y == 0f 142 | } 143 | 144 | companion object{ 145 | /** 146 | * Returns a random vector within the given [width] and [height]. 147 | */ 148 | fun random(fromWidth: Float = 0f, width: Float, fromHeight: Float = 0f, height: Float): Vector2 { 149 | return Vector2( 150 | Random.nextDouble(fromWidth.toDouble(), width.toDouble()).toFloat(), 151 | Random.nextDouble(fromHeight.toDouble(), height.toDouble()).toFloat() 152 | ) 153 | } 154 | 155 | fun fromAngle(angle: Float, magnitude: Float): Vector2 { 156 | return Vector2( 157 | magnitude * cos(angle), 158 | magnitude * sin(angle) 159 | ) 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/Color.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | //generated by https://m3.material.io/theme-builder#/custom 6 | //Color palette was taken here: https://colorhunt.co/palettes/popular 7 | 8 | internal val md_theme_light_primary = Color(0xFF00687A) 9 | internal val md_theme_light_onPrimary = Color(0xFFFFFFFF) 10 | internal val md_theme_light_primaryContainer = Color(0xFFABEDFF) 11 | internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26) 12 | internal val md_theme_light_secondary = Color(0xFF00696E) 13 | internal val md_theme_light_onSecondary = Color(0xFFFFFFFF) 14 | internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE) 15 | internal val md_theme_light_onSecondaryContainer = Color(0xFF002022) 16 | internal val md_theme_light_tertiary = Color(0xFF904D00) 17 | internal val md_theme_light_onTertiary = Color(0xFFFFFFFF) 18 | internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2) 19 | internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500) 20 | internal val md_theme_light_error = Color(0xFFBA1A1A) 21 | internal val md_theme_light_errorContainer = Color(0xFFFFDAD6) 22 | internal val md_theme_light_onError = Color(0xFFFFFFFF) 23 | internal val md_theme_light_onErrorContainer = Color(0xFF410002) 24 | internal val md_theme_light_background = Color(0xFFFFFBFF) 25 | internal val md_theme_light_onBackground = Color(0xFF221B00) 26 | internal val md_theme_light_surface = Color(0xFFFFFBFF) 27 | internal val md_theme_light_onSurface = Color(0xFF221B00) 28 | internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7) 29 | internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B) 30 | internal val md_theme_light_outline = Color(0xFF70797B) 31 | internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0) 32 | internal val md_theme_light_inverseSurface = Color(0xFF3A3000) 33 | internal val md_theme_light_inversePrimary = Color(0xFF55D6F4) 34 | internal val md_theme_light_shadow = Color(0xFF000000) 35 | internal val md_theme_light_surfaceTint = Color(0xFF00687A) 36 | internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB) 37 | internal val md_theme_light_scrim = Color(0xFF000000) 38 | 39 | internal val md_theme_dark_primary = Color(0xFF55D6F4) 40 | internal val md_theme_dark_onPrimary = Color(0xFF003640) 41 | internal val md_theme_dark_primaryContainer = Color(0xFF004E5C) 42 | internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF) 43 | internal val md_theme_dark_secondary = Color(0xFF4CD9E2) 44 | internal val md_theme_dark_onSecondary = Color(0xFF00373A) 45 | internal val md_theme_dark_secondaryContainer = Color(0xFF004F53) 46 | internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE) 47 | internal val md_theme_dark_tertiary = Color(0xFFFFB77C) 48 | internal val md_theme_dark_onTertiary = Color(0xFF4D2700) 49 | internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900) 50 | internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2) 51 | internal val md_theme_dark_error = Color(0xFFFFB4AB) 52 | internal val md_theme_dark_errorContainer = Color(0xFF93000A) 53 | internal val md_theme_dark_onError = Color(0xFF690005) 54 | internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 55 | internal val md_theme_dark_background = Color(0xFF221B00) 56 | internal val md_theme_dark_onBackground = Color(0xFFFFE264) 57 | internal val md_theme_dark_surface = Color(0xFF221B00) 58 | internal val md_theme_dark_onSurface = Color(0xFFFFE264) 59 | internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B) 60 | internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB) 61 | internal val md_theme_dark_outline = Color(0xFF899295) 62 | internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00) 63 | internal val md_theme_dark_inverseSurface = Color(0xFFFFE264) 64 | internal val md_theme_dark_inversePrimary = Color(0xFF00687A) 65 | internal val md_theme_dark_shadow = Color(0xFF000000) 66 | internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4) 67 | internal val md_theme_dark_outlineVariant = Color(0xFF3F484B) 68 | internal val md_theme_dark_scrim = Color(0xFF000000) 69 | 70 | 71 | internal val seed = Color(0xFF2C3639) 72 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Surface 6 | import androidx.compose.material.darkColors 7 | import androidx.compose.material.lightColors 8 | import androidx.compose.runtime.Composable 9 | 10 | private val LightColors = lightColors( 11 | primary = md_theme_light_primary, 12 | onPrimary = md_theme_light_onPrimary, 13 | secondary = md_theme_light_secondary, 14 | onSecondary = md_theme_light_onSecondary, 15 | error = md_theme_light_error, 16 | onError = md_theme_light_onError, 17 | background = md_theme_light_background, 18 | onBackground = md_theme_light_onBackground, 19 | surface = md_theme_light_surface, 20 | onSurface = md_theme_light_onSurface, 21 | ) 22 | 23 | private val DarkColors = darkColors( 24 | primary = md_theme_dark_primary, 25 | onPrimary = md_theme_dark_onPrimary, 26 | secondary = md_theme_dark_secondary, 27 | onSecondary = md_theme_dark_onSecondary, 28 | error = md_theme_dark_error, 29 | onError = md_theme_dark_onError, 30 | background = md_theme_dark_background, 31 | onBackground = md_theme_dark_onBackground, 32 | surface = md_theme_dark_surface, 33 | onSurface = md_theme_dark_onSurface, 34 | ) 35 | 36 | @Composable 37 | internal fun AppTheme( 38 | useDarkTheme: Boolean = isSystemInDarkTheme(), 39 | content: @Composable() () -> Unit 40 | ) { 41 | val colors = if (!useDarkTheme) { 42 | LightColors 43 | } else { 44 | DarkColors 45 | } 46 | 47 | MaterialTheme( 48 | colors = colors, 49 | content = { 50 | Surface(content = content) 51 | } 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/composable/App.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui.composable 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.nezihyilmaz.multiplatform.particles.controller.Engine 5 | import com.nezihyilmaz.multiplatform.particles.ui.AppTheme 6 | 7 | @Composable 8 | internal fun App() = AppTheme { 9 | Space(Engine()) 10 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/composable/Fabric.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui.composable 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.geometry.Offset 7 | import androidx.compose.ui.graphics.Brush 8 | import androidx.compose.ui.graphics.Color 9 | 10 | 11 | /** 12 | * A [Fabric] is a grid of lines that is used to visualize the space. 13 | */ 14 | @Composable 15 | fun Fabric(modifier: Modifier) { 16 | 17 | Canvas(modifier = modifier) { 18 | drawRect(color = Color.White) 19 | val canvasWidth = size.width 20 | val canvasHeight = size.height 21 | val lineGap = 150f 22 | val strokeWidth = 4f 23 | 24 | val verticalGradient = Brush.verticalGradient( 25 | colors = listOf(Color.Transparent, Color.LightGray, Color.Transparent) 26 | ) 27 | 28 | val horizontalGradient = Brush.horizontalGradient( 29 | colors = listOf(Color.Transparent, Color.LightGray, Color.Transparent) 30 | ) 31 | 32 | // draw vertical lines 33 | var x = 0f 34 | while (x < canvasWidth) { 35 | drawLine( 36 | brush = verticalGradient, 37 | start = Offset(x, 0f), 38 | end = Offset(x, canvasHeight), 39 | strokeWidth = strokeWidth 40 | ) 41 | x += lineGap 42 | } 43 | 44 | // draw horizontal lines 45 | var y = 0f 46 | while (y < canvasHeight) { 47 | drawLine( 48 | brush = horizontalGradient, 49 | start = Offset(0f, y), 50 | end = Offset(canvasWidth, y), 51 | strokeWidth = strokeWidth 52 | ) 53 | y += lineGap 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/composable/Renderer.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui.composable 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.drawscope.DrawScope 6 | import com.nezihyilmaz.multiplatform.particles.physics.Particle 7 | 8 | fun DrawScope.renderParticles(particles: List) { 9 | particles.forEach { 10 | drawCircle( 11 | color = it.particleProperties.color, 12 | radius = it.particleProperties.radius, 13 | center = Offset(it.position.x, it.position.y) 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/composable/Space.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui.composable 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.layout.onSizeChanged 10 | import com.nezihyilmaz.multiplatform.particles.controller.Engine 11 | import com.nezihyilmaz.multiplatform.particles.ui.modifier.handleGestures 12 | 13 | @Composable 14 | fun Space(engine: Engine) { 15 | val time by Time() 16 | Box( 17 | modifier = Modifier.fillMaxSize().onSizeChanged { size -> 18 | engine.create( 19 | size.width.toFloat(), 20 | size.height.toFloat() 21 | ) 22 | }.handleGestures(engine) 23 | ) 24 | { 25 | Fabric(Modifier.fillMaxSize()) 26 | Canvas(modifier = Modifier.fillMaxSize()) { 27 | engine.update(time) 28 | renderParticles(engine.particles) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/composable/Time.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui.composable 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.animate 5 | import androidx.compose.animation.core.infiniteRepeatable 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.MutableState 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | 13 | @Composable 14 | fun Time(): MutableState { 15 | val time = remember { mutableStateOf(0f) } 16 | LaunchedEffect(Unit) { 17 | animate( 18 | initialValue = 0f, 19 | targetValue = 1f, 20 | animationSpec = infiniteRepeatable( 21 | tween(durationMillis = 1000, easing = LinearEasing) 22 | ) 23 | ) { value, velocity -> 24 | time.value = value 25 | } 26 | } 27 | return time 28 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/nezihyilmaz/multiplatform/particles/ui/modifier/GestureHandler.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles.ui.modifier 2 | 3 | import androidx.compose.foundation.gestures.detectDragGestures 4 | import androidx.compose.foundation.gestures.detectTapGestures 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.input.pointer.pointerInput 7 | import com.nezihyilmaz.multiplatform.particles.controller.Engine 8 | 9 | fun Modifier.handleGestures(engine: Engine): Modifier { 10 | val modifier = pointerInput(Unit) { 11 | detectTapGestures(onPress = { offset -> 12 | engine.createForceField(offset.x, offset.y) 13 | }) { 14 | engine.removeForceField() 15 | } 16 | 17 | }.pointerInput(Unit) { 18 | detectDragGestures(onDragStart = { 19 | 20 | }, onDragEnd = { 21 | engine.removeForceField() 22 | }, onDragCancel = {}) { change, dragAmount -> 23 | change.consume() 24 | dragAmount.x 25 | dragAmount.y 26 | engine.moveForceFieldBy( 27 | dragAmount.x, dragAmount.y 28 | ) 29 | } 30 | } 31 | return modifier 32 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/nezihyilmaz/multiplatform/particles/App.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles 2 | 3 | actual val DEFAULT_RESOURCE = FormattedSVGResource.TEXT_PLAY -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.unit.dp 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import androidx.compose.ui.window.rememberWindowState 5 | import com.nezihyilmaz.multiplatform.particles.ui.composable.App 6 | 7 | fun main() = application { 8 | Window( 9 | title = "Multiplatform-Particles", 10 | state = rememberWindowState(width = 800.dp, height = 600.dp), 11 | onCloseRequest = ::exitApplication, 12 | ) { App() } 13 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/com/nezihyilmaz/multiplatform/particles/App.ios.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles 2 | 3 | actual val DEFAULT_RESOURCE = FormattedSVGResource.IOS_ICON_AND_TEXT -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import com.nezihyilmaz.multiplatform.particles.ui.composable.App 3 | import platform.UIKit.UIViewController 4 | 5 | fun MainViewController(): UIViewController { 6 | return ComposeUIViewController { App() } 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/BrowserViewportWindow.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INVISIBLE_MEMBER", 3 | "INVISIBLE_REFERENCE", 4 | "EXPOSED_PARAMETER_TYPE" 5 | ) // WORKAROUND: ComposeWindow and ComposeLayer are internal 6 | 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.window.ComposeWindow 9 | import kotlinx.browser.document 10 | import kotlinx.browser.window 11 | import org.w3c.dom.HTMLCanvasElement 12 | import org.w3c.dom.HTMLStyleElement 13 | import org.w3c.dom.HTMLTitleElement 14 | 15 | private const val CANVAS_ELEMENT_ID = "ComposeTarget" // Hardwired into ComposeWindow 16 | 17 | /** 18 | * A Skiko/Canvas-based top-level window using the browser's entire viewport. Supports resizing. 19 | * Author: https://github.com/OliverO2 20 | * Source: https://github.com/OliverO2/compose-counting-grid/blob/eb79f7c8be7804d4323114be30ce498cfac6d2b0/src/frontendWebMain/kotlin/BrowserViewportWindow.kt 21 | */ 22 | @Suppress("FunctionName") 23 | fun BrowserViewportWindow( 24 | title: String = "Untitled", 25 | content: @Composable ComposeWindow.() -> Unit 26 | ) { 27 | val htmlHeadElement = document.head!! 28 | htmlHeadElement.appendChild( 29 | (document.createElement("style") as HTMLStyleElement).apply { 30 | type = "text/css" 31 | appendChild( 32 | document.createTextNode( 33 | """ 34 | html, body { 35 | overflow: hidden; 36 | margin: 0 !important; 37 | padding: 0 !important; 38 | } 39 | 40 | #$CANVAS_ELEMENT_ID { 41 | outline: none; 42 | } 43 | """.trimIndent() 44 | ) 45 | ) 46 | } 47 | ) 48 | 49 | fun HTMLCanvasElement.fillViewportSize() { 50 | setAttribute("width", "${window.innerWidth}") 51 | setAttribute("height", "${window.innerHeight}") 52 | } 53 | 54 | val canvas = (document.getElementById(CANVAS_ELEMENT_ID) as HTMLCanvasElement).apply { 55 | fillViewportSize() 56 | } 57 | 58 | ComposeWindow().apply { 59 | window.addEventListener("resize", { 60 | val scale = layer.layer.contentScale 61 | val density = window.devicePixelRatio.toFloat() 62 | canvas.fillViewportSize() 63 | layer.layer.attachTo(canvas) 64 | layer.layer.needRedraw() 65 | layer.setSize((canvas.width / scale * density).toInt(), (canvas.height / scale * density).toInt()) 66 | }) 67 | 68 | // WORKAROUND: ComposeWindow does not implement `setTitle(title)` 69 | val htmlTitleElement = ( 70 | htmlHeadElement.getElementsByTagName("title").item(0) 71 | ?: document.createElement("title").also { htmlHeadElement.appendChild(it) } 72 | ) as HTMLTitleElement 73 | htmlTitleElement.textContent = title 74 | 75 | setContent { 76 | content(this) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/com/nezihyilmaz/multiplatform/particles/App.js.kt: -------------------------------------------------------------------------------- 1 | package com.nezihyilmaz.multiplatform.particles 2 | 3 | actual val DEFAULT_RESOURCE = FormattedSVGResource.TEXT_PLAY 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import com.nezihyilmaz.multiplatform.particles.ui.composable.App 2 | import org.jetbrains.skiko.wasm.onWasmReady 3 | 4 | fun main() { 5 | onWasmReady { 6 | BrowserViewportWindow("Multiplatform-Particles") { 7 | App() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Multiplatform-Particles 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /gradle.bat: -------------------------------------------------------------------------------- 1 | 2 | @rem 3 | @rem Copyright 2015 the original author or authors. 4 | @rem 5 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 6 | @rem you may not use this file except in compliance with the License. 7 | @rem You may obtain a copy of the License at 8 | @rem 9 | @rem https://www.apache.org/licenses/LICENSE-2.0 10 | @rem 11 | @rem Unless required by applicable law or agreed to in writing, software 12 | @rem distributed under the License is distributed on an "AS IS" BASIS, 13 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | @rem See the License for the specific language governing permissions and 15 | @rem limitations under the License. 16 | @rem 17 | 18 | @if "%DEBUG%" == "" @echo off 19 | @rem ########################################################################## 20 | @rem 21 | @rem Gradle startup script for Windows 22 | @rem 23 | @rem ########################################################################## 24 | 25 | @rem Set local scope for the variables with windows NT shell 26 | if "%OS%"=="Windows_NT" setlocal 27 | 28 | set DIRNAME=%~dp0 29 | if "%DIRNAME%" == "" set DIRNAME=. 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if "%ERRORLEVEL%" == "0" goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | 2 | #Gradle 3 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 4 | 5 | #Kotlin 6 | kotlin.code.style=official 7 | kotlin.js.compiler=ir 8 | 9 | #MPP 10 | kotlin.mpp.enableCInteropCommonization=true 11 | kotlin.mpp.androidSourceSetLayoutVersion=2 12 | 13 | #Compose 14 | org.jetbrains.compose.experimental.uikit.enabled=true 15 | org.jetbrains.compose.experimental.jscanvas.enabled=true 16 | kotlin.native.cacheKind=none 17 | 18 | #Android 19 | android.useAndroidX=true 20 | android.nonTransitiveRClass=true 21 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kotlin = "1.8.20" 4 | agp = "7.4.2" 5 | compose = "1.4.0" 6 | androidx-appcompat = "1.6.1" 7 | androidx-activityCompose = "1.7.0" 8 | compose-uitooling = "1.4.2" 9 | libres = "1.1.8" 10 | kotlinx-coroutines = "1.6.4" 11 | kotlinx-serialization = "1.8.10" 12 | kotlinx-serialization-json = "1.5.0" 13 | 14 | [libraries] 15 | 16 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } 17 | androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 18 | compose-uitooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-uitooling" } 19 | libres = { module = "io.github.skeptick.libres:libres-compose", version.ref = "libres" } 20 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 21 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } 22 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } 23 | 24 | [plugins] 25 | 26 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 27 | cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } 28 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 29 | android-application = { id = "com.android.application", version.ref = "agp" } 30 | libres = { id = "io.github.skeptick.libres", version.ref = "libres" } 31 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinx-serialization" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nezih94/Kotlin-Multiplatform-Particles/853480c087dc7c46e5beb773600e99aa9a5fd664/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env sh 3 | 4 | # 5 | # Copyright 2015 the original author or authors. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | ############################################################################## 21 | ## 22 | ## Gradle start up script for UN*X 23 | ## 24 | ############################################################################## 25 | 26 | # Attempt to set APP_HOME 27 | # Resolve links: ${'$'}0 may be a link 28 | PRG="$0" 29 | # Need this for relative symlinks. 30 | while [ -h "$PRG" ] ; do 31 | ls=`ls -ld "$PRG"` 32 | link=`expr "$ls" : '.*-> \(.*\)${'$'}'` 33 | if expr "$link" : '/.*' > /dev/null; then 34 | PRG="$link" 35 | else 36 | PRG=`dirname "$PRG"`"/$link" 37 | fi 38 | done 39 | SAVED="`pwd`" 40 | cd "`dirname \"$PRG\"`/" >/dev/null 41 | APP_HOME="`pwd -P`" 42 | cd "$SAVED" >/dev/null 43 | 44 | APP_NAME="Gradle" 45 | APP_BASE_NAME=`basename "$0"` 46 | 47 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 48 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 49 | 50 | # Use the maximum available, or set MAX_FD != -1 to use that value. 51 | MAX_FD="maximum" 52 | 53 | warn () { 54 | echo "${'$'}*" 55 | } 56 | 57 | die () { 58 | echo 59 | echo "${'$'}*" 60 | echo 61 | exit 1 62 | } 63 | 64 | # OS specific support (must be 'true' or 'false'). 65 | cygwin=false 66 | msys=false 67 | darwin=false 68 | nonstop=false 69 | case "`uname`" in 70 | CYGWIN* ) 71 | cygwin=true 72 | ;; 73 | Darwin* ) 74 | darwin=true 75 | ;; 76 | MSYS* | MINGW* ) 77 | msys=true 78 | ;; 79 | NONSTOP* ) 80 | nonstop=true 81 | ;; 82 | esac 83 | 84 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 85 | 86 | 87 | # Determine the Java command to use to start the JVM. 88 | if [ -n "$JAVA_HOME" ] ; then 89 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 90 | # IBM's JDK on AIX uses strange locations for the executables 91 | JAVACMD="$JAVA_HOME/jre/sh/java" 92 | else 93 | JAVACMD="$JAVA_HOME/bin/java" 94 | fi 95 | if [ ! -x "$JAVACMD" ] ; then 96 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 97 | 98 | Please set the JAVA_HOME variable in your environment to match the 99 | location of your Java installation." 100 | fi 101 | else 102 | JAVACMD="java" 103 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 104 | 105 | Please set the JAVA_HOME variable in your environment to match the 106 | location of your Java installation." 107 | fi 108 | 109 | # Increase the maximum file descriptors if we can. 110 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 111 | MAX_FD_LIMIT=`ulimit -H -n` 112 | if [ $? -eq 0 ] ; then 113 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 114 | MAX_FD="$MAX_FD_LIMIT" 115 | fi 116 | ulimit -n $MAX_FD 117 | if [ $? -ne 0 ] ; then 118 | warn "Could not set maximum file descriptor limit: $MAX_FD" 119 | fi 120 | else 121 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 122 | fi 123 | fi 124 | 125 | # For Darwin, add options to specify how the application appears in the dock 126 | if $darwin; then 127 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 128 | fi 129 | 130 | # For Cygwin or MSYS, switch paths to Windows format before running java 131 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 132 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 133 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 134 | 135 | JAVACMD=`cygpath --unix "$JAVACMD"` 136 | 137 | # We build the pattern for arguments to be converted via cygpath 138 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 139 | SEP="" 140 | for dir in $ROOTDIRSRAW ; do 141 | ROOTDIRS="$ROOTDIRS$SEP$dir" 142 | SEP="|" 143 | done 144 | OURCYGPATTERN="(^($ROOTDIRS))" 145 | # Add a user-defined pattern to the cygpath arguments 146 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 147 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 148 | fi 149 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 150 | i=0 151 | for arg in "$@" ; do 152 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 153 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 154 | 155 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 156 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 157 | else 158 | eval `echo args$i`="\"$arg\"" 159 | fi 160 | i=`expr $i + 1` 161 | done 162 | case $i in 163 | 0) set -- ;; 164 | 1) set -- "$args0" ;; 165 | 2) set -- "$args0" "$args1" ;; 166 | 3) set -- "$args0" "$args1" "$args2" ;; 167 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 168 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 169 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 170 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 171 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 172 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 173 | esac 174 | fi 175 | 176 | # Escape application args 177 | save () { 178 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 179 | echo " " 180 | } 181 | APP_ARGS=`save "$@"` 182 | 183 | # Collect all arguments for the java command, following the shell quoting and substitution rules 184 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 185 | 186 | exec "$JAVACMD" "$@" 187 | -------------------------------------------------------------------------------- /iosApp/Podfile: -------------------------------------------------------------------------------- 1 | target 'iosApp' do 2 | use_frameworks! 3 | platform :ios, '16.2' 4 | pod 'composeApp', :path => '../composeApp' 5 | end 6 | -------------------------------------------------------------------------------- /iosApp/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - composeApp (1.0.0) 3 | 4 | DEPENDENCIES: 5 | - composeApp (from `../composeApp`) 6 | 7 | EXTERNAL SOURCES: 8 | composeApp: 9 | :path: "../composeApp" 10 | 11 | SPEC CHECKSUMS: 12 | composeApp: 5ecf7ce392a8963914a02607119428a19ae416d7 13 | 14 | PODFILE CHECKSUM: 3313a530e4ca4361dd2a9b97cf3c536967731c31 15 | 16 | COCOAPODS: 1.12.0 17 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* iosApp.swift */; }; 11 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A953E29CC810D00F8E227 /* Assets.xcassets */; }; 12 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A954129CC810D00F8E227 /* Preview Assets.xcassets */; }; 13 | D51586C6B134BDB93BE1DFFB /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE080622DF072CBDC3D9C873 /* Pods_iosApp.framework */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 05E67C72B2BDBC81379103CB /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; 18 | 73FFDDCA9C728FEE3DFEF2F4 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; 19 | A93A953729CC810C00F8E227 /* Multiplatform-Particles.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Multiplatform-Particles.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | A93A953A29CC810C00F8E227 /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; }; 21 | A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 23 | EE080622DF072CBDC3D9C873 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | A93A953429CC810C00F8E227 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | D51586C6B134BDB93BE1DFFB /* Pods_iosApp.framework in Frameworks */, 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXFrameworksBuildPhase section */ 36 | 37 | /* Begin PBXGroup section */ 38 | 979913F6AE5D271756D4649D /* Pods */ = { 39 | isa = PBXGroup; 40 | children = ( 41 | 73FFDDCA9C728FEE3DFEF2F4 /* Pods-iosApp.debug.xcconfig */, 42 | 05E67C72B2BDBC81379103CB /* Pods-iosApp.release.xcconfig */, 43 | ); 44 | path = Pods; 45 | sourceTree = ""; 46 | }; 47 | A93A952E29CC810C00F8E227 = { 48 | isa = PBXGroup; 49 | children = ( 50 | A93A953929CC810C00F8E227 /* iosApp */, 51 | A93A953829CC810C00F8E227 /* Products */, 52 | 979913F6AE5D271756D4649D /* Pods */, 53 | C4127409AE3703430489E7BC /* Frameworks */, 54 | ); 55 | sourceTree = ""; 56 | }; 57 | A93A953829CC810C00F8E227 /* Products */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | A93A953729CC810C00F8E227 /* Multiplatform-Particles.app */, 61 | ); 62 | name = Products; 63 | sourceTree = ""; 64 | }; 65 | A93A953929CC810C00F8E227 /* iosApp */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | A93A953A29CC810C00F8E227 /* iosApp.swift */, 69 | A93A953E29CC810D00F8E227 /* Assets.xcassets */, 70 | A93A954029CC810D00F8E227 /* Preview Content */, 71 | ); 72 | path = iosApp; 73 | sourceTree = ""; 74 | }; 75 | A93A954029CC810D00F8E227 /* Preview Content */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */, 79 | ); 80 | path = "Preview Content"; 81 | sourceTree = ""; 82 | }; 83 | C4127409AE3703430489E7BC /* Frameworks */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | EE080622DF072CBDC3D9C873 /* Pods_iosApp.framework */, 87 | ); 88 | name = Frameworks; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | A93A953629CC810C00F8E227 /* iosApp */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */; 97 | buildPhases = ( 98 | D3665D361753A60B1A6EEEB7 /* [CP] Check Pods Manifest.lock */, 99 | A93A953329CC810C00F8E227 /* Sources */, 100 | A93A953429CC810C00F8E227 /* Frameworks */, 101 | A93A953529CC810C00F8E227 /* Resources */, 102 | 4D7B9BBE13F2636D045A0A84 /* [CP] Copy Pods Resources */, 103 | ); 104 | buildRules = ( 105 | ); 106 | dependencies = ( 107 | ); 108 | name = iosApp; 109 | productName = iosApp; 110 | productReference = A93A953729CC810C00F8E227 /* Multiplatform-Particles.app */; 111 | productType = "com.apple.product-type.application"; 112 | }; 113 | /* End PBXNativeTarget section */ 114 | 115 | /* Begin PBXProject section */ 116 | A93A952F29CC810C00F8E227 /* Project object */ = { 117 | isa = PBXProject; 118 | attributes = { 119 | BuildIndependentTargetsInParallel = 1; 120 | LastSwiftUpdateCheck = 1420; 121 | LastUpgradeCheck = 1420; 122 | TargetAttributes = { 123 | A93A953629CC810C00F8E227 = { 124 | CreatedOnToolsVersion = 14.2; 125 | }; 126 | }; 127 | }; 128 | buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */; 129 | compatibilityVersion = "Xcode 14.0"; 130 | developmentRegion = en; 131 | hasScannedForEncodings = 0; 132 | knownRegions = ( 133 | en, 134 | Base, 135 | ); 136 | mainGroup = A93A952E29CC810C00F8E227; 137 | productRefGroup = A93A953829CC810C00F8E227 /* Products */; 138 | projectDirPath = ""; 139 | projectRoot = ""; 140 | targets = ( 141 | A93A953629CC810C00F8E227 /* iosApp */, 142 | ); 143 | }; 144 | /* End PBXProject section */ 145 | 146 | /* Begin PBXResourcesBuildPhase section */ 147 | A93A953529CC810C00F8E227 /* Resources */ = { 148 | isa = PBXResourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */, 152 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXShellScriptBuildPhase section */ 159 | 4D7B9BBE13F2636D045A0A84 /* [CP] Copy Pods Resources */ = { 160 | isa = PBXShellScriptBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | ); 164 | inputFileListPaths = ( 165 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", 166 | ); 167 | name = "[CP] Copy Pods Resources"; 168 | outputFileListPaths = ( 169 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | shellPath = /bin/sh; 173 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; 174 | showEnvVarsInLog = 0; 175 | }; 176 | D3665D361753A60B1A6EEEB7 /* [CP] Check Pods Manifest.lock */ = { 177 | isa = PBXShellScriptBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | ); 181 | inputFileListPaths = ( 182 | ); 183 | inputPaths = ( 184 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 185 | "${PODS_ROOT}/Manifest.lock", 186 | ); 187 | name = "[CP] Check Pods Manifest.lock"; 188 | outputFileListPaths = ( 189 | ); 190 | outputPaths = ( 191 | "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | shellPath = /bin/sh; 195 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 196 | showEnvVarsInLog = 0; 197 | }; 198 | /* End PBXShellScriptBuildPhase section */ 199 | 200 | /* Begin PBXSourcesBuildPhase section */ 201 | A93A953329CC810C00F8E227 /* Sources */ = { 202 | isa = PBXSourcesBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXSourcesBuildPhase section */ 210 | 211 | /* Begin XCBuildConfiguration section */ 212 | A93A954329CC810D00F8E227 /* Debug */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | CLANG_ANALYZER_NONNULL = YES; 217 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 218 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_ENABLE_OBJC_WEAK = YES; 222 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 223 | CLANG_WARN_BOOL_CONVERSION = YES; 224 | CLANG_WARN_COMMA = YES; 225 | CLANG_WARN_CONSTANT_CONVERSION = YES; 226 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 227 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 228 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 229 | CLANG_WARN_EMPTY_BODY = YES; 230 | CLANG_WARN_ENUM_CONVERSION = YES; 231 | CLANG_WARN_INFINITE_RECURSION = YES; 232 | CLANG_WARN_INT_CONVERSION = YES; 233 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 234 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 235 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 236 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 237 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 238 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 239 | CLANG_WARN_STRICT_PROTOTYPES = YES; 240 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 241 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | COPY_PHASE_STRIP = NO; 245 | DEBUG_INFORMATION_FORMAT = dwarf; 246 | ENABLE_STRICT_OBJC_MSGSEND = YES; 247 | ENABLE_TESTABILITY = YES; 248 | GCC_C_LANGUAGE_STANDARD = gnu11; 249 | GCC_DYNAMIC_NO_PIC = NO; 250 | GCC_NO_COMMON_BLOCKS = YES; 251 | GCC_OPTIMIZATION_LEVEL = 0; 252 | GCC_PREPROCESSOR_DEFINITIONS = ( 253 | "DEBUG=1", 254 | "$(inherited)", 255 | ); 256 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 257 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 258 | GCC_WARN_UNDECLARED_SELECTOR = YES; 259 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 260 | GCC_WARN_UNUSED_FUNCTION = YES; 261 | GCC_WARN_UNUSED_VARIABLE = YES; 262 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 263 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 264 | MTL_FAST_MATH = YES; 265 | ONLY_ACTIVE_ARCH = YES; 266 | SDKROOT = iphoneos; 267 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 268 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 269 | }; 270 | name = Debug; 271 | }; 272 | A93A954429CC810D00F8E227 /* Release */ = { 273 | isa = XCBuildConfiguration; 274 | buildSettings = { 275 | ALWAYS_SEARCH_USER_PATHS = NO; 276 | CLANG_ANALYZER_NONNULL = YES; 277 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 278 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 279 | CLANG_ENABLE_MODULES = YES; 280 | CLANG_ENABLE_OBJC_ARC = YES; 281 | CLANG_ENABLE_OBJC_WEAK = YES; 282 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 283 | CLANG_WARN_BOOL_CONVERSION = YES; 284 | CLANG_WARN_COMMA = YES; 285 | CLANG_WARN_CONSTANT_CONVERSION = YES; 286 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 287 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 288 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 289 | CLANG_WARN_EMPTY_BODY = YES; 290 | CLANG_WARN_ENUM_CONVERSION = YES; 291 | CLANG_WARN_INFINITE_RECURSION = YES; 292 | CLANG_WARN_INT_CONVERSION = YES; 293 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 295 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 296 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 297 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 298 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 299 | CLANG_WARN_STRICT_PROTOTYPES = YES; 300 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 301 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 302 | CLANG_WARN_UNREACHABLE_CODE = YES; 303 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 304 | COPY_PHASE_STRIP = NO; 305 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 306 | ENABLE_NS_ASSERTIONS = NO; 307 | ENABLE_STRICT_OBJC_MSGSEND = YES; 308 | GCC_C_LANGUAGE_STANDARD = gnu11; 309 | GCC_NO_COMMON_BLOCKS = YES; 310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 312 | GCC_WARN_UNDECLARED_SELECTOR = YES; 313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 314 | GCC_WARN_UNUSED_FUNCTION = YES; 315 | GCC_WARN_UNUSED_VARIABLE = YES; 316 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 317 | MTL_ENABLE_DEBUG_INFO = NO; 318 | MTL_FAST_MATH = YES; 319 | SDKROOT = iphoneos; 320 | SWIFT_COMPILATION_MODE = wholemodule; 321 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 322 | VALIDATE_PRODUCT = YES; 323 | }; 324 | name = Release; 325 | }; 326 | A93A954629CC810D00F8E227 /* Debug */ = { 327 | isa = XCBuildConfiguration; 328 | baseConfigurationReference = 73FFDDCA9C728FEE3DFEF2F4 /* Pods-iosApp.debug.xcconfig */; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 332 | CODE_SIGN_STYLE = Automatic; 333 | CURRENT_PROJECT_VERSION = 1; 334 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 335 | ENABLE_PREVIEWS = YES; 336 | GENERATE_INFOPLIST_FILE = YES; 337 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 338 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 339 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 340 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 341 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 342 | LD_RUNPATH_SEARCH_PATHS = ( 343 | "$(inherited)", 344 | "@executable_path/Frameworks", 345 | ); 346 | MARKETING_VERSION = 1.0; 347 | PRODUCT_BUNDLE_IDENTIFIER = com.nezihyilmaz.multiplatform.particles.iosApp; 348 | PRODUCT_NAME = "Multiplatform-Particles"; 349 | SWIFT_EMIT_LOC_STRINGS = YES; 350 | SWIFT_VERSION = 5.0; 351 | TARGETED_DEVICE_FAMILY = "1,2"; 352 | }; 353 | name = Debug; 354 | }; 355 | A93A954729CC810D00F8E227 /* Release */ = { 356 | isa = XCBuildConfiguration; 357 | baseConfigurationReference = 05E67C72B2BDBC81379103CB /* Pods-iosApp.release.xcconfig */; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 360 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 361 | CODE_SIGN_STYLE = Automatic; 362 | CURRENT_PROJECT_VERSION = 1; 363 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 364 | ENABLE_PREVIEWS = YES; 365 | GENERATE_INFOPLIST_FILE = YES; 366 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 367 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 368 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 369 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 370 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 371 | LD_RUNPATH_SEARCH_PATHS = ( 372 | "$(inherited)", 373 | "@executable_path/Frameworks", 374 | ); 375 | MARKETING_VERSION = 1.0; 376 | PRODUCT_BUNDLE_IDENTIFIER = com.nezihyilmaz.multiplatform.particles.iosApp; 377 | PRODUCT_NAME = "Multiplatform-Particles"; 378 | SWIFT_EMIT_LOC_STRINGS = YES; 379 | SWIFT_VERSION = 5.0; 380 | TARGETED_DEVICE_FAMILY = "1,2"; 381 | }; 382 | name = Release; 383 | }; 384 | /* End XCBuildConfiguration section */ 385 | 386 | /* Begin XCConfigurationList section */ 387 | A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */ = { 388 | isa = XCConfigurationList; 389 | buildConfigurations = ( 390 | A93A954329CC810D00F8E227 /* Debug */, 391 | A93A954429CC810D00F8E227 /* Release */, 392 | ); 393 | defaultConfigurationIsVisible = 0; 394 | defaultConfigurationName = Release; 395 | }; 396 | A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 397 | isa = XCConfigurationList; 398 | buildConfigurations = ( 399 | A93A954629CC810D00F8E227 /* Debug */, 400 | A93A954729CC810D00F8E227 /* Release */, 401 | ); 402 | defaultConfigurationIsVisible = 0; 403 | defaultConfigurationName = Release; 404 | }; 405 | /* End XCConfigurationList section */ 406 | }; 407 | rootObject = A93A952F29CC810C00F8E227 /* Project object */; 408 | } 409 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/iosApp.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | @main 6 | struct iosApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | ContentView() 10 | } 11 | } 12 | } 13 | 14 | struct ContentView: View { 15 | var body: some View { 16 | ComposeView().ignoresSafeArea(.keyboard) 17 | } 18 | } 19 | 20 | struct ComposeView: UIViewControllerRepresentable { 21 | func makeUIViewController(context: Context) -> UIViewController { 22 | MainKt.MainViewController() 23 | } 24 | 25 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 26 | } 27 | -------------------------------------------------------------------------------- /screenshot/ss_desktop_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nezih94/Kotlin-Multiplatform-Particles/853480c087dc7c46e5beb773600e99aa9a5fd664/screenshot/ss_desktop_action.png -------------------------------------------------------------------------------- /screenshot/ss_desktop_idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nezih94/Kotlin-Multiplatform-Particles/853480c087dc7c46e5beb773600e99aa9a5fd664/screenshot/ss_desktop_idle.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Multiplatform-Particles" 2 | include(":composeApp") 3 | 4 | pluginManagement { 5 | repositories { 6 | google() 7 | gradlePluginPortal() 8 | mavenCentral() 9 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 18 | } 19 | } 20 | --------------------------------------------------------------------------------