├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── k5-compose
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── K5.kt
│ ├── math
│ ├── Calculations.kt
│ ├── Constants.kt
│ ├── Noise.kt
│ ├── Random.kt
│ ├── Trigonometry.kt
│ └── Vector2D.kt
│ └── ui
│ └── SketchView.kt
├── settings.gradle.kts
└── src
└── main
└── kotlin
├── Main.kt
└── examples
├── angularmotion
└── Rotation.kt
├── art
├── circle_packing.kt
├── piet_mondrian.kt
└── triangular_mesh.kt
├── elye
├── classicyarnanimated.kt
├── danceYarnAuto.kt
├── danceYarnMouse.kt
├── landscapeInspection.kt
├── mandelbrot.kt
├── perlinnoise1d.kt
├── perlinnoise2d.kt
└── simplegame.kt
├── forces
├── Ball.kt
├── BouncingBall.kt
└── GravitationalAttraction.kt
├── kinematics
└── SimpleMover.kt
├── mathematics
├── circle-loop.kt
├── parametric-equation.kt
├── recursion.kt
└── ribbon.kt
├── noise
├── blackhole.kt
├── classic-yarns.kt
└── noise-walker.kt
├── particles
├── Rain.kt
├── StarField.kt
├── fireworks.kt
└── particles-js-simulation.kt
├── shm
└── simpleHarmonicMotion.kt
├── simulations
├── Chains.kt
├── CircleGrid.kt
├── Phyllotaxis.kt
├── TenPrint.kt
├── gameOfLife.kt
├── green-rain.kt
├── hearts.kt
├── recursive-tree.kt
├── rotatingsquares.kt
└── wavemaker.kt
└── vectors
└── RandomWalkers.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .gradle
3 | /build
4 | local.properties
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # k5-compose
2 |
3 | **k5-compose** is a sketchy port of [P5.js](https://p5js.org/) for Jetpack Compose Desktop.
4 |
5 | [](https://github.com/CuriousNikhil/k5-compose/releases/tag/v1.0.1) [](https://github.com/KotlinBy/awesome-kotlin)
6 |
7 |
8 | This library provides you a playground to play with your sketches so you don't have to worry about maintaining/remembering states and setting up the animations etc.
9 | You can focus on creating awesome sketches, creating generative art. This library also provides you necessary physics and math functions which are ported from p5.js.
10 |
11 | Say for example you can do something like this in just 20 lines of code -
12 |
13 | | Moving Vehicle code | K5 Sketch |
14 | |---|---|
15 | |  |  |
16 |
17 |
18 | ## Few examples...
19 |
20 | | [parametric eq](https://github.com/CuriousNikhil/k5-compose/blob/main/src/main/kotlin/examples/mathematics/parametric-equation.kt) | [particles js](https://github.com/CuriousNikhil/k5-compose/blob/main/src/main/kotlin/examples/simulations/particles-js-simulation.kt) | [gravitation](https://github.com/CuriousNikhil/k5-compose/blob/main/src/main/kotlin/examples/forces/GravitationalAttraction.kt) | [game of life automaton](https://github.com/CuriousNikhil/k5-compose/blob/main/src/main/kotlin/examples/simulations/gameOfLife.kt) |
21 | |---|---|---|---|
22 | | | | | |
23 | | Starfield | Rain drops | Fractal tree | Fireworks |
24 | | | | | |
25 | | Circle waves | CircleGrid | danceYarn (by [@elye_project](https://twitter.com/elye_project)) | landscapeInspection (by [@elye_project](https://twitter.com/elye_project)) |
26 | | | | | |
27 |
28 |
29 |
30 |
31 | **Also don't forget to check awesome [examples and blog posts by few contributors](#contributors)**
32 |
33 | Click on the link to go to the code. Code explains the things in details. Try playing with those by tweaking values and running on your own. 😄 (I have added videos instead of gifs just so you can view these without loosing any frames 😉)
34 |
35 |
36 |
37 | ### Generative Art with K5-Compose
38 |
39 | All the examples can be found [here](https://github.com/CuriousNikhil/k5-compose/tree/main/src/main/kotlin/examples)
40 |
41 | | Blackhole | 10Print | Circle Loop |
42 | |---|---|---|
43 | |  |  |  |
44 | | Threads with Perlin-Noise | Phyllotaxis | Mandlebrot (by [@elye_project](https://twitter.com/elye_project)) |
45 | |  |  |  |
46 | | Perlin noise1d (by [@elye_project](https://twitter.com/elye_project)) | Perlin noise2d (by [@elye_project](https://twitter.com/elye_project)) | Ribbon |
47 | |  |  |  |
48 | | Circle Packing | | |
49 | |
| | |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | ## Getting started
58 |
59 | In order to understand using this library for creating experiments, I would recommend to go through the Nature of Code book by Daniel Shiffman - https://natureofcode.com/book/.
60 | This will give you the overall knowledge about how a physics system works in simplest way in p5/k5 or in general computer world.
61 | However, you could start digging into it right away by checking examples.
62 |
63 | 1. Create your [Jetpack Compose Desktop](https://github.com/JetBrains/compose-jb) project. Add the following dependency in your `build.gradle` along with the `compose` dependencies
64 |
65 | ```kotlin
66 | implementation("me.nikhilchaudhari:k5-compose:{latest-version}")
67 | ```
68 |
69 | 2. Use `k5` dsl construct to create K5 sketch
70 |
71 | ```kotlin
72 | fun main() = k5{
73 |
74 | // Initialise your variables, objects
75 |
76 | show { drawScope ->
77 | // use drawScope to draw the shapes
78 | }
79 | }
80 | ```
81 |
82 | 3. Use [library apis](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/index.html) for calculations and you are good to go! :p
83 |
84 | No need to manage/remember states, animation or anything. Just focus on your logic to design sketch rest of the things are handled by the library.
85 |
86 |
87 | ## How do I do that?
88 |
89 | ### k5
90 |
91 | It's very easy, you have to use `k5{...}` builder in order to initialise your k5-sketch. The things you define in here will be initialised only once.
92 |
93 | ```kotlin
94 | fun main() = k5{
95 | // you can define all your properties, variables, vectors here.
96 | val position = Vector2D(20f, 20f)
97 |
98 | // Say you want to have a control over your shape and few other parameters like, force, acceleration, mass etc. You can use classes to represent your shapes.
99 | val spaceShip = SpaceShip(position, size) // some data class representing spaceship
100 |
101 | //...
102 | }
103 | ```
104 |
105 | You can pass `size` param in `k5()`
106 |
107 | ```kotlin
108 | fun main() = k5(size = Size(800f, 500f)) { ... }
109 | ```
110 |
111 | And there are more number of configurations you can do to k5-compose playground which will be applied to the `Window{..}` composable. You can check [API docs](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/index.html).
112 |
113 |
114 | ### show
115 |
116 | Once you have initialised your necessary stuff which is going to change while running the frame of animation, you have to draw your shape/sketch in the `show{...}` function. `show` function gives you a canvas drawscope which you can used to draw into the k5 compose playground.
117 |
118 | Note: Whatever you pass in `show{...}` lambda will run for every frame. So if you want to update the values you've initialised and draw the sketch every frame you can pass those things in the lambda. For example -
119 |
120 | ```kotlin
121 | fun main() = k5{
122 | val vehiclePosition = Vector2D(20f, 20f)
123 |
124 | show{ drawScope ->
125 |
126 | //update your vehicle position,
127 | vehiclePosition.add(Vector2D.randomVector() * 2f)
128 |
129 | drawScope.drawCircle(color = Color.White, radius = 30f, center = vehiclePosition.toOffSet())
130 | }
131 | }
132 | ```
133 |
134 | You can apply all the compose `Modifiers` to the playground like changing background, color and taking keyboard and mouse pointer input.
135 |
136 | ```kotlin
137 | show(modifier = Modifier
138 | .background(Color.Yellow)
139 | .pointerMoveFilter(
140 | onMove = {
141 | //use mouse pointer values here
142 | false
143 | }
144 | )
145 | ) {
146 | // Draw your sketch
147 | }
148 | ```
149 |
150 | ### showWithControls
151 |
152 | If you want to add some interactivity with controls for your sketch, you can use `showWithControls(){..}`. You can add readily available Jetpack Compose elements like Slider, Checkbox, Toggle, Button, etc. And use `State` to change, update, take the inputs to your k5 sketch.
153 |
154 | ```kotlin
155 |
156 | fun main() = k5 {
157 |
158 | val numbers = mutableStateOf(0.3f)
159 |
160 | // Here Slider is pure Jetpack Compose element
161 | showWithControls(controls = {
162 | Text("Number of squares")
163 | Slider(
164 | value = numbers.value,
165 | onValueChange = { numbers.value = it }
166 | )
167 | }){ drawScope ->
168 |
169 | // Use the numbers value here
170 | // Draw your sketch.
171 | }
172 | ```
173 | Few examples [here](https://github.com/CuriousNikhil/k5-compose/blob/main/src/main/kotlin/examples/simulations/rotatingsquares.kt) and [here](https://github.com/CuriousNikhil/k5-compose/blob/main/src/main/kotlin/examples/particles/particles-js-simulation.kt). Which looks something like this - [video](https://user-images.githubusercontent.com/16976114/150638237-d64c0a2f-9bdc-4e14-b71b-56b3aff357f6.mov)
174 |
175 |
176 |
177 |
178 | This adds 2/3rd of your specified `width` to the k5 compose playground window.
179 |
180 | ## Few handy Apis
181 |
182 | **noLoop**
183 |
184 | Here, the `vehiclePosition` is constantly updated by adding a random vector to it's previous position and then a new circle is drawn on the playground based on the updated position. Simple, right?
185 |
186 | Let's say you don't want to keep your sketch in loop. Let's say you want to draw it only once. You can use `noLoop()` method in `k5{...}`.
187 |
188 | **Playground size**
189 |
190 | You can use `dimensInt` or `dimensFloat` properties available in `k5{...}` to get the size of the playground. You can pass the `size` in `k5()` as well but there are few density display metrics related issues in using floating point values. So to avoid any miscalculations, these `Size` values can be used to get the precise height and width of your playground.
191 |
192 |
193 | ```kotlin
194 | fun main() = k5 {
195 |
196 | // Use noLoop to draw your content only once
197 | noLoop()
198 |
199 | show {
200 | // this will be drawn only once
201 | }
202 |
203 | }
204 | ```
205 |
206 |
207 | ## How do I use math and physics functions?
208 |
209 | To use and understand mathematics and physics I would recommend [Nature Of Code](https://natureofcode.com/book/) book and [Video series](https://www.youtube.com/watch?v=70MQ-FugwbI&list=PLRqwX-V7Uu6ZV4yEcW3uDwOgGXKUUsPOM) by Daniel Shiffman. Or you can go through the [examples](https://github.com/CuriousNikhil/k5-compose/tree/main/src/main/kotlin/examples) section in the repo.
210 |
211 | ### Vectors
212 |
213 | `Vector2D` is data class - a vector representing any vector quantity in 2D plane. You can create a simple vector like `val acceleration = Vector2D(x = 2f, y = 3f)`, this means that the acceleration vector's x component is 2f and y component is 3f.
214 |
215 | To perform vector operations there are extensions and operators available to give you all the necessary vector algebra APIs. You can take a look at those methods here in the [API Docs](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/-vector2-d/index.html).
216 |
217 | Few helper methods available to create vectors from angle and also to create random vectors -
218 |
219 | **Random Vector**
220 | You can create random unit vector by using static method `randomVector` of `Vector2D` class. This creates a random unit vector.
221 |
222 | ```kotlin
223 | val position = Vector2D.randomVector()
224 | ```
225 |
226 | **Vector from angle**
227 | If you want to create a vector using any `angle` you can use `fromAngle` static method. For ex - the below code will create a vector with angle as PI radians and length of 2. (means x = 2 * cos(angle), y = 2 * sin(angle))
228 |
229 | ```kotlin
230 | val position = Vector2D.fromAnAngle(angle = PI, length = 2f)
231 | ```
232 |
233 | **toOffset**
234 | There's another handy method to convert your vector to the `Offset` type since you need to pass Offset types while drawing in Jetpack Compose. So you can convert Vector2D into Offset. Using `toOffset` on vector value.
235 |
236 | ```kotlin
237 | val position = Vector2D(2f, 5f)
238 | position.toOffSet()
239 | ```
240 |
241 | ### Random
242 |
243 | You can use the `Random` functions available in Kotlin by default.
244 | To quickly generate any random number within particular range, there are helper extensions available over `ClosedRange` for any Number data types.
245 |
246 | For ex - `(-12f..-8f).random()` or `(1..40).random()` or `(1.0..10.0).random()` etc
247 |
248 | If you want ot generate `randomGaussian` or a random number with your custom set `seed` value, apis are available to set the seed for randomness etc. You can check it [here](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/-random-kt/index.html). And you can use `k5Random`([api doc](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/-random-kt/k5-random.html)) and `randomGaussian`([api doc](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/-random-kt/random-gaussian.html)) functions to generate random values.
249 |
250 |
251 | ### Noise
252 |
253 | Noise is used a lot in p5 to generate a smooth randomness in numbers. This library contains Perlin noise helper methods which will generate noise values. You can check it [here](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/-noise-kt/index.html).
254 |
255 | There are three methods available for generating noise values - `noise1D(x: Double)`, `fun noise2D(x: Double, y: Double)` and `fun noise3D(x: Double, y: Double, z: Double)`
256 |
257 | If you don't know what noise is please take a look [here](https://en.wikipedia.org/wiki/Perlin_noise)
258 |
259 | ### Trigonometry
260 |
261 | You could of course use the basic kotlin.math trigonometric functions but just to keep it in handy this library has extensions functions over `Float` values. So you can just convert any float value to trigonometric function value over this float.
262 |
263 | ```kotlin
264 | val tan = 1f.tan()
265 | val cos = 0.2f.cos()
266 | val sin = 0.1f.sin()
267 |
268 | val atan = 1f.atan()
269 | val acos = 0.2f.acos()
270 | val asin = 0.1f.asin()
271 | ```
272 |
273 | You can check few more trigonometric functions [here](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/-trigonometry-kt/index.html)
274 |
275 |
276 | **Angle**
277 | The default angle measurement is in radians. If you want to change the angles to be measured in the degrees, then you can set the `angleMode` to degrees. And degress will be used to measure the angle and in all the functions related to angles.
278 |
279 | ```kotlin
280 | angleMode = AngleMode.DEGREES
281 | ```
282 |
283 | ### Calculations
284 |
285 | There are certain calculations that are related to vector, numbers etc which are required when you write physics system in a 2D environment. Those few methods are directly ported from p5.js. You can find some functions like `lerp`, `map`, `norm`, `constrain` etc. [here](https://javadoc.io/doc/me.nikhilchaudhari/k5-compose/latest/k5-compose/math/-calculations-kt/index.html)
286 |
287 |
288 | ## Contributors
289 |
290 | * [@elye_project](https://twitter.com/elye_project) has added awesome animations using perlin noise check them out [here](https://github.com/CuriousNikhil/k5-compose/tree/main/src/main/kotlin/examples/elye)
291 | * He has also written a blog post- [How to write animations under 50 lines using K5 Compose in Jetpack Compose](https://medium.com/mobile-app-development-publication/jetpack-compose-animation-under-50-lines-using-k5-compose-playground-bef35060c471)
292 |
293 |
294 | ### Contribution Guide
295 |
296 | PRs are welcomed! Please check contribution guide [here](https://github.com/CuriousNikhil/k5-compose/wiki/Contribution-Guide)
297 |
298 | ### License
299 | Licensed under Apache License, Version 2.0 [here](https://github.com/CuriousNikhil/k5-compose/blob/main/LICENSE)
300 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.compose
2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
4 |
5 | plugins {
6 | kotlin("jvm") version "1.6.10"
7 | id("org.jetbrains.compose") version "1.0.1"
8 | id("org.jlleitschuh.gradle.ktlint") version "10.2.0"
9 | id("org.jetbrains.dokka") version "1.5.30"
10 | }
11 |
12 | group = "me.nikhilchaudhari"
13 | version = "1.0.0"
14 |
15 | repositories {
16 | mavenCentral()
17 | google()
18 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
19 | }
20 |
21 | dependencies {
22 | implementation(compose.desktop.macos_arm64)
23 | implementation(project("k5-compose"))
24 | // implementation("me.nikhilchaudhari:k5-compose:1.0.0-alpha01")
25 | dokkaHtmlPlugin("org.jetbrains.dokka:kotlin-as-java-plugin:1.5.30")
26 | }
27 |
28 | tasks.withType() {
29 | kotlinOptions.jvmTarget = "11"
30 | }
31 |
32 | compose.desktop {
33 | application {
34 | mainClass = "MainKt"
35 | nativeDistributions {
36 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
37 | packageName = "k5-compose-examples"
38 | packageVersion = "1.0.0"
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CuriousNikhil/k5-compose/3148953fd4a161a2d800b4e0818f46b94559e784/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/k5-compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/k5-compose/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 |
3 | plugins {
4 | kotlin("jvm")
5 | id("org.jetbrains.compose")
6 | `java-library`
7 | `maven-publish`
8 | signing
9 | id("org.jetbrains.dokka")
10 | }
11 | apply(plugin = "org.jlleitschuh.gradle.ktlint")
12 |
13 | // Stub secrets to let the project sync and build without the publication values set up
14 | ext["signing.keyId"] = null
15 | ext["signing.password"] = null
16 | ext["signing.secretKeyRingFile"] = null
17 | ext["ossrhUsername"] = null
18 | ext["ossrhPassword"] = null
19 |
20 | // Grabbing secrets from local.properties file or from environment variables, which could be used on CI
21 | val secretPropsFile = project.rootProject.file("local.properties")
22 | if (secretPropsFile.exists()) {
23 | secretPropsFile.reader().use {
24 | Properties().apply {
25 | load(it)
26 | }
27 | }.onEach { (name, value) ->
28 | ext[name.toString()] = value
29 | }
30 | } else {
31 | ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID")
32 | ext["signing.password"] = System.getenv("SIGNING_PASSWORD")
33 | ext["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE")
34 | ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME")
35 | ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD")
36 | }
37 |
38 | val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class)
39 | val javaDocJar: TaskProvider by tasks.registering(Jar::class) {
40 | dependsOn()
41 | archiveClassifier.set("javadoc")
42 | from(dokkaHtml.outputDirectory)
43 | }
44 |
45 | java {
46 | withSourcesJar()
47 | }
48 |
49 | fun getExtraString(name: String) = ext[name]?.toString()
50 |
51 | group = "me.nikhilchaudhari"
52 | version = "1.0.1"
53 | val pubName = "k5-compose"
54 |
55 | publishing {
56 | // Configure maven central repository
57 | repositories {
58 | maven {
59 | name = pubName
60 | setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
61 | credentials {
62 | username = getExtraString("ossrhUsername")
63 | password = getExtraString("ossrhPassword")
64 | }
65 | }
66 | }
67 |
68 | // Configure all publications
69 | publications.create(pubName) {
70 | artifactId = pubName
71 | artifact(javaDocJar)
72 | from(components["java"])
73 |
74 | pom {
75 | name.set("k5-compose")
76 | description.set("p5.js port for jetpack compose desktop")
77 | url.set("git@github.com:CuriousNikhil/k5-compose.git")
78 | licenses {
79 | license {
80 | name.set("Apache 2.0")
81 | url.set("https://github.com/CuriousNikhil/k5-compose/blob/main/LICENSE")
82 | }
83 | }
84 | developers {
85 | developer {
86 | id.set("curiousnikhil")
87 | name.set("Nikhil Chaudhari")
88 | email.set("nikhyl777@gmail.com")
89 | }
90 | }
91 | scm {
92 | connection.set("scm:git:git://github.com/CuriousNikhil/k5-compose.git")
93 | developerConnection.set("scm:git:ssh://github.com/CuriousNikhil/k5-compose.git")
94 | url.set("https://github.com/CuriousNikhil/k5-compose")
95 | }
96 | }
97 | }
98 | }
99 |
100 | // Signing artifacts. Signing.* extra properties values will be used
101 | signing {
102 | sign(publishing.publications[pubName])
103 | }
104 |
105 | repositories {
106 | mavenCentral()
107 | google()
108 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
109 | }
110 |
111 | dependencies {
112 | implementation(compose.desktop.currentOs)
113 | dokkaHtmlPlugin("org.jetbrains.dokka:kotlin-as-java-plugin:1.5.30")
114 | }
115 |
116 | tasks.withType() {
117 | kotlinOptions.jvmTarget = "11"
118 | }
119 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/K5.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.foundation.Canvas
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.draw.clipToBounds
14 | import androidx.compose.ui.geometry.Size
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.drawscope.DrawScope
17 | import androidx.compose.ui.graphics.painter.Painter
18 | import androidx.compose.ui.input.key.KeyEvent
19 | import androidx.compose.ui.platform.LocalDensity
20 | import androidx.compose.ui.unit.IntSize
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.window.Window
23 | import androidx.compose.ui.window.application
24 | import androidx.compose.ui.window.rememberWindowState
25 | import ui.SketchView
26 |
27 | /**
28 | * Builder construct for the K5-compose.
29 | * Can be called like, main() = k5 {...}
30 | * All the params passed are applied to a [Window] component
31 | *
32 | * @param size The size param for the size of window - [Size]
33 | * The size is taken in floats in order to perform canvas related operations easily on floats which includes the
34 | * dimension of window
35 | * @param title The title of the window.
36 | * The title is displayed in the windows's native border.
37 | * @param size The initial size of the window.
38 | * @param icon The icon for the window displayed on the system taskbar.
39 | * @param undecorated Removes the native window border if set to true. The default value is false.
40 | * @param resizable Makes the window resizable if is set to true and unresizable if is set to
41 | * false. The default value is true.
42 | * @param focusable Can window receive focus
43 | * @param alwaysOnTop Should window always be on top of another windows
44 | * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
45 | * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
46 | * Return true to stop propagation of this event. If you return false, the key event will be
47 | * sent to this [onPreviewKeyEvent]'s child. If none of the children consume the event,
48 | * it will be sent back up to the root using the onKeyEvent callback.
49 | * @param onKeyEvent This callback is invoked when the user interacts with the hardware
50 | * keyboard. While implementing this callback, return true to stop propagation of this event.
51 | * If you return false, the key event will be sent to this [onKeyEvent]'s parent.
52 | */
53 | fun k5(
54 | size: Size = Size(1000f, 1000f),
55 | title: String = "K5 Compose Playground",
56 | icon: Painter? = null,
57 | undecorated: Boolean = false,
58 | resizable: Boolean = false,
59 | enabled: Boolean = true,
60 | focusable: Boolean = true,
61 | alwaysOnTop: Boolean = false,
62 | onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
63 | onKeyEvent: (KeyEvent) -> Boolean = { false },
64 | init: K5.() -> Unit
65 | ) {
66 | K5(
67 | size,
68 | title,
69 | icon,
70 | undecorated,
71 | resizable,
72 | enabled,
73 | focusable,
74 | alwaysOnTop,
75 | onPreviewKeyEvent,
76 | onKeyEvent
77 | ).init()
78 | }
79 |
80 | class K5(
81 | val size: Size,
82 | val title: String,
83 | val icon: Painter?,
84 | val undecorated: Boolean,
85 | val resizable: Boolean,
86 | val enabled: Boolean,
87 | val focusable: Boolean,
88 | val alwaysOnTop: Boolean,
89 | val onPreviewKeyEvent: (KeyEvent) -> Boolean,
90 | val onKeyEvent: (KeyEvent) -> Boolean,
91 | ) {
92 |
93 | private var stopLoop = false
94 |
95 | /**
96 | * Call method to stop the looping of canvas
97 | * You can also call it to freeze the time frame for a canvas
98 | */
99 | fun noLoop() {
100 | this.stopLoop = true
101 | }
102 |
103 | /**
104 | * Use this property to get the actual [k5] Playground size in Floats. Subtracting the 56f - which is the toolbar height of the window.
105 | * When the size of the window is set with `size` param in [k5] builder, it's applied to window and when
106 | * the canvas is rendered in the window with [Modifier.fillMaxSize] it takes whole window except the toolbar.
107 | *
108 | * TODO: Fix the dimensions for a given k5 playground considering density
109 | */
110 | val dimensFloat = Size(size.width, size.height - 56f)
111 |
112 | /**
113 | * Use this property to get the actual [k5] Playground size in Ints
114 | */
115 | val dimensInt = IntSize(size.width.toInt(), size.height.toInt() - 56)
116 |
117 | /**
118 | * Shows the canvas window and renders it for each frame repetitively.
119 | * Internally, this starts the Jetpack Compose Window and renders the [sketch] requested by user into the Jetpack Compose
120 | * [Canvas] Composable. The size of the [Canvas] will be same as the [size] passed in [k5] method by default.
121 | * One can change the canvas size and window size with the help of modifiers.
122 | * In order to keep the animation running (rendering canvas continuously), it requests to run the frame of animation in nanos.
123 | * All the [modifier] will be applied to the [Canvas].
124 | *
125 | * @param modifier Jetpack compose [Modifier]
126 | * @param sketch drawScope - Compose canvas drawscope
127 | */
128 | fun show(
129 | modifier: Modifier = Modifier.fillMaxSize(),
130 | sketch: (drawScope: DrawScope) -> Unit
131 | ) {
132 | render(modifier, sketch)
133 | }
134 |
135 | /**
136 | * Shows canvas window as well as controls view side by side.
137 | * Internally, this starts the Jetpack Compose Window and renders the [sketch] requested by user into the Jetpack Compose
138 | * [Canvas] Composable. The size of the [Canvas] will be same as the [size] passed in [k5] method by default.
139 | * One can change the canvas size and window size with the help of modifiers.
140 | * In order to keep the animation running (rendering canvas continuously), it requests to run the frame of animation in nanos.
141 | *
142 | * Also, you can add your controls like sliders, checkboxes, radio-buttons, pickers etc as a control to change/provide
143 | * inputs to your sketch. This is just simple [Composable] function, so you can add all the [Composable] elements you want.
144 | * The controls are shown on the right side in the window. You can use Compose States to store/change your input and use it in your sketch.
145 | */
146 | fun showWithControls(
147 | modifier: Modifier = Modifier.background(Color.Transparent),
148 | controls: (@Composable () -> Unit)? = null,
149 | sketch: (drawScope: DrawScope) -> Unit
150 | ) {
151 | render(modifier, sketch, controls)
152 | }
153 |
154 | /**
155 | * Starts the Jetpack Compose Window and renders the [sketch] requested by user into the Jetpack Compose
156 | * [Canvas] Composable.
157 | *
158 | * @param modifier - Jetpack compose [Modifier]. Will be applied to the [Canvas]
159 | * @param sketch - The content to be drawn on to [Canvas]
160 | */
161 | private fun render(
162 | modifier: Modifier,
163 | sketch: (drawScope: DrawScope) -> Unit,
164 | controls: (@Composable () -> Unit)? = null
165 | ) = application {
166 | val areControlsActive = controls != null
167 |
168 | val (width, height) = with(LocalDensity.current) { Pair(size.width.toDp(), size.height.toDp()) }
169 | val calculatedWidth = if (areControlsActive) width + width * 2 / 3 else width
170 | Window(
171 | onCloseRequest = ::exitApplication,
172 | state = rememberWindowState(width = calculatedWidth, height = height),
173 | title = title,
174 | icon = icon,
175 | undecorated = undecorated,
176 | resizable = resizable,
177 | enabled = enabled,
178 | focusable = focusable,
179 | alwaysOnTop = alwaysOnTop,
180 | onPreviewKeyEvent = onPreviewKeyEvent,
181 | onKeyEvent = onKeyEvent
182 | ) {
183 | Row(modifier = Modifier.fillMaxSize()) {
184 | Box(
185 | modifier = Modifier
186 | .width(width)
187 | .height(height)
188 | .clipToBounds()
189 | ) {
190 | SketchView(modifier, stopLoop, sketch)
191 | }
192 | Box(modifier = Modifier.height(height).background(Color.LightGray).padding(16.dp)) {
193 | Column {
194 | controls?.let { it() }
195 | }
196 | }
197 | }
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/math/Calculations.kt:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import kotlin.math.max
4 | import kotlin.math.min
5 |
6 | /**
7 | * Constrains a value between a minimum and maximum value.
8 | *
9 | * @method constrain
10 | * @param n number to constrain
11 | * @param low minimum limit
12 | * @param high maximum limit
13 | */
14 | fun constrain(n: Float, low: Float, high: Float): Float {
15 | return max(min(n, high), low)
16 | }
17 |
18 | /**
19 | * Re-maps a number from one range to another.
20 | *
21 | * @param value the incoming value to be converted
22 | * @param start1 lower bound of the value's current range
23 | * @param stop1 upper bound of the value's current range
24 | * @param start2 lower bound of the value's target range
25 | * @param stop2 upper bound of the value's target range
26 | * @param withinBounds constrain the value to the newly mapped range
27 | * @return remapped number
28 | */
29 | fun map(n: Float, start1: Float, stop1: Float, start2: Float, stop2: Float, withBounds: Boolean = true): Float {
30 | val newval = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2
31 | return if (!withBounds) {
32 | newval
33 | } else {
34 | if (start2 < stop2) {
35 | constrain(newval, start2, stop2)
36 | } else {
37 | constrain(newval, stop2, start2)
38 | }
39 | }
40 | }
41 |
42 | /**
43 | * Calculates a number between two numbers at a specific increment. The amt
44 | * parameter is the amount to interpolate between the two values where 0.0
45 | * equal to the first point, 0.1 is very near the first point, 0.5 is
46 | * half-way in between, and 1.0 is equal to the second point. If the
47 | * value of amt is more than 1.0 or less than 0.0, the number will be
48 | * calculated accordingly in the ratio of the two given numbers. The lerp
49 | * function is convenient for creating motion along a straight
50 | * path and for drawing dotted lines.
51 | *
52 | * @method lerp
53 | * @param start first value
54 | * @param stop second value
55 | * @param amt number
56 | * @return lerped value
57 | */
58 | fun lerp(start: Float, stop: Float, amt: Float): Float {
59 | return amt * (stop - start) + start
60 | }
61 |
62 | /**
63 | * Normalizes a number from another range into a value between 0 and 1.
64 | * Identical to map(value, low, high, 0, 1).
65 | * Numbers outside of the range are not clamped to 0 and 1, because
66 | * out-of-range values are often intentional and useful. (See the example above.)
67 | *
68 | * @method norm
69 | * @param n incoming value to be normalized
70 | * @param start lower bound of the value's current range
71 | * @param stop upper bound of the value's current range
72 | */
73 | fun norm(n: Float, start: Float, stop: Float): Float {
74 | return map(n, start, stop, 0f, 1f)
75 | }
76 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/math/Constants.kt:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | /**
4 | * Constants
5 | */
6 |
7 | internal const val _PI = Math.PI
8 |
9 | const val HALF_PI = _PI / 2
10 |
11 | const val PI = _PI
12 |
13 | const val QUARTER_PI = _PI / 4
14 |
15 | const val TWO_PI = _PI * 2
16 |
17 | enum class AngleMode {
18 | DEGREES,
19 | RADIANS
20 | }
21 |
22 | const val DEG_TO_RAD = _PI / 180.0
23 |
24 | const val RAD_TO_DEG = 180.0 / _PI
25 |
26 | const val TAU = 6.2831855
27 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/math/Noise.kt:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import kotlin.math.absoluteValue
4 | import kotlin.math.floor
5 |
6 | /**
7 | * Generates perlin noise value in 1D for given x offset.
8 | * Linear sequence of noise values in a one-dimensional space, and we can ask for a value at a specific x-location whenever we want.
9 | *
10 | * @param [x] x offset on plane.
11 | * @return The value of noise at specific x location
12 | */
13 | fun noise1D(x: Double): Double {
14 | return Perlin.noise(x, 0.0, 0.0).absoluteValue
15 | }
16 |
17 | /**
18 | * Generates perlin noise values for 2D for given x and y offset
19 | *
20 | * @param [x] [y] x and y offset
21 | * @return The perlin noise value at x and y location at given point it time
22 | */
23 | fun noise2D(x: Double, y: Double): Double {
24 | return Perlin.noise(x, y, 0.0).absoluteValue
25 | }
26 |
27 | /**
28 | * Generates perlin noise values for 3D
29 | *
30 | * @param [x] [y] [z] x, y & z offset
31 | * @return The perlin noise value at (x, y, z) location at given point it time
32 | */
33 | fun noise3D(x: Double, y: Double, z: Double): Double {
34 | return Perlin.noise(x, y, z).absoluteValue
35 | }
36 |
37 | /**
38 | * Represents a simple Perlon noise generator
39 | * This generates smooth noise which gives a normal distribution of random values.
40 | * Perlin noise https://en.wikipedia.org/wiki/Perlin_noise
41 | * from: https://rosettacode.org/wiki/Perlin_noise#Kotlin
42 | */
43 | object Perlin {
44 | private val permutation = intArrayOf(
45 | 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225,
46 | 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148,
47 | 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32,
48 | 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175,
49 | 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
50 | 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54,
51 | 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169,
52 | 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64,
53 | 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212,
54 | 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
55 | 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
56 | 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
57 | 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241,
58 | 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
59 | 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
60 | 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180
61 | )
62 |
63 | private val p = IntArray(512) {
64 | if (it < 256) permutation[it] else permutation[it - 256]
65 | }
66 |
67 | fun noise(x: Double, y: Double, z: Double): Double {
68 | // Find unit cube that contains point
69 | val xi = floor(x).toInt() and 255
70 | val yi = floor(y).toInt() and 255
71 | val zi = floor(z).toInt() and 255
72 |
73 | // Find relative x, y, z of point in cube
74 | val xx = x - floor(x)
75 | val yy = y - floor(y)
76 | val zz = z - floor(z)
77 |
78 | // Compute fade curves for each of xx, yy, zz
79 | val u = fade(xx)
80 | val v = fade(yy)
81 | val w = fade(zz)
82 |
83 | // Hash co-ordinates of the 8 cube corners
84 | // and add blended results from 8 corners of cube
85 |
86 | val a = p[xi] + yi
87 | val aa = p[a] + zi
88 | val ab = p[a + 1] + zi
89 | val b = p[xi + 1] + yi
90 | val ba = p[b] + zi
91 | val bb = p[b + 1] + zi
92 |
93 | return lerp(
94 | w,
95 | lerp(
96 | v,
97 | lerp(
98 | u, grad(p[aa], xx, yy, zz),
99 | grad(p[ba], xx - 1, yy, zz)
100 | ),
101 | lerp(
102 | u, grad(p[ab], xx, yy - 1, zz),
103 | grad(p[bb], xx - 1, yy - 1, zz)
104 | )
105 | ),
106 | lerp(
107 | v,
108 | lerp(
109 | u, grad(p[aa + 1], xx, yy, zz - 1),
110 | grad(p[ba + 1], xx - 1, yy, zz - 1)
111 | ),
112 | lerp(
113 | u, grad(p[ab + 1], xx, yy - 1, zz - 1),
114 | grad(p[bb + 1], xx - 1, yy - 1, zz - 1)
115 | )
116 | )
117 | )
118 | }
119 |
120 | private fun fade(t: Double) = t * t * t * (t * (t * 6 - 15) + 10)
121 |
122 | private fun lerp(t: Double, a: Double, b: Double) = a + t * (b - a)
123 |
124 | private fun grad(hash: Int, x: Double, y: Double, z: Double): Double {
125 | // Convert low 4 bits of hash code into 12 gradient directions
126 | val h = hash and 15
127 | val u = if (h < 8) x else y
128 | val v = if (h < 4) y else if (h == 12 || h == 14) x else z
129 | return (if ((h and 1) == 0) u else -u) +
130 | (if ((h and 2) == 0) v else -v)
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/math/Random.kt:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import java.util.concurrent.ThreadLocalRandom
4 | import kotlin.math.ln
5 | import kotlin.math.sqrt
6 | import kotlin.random.Random
7 |
8 | // Set to values from http://en.wikipedia.org/wiki/Numerical_Recipes
9 | // m is basically chosen to be large (as it is the max period)
10 | // and for its relationships to a and c
11 | internal const val m = 4294967296
12 |
13 | // a - 1 should be divisible by m's prime factors
14 | internal const val a = 1664525
15 |
16 | // c and m should be co-prime
17 | internal const val c = 1013904223
18 | var y2 = 0f
19 |
20 | internal val stateValueMap = mutableMapOf()
21 |
22 | const val randomStateKey = "_lcg_random_state"
23 |
24 | internal var gaussianPrevious = false
25 |
26 | fun lcg(state: String): Long {
27 | val stateVal = stateValueMap[state] ?: 0
28 | val newVal = (a * stateVal + c) % m
29 | stateValueMap[state] = newVal
30 | return newVal / m
31 | }
32 |
33 | fun lcgSetSeed(state: String, value: Int?) {
34 | stateValueMap[state] = (value ?: (Random.nextFloat() * m)).toLong()
35 | }
36 |
37 | fun randomSeed(seed: Int) {
38 | lcgSetSeed(randomStateKey, seed)
39 | gaussianPrevious = false
40 | }
41 |
42 | /**
43 | * A simple random function to return float values between range of
44 | * [min] and [max]
45 | *
46 | * The default value for [min] is zero
47 | */
48 | fun k5Random(min: Int = 0, max: Int): Float {
49 | var minimum = min
50 | var maximum = max
51 | val rand = if (stateValueMap.isNotEmpty()) {
52 | lcg(randomStateKey)
53 | } else {
54 | Random.nextFloat()
55 | }
56 | if (min > max) {
57 | minimum = max
58 | maximum = min
59 | }
60 | return rand.toFloat() * (maximum - minimum) + min
61 | }
62 |
63 | /**
64 | * Generate random integer number within [this] range
65 | */
66 | fun ClosedRange.random() = ThreadLocalRandom.current().nextInt(endInclusive - start) + start
67 |
68 | /**
69 | * Generate random long number within [this] range
70 | */
71 | fun ClosedRange.random() = ThreadLocalRandom.current().nextLong(endInclusive - start) + start
72 |
73 | /**
74 | * Generate random float number within [this] range
75 | */
76 | fun ClosedRange.random() = ThreadLocalRandom.current().nextFloat() * (endInclusive - start) + start
77 |
78 | /**
79 | * Generate random double number within [this] range
80 | */
81 | fun ClosedRange.random() = ThreadLocalRandom.current().nextDouble(endInclusive - start) + start
82 |
83 | /**
84 | * Generates a random gaussian with [mean] value and [standardDeviation]
85 | * default value for [standardDeviation] is 1
86 | */
87 | fun randomGaussian(mean: Float, standardDeviation: Float = 1f): Float {
88 | var y1 = 0f
89 | var x1 = 0f
90 | var x2 = 0f
91 | var w = 0f
92 |
93 | if (gaussianPrevious) {
94 | y1 = y2
95 | gaussianPrevious = false
96 | } else {
97 | do {
98 | x1 = k5Random(0, 2) - 1
99 | x2 = k5Random(0, 2) - 1
100 | w = (x1 * x1 + x2 * x2)
101 | } while (w >= 1)
102 | w = sqrt(-2 * ln(w) / w)
103 | y1 = x1 * w
104 | y2 = x2 * w
105 | gaussianPrevious = true
106 | }
107 | return y1 * standardDeviation + m
108 | }
109 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/math/Trigonometry.kt:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import kotlin.math.cos
4 | import kotlin.math.sin
5 | import kotlin.math.tan
6 |
7 | /**
8 | * Set global angle mode DEGREES/RADIANS [AngleMode]
9 | */
10 | var angleMode = AngleMode.RADIANS
11 |
12 | fun Float.tan(): Float {
13 | return tan(this.toRadians())
14 | }
15 |
16 | fun Float.sin(): Float {
17 | return sin(this.toRadians())
18 | }
19 |
20 | fun Float.cos(): Float {
21 | return cos(this.toRadians())
22 | }
23 |
24 | fun atan2(y: Float, x: Float): Float {
25 | return kotlin.math.atan2(y.toDouble(), x.toDouble()).toFloat().toRadians()
26 | }
27 |
28 | fun Float.atan(): Float {
29 | return kotlin.math.atan(this).toRadians()
30 | }
31 |
32 | fun Float.asin(): Float {
33 | return kotlin.math.asin(this).toRadians()
34 | }
35 |
36 | fun Float.acos(): Float {
37 | return kotlin.math.acos(this).toRadians()
38 | }
39 |
40 | /**
41 | * Converts float to radians
42 | */
43 | fun Float.toRadians(): Float {
44 | if (angleMode == AngleMode.DEGREES) {
45 | return this * DEG_TO_RAD.toFloat()
46 | }
47 | return this
48 | }
49 |
50 | /**
51 | * Converts float to degrees
52 | */
53 | fun Float.toDegrees(): Float {
54 | if (angleMode == AngleMode.RADIANS) {
55 | return this * RAD_TO_DEG.toFloat()
56 | }
57 | return this
58 | }
59 |
60 | /**
61 | * Converts [this] degrees to radians
62 | */
63 | fun Float.fromRadians(): Float {
64 | if (angleMode == AngleMode.RADIANS) {
65 | return this * RAD_TO_DEG.toFloat()
66 | }
67 | return this
68 | }
69 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/math/Vector2D.kt:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import kotlin.math.acos
5 | import kotlin.math.atan2
6 | import kotlin.math.cos
7 | import kotlin.math.max
8 | import kotlin.math.min
9 | import kotlin.math.sin
10 | import kotlin.math.sqrt
11 | import kotlin.random.Random
12 |
13 | /**
14 | * Represents a 2-dimensional vector in plane
15 | *
16 | * This class can be used to create a vector for any entity with [x] and [y] as
17 | * x and y points in canvas plane. The canvas plane follows cartesian coordinate system but with origin at
18 | * top left corner and positive x-axis along right direction and positive y-axis along downward direction.
19 | *
20 | * For ex: Here representation of position vector in canvas plane could be given as -
21 | *
22 | * val position = Vector2D(25f, 25f)
23 | *
24 | */
25 | data class Vector2D(var x: Float = 0f, var y: Float = 0f) {
26 |
27 | /**
28 | * Equals method to check the equality between two vectors
29 | */
30 | override fun equals(other: Any?): Boolean {
31 | return other is Vector2D && other.x == this.x && other.y == this.y
32 | }
33 |
34 | override fun hashCode(): Int {
35 | var result = x.hashCode()
36 | result = 31 * result + y.hashCode()
37 | return result
38 | }
39 |
40 | companion object {
41 | /**
42 | * Creates a vector from an [angle] and [length]
43 | *
44 | * Given any angle in radians and length/magnitude of a vector, it'll create a vector with
45 | * it's x-component and y-component
46 | *
47 | * @param angle Angle in radians
48 | * @param length length in float
49 | *
50 | * @return [Vector2D] vector for given angle and with given magnitude
51 | */
52 | fun fromAnAngle(angle: Float, length: Float = 1f): Vector2D {
53 | return Vector2D(length * cos(angle), length * sin(angle))
54 | }
55 |
56 | /**
57 | * Creates a random vector (unit vector)
58 | * Can be used to create a random unit vector along any direction
59 | * @return a random unit [Vector2D]
60 | */
61 | fun randomVector(): Vector2D {
62 | return fromAnAngle((Random.nextFloat() * PI * 2).toFloat())
63 | }
64 | }
65 | }
66 |
67 | /*Below are few simple vector algebra methods*/
68 |
69 | fun Vector2D.add(other: Vector2D): Vector2D {
70 | this.x += other.x
71 | this.y += other.y
72 | return this
73 | }
74 |
75 | operator fun Vector2D.plusAssign(other: Vector2D) {
76 | this.add(other)
77 | }
78 |
79 | /**
80 | * To add a vector to [this] vector and multiplies the [other] vector with [scalar]
81 | */
82 | fun Vector2D.addWithScalarMultiply(other: Vector2D, scalar: Float): Vector2D {
83 | this.x += other.x * scalar
84 | this.y += other.y * scalar
85 | return this
86 | }
87 |
88 | /**
89 | * To subtract a vector from [this] vector
90 | */
91 | fun Vector2D.sub(other: Vector2D): Vector2D {
92 | this.x -= other.x
93 | this.y -= other.y
94 | return this
95 | }
96 |
97 | operator fun Vector2D.minusAssign(other: Vector2D) {
98 | this.sub(other)
99 | }
100 |
101 | /**
102 | * To multiply [this] vector with [x] factor and [y] factor
103 | */
104 | fun Vector2D.multiply(x: Float, y: Float): Vector2D {
105 | this.x *= x
106 | this.y *= y
107 | return this
108 | }
109 |
110 | /**
111 | * To multiply [this] vector with single [factor]
112 | * See also [scalarMultiply]
113 | */
114 | fun Vector2D.multiply(factor: Float): Vector2D {
115 | return multiply(factor, factor)
116 | }
117 |
118 | operator fun Vector2D.timesAssign(factor: Float) {
119 | this.multiply(factor)
120 | }
121 |
122 | /**
123 | * To divide [this] vector by [x] and [y] values
124 | */
125 | fun Vector2D.div(x: Float, y: Float): Vector2D {
126 | this.x /= x
127 | this.y /= y
128 | return this
129 | }
130 |
131 | /**
132 | * To divide [this] vector by single [factor]
133 | */
134 | fun Vector2D.divide(factor: Float): Vector2D {
135 | return div(factor, factor)
136 | }
137 |
138 | operator fun Vector2D.divAssign(factor: Float) {
139 | this.divide(factor)
140 | }
141 |
142 | /**
143 | * Calculates the magnitude (length) of the vector and returns the result asa float
144 | */
145 | fun Vector2D.mag(): Float = sqrt(this.magSq())
146 |
147 | /**
148 | * Calculates the squared magnitude of [this] vector and returns the result as a float
149 | */
150 | fun Vector2D.magSq(): Float = this.x * this.x + this.y * this.y
151 |
152 | /**
153 | * Calculates the dot product of two vectors
154 | */
155 | fun Vector2D.dot(other: Vector2D): Float = this.x * other.x + this.y * other.y
156 |
157 | /**
158 | * Calculates Euclidean distance between two vectors
159 | */
160 | fun Vector2D.distance(other: Vector2D): Float = this.copy().sub(other).mag()
161 |
162 | /**
163 | * Normalizes the vector to length 1 - making unit vector
164 | *
165 | * @return [Vector2D]
166 | */
167 | fun Vector2D.normalize(): Vector2D {
168 | val len = this.mag()
169 | if (len != 0f) {
170 | this.scalarMultiply(1 / len)
171 | }
172 | return this
173 | }
174 |
175 | /**
176 | * Set the magnitude of this vector to the value used for the n
177 | * parameter.
178 | *
179 | * @param n the new length of the vector
180 | */
181 | fun Vector2D.setMag(n: Float): Vector2D {
182 | return this.normalize().multiply(n)
183 | }
184 |
185 | /**
186 | * Limit the magnitude of this vector to the value used for the max
187 | * parameter.
188 | *
189 | * val v = Vector2D(10, 20);
190 | * // v has components [10.0, 20.0]
191 | * v.limit(5);
192 | * // v's components are set to
193 | * // [2.2271771, 4.4543543 ]
194 | *
195 | */
196 | fun Vector2D.limit(max: Float): Vector2D {
197 | val magSq = this.magSq()
198 | if (magSq > max * max) {
199 | val norm = sqrt(magSq)
200 | this.div(norm, norm).scalarMultiply(max)
201 | }
202 | return this
203 | }
204 |
205 | /**
206 | * Calculate the angle of rotation for this vector(only 2D vectors).
207 | */
208 | fun Vector2D.heading(): Float {
209 | return atan2(this.y, this.x).fromRadians()
210 | }
211 |
212 | /**
213 | * Rotate the vector to a specific angle, magnitude remains the same
214 | * @param angle - Angle in radians
215 | */
216 | fun Vector2D.setHeading(angle: Float): Vector2D {
217 | val mag = this.mag()
218 | this.x = mag * cos(angle)
219 | this.y = mag * sin(angle)
220 | return this
221 | }
222 |
223 | /**
224 | * Rotates [this] vector by given [angle]
225 | */
226 | fun Vector2D.rotate(angle: Float): Vector2D {
227 | val newHeading = (this.heading() + angle).toRadians()
228 | val mag = this.mag()
229 | this.x = cos(newHeading) * mag
230 | this.y = sin(newHeading) * mag
231 | return this
232 | }
233 |
234 | /**
235 | * Sets [this] vector as [other] vector
236 | */
237 | fun Vector2D.set(other: Vector2D): Vector2D {
238 | this.x = other.x
239 | this.y = other.y
240 | return this
241 | }
242 |
243 | /**
244 | * Returns angle between [this] vector and [other] vector
245 | *
246 | * @return Angle in radians
247 | */
248 | fun Vector2D.angleBetween(other: Vector2D): Float {
249 | val dotmag = this.dot(other) / (this.mag() * other.mag())
250 | val angle = acos(min(1f, max(-1f, dotmag)).toDouble()).toFloat()
251 | // angle = angle * Math.signum(this.c)
252 | return angle.toRadians()
253 | }
254 |
255 | /**
256 | * Linear interpolate the vector to another vector
257 | * @param x the x component
258 | * @param y the y component
259 | * @param amt the amount of interpolation; some value between 0.0
260 | * (old vector) and 1.0 (new vector). 0.9 is very near
261 | * the new vector. 0.5 is halfway in between.
262 | */
263 | fun Vector2D.lerp(x: Float, y: Float, amt: Float): Vector2D {
264 | this.x += (x - this.x) * amt
265 | this.y += (y - this.y) * amt
266 | return this
267 | }
268 |
269 | /**
270 | * Reflect the incoming vector about a normal to a line in 2D, or about a normal to a plane in 3D
271 | * This method acts on the vector directly
272 | * @param surfaceNormal the [Vector2D] to reflect about, will be normalized by this method
273 | */
274 | fun Vector2D.reflect(surfaceNormal: Vector2D): Vector2D {
275 | surfaceNormal.normalize()
276 | return this.sub(surfaceNormal.scalarMultiply(2 * this.dot(surfaceNormal)))
277 | }
278 |
279 | fun Vector2D.scalarMultiply(scalar: Float): Vector2D {
280 | this.x *= scalar
281 | this.y *= scalar
282 | return this
283 | }
284 |
285 | /**
286 | * Increments [this] vector by [factor]
287 | */
288 | operator fun Vector2D.inc(factor: Float): Vector2D {
289 | this.x += factor
290 | this.y += factor
291 | return this
292 | }
293 |
294 | /**
295 | * Decrements [this] vector by [factor]
296 | */
297 | operator fun Vector2D.dec(factor: Float): Vector2D {
298 | this.x -= factor
299 | this.y -= factor
300 | return this
301 | }
302 |
303 | /**
304 | * Converts a [this] [Vector2D] into [Offset] value
305 | *
306 | * @return [Offset] with x and y value as x and y component of vector
307 | */
308 | fun Vector2D.toOffSet(): Offset {
309 | return Offset(this.x, this.y)
310 | }
311 |
--------------------------------------------------------------------------------
/k5-compose/src/main/kotlin/ui/SketchView.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
4 | import androidx.compose.foundation.Canvas
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.fillMaxSize
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 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.drawscope.DrawScope
15 |
16 | /**
17 | * Run frame time with nanoseconds
18 | * @param dt - Change it time
19 | * @param previousTime - previous time to calculate change in time
20 | */
21 | @Composable
22 | private fun requestAnimationFrame(dt: MutableState, previousTime: MutableState) {
23 | LaunchedEffect(Unit) {
24 | while (true) {
25 | withInfiniteAnimationFrameMillis {
26 | dt.value = ((it - previousTime.value) / 1E7).toFloat()
27 | previousTime.value = it
28 | }
29 | }
30 | }
31 | }
32 |
33 | /**
34 | * Simple canvas view to render the sketch
35 | */
36 | @Composable
37 | internal fun SketchView(modifier: Modifier, stopLoop: Boolean, sketch: (drawScope: DrawScope) -> Unit) {
38 |
39 | // dt = change in time
40 | val dt = remember { mutableStateOf(0f) }
41 | // TODO : Show elapsed time and frames per second on toolbar of window
42 | var startTime = remember { mutableStateOf(0L) }
43 | val previousTime = remember { mutableStateOf(System.nanoTime()) }
44 | Canvas(modifier = Modifier.fillMaxSize().background(Color.Black).then(modifier)) {
45 | val stepFrame = dt.value
46 | sketch(this)
47 | }
48 |
49 | if (!stopLoop) {
50 | requestAnimationFrame(dt, previousTime)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
6 | }
7 | }
8 | rootProject.name = "k5-compose-examples"
9 | include("k5-compose")
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.ui.ExperimentalComposeUiApi
3 | import androidx.compose.ui.graphics.ExperimentalGraphicsApi
4 | import examples.art.showTriangularMesh
5 |
6 | @ExperimentalGraphicsApi
7 | @ExperimentalComposeUiApi
8 | fun main() {
9 | // simpleRandomWalk()
10 | // levyFlightWalker()
11 | // simpleMover()
12 | // bouncingBall()
13 |
14 | // gravitationalPull()
15 |
16 | // rotateRectangle()
17 |
18 | // gameOfLife()
19 | // chainLoop()
20 | // particleJs()
21 | // parametricEquation()
22 | // simplexNoise()
23 | // simulateSineWave()
24 |
25 | // simulateRecursion()
26 | // preserveDrawBufferTest()
27 |
28 | // showStarField()
29 | // showPhyllotaxis()
30 | // theTenPrint()
31 | // simplexNoise()
32 | // showYarns()
33 | // showBlackHole()
34 | // showFireWorks()
35 | // showCircleLoop()
36 | // showRain()
37 | // showSkyline()
38 | // showRotatingSquares()
39 |
40 | // showMatrixGreenRain()
41 |
42 | // showRecursiveTree()
43 | // showWaveMaker()
44 | // showCircleGrid()
45 | // showAbstractDots()
46 | // showVoid()
47 | // showHearts()
48 | // simpleGame()
49 | // mandelbrot()
50 | // perlinnoise1d()
51 | // perlinnoise2d()
52 | // classicyarnanimated()
53 | // danceYarnMouse()
54 | // danceYarnAuto()
55 | // landscapeInspection()
56 | // showPietMondrian()
57 | // hypnoticSquares()
58 | // showCirclePacking()
59 | showTriangularMesh()
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/angularmotion/Rotation.kt:
--------------------------------------------------------------------------------
1 | package examples.angularmotion
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.geometry.Size
7 | import androidx.compose.ui.geometry.center
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.graphics.drawscope.rotateRad
10 | import androidx.compose.ui.input.pointer.pointerMoveFilter
11 | import k5
12 | import math.Vector2D
13 | import math.constrain
14 | import math.map
15 | import math.toOffSet
16 |
17 | @OptIn(ExperimentalComposeUiApi::class)
18 | fun rotateRectangle() = k5 {
19 |
20 | var angle = 0f
21 | var angularVelocity = 0f
22 | var angularAcc = 0.001f
23 |
24 | val position = Vector2D(dimensFloat.width / 2, dimensFloat.height / 2).toOffSet()
25 |
26 | var mouseX = 0f
27 | show(
28 | modifier = Modifier.pointerMoveFilter(onMove = {
29 | mouseX = it.x
30 | false
31 | })
32 | ) { drawScope ->
33 |
34 | // Map the angular acceleration between -0.01f to 0.01f.
35 | // Map function basically re-maps a number from one range to another.
36 | angularAcc = map(mouseX, 0f, dimensFloat.width, -0.01f, 0.01f)
37 |
38 | // add constrain on the angular velocity value between given low and high
39 | angularVelocity = constrain(angularVelocity, -0.2f, 0.2f)
40 |
41 | drawScope.rotateRad(angle, pivot = drawScope.size.center) {
42 | drawScope.drawRect(Color.Cyan, topLeft = Offset(position.x - 30, position.y - 90), size = Size(60f, 180f))
43 | }
44 |
45 | // THe concept of angular velocity and angular acceleration works the same way!
46 | // add angular velocity to angular acceleration
47 | angularVelocity += angularAcc
48 | // Add angular velocity to angle
49 | angle += angularVelocity
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/art/circle_packing.kt:
--------------------------------------------------------------------------------
1 | package examples.art
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Brush
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.drawscope.DrawScope
7 | import androidx.compose.ui.graphics.drawscope.Stroke
8 | import k5
9 | import math.random
10 | import kotlin.math.sqrt
11 |
12 | data class Circle(var x: Float, var y: Float, var radius: Float)
13 |
14 | fun showCirclePacking() = k5 {
15 |
16 | val colors =
17 | listOf(Color.Gray, Color.Red, Color.Blue, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White)
18 | val circles = mutableListOf()
19 | val minRadius = 10
20 | val maxRadius = 300
21 | val totalCircles = 300
22 | val createCircleAttempts = 500
23 |
24 | fun doesCircleHaveCollision(circle: Circle): Boolean {
25 | circles.forEach { other ->
26 | val a = circle.radius + other.radius
27 | val x = circle.x - other.x
28 | val y = circle.y - other.y
29 | if (a >= sqrt(((x * x) + (y * y)))) {
30 | return true
31 | }
32 | }
33 |
34 | if (circle.x + circle.radius >= dimensFloat.width ||
35 | circle.x - circle.radius <= 0
36 | ) {
37 | return true
38 | }
39 |
40 | if (circle.y + circle.radius >= dimensFloat.height ||
41 | circle.y - circle.radius <= 0
42 | ) {
43 | return true
44 | }
45 |
46 | return false
47 | }
48 |
49 | fun createAndDrawCircle(drawScope: DrawScope) {
50 |
51 | lateinit var newCircle: Circle
52 | var circleSafeToDraw = false
53 |
54 | for (tries in 0 until createCircleAttempts) {
55 | newCircle =
56 | Circle(
57 | x = (5f..dimensFloat.width).random(),
58 | y = (5f..dimensFloat.height).random(),
59 | radius = minRadius.toFloat()
60 | )
61 |
62 | if (doesCircleHaveCollision(newCircle)) {
63 | continue
64 | } else {
65 | circleSafeToDraw = true
66 | break
67 | }
68 | }
69 |
70 | if (circleSafeToDraw.not()) {
71 | return
72 | }
73 |
74 | for (radiusSize in minRadius until maxRadius) {
75 | newCircle.radius = radiusSize.toFloat()
76 | if (doesCircleHaveCollision(newCircle)) {
77 | newCircle.radius--
78 | break
79 | }
80 | }
81 |
82 | circles.add(newCircle)
83 | drawScope.drawCircle(
84 | Brush.linearGradient(colors),
85 | newCircle.radius,
86 | Offset(newCircle.x, newCircle.y),
87 | style = Stroke(width = 2f)
88 | )
89 | }
90 |
91 | noLoop()
92 | show {
93 | for (i in 1..totalCircles) {
94 | createAndDrawCircle(it)
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/art/piet_mondrian.kt:
--------------------------------------------------------------------------------
1 | package examples.art
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.Slider
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.geometry.Offset
12 | import androidx.compose.ui.geometry.Size
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.drawscope.DrawScope
15 | import androidx.compose.ui.graphics.drawscope.Fill
16 | import androidx.compose.ui.graphics.drawscope.Stroke
17 | import androidx.compose.ui.text.font.FontFamily
18 | import androidx.compose.ui.text.font.FontWeight
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.unit.sp
22 | import k5
23 | import kotlin.random.Random
24 |
25 | data class Square(
26 | val x: Float,
27 | val y: Float,
28 | val width: Float,
29 | val height: Float,
30 | var color: Color = Color.White
31 | ) {
32 |
33 | fun show(drawScope: DrawScope) {
34 | drawScope.drawRect(
35 | color = Color.Black,
36 | topLeft = Offset(x, y),
37 | size = Size(width, height),
38 | style = Stroke(width = 30f)
39 | )
40 | drawScope.drawRect(
41 | color = color,
42 | topLeft = Offset(x, y),
43 | size = Size(width, height),
44 | style = Fill
45 | )
46 | }
47 | }
48 |
49 | fun showPietMondrian() = k5 {
50 |
51 | noLoop()
52 | var stepDivider by mutableStateOf(8f)
53 | var white = 0xffF2F5F1
54 | var colors = listOf(Color.Blue, Color.Black, Color.Yellow, Color.Red)
55 |
56 | val squares = mutableListOf()
57 |
58 | fun flushAndAdd() {
59 | squares.clear()
60 | squares.add(Square(0f, 0f, dimensFloat.width, dimensFloat.height))
61 | }
62 | flushAndAdd()
63 |
64 | fun splitOnX(square: Square, splitAt: Float) {
65 | val squareA = Square(
66 | x = square.x,
67 | y = square.y,
68 | width = square.width - (square.width - splitAt + square.x),
69 | height = square.height
70 | )
71 |
72 | val squareB = Square(
73 | x = splitAt,
74 | y = square.y,
75 | width = square.width - splitAt + square.x,
76 | height = square.height
77 | )
78 |
79 | squares.add(squareA)
80 | squares.add(squareB)
81 | }
82 |
83 | fun splitOnY(square: Square, splitAt: Float) {
84 | val squareA = Square(
85 | x = square.x,
86 | y = square.y,
87 | width = square.width,
88 | height = square.height - (square.height - splitAt + square.y)
89 | )
90 |
91 | val squareB = Square(
92 | x = square.x,
93 | y = splitAt,
94 | width = square.width,
95 | height = square.height - splitAt + square.y
96 | )
97 |
98 | squares.add(squareA)
99 | squares.add(squareB)
100 | }
101 |
102 | fun splitSquaresWith(x: Float, y: Float) {
103 | var i = squares.size - 1
104 | while (i >= 0) {
105 | val square = squares[i]
106 | if (x > 0f && x > square.x && x < square.x + square.width) {
107 | if (Random.nextFloat() > 0.5f) {
108 | squares.removeAt(i)
109 | splitOnX(square, x)
110 | }
111 | }
112 | if (y > 0f && y > square.y && y < square.y + square.height) {
113 | if (Random.nextFloat() > 0.5f) {
114 | squares.removeAt(i)
115 | splitOnY(square, y)
116 | }
117 | }
118 | i--
119 | }
120 | }
121 |
122 | fun generateSquares() {
123 | var step = dimensInt.width / stepDivider
124 | for (i in 0 until dimensInt.width step step.toInt()) {
125 | splitSquaresWith(0f, i.toFloat())
126 | splitSquaresWith(i.toFloat(), 0f)
127 | }
128 | }
129 |
130 | generateSquares()
131 | showWithControls(modifier = Modifier.background(Color(white)), controls = {
132 | Text(
133 | text = "Piet Mondrian Artwork",
134 | fontFamily = FontFamily.Cursive,
135 | fontWeight = FontWeight.ExtraBold,
136 | textAlign = TextAlign.Center,
137 | fontSize = 30.sp,
138 | modifier = Modifier.padding(12.dp)
139 | )
140 | Text("Squares' factor - ${stepDivider.toInt()}")
141 | Slider(
142 | value = stepDivider,
143 | onValueChange =
144 | {
145 | flushAndAdd()
146 | stepDivider = it
147 | generateSquares()
148 | },
149 | valueRange = 8f..16f
150 | )
151 | }) { scope ->
152 |
153 | for (i in 0..stepDivider.toInt()) {
154 | squares[(0 until squares.size).random()].color = colors[(colors.indices).random()]
155 | }
156 | squares.forEach {
157 | it.show(scope)
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/art/triangular_mesh.kt:
--------------------------------------------------------------------------------
1 | package examples.art
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.Path
5 | import androidx.compose.ui.graphics.drawscope.DrawScope
6 | import k5
7 | import math.Vector2D
8 | import kotlin.random.Random
9 |
10 | fun showTriangularMesh() = k5 {
11 |
12 | lateinit var line: MutableList
13 | val grid = mutableListOf>()
14 | var odd = false
15 |
16 | val gap = dimensInt.width / 9
17 |
18 | noLoop()
19 | val gray = listOf(0xff808080, 0xff888888, 0xff909090, 0xff989898, 0xffA0A0A0, 0xffA8A8A8)
20 | val colors = listOf(Color.Gray, Color.Red, Color.Blue, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White)
21 | val lightColors = listOf(0xff55efc4, 0xff81ecec, 0xffa29bfe, 0xffdfe6e9, 0xffffeaa7, 0xffff7675, 0xffe17055, 0xff0984e3, 0xffe84393)
22 | val ltColors = listOf(0xfff1c40f, 0xffe67e22, 0xffe74c3c, 0xff3498db, 0xff9b59b6, 0xff34495e, 0xff2ecc71)
23 |
24 | fun DrawScope.drawTriangle(p1: Vector2D, p2: Vector2D, p3: Vector2D) {
25 | val path = Path()
26 | path.moveTo(p1.x, p1.y)
27 | path.lineTo(p2.x, p2.y)
28 | path.lineTo(p3.x, p3.y)
29 | path.lineTo(p1.x, p1.y)
30 | path.close()
31 | this.drawPath(path, Color(ltColors[(ltColors.indices).random()]))
32 | }
33 |
34 | show {
35 |
36 | for (y in (gap / 2)..dimensInt.width step gap) {
37 | odd = !odd
38 | line = mutableListOf()
39 | for (x in (gap / 2)..dimensInt.width step gap) {
40 | val vector = Vector2D(x.toFloat() + if (odd) gap / 2 else 0, y.toFloat())
41 | line.add(
42 | Vector2D(
43 | x = x + (Random.nextFloat() * 0.8f - 0.4f) * gap + (if (odd) gap / 2 else 0),
44 | y = y + (Random.nextFloat() * 0.8f - 0.4f) * gap
45 | )
46 | // vector
47 | )
48 | }
49 | grid.add(line)
50 | }
51 |
52 | lateinit var dotLine: MutableList
53 | odd = true
54 | for (y in 0 until grid.size - 1) {
55 | odd = !odd
56 | dotLine = mutableListOf()
57 | for (i in 0 until grid[y].size) {
58 | dotLine.add(if (odd) grid[y][i] else grid[y + 1][i])
59 | dotLine.add(if (odd) grid[y + 1][i] else grid[y][i])
60 | }
61 | for (j in 0 until dotLine.size - 2) {
62 | it.drawTriangle(dotLine[j], dotLine[j + 1], dotLine[j + 2])
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/classicyarnanimated.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.Path
8 | import androidx.compose.ui.graphics.drawscope.Stroke
9 | import k5
10 | import math.noise1D
11 |
12 | var offset = 0.0
13 |
14 | fun classicyarnanimated() = k5 {
15 |
16 | show(modifier = Modifier.fillMaxSize().background(Color(0xFFFFA500))) {
17 | it.apply {
18 | val x0 = 2 * dimensFloat.width * noise1D(offset + 15)
19 | val x1 = 2 * dimensFloat.width * noise1D(offset + 25)
20 | val x2 = 2 * dimensFloat.width * noise1D(offset + 35)
21 | val x3 = 2 * dimensFloat.width * noise1D(offset + 45)
22 | val y0 = 2 * dimensFloat.height * noise1D(offset + 55)
23 | val y1 = 2 * dimensFloat.height * noise1D(offset + 65)
24 | val y2 = 2 * dimensFloat.height * noise1D(offset + 75)
25 | val y3 = 2 * dimensFloat.height * noise1D(offset + 85)
26 | val path = Path()
27 | path.moveTo(x0.toFloat(), y0.toFloat())
28 | path.cubicTo(
29 | x1.toFloat(), y1.toFloat(),
30 | x2.toFloat(), y2.toFloat(),
31 | x3.toFloat(), y3.toFloat()
32 | )
33 | drawPath(path, Color.Black, alpha = 0.5f, style = Stroke(width = 1f))
34 | }
35 |
36 | offset += 0.002
37 | if (offset > 10) {
38 | offset = 0.0
39 | print("reset \n")
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/danceYarnAuto.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.Path
7 | import androidx.compose.ui.graphics.drawscope.Stroke
8 | import androidx.compose.ui.input.pointer.pointerMoveFilter
9 | import k5
10 | import math.Vector2D
11 | import math.noise3D
12 | import math.plusAssign
13 | import java.lang.Float.max
14 | import java.lang.Float.min
15 | import kotlin.math.abs
16 |
17 | @OptIn(ExperimentalComposeUiApi::class)
18 | fun danceYarnAuto() = k5 {
19 | val loop = 10000
20 | val mouseVector = Vector2D()
21 | var m2d3dvec = 0
22 | var isUp = true
23 | show(
24 | modifier = Modifier
25 | .pointerMoveFilter(onMove = {
26 | mouseVector.x = it.x
27 | mouseVector.y = it.y
28 | false
29 | })
30 | ) {
31 | it.apply {
32 | when {
33 | isUp && m2d3dvec > loop * loop -> {
34 | isUp = false
35 | }
36 | isUp -> {
37 | m2d3dvec++
38 | }
39 | m2d3dvec <= 0 -> {
40 | isUp = true
41 | }
42 | else -> {
43 | m2d3dvec--
44 | }
45 | }
46 | var offset = 0.0
47 | mouseVector += Vector2D((-1..1).random().toFloat(), (-1..1).random().toFloat())
48 | val m2d = m2d3dvec / loop * 0.002
49 | val m3d = abs(m2d3dvec % loop - loop / 2) * 0.002
50 | for (i in 0 until max(0f, mouseVector.x - 2).toInt()) {
51 | fun noiseX(variant: Double) = 2 * dimensFloat.width * noise3D(variant, m2d, m3d)
52 | fun noiseY(variant: Double) = 2 * dimensFloat.height * noise3D(offset + variant, m2d, m3d)
53 | fun color(variant: Double) = 0xFF - (0xFF * noise3D(variant, m2d, m3d)).toInt()
54 | val x = FloatArray(4) { index -> noiseX(offset + 5 + index * 10).toFloat() }
55 | val y = FloatArray(5) { index -> noiseY(offset + 5 + index * 10).toFloat() }
56 | val path = Path()
57 | path.moveTo(x[0], y[0])
58 | path.cubicTo(x[1], y[1], x[2], y[2], x[2], y[2])
59 | val red = color(offset + 35)
60 | val green = color(offset + 25)
61 | val blue = color(offset + 15)
62 | drawPath(
63 | path,
64 | Color(red, green, blue),
65 | alpha = max(min(mouseVector.y / dimensFloat.height, 1f), 0f),
66 | style = Stroke(width = 0.3f)
67 | )
68 | offset += 0.002
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/danceYarnMouse.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.Path
7 | import androidx.compose.ui.graphics.drawscope.Stroke
8 | import androidx.compose.ui.input.pointer.pointerMoveFilter
9 | import k5
10 | import math.Vector2D
11 | import math.noise3D
12 |
13 | @OptIn(ExperimentalComposeUiApi::class)
14 | fun danceYarnMouse() = k5 {
15 | val mouseVector = Vector2D()
16 | show(
17 | modifier = Modifier
18 | .pointerMoveFilter(onMove = {
19 | mouseVector.x = it.x
20 | mouseVector.y = it.y
21 | false
22 | })
23 | ) {
24 | it.apply {
25 | var offset = 0.0
26 | val m2d = mouseVector.x * 0.002
27 | val m3d = mouseVector.y * 0.002
28 | for (i in 1000 until 1200) {
29 | fun noiseX(variant: Double) = 2 * dimensFloat.width * noise3D(variant, m2d, m3d)
30 | fun noiseY(variant: Double) = 2 * dimensFloat.height * noise3D(offset + variant, m2d, m3d)
31 | fun color(variant: Double) = 0xFF - (0xFF * noise3D(variant, m2d, m3d)).toInt()
32 | val x = FloatArray(3) { index -> noiseX(offset + 5 + index * 10).toFloat() }
33 | val y = FloatArray(3) { index -> noiseY(offset + 5 + index * 10).toFloat() }
34 | val path = Path()
35 | path.moveTo(mouseVector.x, mouseVector.y)
36 | path.cubicTo(x[0], y[0], x[1], y[1], x[2], y[2])
37 | val red = color(offset + 35)
38 | val green = color(offset + 25)
39 | val blue = color(offset + 15)
40 | drawPath(path, Color(red, green, blue), style = Stroke(width = 0.3f))
41 | offset += 0.002
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/landscapeInspection.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.PointMode
8 | import androidx.compose.ui.input.pointer.pointerMoveFilter
9 | import k5
10 | import math.Vector2D
11 | import math.noise2D
12 |
13 | @OptIn(ExperimentalComposeUiApi::class)
14 | fun landscapeInspection() {
15 | val loop = 1000
16 | val slices = 250
17 | val mouseInsensitivity = 50
18 | val mouseVector = Vector2D()
19 | k5 {
20 | show(
21 | modifier =
22 | Modifier.pointerMoveFilter(onMove = {
23 | mouseVector.x = it.x
24 | mouseVector.y = it.y
25 | false
26 | })
27 | ) {
28 | for (offset in 0 until slices) {
29 | for (x in 0 until loop) {
30 | val noiseInputX = x * 0.002
31 | val noiseInputY = offset * 0.02
32 | val valueY = dimensFloat.height - noise2D(noiseInputX, noiseInputY) * dimensFloat.height
33 | val colorRed = (noise2D(noiseInputX, noiseInputY) * 0xFF).toInt()
34 | val colorGreen = (noise2D(noiseInputX, noiseInputY + 10) * 0xFF).toInt()
35 | val colorBlue = (noise2D(noiseInputX, noiseInputY + 20) * 0xFF).toInt()
36 |
37 | val pointVector2D = Vector2D(x.toFloat() / loop * dimensFloat.width, valueY.toFloat())
38 | it.drawPoints(
39 | listOf(
40 | Offset(
41 | pointVector2D.x + ((mouseVector.x - dimensFloat.width / 2) / mouseInsensitivity) * (offset - slices / 2),
42 | pointVector2D.y + ((mouseVector.y) / mouseInsensitivity) * (offset - slices / 2)
43 | )
44 | ),
45 | PointMode.Points,
46 | Color(colorRed, colorGreen, colorBlue, 0xFF),
47 | 2f
48 | )
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/mandelbrot.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.PointMode
6 | import k5
7 |
8 | private const val MAX_ITER = 200
9 | private const val ZOOM = 200
10 |
11 | fun mandelbrot() {
12 | k5 {
13 | noLoop()
14 | show {
15 | for (y in 0 until it.size.height.toInt()) {
16 | for (x in 0 until it.size.width.toInt()) {
17 | var zx = 0.0
18 | var zy = 0.0
19 | val cX = (x - it.size.width / 2) / ZOOM
20 | val cY = (y - it.size.height / 2) / ZOOM
21 | var iter = MAX_ITER
22 | while (zx * zx + zy * zy < 4.0 && iter > 0) {
23 | val tmp = zx * zx - zy * zy + cX
24 | zy = 2.0 * zx * zy + cY
25 | zx = tmp
26 | iter--
27 | }
28 |
29 | it.drawPoints(
30 | listOf(Offset(x.toFloat(), y.toFloat())),
31 | PointMode.Points,
32 | Color(
33 | ((x / it.size.width) * 0xFF).toInt(),
34 | ((y / it.size.height) * 0xFF).toInt(),
35 | (
36 | (iter or (iter shl 7)).toFloat() /
37 | ((MAX_ITER or MAX_ITER shl 7)) * 0xFF
38 | ).toInt()
39 | ),
40 | 1f
41 | )
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/perlinnoise1d.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.PointMode
6 | import k5
7 | import math.noise1D
8 |
9 | fun perlinnoise1d() {
10 | val loop = 3000
11 | k5 {
12 | noLoop()
13 | show {
14 | for (offset in 0 until 10) {
15 | val colorRed = (0..255).random()
16 | val colorGreen = (0..255).random()
17 | val colorBlue = (0..255).random()
18 | for (x in 0 until loop) {
19 | val noiseInputX = x * 0.002 + offset * 10
20 | val valueY = noise1D((noiseInputX)) * dimensFloat.height * 2
21 |
22 | it.drawPoints(
23 | listOf(Offset(x.toFloat() / loop * dimensFloat.width, valueY.toFloat())),
24 | PointMode.Points,
25 | Color(colorRed, colorGreen, colorBlue, 0x88),
26 | 2f
27 | )
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/perlinnoise2d.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.PointMode
6 | import k5
7 | import math.noise2D
8 |
9 | fun perlinnoise2d() {
10 | val loop = 3000
11 | k5 {
12 | noLoop()
13 | show {
14 | for (offset in 0 until loop) {
15 | val colorRed = (0..255).random()
16 | val colorGreen = (0..255).random()
17 | val colorBlue = (0..255).random()
18 | for (x in 0 until loop) {
19 | val noiseInputX = x * 0.002
20 | val noiseInputY = offset * 0.002
21 | val valueY = noise2D(noiseInputX, noiseInputY) * dimensFloat.height * 2
22 |
23 | it.drawPoints(
24 | listOf(Offset(x.toFloat() / loop * dimensFloat.width, valueY.toFloat())),
25 | PointMode.Points,
26 | Color(colorRed, colorGreen, colorBlue, 0x88),
27 | 2f
28 | )
29 | }
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/elye/simplegame.kt:
--------------------------------------------------------------------------------
1 | package examples.elye
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.input.pointer.pointerMoveFilter
7 | import k5
8 | import math.Vector2D
9 | import math.limit
10 | import math.plusAssign
11 | import math.random
12 | import math.set
13 | import math.sub
14 | import math.toOffSet
15 |
16 | private const val BALL_RADIUS = 30f
17 |
18 | @OptIn(ExperimentalComposeUiApi::class)
19 | fun simpleGame() = k5 {
20 | var velocity = 5f
21 | val vehicleY = dimensFloat.height - 250f
22 | val vehicleStartX = dimensFloat.width / 2
23 | val vehiclePosition = Vector2D(vehicleStartX, vehicleY)
24 | val mouseVector = Vector2D(vehicleStartX, vehicleY)
25 | fun resetStarX() = (40 until dimensInt.width - 40).random().toFloat()
26 | val starPosition = Vector2D(resetStarX(), 0f)
27 | fun resetStar() { starPosition.set(Vector2D(resetStarX(), 0f)) }
28 | fun hitEndPoint() = starPosition.y >= dimensFloat.height
29 | fun moveStar() { starPosition += Vector2D(0f, velocity) }
30 | fun collide() =
31 | (kotlin.math.abs(starPosition.x - vehiclePosition.x) < BALL_RADIUS * 2) &&
32 | (kotlin.math.abs(starPosition.y - vehiclePosition.y) < BALL_RADIUS * 2)
33 | show(
34 | modifier =
35 | Modifier.pointerMoveFilter(onMove = {
36 | mouseVector.x = it.x
37 | mouseVector.y = vehicleY
38 | false
39 | })
40 | ) {
41 | vehiclePosition += mouseVector.copy().sub(vehiclePosition).limit(5f)
42 |
43 | when {
44 | hitEndPoint() -> noLoop()
45 | collide() -> { resetStar(); velocity++ }
46 | else -> moveStar()
47 | }
48 | it.drawCircle(color = Color.Red, radius = BALL_RADIUS, center = starPosition.toOffSet())
49 | it.drawCircle(color = Color.White, radius = BALL_RADIUS, center = vehiclePosition.toOffSet())
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/forces/Ball.kt:
--------------------------------------------------------------------------------
1 | package examples.forces
2 |
3 | import androidx.compose.ui.geometry.Size
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.drawscope.DrawScope
6 | import math.Vector2D
7 | import math.add
8 | import math.divide
9 | import math.magSq
10 | import math.multiply
11 | import math.normalize
12 | import math.setMag
13 | import math.toOffSet
14 | import kotlin.math.sqrt
15 |
16 | /**
17 | * Represents a simple ball object
18 | */
19 | data class Ball(
20 | var x: Float,
21 | var y: Float,
22 | var m: Float,
23 | var ballMassFactor: Float,
24 | var coeffOfDrag: Float,
25 | var playgroundSize: Size
26 | ) {
27 |
28 | val height = playgroundSize.height
29 | val width = playgroundSize.width
30 |
31 | // Position of ball
32 | val position = Vector2D(x, y)
33 |
34 | // create a random unit velocity vector for ball
35 | val velocity = Vector2D(0f, 0f)
36 | val acceleration = Vector2D(0f, 0f)
37 |
38 | val mass = m * ballMassFactor
39 |
40 | // Create the radius of the object based on the mass. The more the mass, the more the size of the ball
41 | val radius = sqrt(mass) * 10
42 | val diameter = radius * 2
43 |
44 | /**
45 | * Applies the force over an object
46 | *
47 | * The application of force is basically euler's integration. Where instead of calculating derivatives of position to get velocity,
48 | * the velocity derivative to get acceleration and so on..
49 | * Euler's way is the opposite way of calculating position by integrating velocity, velocity by integrating acceleration and so on.
50 | * Thus this way you are adding all the vectors to position one by one
51 | * This is easy to understand but not a perfect integration for realy physics simulation
52 | *
53 | * Force = Mass * Acceleration
54 | * But if Mass is unit, the Force = Acceleration
55 | *
56 | * The net force being applied on the body is the sum of all forces. That's why we are adding all the forces to acceleration
57 | *
58 | * If [force = mass * acceleration] and considering the ball has some mass,
59 | * acceleration = force / mass
60 | * Thus, more the mass [or consider it as weight force] of an object, lesser will it move.
61 | */
62 | fun applyForce(force: Vector2D) {
63 | val f = force.divide(mass)
64 | acceleration.add(f)
65 | }
66 |
67 | /**
68 | * Friction force which is applied when body comes in contact with a surface
69 | * The magnitude of friction force according to formula is F = (-1) * `mu` * N * (->)velocity_unit_Vector
70 | * mu - a greek letter mu stands for friction coefficient and N is the constant based on the mass of the body
71 | */
72 | fun friction() {
73 | val diff = height - (position.y + radius)
74 | if (diff < 1) {
75 |
76 | // Direction of the friction is opposite
77 | val friction = velocity.copy()
78 | friction.normalize()
79 | friction.multiply(-1f)
80 |
81 | // Magnitude of friction
82 | val normal = mass
83 | val mu = 0.2f
84 | friction.setMag(mu * normal)
85 |
86 | // Apply the friction force
87 | applyForce(friction)
88 |
89 | // If we consider this idea - eventually it's reducing the velocity of body so
90 | // a shortcut to friction could be just remove some percent of velocity
91 |
92 | // velocity.multiply(0.95f)
93 | }
94 | }
95 |
96 | /**
97 | * Drag force - https://en.wikipedia.org/wiki/Drag_(physics)
98 | * Drag force is a force due to air, or any fluid substance resisting on a body in a medium
99 | *
100 | */
101 | fun drag() {
102 | // Direction of the drag
103 | val drag = velocity.copy()
104 | drag.normalize()
105 | drag.multiply(-1f)
106 |
107 | // Coefficient of drag - depends on medium
108 | val coefficientOfDrag = coeffOfDrag
109 |
110 | // magSq() returns magnitude square
111 | val speedSq = velocity.magSq()
112 | // set the magnitude
113 | drag.setMag(coefficientOfDrag * speedSq)
114 | // apply the drag force
115 | applyForce(drag)
116 | }
117 |
118 | /**
119 | * Calculating the edges so that ball doesn't bounce off the window
120 | */
121 | fun edges() {
122 | if (position.y >= height - radius) {
123 | position.y = height - radius
124 | velocity.y *= -1
125 | }
126 | if (position.x >= width - radius) {
127 | position.x = width - radius
128 | velocity.x *= -1
129 | } else if (position.x <= radius) {
130 | position.x = radius
131 | velocity.x *= -1
132 | }
133 | }
134 |
135 | fun update() {
136 | // Add acceleration to velocity
137 | velocity.add(acceleration)
138 |
139 | // finally, add velocity to position
140 | position.add(velocity)
141 |
142 | // set the acceleration to zero, because at a given point in time we want to uniformly apply forces
143 | // acting on a body
144 | acceleration.multiply(0f)
145 | }
146 |
147 | // this draws a circle
148 | fun render(drawScope: DrawScope) {
149 | drawScope.drawCircle(Color.White, radius, position.toOffSet())
150 | // drawScope.drawOval(Color.White, position.toOffSet(), Size(ballOvalWidth, ballOvalHeight), alpha = 0.6f)
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/forces/BouncingBall.kt:
--------------------------------------------------------------------------------
1 | package examples.forces
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.combinedClickable
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.Checkbox
8 | import androidx.compose.material.Slider
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.geometry.Offset
15 | import androidx.compose.ui.geometry.Size
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.graphics.drawscope.DrawScope
18 | import androidx.compose.ui.unit.dp
19 | import k5
20 | import math.Vector2D
21 | import math.multiply
22 | import math.random
23 |
24 | @OptIn(ExperimentalFoundationApi::class)
25 | fun bouncingBall() = k5 {
26 |
27 | var enableWaterDrag by mutableStateOf(false)
28 | var enableFriction by mutableStateOf(false)
29 | var gravity by mutableStateOf(0.2f)
30 | var ballMassFactor by mutableStateOf(1f)
31 | var coeffOfDrag by mutableStateOf(0.3f)
32 |
33 | val playgroundSize = dimensFloat
34 |
35 | val balls = List(5) {
36 | Ball(
37 | (2f..dimensFloat.width).random(),
38 | 6f,
39 | m = (8f..50f).random(),
40 | ballMassFactor = ballMassFactor,
41 | coeffOfDrag = coeffOfDrag,
42 | playgroundSize
43 | )
44 | }
45 |
46 | fun showDrag(ball: Ball) {
47 | if (ball.position.y >= playgroundSize.height / 2) {
48 | ball.drag()
49 | }
50 | }
51 |
52 | fun showWater(scope: DrawScope) {
53 | scope.drawRect(
54 | color = Color.Blue,
55 | topLeft = Offset(0f, playgroundSize.height / 2),
56 | size = Size(playgroundSize.width, playgroundSize.height / 2)
57 | )
58 | }
59 |
60 | showWithControls(
61 | modifier = Modifier.combinedClickable(
62 | onClick = {
63 | /**
64 | * On mouse click, apply wind force in right direction
65 | */
66 | val wind = Vector2D(50f, 0f)
67 | balls.forEach { it.applyForce(wind) }
68 | }
69 | ),
70 | controls = {
71 | Row(modifier = Modifier.padding(4.dp)) {
72 | Checkbox(checked = enableWaterDrag, onCheckedChange = { enableWaterDrag = it })
73 | Text("Enable water drag")
74 | }
75 | Row(modifier = Modifier.padding(4.dp)) {
76 | Checkbox(checked = enableFriction, onCheckedChange = { enableFriction = it })
77 | Text("Enable friction force")
78 | }
79 |
80 | Text(modifier = Modifier.padding(top = 4.dp), text = "Gravity")
81 | Slider(value = gravity, onValueChange = { gravity = it })
82 |
83 | Text(text = "Coefficient of Drag")
84 | Slider(value = coeffOfDrag, onValueChange = { coeffOfDrag = it })
85 |
86 | Text(text = "Ball's mass")
87 | Slider(value = ballMassFactor, onValueChange = { ballMassFactor = it })
88 | }
89 | ) { scope ->
90 | // Apply gravity force every time on ball
91 | val gravityVector = Vector2D(0f, gravity)
92 |
93 | /*
94 | * This will create some sort of water and air medium. When ball will hit the water
95 | * it'll experience drag force
96 | */
97 | if (enableWaterDrag) {
98 | showWater(scope)
99 | }
100 |
101 | balls.forEach { ball ->
102 | val weight = gravityVector.multiply(ball.mass)
103 | ball.applyForce(weight)
104 |
105 | if (enableFriction) {
106 | ball.friction()
107 | }
108 |
109 | /*
110 | * This will create some sort of water and air medium. When ball will hit the water
111 | * it'll experience drag force
112 | */
113 | if (enableWaterDrag) {
114 | showDrag(ball)
115 | }
116 |
117 | ball.update()
118 | ball.edges()
119 | ball.render(scope)
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/forces/GravitationalAttraction.kt:
--------------------------------------------------------------------------------
1 | package examples.forces
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.drawscope.DrawScope
7 | import androidx.compose.ui.input.pointer.pointerMoveFilter
8 | import k5
9 | import math.Vector2D
10 | import math.constrain
11 | import math.divide
12 | import math.k5Random
13 | import math.magSq
14 | import math.plusAssign
15 | import math.set
16 | import math.setMag
17 | import math.sub
18 | import math.timesAssign
19 | import math.toOffSet
20 | import kotlin.math.sqrt
21 | import kotlin.random.Random
22 |
23 | data class Moon(val x: Float, val y: Float, val m: Float) {
24 | val position = Vector2D(x, y)
25 | val velocity = Vector2D.randomVector()
26 | val acceleration = Vector2D(0f, 0f)
27 | val mass = m
28 | val r = sqrt(mass) * 2
29 |
30 | init {
31 | velocity *= 10f
32 | }
33 |
34 | fun applyForce(force: Vector2D) {
35 | val f = force.divide(mass)
36 | acceleration += f
37 | }
38 |
39 | fun update() {
40 | velocity += acceleration
41 | position += velocity
42 | acceleration *= 0f
43 | }
44 |
45 | fun render(drawScope: DrawScope) {
46 | drawScope.drawCircle(Color.White, r, position.toOffSet())
47 | }
48 | }
49 |
50 | data class Attractor(val x: Float, val y: Float, val m: Float) {
51 |
52 | val position = Vector2D(x, y)
53 | val mass = m
54 | val radius = sqrt(mass) * 2
55 |
56 | fun attract(ball: Moon) {
57 | val attractorPosition = position.copy()
58 | val force = attractorPosition.sub(ball.position)
59 | val distanceSq = constrain(force.magSq(), 100f, 1000f)
60 |
61 | val G = 5 // Universal Gravitational Constant
62 |
63 | val gPull = G * (mass * ball.mass) / distanceSq
64 |
65 | force.setMag(gPull)
66 |
67 | ball.applyForce(force)
68 | }
69 |
70 | fun render(drawScope: DrawScope) {
71 | drawScope.drawCircle(Color.Magenta, radius, position.toOffSet())
72 | }
73 | }
74 |
75 | @OptIn(ExperimentalComposeUiApi::class)
76 | fun gravitationalPull() = k5 {
77 |
78 | val moons = List(15) {
79 | Moon(
80 | k5Random(50, dimensFloat.width.toInt()),
81 | k5Random(50, dimensFloat.height.toInt()),
82 | Random.nextInt(30, 100).toFloat()
83 | )
84 | }
85 |
86 | val attractor = Attractor(dimensFloat.width / 2, dimensFloat.height / 2, 600f)
87 |
88 | show(
89 | modifier = Modifier.pointerMoveFilter(
90 | onMove = {
91 | attractor.position.set(Vector2D(it.x, it.y))
92 | false
93 | }
94 | )
95 | ) { drawScope ->
96 | attractor.render(drawScope)
97 | for (moon in moons) {
98 | moon.update()
99 | moon.render(drawScope)
100 | attractor.attract(moon)
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/kinematics/SimpleMover.kt:
--------------------------------------------------------------------------------
1 | package examples.kinematics
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Size
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.drawscope.DrawScope
8 | import androidx.compose.ui.input.pointer.pointerMoveFilter
9 | import k5
10 | import math.Vector2D
11 | import math.limit
12 | import math.plusAssign
13 | import math.scalarMultiply
14 | import math.setMag
15 | import math.sub
16 | import math.toOffSet
17 | import kotlin.random.Random
18 |
19 | /**
20 | * Simple mover class
21 | *
22 | * This mover will represent a circle which will move according to the
23 | * velocity and acceleration that we'll assign to it.
24 | *
25 | * We'll also use mouse pointer to alter the velocity and acceleration of mover
26 | *
27 | * You can play with it by tweaking any of the values as you like :p
28 | */
29 | data class Mover(
30 | var x: Float,
31 | var y: Float
32 | ) {
33 | // Position of mover
34 | val position = Vector2D(x, y)
35 |
36 | // create a random unit velocity vector for mover
37 | val velocity = Vector2D.randomVector()
38 |
39 | init {
40 | // scaling the velocity by some constant factor
41 | velocity.scalarMultiply(Random.nextInt(3).toFloat())
42 | }
43 |
44 | fun update(mouse: Vector2D) {
45 | // create acceleration vector by subtracting position from mouse pointer position
46 | val acceleration = mouse.sub(position)
47 | // setting magnitude of acceleration to constant value
48 | acceleration.setMag(1f)
49 |
50 | /**
51 | * In order to understand how velocity, acceleration applies in 2d physics engine
52 | * I would recommend watching the Daniel Shiffman's Nature of code videos with p5.js
53 | * https://www.youtube.com/playlist?list=PLRqwX-V7Uu6ZV4yEcW3uDwOgGXKUUsPOM
54 | */
55 |
56 | // Add acceleration to velocity
57 | velocity += acceleration
58 | // set limit to velocity some constant value, so it won't bounce off
59 | velocity.limit(5f)
60 | // finally, add velocity to position
61 | position += velocity
62 | }
63 |
64 | // this draws a circle
65 | fun render(drawScope: DrawScope) {
66 | drawScope.drawOval(Color.White, position.toOffSet(), Size(30f, 30f))
67 | }
68 | }
69 |
70 | @OptIn(ExperimentalComposeUiApi::class)
71 | fun simpleMover() = k5 {
72 |
73 | // Create a mover object with initial position
74 | val mover = Mover(400f, 400f)
75 |
76 | // create a mouse vector for mouse pointer position
77 | val mouse = Vector2D(0f, 0f)
78 |
79 | show(
80 | Modifier.pointerMoveFilter(
81 | // get mouse pointer location/position
82 | onMove = {
83 | mouse.x = it.x
84 | mouse.y = it.y
85 | false
86 | }
87 | )
88 | ) { scope ->
89 | // update the mover
90 | mover.update(mouse)
91 | // render the mover
92 | mover.render(scope)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/mathematics/circle-loop.kt:
--------------------------------------------------------------------------------
1 | package examples.mathematics
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.drawscope.Stroke
7 | import androidx.compose.ui.graphics.drawscope.translate
8 | import k5
9 | import math.AngleMode
10 | import math.PI
11 | import math.angleMode
12 | import kotlin.math.cos
13 | import kotlin.math.sin
14 |
15 | fun showCircleLoop() = k5 {
16 |
17 | angleMode = AngleMode.DEGREES
18 | noLoop()
19 | val radius = 100f
20 |
21 | show {
22 |
23 | it.apply {
24 | var theta = 0.0
25 | var r = radius.toDouble()
26 | for (i in 0..1000) {
27 | translate(
28 | this.size.width / 2 - r.toFloat() / 2,
29 | this.size.height / 2 - r.toFloat() / 2
30 | ) {
31 | val x = radius * cos(Math.toRadians(theta))
32 | val y = radius * sin(Math.toRadians(theta * 2))
33 |
34 | drawOval(
35 | color = Color(0x1EFFA500),
36 | topLeft = Offset(x.toFloat(), y.toFloat()),
37 | size = Size(r.toFloat(), r.toFloat()),
38 | style = Stroke(width = 1.0f)
39 | )
40 | }
41 | r += cos(theta) * sin(theta / 2) + sin(theta) * cos(theta / 2)
42 | theta += PI / 2
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/mathematics/parametric-equation.kt:
--------------------------------------------------------------------------------
1 | package examples.mathematics
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.drawscope.translate
8 | import k5
9 | import kotlin.math.cos
10 | import kotlin.math.sin
11 |
12 | fun parametricEquation() = k5 {
13 |
14 | var theta = 0f
15 |
16 | fun x1(t: Float) = sin(t / 10f) * 125f + sin(t / 20) * 125f + sin(t / 30f) * 125f
17 |
18 | fun y1(t: Float) = cos(t / 10f) * 125f + cos(t / 20f) * 125f + cos(t / 30f) * 125f
19 |
20 | fun x2(t: Float) = sin(t / 15f) * 125f + sin(t / 25f) * 125f + sin(t / 35f) * 125f
21 |
22 | fun y2(t: Float) = cos(t / 15f) * 125f + cos(t / 25f) * 125f + cos(t / 35f) * 125f
23 |
24 | show(Modifier.background(Color.White)) { scope ->
25 | scope.translate(dimensFloat.width / 2, dimensFloat.height / 2) {
26 | for (i in 0..100) {
27 | this.drawLine(
28 | Color.Black,
29 | Offset(x1(theta + i), y1(theta + i)),
30 | Offset(x2(theta + i) + 50, y2(theta + i) + 50)
31 | )
32 | theta += 0.006f
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/mathematics/recursion.kt:
--------------------------------------------------------------------------------
1 | package examples.mathematics
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 androidx.compose.ui.graphics.drawscope.Stroke
7 | import k5
8 |
9 | fun simulateRecursion() = k5 {
10 |
11 | noLoop()
12 |
13 | show {
14 | it.showCircle(dimensFloat.width / 2, dimensFloat.height / 2, 500f)
15 | }
16 | }
17 |
18 | fun DrawScope.showCircle(x: Float, y: Float, d: Float) {
19 | this.drawCircle(Color.White, d / 2, Offset(x, y), style = Stroke(5f))
20 | if (d > 2) {
21 | val newD = d * 0.5f
22 | showCircle(x + newD, y, newD)
23 | showCircle(x - newD, y, newD)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/mathematics/ribbon.kt:
--------------------------------------------------------------------------------
1 | package examples.mathematics
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.ui.ExperimentalComposeUiApi
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.input.pointer.pointerMoveFilter
9 | import k5
10 | import math.map
11 | import kotlin.math.cos
12 | import kotlin.math.sin
13 |
14 | @OptIn(ExperimentalComposeUiApi::class)
15 | fun showRibbon() = k5 {
16 |
17 | var r = 50f
18 | var t = 0.0
19 | var dt = 0.0001
20 |
21 | noLoop()
22 | show(
23 | modifier = Modifier.background(Color(0xFFE6E6FA)).pointerMoveFilter(onMove = {
24 | r = map(it.x, 0f, dimensFloat.width, 10f, 100f)
25 | false
26 | })
27 | ) {
28 | it.apply {
29 |
30 | for (i in 0 until 150000) {
31 | val delta = 2.0 * r * cos(4.0 * dt * t) + r * cos(1.0 * t)
32 | val blue = 2.0 * r * sin(t) - r * cos(3.0 * dt * t)
33 | val color = Color(delta.toInt(), blue.toInt(), 100, 10)
34 |
35 | val x = 2.0 * r * sin(2.0 * dt * t) + r * cos(1.0 * t * dt) + this.size.width / 2
36 | val y = 2.0 * r * sin(1.0 * dt * t) - r * sin(5.0 * t) + this.size.height / 2
37 | drawCircle(color, 1.0f, Offset(x.toFloat(), y.toFloat()))
38 |
39 | t += 0.01
40 | dt += 0.1
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/noise/blackhole.kt:
--------------------------------------------------------------------------------
1 | package examples.noise
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import k5
6 | import math.AngleMode
7 | import math.angleMode
8 | import math.cos
9 | import math.noise3D
10 | import math.random
11 | import math.sin
12 | import kotlin.math.pow
13 | import kotlin.math.sqrt
14 |
15 | fun showBlackHole() = k5 {
16 |
17 | noLoop()
18 | angleMode = AngleMode.DEGREES
19 |
20 | val circleNumbers = 200
21 | val circleGap = 0.01f
22 | var i = 0
23 | show {
24 |
25 | it.apply {
26 | while (i <= circleNumbers) {
27 | val radius = (this.size.width / 10) + i * 0.05f
28 | val k = (0.5f..1f).random() * sqrt(1.0 * i / circleNumbers).toFloat()
29 | val noisiness = 400f * (1.0f * i / circleNumbers).pow(2)
30 |
31 | var theta = 0f
32 | var bx = 0f
33 | var by = 0f
34 | while (theta < 361f) {
35 | val r1 = theta.cos()
36 | val r2 = theta.sin()
37 | val r = radius + noise3D(1.0 * k * r1, 1.0 * k * r2, 1.0 * i * circleGap).toFloat() * noisiness
38 |
39 | val x = this.size.width / 2 + r * theta.cos()
40 | val y = this.size.height / 2 + r * theta.sin()
41 |
42 | if (bx == 0f && by == 0f) {
43 | bx = x
44 | by = y
45 | }
46 |
47 | drawLine(Color(0xfff8a614), Offset(bx, by), Offset(x, y), 0.4f)
48 | bx = x
49 | by = y
50 | theta += 1.0f
51 | }
52 | i++
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/noise/classic-yarns.kt:
--------------------------------------------------------------------------------
1 | package examples.noise
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.Path
8 | import androidx.compose.ui.graphics.drawscope.Stroke
9 | import k5
10 | import math.noise1D
11 |
12 | fun showYarns() = k5 {
13 |
14 | noLoop()
15 | show(modifier = Modifier.fillMaxSize().background(Color(0xFFFFA500))) {
16 | it.apply {
17 | var offset = 0.0
18 | for (i in 0 until 3000) {
19 | println("$dimensFloat")
20 | println("size in canvas ${it.size}")
21 | val x0 = 2 * dimensFloat.width * noise1D(offset + 15)
22 | val x1 = 2 * dimensFloat.width * noise1D(offset + 25)
23 | val x2 = 2 * dimensFloat.width * noise1D(offset + 35)
24 | val x3 = 2 * dimensFloat.width * noise1D(offset + 45)
25 | val y0 = 2 * dimensFloat.height * noise1D(offset + 55)
26 | val y1 = 2 * dimensFloat.height * noise1D(offset + 65)
27 | val y2 = 2 * dimensFloat.height * noise1D(offset + 75)
28 | val y3 = 2 * dimensFloat.height * noise1D(offset + 85)
29 | val path = Path()
30 | path.moveTo(x0.toFloat(), y0.toFloat())
31 | path.cubicTo(
32 | x1.toFloat(), y1.toFloat(),
33 | x2.toFloat(), y2.toFloat(),
34 | x3.toFloat(), y3.toFloat()
35 | )
36 | drawPath(path, Color.Black, alpha = 0.5f, style = Stroke(width = 0.3f))
37 | offset += 0.002
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/noise/noise-walker.kt:
--------------------------------------------------------------------------------
1 | package examples.noise
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.PointMode
5 | import androidx.compose.ui.graphics.drawscope.DrawScope
6 | import androidx.compose.ui.unit.IntSize
7 | import k5
8 | import math.TWO_PI
9 | import math.Vector2D
10 | import math.k5Random
11 | import math.limit
12 | import math.noise2D
13 | import math.plusAssign
14 | import math.setMag
15 | import math.timesAssign
16 | import math.toOffSet
17 |
18 | var inc = 0.1f
19 | var scl = 10
20 |
21 | data class Particle(val dimens: IntSize) {
22 |
23 | val position = Vector2D(k5Random(max = dimens.width), k5Random(max = dimens.height))
24 | val velocity = Vector2D(0f, 0f)
25 | val acceleration = Vector2D(0f, 0f)
26 | val maxSpeed = 4f
27 | var skipPoint = false
28 | val points = mutableListOf(position.toOffSet())
29 |
30 | fun update() {
31 | velocity += acceleration
32 | velocity.limit(maxSpeed)
33 | position += velocity
34 | acceleration *= 0f
35 | }
36 |
37 | fun follow(force: Vector2D) {
38 | val x = position.x / scl
39 | val y = position.y / scl
40 | applyForce(force)
41 | }
42 |
43 | private fun applyForce(force: Vector2D) {
44 | acceleration += force
45 | }
46 |
47 | fun render(drawScope: DrawScope) {
48 | points.add(position.toOffSet())
49 | drawScope.drawPoints(points, pointMode = PointMode.Polygon, Color.White)
50 | }
51 |
52 | fun edges() {
53 | skipPoint = false
54 | if (position.x > dimens.width) {
55 | position.x = 0f
56 | skipPoint = true
57 | }
58 | if (position.x < 0) {
59 | position.x = dimens.width.toFloat()
60 | skipPoint = true
61 | }
62 | if (position.y > dimens.height) {
63 | position.y = 0f
64 | skipPoint = true
65 | }
66 | if (position.y < 0) {
67 | position.y = dimens.height.toFloat()
68 | skipPoint = true
69 | }
70 | }
71 | }
72 |
73 | fun simplexNoise() = k5 {
74 |
75 | val particle = Particle(dimensInt)
76 | var yoff = 0f
77 | var xoff = 0f
78 | show { drawScope ->
79 | val angle = noise2D(x = xoff.toDouble(), y = yoff.toDouble()) * TWO_PI + 4
80 | val v = Vector2D.fromAnAngle(angle.toFloat())
81 | v.setMag(1f)
82 | xoff += inc
83 | yoff += inc
84 |
85 | particle.apply {
86 | follow(v)
87 | update()
88 | // it.edges()
89 | render(drawScope)
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/particles/Rain.kt:
--------------------------------------------------------------------------------
1 | package examples.particles
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.Color
7 | import k5
8 | import math.PI
9 | import math.TAU
10 | import kotlin.math.tan
11 | import kotlin.random.Random
12 |
13 | fun showRain() = k5 {
14 |
15 | val N = 200
16 | var t = 0f
17 | val r = List(N) { Random.nextFloat() }
18 |
19 | show(modifier = Modifier.background(Color(0xFFE8FFFF))) {
20 |
21 | t += 0.002f
22 | for (i in 0 until N) {
23 | val x = dimensFloat.width * r[(i + 2) % N]
24 | val y = (tan(-PI / 2 + TAU * (t + r[i])) * 10 + dimensFloat.width * (1 - r[(i + 1) % N])).toFloat()
25 | val radius = 10 * r[(i + 3) % N]
26 | it.drawCircle(
27 | Color(0xff3498db),
28 | center = Offset(
29 | x = x,
30 | y = y
31 | ),
32 | radius = radius,
33 | alpha = 0.7f
34 | )
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/particles/StarField.kt:
--------------------------------------------------------------------------------
1 | package examples.particles
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.drawscope.DrawScope
7 | import androidx.compose.ui.graphics.drawscope.translate
8 | import k5
9 | import math.map
10 | import math.random
11 |
12 | data class Star(val dimens: Size) {
13 | val width = dimens.width
14 | val height = dimens.height
15 | var x = (-width..width).random()
16 | var y = (-height..height).random()
17 | var z = (0f..width).random()
18 | var pz = z
19 |
20 | fun update() {
21 | z -= 20
22 | if (z < 1) {
23 | z = dimens.width
24 | x = (-width..width).random()
25 | y = (-height..height).random()
26 | pz = z
27 | }
28 | }
29 |
30 | fun showStar(drawScope: DrawScope) {
31 | val sx = map(this.x / this.z, -1f, 1f, -dimens.width, dimens.width)
32 | val sy = map(this.y / this.z, -1f, 1f, -dimens.height, dimens.height)
33 | val r = map(this.z, 0f, dimens.width, 16f, 0f)
34 | drawScope.drawCircle(Color.White, r, Offset(sx, sy))
35 |
36 | val px = map(this.x / this.pz, -1f, 1f, -dimens.width, dimens.width)
37 | val py = map(this.y / this.pz, -1f, 1f, -dimens.height, dimens.height)
38 | val stroke = map(this.pz, 0f, dimens.width, 16f, 1f)
39 | pz = z
40 | drawScope.drawLine(Color.White, Offset(px, py), Offset(sx, sy), strokeWidth = stroke)
41 | }
42 | }
43 |
44 | fun showStarField() = k5 {
45 | val stars = List(600) { Star(this.size) }
46 |
47 | show {
48 | it.translate(dimensFloat.width / 2, dimensFloat.height / 2) {
49 | for (star in stars) {
50 | star.update()
51 | star.showStar(it)
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/particles/fireworks.kt:
--------------------------------------------------------------------------------
1 | package examples.particles
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.ExperimentalGraphicsApi
7 | import androidx.compose.ui.graphics.drawscope.DrawScope
8 | import k5
9 | import math.Vector2D
10 | import math.map
11 | import math.multiply
12 | import math.plusAssign
13 | import math.random
14 | import math.toOffSet
15 | import kotlin.random.Random
16 |
17 | val gravity = Vector2D(0f, 0.2f)
18 |
19 | data class FireWorkParticle(
20 | val x: Float,
21 | val y: Float,
22 | val hu: Float,
23 | val isFireWork: Boolean
24 | ) {
25 | val position = Vector2D(x, y)
26 | var lifespan = 255f
27 | val acceleration = Vector2D(0f, 0f)
28 | val velocity: Vector2D
29 |
30 | init {
31 | if (isFireWork) {
32 | velocity = Vector2D(0f, (-15f..-18f).random())
33 | } else {
34 | velocity = Vector2D.randomVector()
35 | velocity.multiply((2f..10f).random())
36 | }
37 | }
38 |
39 | fun applyForece(force: Vector2D) {
40 | acceleration += force
41 | }
42 |
43 | fun update() {
44 | if (!isFireWork) {
45 | velocity.multiply(0.9f)
46 | lifespan -= 4
47 | }
48 | velocity += acceleration
49 | position += velocity
50 | acceleration.multiply(0f)
51 | }
52 |
53 | fun isDone() = lifespan < 0
54 |
55 | @ExperimentalGraphicsApi
56 | fun show(drawScope: DrawScope) {
57 | if (!isFireWork) {
58 | val alpha = map(lifespan, 255f, 1f, 1f, 0f)
59 | drawScope.drawCircle(Color.hsv(hu, 1f, 1f), 5f, position.toOffSet(), alpha = alpha)
60 | } else {
61 | drawScope.drawRect(Color.hsv(hu, 1f, 1f), position.toOffSet(), Size(5f, 10f))
62 | val offset = Offset(position.x + 2f, position.y)
63 | drawScope.drawCircle(Color.hsv(hu, 1f, 1f), 5f, offset)
64 | }
65 | }
66 | }
67 |
68 | data class FireWork(val dimens: Size) {
69 | val hue = (0f..360f).random()
70 | val firework = FireWorkParticle((1f..dimens.width).random(), dimens.height, hue, true)
71 | var isExploded = false
72 | val particles = mutableListOf()
73 |
74 | fun isDone() = isExploded && particles.isEmpty()
75 |
76 | fun update() {
77 | if (!isExploded) {
78 | firework.applyForece(gravity)
79 | firework.update()
80 |
81 | if (firework.velocity.y >= 0f) {
82 | isExploded = true
83 | explode()
84 | }
85 | }
86 |
87 | if (particles.isNotEmpty()) {
88 | for (i in particles.size - 1 downTo 0) {
89 | particles[i].applyForece(gravity)
90 | particles[i].update()
91 |
92 | particles.removeAll { it.isDone() }
93 | }
94 | }
95 | }
96 |
97 | fun explode() {
98 | repeat(100) {
99 | val p = FireWorkParticle(firework.position.x, firework.position.y, hue, false)
100 | particles.add(p)
101 | }
102 | }
103 |
104 | @ExperimentalGraphicsApi
105 | fun show(drawScope: DrawScope) {
106 | if (!isExploded) {
107 | firework.show(drawScope)
108 | }
109 | particles.forEach {
110 | it.show(drawScope)
111 | }
112 | }
113 | }
114 |
115 | @ExperimentalGraphicsApi
116 | fun showFireWorks() = k5 {
117 |
118 | val fireworks = MutableList(10) { FireWork(dimensFloat) }
119 |
120 | show { drawScope ->
121 |
122 | if (Random.nextFloat() < 0.04f) {
123 | fireworks.add(FireWork(dimensFloat))
124 | }
125 |
126 | for (i in (fireworks.size - 1) downTo 0) {
127 | fireworks[i].update()
128 | fireworks[i].show(drawScope)
129 |
130 | fireworks.removeAll { it.isDone() }
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/particles/particles-js-simulation.kt:
--------------------------------------------------------------------------------
1 | package examples.particles
2 |
3 | import androidx.compose.material.Slider
4 | import androidx.compose.material.Text
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.mutableStateListOf
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.ui.geometry.Size
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.drawscope.DrawScope
11 | import k5
12 | import math.Vector2D
13 | import math.distance
14 | import math.divide
15 | import math.multiply
16 | import math.plusAssign
17 | import math.random
18 | import math.toOffSet
19 | import kotlin.math.abs
20 |
21 | class Particle(val dimension: Size, val velocityFactor: State, val distance: State) {
22 |
23 | val position = Vector2D(
24 | (0f..dimension.width).random(),
25 | (0f..dimension.height).random()
26 | )
27 |
28 | var r = (1f..8f).random()
29 |
30 | var velocity = Vector2D(
31 | (-2.0f..2.0f).random(),
32 | (-1.0f..1.5f).random()
33 | )
34 |
35 | fun createParticle(drawScope: DrawScope) {
36 | drawScope.drawCircle(Color.White, r, position.toOffSet())
37 | }
38 |
39 | fun moveParticle() {
40 | velocity.multiply(velocityFactor.value)
41 | if (position.x < 0f || position.x > dimension.width) {
42 | velocity.x *= -1
43 | }
44 | if (position.y < 0f || position.y > dimension.height) {
45 | velocity.y *= -1
46 | }
47 | position += velocity
48 | velocity.divide(velocityFactor.value)
49 | }
50 |
51 | fun joinParticles(drawScope: DrawScope, particles: List) {
52 | particles.forEach {
53 | val dist = this.position.distance(it.position)
54 | if (dist < distance.value) {
55 | drawScope.drawLine(Color.Cyan, this.position.toOffSet(), it.position.toOffSet(), alpha = 0.5f)
56 | }
57 | }
58 | }
59 | }
60 |
61 | fun particleJs() = k5 {
62 |
63 | var numberOfParticles = mutableStateOf(50)
64 | val velocityFactor = mutableStateOf(1f)
65 | val distance = mutableStateOf(100f)
66 | val particles = mutableStateListOf()
67 | repeat(numberOfParticles.value) {
68 | particles.add(Particle(dimensFloat, velocityFactor, distance))
69 | }
70 |
71 | showWithControls(controls = {
72 | Text("Number of particles")
73 | Slider(
74 | value = numberOfParticles.value.toFloat(),
75 | onValueChange = { numberOfParticles.value = it.toInt() },
76 | valueRange = 50f..200f,
77 | onValueChangeFinished = {
78 | if (numberOfParticles.value < particles.size) {
79 | repeat(particles.size - numberOfParticles.value) {
80 | particles.removeLast()
81 | }
82 | } else {
83 | repeat(abs(numberOfParticles.value - particles.size)) {
84 | particles.add(Particle(dimensFloat, velocityFactor, distance))
85 | }
86 | }
87 | }
88 | )
89 | Text("Velocity")
90 | Slider(
91 | value = velocityFactor.value,
92 | onValueChange = { velocityFactor.value = it },
93 | valueRange = 1f..5f
94 | )
95 | Text("Lines")
96 | Slider(
97 | value = distance.value,
98 | onValueChange = { distance.value = it },
99 | valueRange = 50f..200f
100 | )
101 | }) { drawScope ->
102 | particles.forEachIndexed { index, particle ->
103 | particle.createParticle(drawScope)
104 | particle.moveParticle()
105 | particle.joinParticles(drawScope, particles.slice(0 until index))
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/shm/simpleHarmonicMotion.kt:
--------------------------------------------------------------------------------
1 | package examples.shm
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.drawscope.translate
6 | import k5
7 | import math.TWO_PI
8 | import math.map
9 | import kotlin.math.sin
10 |
11 | fun simulateSineWave() = k5 {
12 |
13 | val r = 6
14 | val total = dimensInt.width / (r * 2)
15 | val angles = MutableList(total) {
16 | map(it.toFloat(), 0f, total.toFloat(), 0f, 2 * TWO_PI.toFloat())
17 | }
18 |
19 | show { drawScope ->
20 | drawScope.translate(500f, 500f) {
21 | for (i in 0 until angles.size) {
22 | val y = map(sin(angles[i]), -1f, 1f, -400f, 400f)
23 | val x = map(i.toFloat(), 0f, angles.size.toFloat(), -400f, 400f)
24 | this.drawCircle(Color.Yellow, r.toFloat(), Offset(x, y))
25 | angles[i] += 0.02f
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/Chains.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.drawscope.DrawScope
8 | import androidx.compose.ui.input.pointer.pointerMoveFilter
9 | import k5
10 | import math.Vector2D
11 |
12 | val gravity = 5f
13 | val mass = 2f
14 |
15 | // TODO: WIP
16 | class Spring(
17 | var x: Float,
18 | var y: Float,
19 | ) {
20 |
21 | var vx = 0f // vx velocity on x
22 | var vy = 0f // vy velocity on y
23 |
24 | val radius = 60f
25 | val stiffness = 0.2f
26 | val damping = 0.3f
27 |
28 | fun update(targetX: Float, targetY: Float) {
29 | val force = (targetX - x) * stiffness
30 | val ax = force / mass
31 | vx = damping * (vx + ax)
32 | x += vx
33 |
34 | var forceY = (targetY - y) * stiffness
35 | forceY += gravity
36 | val ay = force / mass
37 | vy = damping * (vy + ay)
38 | y += vy
39 | }
40 |
41 | fun render(nx: Float, ny: Float, scope: DrawScope) {
42 | scope.drawLine(Color.White, Offset(x, y), Offset(nx, ny))
43 | scope.drawCircle(Color.White, radius, Offset(x, y), alpha = 0.4f)
44 | }
45 | }
46 |
47 | @OptIn(ExperimentalComposeUiApi::class)
48 | fun chainLoop() = k5 {
49 | val dimens = dimensFloat
50 | val l1 = Spring(0f, dimens.width / 2)
51 | val l2 = Spring(0f, dimens.width / 2)
52 |
53 | val mouse = Vector2D(0f, 0f)
54 | show(
55 | Modifier.pointerMoveFilter(
56 | onMove = {
57 | println("${it.x} ${it.y}")
58 | mouse.x = it.x
59 | mouse.y = it.y
60 | true
61 | }
62 | )
63 | ) { scope ->
64 | l1.update(mouse.x, mouse.y)
65 | l1.render(mouse.x, mouse.y, scope)
66 | // l2.update(l1.x, l1.y)
67 | // l2.render(l1.x, l1.y, scope)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/CircleGrid.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.animation.core.FastOutSlowInEasing
4 | import androidx.compose.ui.geometry.Offset
5 | import androidx.compose.ui.graphics.Brush
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.drawscope.Stroke
8 | import k5
9 | import math.map
10 |
11 | fun showCircleGrid() = k5 {
12 |
13 | val colorsList = listOf(Color.White, Color.Gray, Color.Magenta, Color.Cyan, Color.Blue, Color.Green, Color.Yellow, Color.Red)
14 |
15 | // This is not a design term of anything, this movement of contraction and expansion of muscle is called as peristalsis.
16 | // I'm just relating this here because circles are contracting and expanding. They say you must be very good at naming variables ;-)
17 | val peristalsisFactor = 0.5f + (1.75f - 0.5f)
18 |
19 | var scale = 0f
20 | var factor = 0f
21 | var reverse = false
22 |
23 | show {
24 | factor += 1000f
25 | if (factor > 400000f) {
26 | reverse = !reverse
27 | factor -= 400000f
28 | }
29 | val fraction = map(factor, 0f, 400000f, 0f, 1f)
30 | scale = if (reverse) {
31 | peristalsisFactor * FastOutSlowInEasing.transform(fraction)
32 | } else {
33 | peristalsisFactor * (1.0f - FastOutSlowInEasing.transform(fraction))
34 | }
35 |
36 | val radius = 50f
37 | val n = 25
38 | var cx: Float
39 | var cy = 0f
40 | for (row in 1..n) {
41 | cx = if (row % 2 == 0) {
42 | radius
43 | } else {
44 | 0f
45 | }
46 | for (col in 1..n) {
47 | it.drawCircle(
48 | brush = Brush.radialGradient(colorsList),
49 | radius = radius * scale,
50 | center = Offset(
51 | cx,
52 | cy
53 | ),
54 | style = Stroke(width = 3f)
55 | )
56 | cx += 2 * radius
57 | }
58 | cy += 2 * radius - 10f
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/Phyllotaxis.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.ExperimentalGraphicsApi
6 | import androidx.compose.ui.graphics.drawscope.translate
7 | import k5
8 | import math.cos
9 | import math.map
10 | import math.sin
11 | import math.toRadians
12 | import kotlin.math.sqrt
13 |
14 | @ExperimentalGraphicsApi
15 | fun showPhyllotaxis() = k5 {
16 | var n = 0
17 | val c = 6f
18 | var start = 0
19 |
20 | show {
21 | it.translate(dimensFloat.width / 2, dimensFloat.height / 2) {
22 | for (i in 0 until n) {
23 | val a = i * 137.5f.toRadians()
24 | val r = c * sqrt(i.toFloat())
25 | val x = r * a.cos()
26 | val y = r * a.sin()
27 | var hu = (start + i * 0.5f).sin()
28 | hu = map(hu, -1f, 1f, 0f, 360f)
29 | println(hu)
30 | this.drawCircle(Color.hsv(hu, 1f, 1f), 4f, Offset(x, y))
31 | }
32 | }
33 | n += 5
34 | start += 5
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/TenPrint.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import k5
6 | import kotlin.random.Random
7 |
8 | fun theTenPrint() = k5 {
9 |
10 | var x = 0f
11 | var y = 0f
12 | val spacing = 40f
13 |
14 | noLoop()
15 |
16 | show {
17 | while (y <= dimensFloat.height) {
18 | if (Random.nextFloat() < 0.5f) {
19 | it.drawLine(Color.Cyan, Offset(x, y), Offset(x + spacing, y + spacing))
20 | } else {
21 | it.drawLine(Color.White, Offset(x, y + spacing), Offset(x + spacing, y))
22 | }
23 |
24 | x += spacing
25 | if (x > dimensInt.width) {
26 | x = 0f
27 | y += spacing
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/gameOfLife.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.graphics.Color
6 | import k5
7 | import kotlin.math.floor
8 | import kotlin.random.Random
9 |
10 | /**
11 | * The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.[1]
12 | * It is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input.
13 | * One interacts with the Game of Life by creating an initial configuration and observing how it evolves.
14 | * It is Turing complete and can simulate a universal constructor or any other Turing machine.
15 | * - Wiki [https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life]
16 | *
17 | * You can read more about game of life and cellular automaton here -
18 | * https://natureofcode.com/book/chapter-7-cellular-automata/
19 | */
20 | class GameOfLife(dimensions: Size) {
21 |
22 | // val columns = floor(dimensions.height / 2).toInt()
23 | // val rows = floor(dimensions.height / 2).toInt()
24 | val columns = 35
25 | val rows = 35
26 |
27 | var board: MutableList> = MutableList(columns) { MutableList(rows) { 0f } }
28 | var nextGen: MutableList> = MutableList(columns) { MutableList(rows) { 0f } }
29 |
30 | init {
31 | for (i in 0 until columns) {
32 | for (j in 0 until rows) {
33 | // Lining the edges with 0s
34 | if (i == 0 || j == 0 || i == columns - 1 || j == rows - 1) {
35 | board[i][j] = 0f
36 | } else {
37 | // Filling the rest randomly
38 | board[i][j] = floor(Random.nextDouble(2.0).toFloat())
39 | }
40 | nextGen[i][j] = 0f
41 | }
42 | }
43 | }
44 |
45 | fun generate() {
46 | // Loop through every spot in our 2D array and check spots neighbors
47 | for (x in 1 until columns - 1) {
48 | for (y in 1 until rows - 1) {
49 | // Add up all the states in a 3x3 surrounding grid
50 | var neighbours = 0f
51 | for (i in -1..1) {
52 | for (j in -1..1) {
53 | neighbours += board[x + i][y + j]
54 | }
55 | }
56 | // subtract the current cell's state since we added it in the above loop
57 | neighbours -= board[x][y]
58 |
59 | // Rules of life
60 | /*
61 | * Game of life is cellular automaton- https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
62 | * Part of chaos theory
63 | */
64 | if ((board[x][y] == 1f) && (neighbours < 2f)) {
65 | // 1. Loneliness
66 | nextGen[x][y] = 0f
67 | } else if (board[x][y] == 1f && neighbours > 3f) {
68 | // 2. Overpopulation
69 | nextGen[x][y] = 0f
70 | } else if (board[x][y] == 0f && neighbours == 3f) {
71 | // 3. Reproduction
72 | nextGen[x][y] = 1f
73 | } else {
74 | // Stasis
75 | nextGen[x][y] = board[x][y]
76 | }
77 | }
78 | }
79 |
80 | // Swap with previous generation
81 | val temp = board
82 | board = nextGen
83 | nextGen = temp
84 | }
85 | }
86 |
87 | fun gameOfLife() = k5 {
88 |
89 | val gameOfLife = GameOfLife(dimensFloat)
90 | val w = 50f
91 |
92 | show { drawScope ->
93 | gameOfLife.generate()
94 | for (i in 0 until gameOfLife.columns) {
95 | for (j in 0 until gameOfLife.rows) {
96 | println("For [$i, $j] = ${gameOfLife.board[i][j]}")
97 | if (gameOfLife.board[i][j] == 1f) {
98 | // dont fill rect
99 | drawScope.drawRect(Color.Black, Offset(i * w, j * w), Size(w - 1, w - 1))
100 | } else {
101 | // fill rect
102 | drawScope.drawRect(Color.White, Offset(i * w, j * w), Size(w - 1, w - 1))
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/green-rain.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.graphics.Path
7 | import androidx.compose.ui.graphics.drawscope.DrawScope
8 | import androidx.compose.ui.graphics.drawscope.Stroke
9 | import androidx.compose.ui.graphics.drawscope.scale
10 | import androidx.compose.ui.graphics.drawscope.translate
11 | import k5
12 | import math.Vector2D
13 | import math.map
14 | import math.plusAssign
15 | import math.random
16 | import kotlin.random.Random
17 |
18 | // https://en.wikipedia.org/wiki/Cistercian_numerals
19 | val SYMBOL_PATH = arrayOf(
20 | arrayOf(0, 0), // 0
21 | arrayOf(0, 0, 1, 0), // 1
22 | arrayOf(0, 1, 1, 1), // 2
23 | arrayOf(0, 0, 1, 1), // 3
24 | arrayOf(0, 1, 1, 0), // 4
25 | arrayOf(0, 0, 1, 0, 0, 1), // 5
26 | arrayOf(1, 0, 1, 1), // 6
27 | arrayOf(0, 0, 1, 0, 1, 1), // 7
28 | arrayOf(0, 1, 1, 1, 1, 0), // 8
29 | arrayOf(0, 0, 1, 0, 1, 1, 0, 1), // 9
30 | )
31 |
32 | fun digitPath(n: Int): Path {
33 | val p = SYMBOL_PATH[n % 10]
34 | val path = Path()
35 | path.moveTo(p[0].toFloat(), p[1].toFloat())
36 | for (i in 2 until p.size step 2) {
37 | path.lineTo(p[i].toFloat(), p[i + 1].toFloat())
38 | }
39 | return path
40 | }
41 |
42 | /**
43 | * This symbol is Cistercian Symbol for integers
44 | * https://en.wikipedia.org/wiki/Cistercian_numerals
45 | */
46 | data class Symbol(
47 | val x: Float,
48 | val y: Float,
49 | val velocity: Vector2D,
50 | val maxLife: Float,
51 | val number: Int,
52 | val symbolSize: Float = (6f..12f).random(),
53 | val symbolColor: Color = Color.Green,
54 | ) {
55 |
56 | var lifetime = maxLife
57 | var alpha = 1f
58 | val position = Vector2D(x, y)
59 | fun update() {
60 | position += velocity
61 | lifetime -= 2f
62 | alpha = map(lifetime, 1f, maxLife, 0f, 1f)
63 | }
64 |
65 | fun isVanished() = lifetime < 0f
66 |
67 | fun drawSymbol(drawScope: DrawScope) {
68 | drawScope.translate(position.x, position.y) {
69 | drawLine(
70 | symbolColor,
71 | Offset(0f, 0f),
72 | Offset(0f, 3f * symbolSize),
73 | strokeWidth = 0.3f * symbolSize,
74 | alpha = alpha
75 | )
76 |
77 | translate(0f, 0f) {
78 | scale(1f * symbolSize, 1f * symbolSize, Offset(0f, 0f)) {
79 | val path1 = digitPath(number % 10)
80 | drawPath(path1, symbolColor, style = Stroke(0.3f), alpha = alpha)
81 | }
82 |
83 | scale(-1f * symbolSize, 1f * symbolSize, Offset(0f, 0f)) {
84 | val path2 = digitPath(number / 10 % 10)
85 | drawPath(path2, symbolColor, style = Stroke(0.3f), alpha = alpha)
86 | }
87 | }
88 | translate(0f, 3f * symbolSize) {
89 | scale(1f * symbolSize, -1f * symbolSize, Offset(0f, 0f)) {
90 | val path3 = digitPath(number / 100 % 10)
91 | drawPath(path3, symbolColor, style = Stroke(0.3f), alpha = alpha)
92 | }
93 |
94 | scale(-1f * symbolSize, -1f * symbolSize, Offset(0f, 0f)) {
95 | val path4 = digitPath(number / 1000 % 10)
96 | drawPath(path4, symbolColor, style = Stroke(0.3f), alpha = alpha)
97 | }
98 | }
99 | }
100 | }
101 | }
102 |
103 | /**
104 | * Symbol emitter emits those Cistercian symbols
105 | */
106 | data class SymbolEmitter(val dimens: Size, val emitterX: Float) {
107 |
108 | val symbolList = mutableListOf()
109 | val maxLife = (200f..400f).random()
110 | val velocity = Vector2D(0f, (15f..20f).random())
111 |
112 | private fun addSymbol() {
113 | symbolList.add(
114 | Symbol(
115 | emitterX, 0f,
116 | velocity,
117 | maxLife,
118 | (10..100).random()
119 | )
120 | )
121 | }
122 |
123 | fun update() {
124 | for (i in symbolList.size - 1 downTo 0) {
125 | symbolList[i].update()
126 | symbolList.removeAll { it.isVanished() }
127 | }
128 |
129 | if (symbolList.size < 15) {
130 | if (Random.nextFloat() > 0.3f) {
131 | addSymbol()
132 | }
133 | }
134 | }
135 |
136 | fun draw(drawScope: DrawScope) {
137 | symbolList.forEach {
138 | it.drawSymbol(drawScope)
139 | }
140 | }
141 | }
142 |
143 | fun showMatrixGreenRain() = k5 {
144 |
145 | var positionx = 0f
146 | val symbolEmitterList = List(25) {
147 | if (positionx < dimensFloat.width) {
148 | positionx += (20f..60f).random()
149 | }
150 | SymbolEmitter(dimensFloat, positionx)
151 | }
152 |
153 | show { drawScope ->
154 | symbolEmitterList.forEach {
155 | it.update()
156 | it.draw(drawScope)
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/hearts.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.Path
6 | import androidx.compose.ui.graphics.drawscope.translate
7 | import k5
8 |
9 | fun showHearts() = k5 {
10 |
11 | var factor = 0f
12 | val scale = 2.0f
13 | var index = 0
14 | val n = 18
15 |
16 | val colors = listOf(
17 | Color(0xFFE03776),
18 | Color(0xFF8F3E98),
19 | Color(0xFF4687BF),
20 | Color(0xFF3BAB6F),
21 | Color(0xFFF9C25E),
22 | Color(0xFFF47274),
23 | )
24 |
25 | show {
26 |
27 | factor += 0.04f
28 | if (factor > scale) {
29 | factor -= scale
30 | index += 1
31 | }
32 |
33 | it.apply {
34 | translate(dimensFloat.width / 2, dimensFloat.height / 2) {
35 | for (i in n downTo 0) {
36 | val ithscale = factor + i * scale
37 | val path = Path()
38 | path.moveTo(0.0f * ithscale, 12.0f * ithscale)
39 | path.cubicTo(
40 | 50.0f * ithscale,
41 | -30.0f * ithscale,
42 | 110.0f * ithscale,
43 | 50.0f * ithscale,
44 | 0.0f * ithscale,
45 | 120.0f * ithscale
46 | )
47 | path.cubicTo(
48 | -110.0f * ithscale,
49 | 50.0f * ithscale,
50 | -50.0f * ithscale,
51 | -30.0f * ithscale,
52 | 0.0f * ithscale,
53 | 12.0f * ithscale
54 | )
55 | path.close()
56 | path.translate(Offset(0.0f, -69.0f * ithscale))
57 |
58 | drawPath(path, colors[(index + (colors.size - i % colors.size)) % colors.size])
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/recursive-tree.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.compose.ui.ExperimentalComposeUiApi
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.drawscope.DrawScope
9 | import androidx.compose.ui.graphics.drawscope.rotate
10 | import androidx.compose.ui.graphics.drawscope.translate
11 | import androidx.compose.ui.input.pointer.pointerMoveFilter
12 | import k5
13 |
14 | /**
15 | * Recursive Fractal tree
16 | * Inspired from https://github.com/V9vek/Fractal-Trees
17 | */
18 | // Implementation logic may differ
19 | @OptIn(ExperimentalComposeUiApi::class)
20 | fun showRecursiveTree() = k5 {
21 |
22 | noLoop()
23 |
24 | val degrees = mutableStateOf(45f)
25 |
26 | show(
27 | modifier = Modifier.pointerMoveFilter(onMove = {
28 | degrees.value = (it.x / dimensFloat.width) * 90f
29 | true
30 | })
31 | ) {
32 | it.apply {
33 | translate(this.size.width / 2, this.size.height) {
34 | // Draw a line
35 | drawLine(Color.White, start = Offset(0f, 0f), end = Offset(0f, -250f))
36 | // Move to the end of that line
37 | translate(0f, -250f) {
38 | // Start the recursive branching
39 | branch(250f, degrees.value)
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
46 | fun DrawScope.branch(height: Float, degree: Float) {
47 | // Each branch will be 2/3rds the size of the previous one
48 | val h = height * 0.66f
49 |
50 | if (h > 2) {
51 | // Rotate by degree
52 | rotate(degree, Offset.Zero) {
53 | // Draw the branch
54 | drawLine(Color.White, start = Offset(0f, 0f), end = Offset(0f, -h))
55 | // Move to the end of the branch
56 | translate(0f, -h) {
57 | // call again to draw two new branches
58 | branch(h, degree)
59 | }
60 | }
61 |
62 | // Repeat the same thing, only branch off to the "left"
63 | rotate(-degree, Offset.Zero) {
64 | drawLine(Color.White, start = Offset(0f, 0f), end = Offset(0f, -h))
65 | translate(0f, -h) {
66 | branch(h, degree)
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/rotatingsquares.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.material.Slider
5 | import androidx.compose.material.Text
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.geometry.Offset
9 | import androidx.compose.ui.geometry.Size
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.SolidColor
12 | import androidx.compose.ui.graphics.drawscope.rotate
13 | import androidx.compose.ui.graphics.drawscope.translate
14 | import androidx.compose.ui.unit.Dp
15 | import androidx.compose.ui.unit.dp
16 | import k5
17 |
18 | data class Rectangles(
19 | val size: Dp,
20 | val offsetX: Dp,
21 | val offsetY: Dp,
22 | val color: Color
23 | )
24 |
25 | fun showRotatingSquares() = k5 {
26 |
27 | val rectList = mutableListOf()
28 | val n = mutableStateOf(35f)
29 | val rectSize = mutableStateOf(15.dp)
30 | var angle = 0f
31 |
32 | val colorList = listOf(
33 | Color(0xffffeaa7),
34 | Color(0xfffab1a0),
35 | Color(0xffa29bfe),
36 | )
37 |
38 | showWithControls(controls = {
39 | Text("Number of squares")
40 | Slider(
41 | value = n.value,
42 | valueRange = 10f..40f,
43 | onValueChange = { n.value = it },
44 | modifier = Modifier.fillMaxWidth()
45 | )
46 | }) {
47 |
48 | for (i in 0..n.value.toInt()) {
49 | val s = rectSize.value * i
50 | val offsetX = -s / 2
51 | val offsetY = -s / 2
52 | val rect = Rectangles(s, offsetX, offsetY, colorList[i % colorList.size])
53 | rectList.add(rect)
54 | }
55 |
56 | it.translate(dimensFloat.width / 2, dimensFloat.height / 2) {
57 | for (i in n.value.toInt() downTo 0) {
58 | this.rotate(angle * (n.value.toInt() - i + 1) * 0.07f, Offset(0f, 0f)) {
59 | drawRect(
60 | brush = SolidColor(rectList[i].color),
61 | Offset(rectList[i].offsetX.toPx(), rectList[i].offsetY.toPx()),
62 | Size(rectList[i].size.toPx(), rectList[i].size.toPx()),
63 | alpha = 0.7f
64 | )
65 | }
66 | }
67 | }
68 |
69 | angle += 0.5f
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/simulations/wavemaker.kt:
--------------------------------------------------------------------------------
1 | package examples.simulations
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.Brush
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.drawscope.withTransform
9 | import androidx.compose.ui.input.pointer.pointerMoveFilter
10 | import k5
11 | import math.map
12 |
13 | @OptIn(ExperimentalComposeUiApi::class)
14 | fun showWaveMaker() = k5 {
15 |
16 | var angle = 0f
17 | var pointer = 0f
18 | val colorsList = listOf(Color.White, Color.Gray, Color.Cyan, Color.Blue, Color.Green, Color.Yellow, Color.Red)
19 |
20 | show(
21 | modifier = Modifier.pointerMoveFilter(
22 | onMove = {
23 | pointer = map(it.x, 0f, dimensFloat.width, 1f, 30f)
24 | true
25 | }
26 | )
27 | ) {
28 | val n = 25
29 | val circleRadius = (dimensFloat.width / n + n / 3)
30 | val spacing = circleRadius + n / 3
31 | val dotRadius = circleRadius / 7
32 | val diff = spacing - circleRadius
33 | var offsetX = 0f
34 | var offsetY = 0f
35 | for (row in 0..n) {
36 | for (col in 0..n) {
37 | // it.drawCircle(
38 | // SolidColor(Color.White),
39 | // radius = circleRadius,
40 | // center = Offset(offsetX, offsetY),
41 | // style = Stroke(width = 3f),
42 | // alpha = 0.3f
43 | // )
44 | it.withTransform({
45 | rotate(
46 | (angle + (row * col) + (col + row) * pointer) % 360,
47 | Offset(offsetX, offsetY - diff - circleRadius)
48 | )
49 | }) {
50 | drawCircle(
51 | brush = Brush.linearGradient(colorsList),
52 | radius = dotRadius,
53 | center = Offset(offsetX - diff, offsetY - diff)
54 | )
55 | }
56 | offsetX += spacing
57 | }
58 | offsetX = 0f
59 | offsetY += spacing
60 | }
61 | angle += 0.01f * 360f
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/kotlin/examples/vectors/RandomWalkers.kt:
--------------------------------------------------------------------------------
1 | package examples.vectors
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.PointMode
9 | import androidx.compose.ui.graphics.StrokeCap
10 | import k5
11 | import math.Vector2D
12 | import math.multiply
13 | import math.plusAssign
14 | import math.setMag
15 | import math.toOffSet
16 | import kotlin.random.Random
17 |
18 | /**
19 | * Simple random walker example
20 | *
21 | * If you want to check out the math and how it works,
22 | * I would recommend watching Nature of code playlist by Daniel Shiffman
23 | * https://youtube.com/playlist?list=PLRqwX-V7Uu6ZV4yEcW3uDwOgGXKUUsPOM
24 | *
25 | * This is just a random walker which walks along 4 directions
26 | * left, right, up, down
27 | */
28 | fun simpleRandomWalk() = k5 {
29 |
30 | var x = 400f
31 | var y = 400f
32 | val points = mutableListOf(Offset(x, y))
33 |
34 | show(modifier = Modifier.fillMaxSize().background(Color.Black)) { drawScope ->
35 | val r = Random.nextInt(0, 5)
36 | when (r) {
37 | 0 -> x += 5
38 | 1 -> x -= 5
39 | 2 -> y += 5
40 | 4 -> y -= 5
41 | }
42 |
43 | points.add(Offset(x, y))
44 | drawScope.drawPoints(
45 | points, pointMode = PointMode.Lines, Color.Yellow,
46 | strokeWidth = 8f,
47 | cap = StrokeCap.Square
48 | )
49 | }
50 | }
51 |
52 | /**
53 | * Levy Flight simulation [Wiki](https://en.wikipedia.org/wiki/L%C3%A9vy_flight)
54 | */
55 | fun levyFlightWalker() = k5 {
56 |
57 | // create a position vector
58 | val position = Vector2D(400f, 400f)
59 | // add position vector to points
60 | val points = mutableListOf(position.toOffSet())
61 |
62 | show { scope ->
63 | // create a random vector
64 | val step = Vector2D.randomVector()
65 | // scalar multiply it by some random factor
66 |
67 | val r = Random.nextInt(100)
68 |
69 | if (r < 1) {
70 | // out of 100 there's 1% chance of step being miltiplied by a random value between 1 -20
71 | // This will make walker jump
72 | step.multiply(Random.nextInt(25, 100).toFloat())
73 | } else {
74 | // we don't want to show the jumping of walker randomly.
75 | // Set the magnitude of the step size some constant
76 | step.setMag(2f)
77 | }
78 | /* If you want to make walker jump randomly,
79 | you can just use any random scalar to multiply with step vector above^*/
80 |
81 | // add the step vector to position vector
82 | position += step
83 | // add new position to points
84 | points.add(position.toOffSet())
85 |
86 | // draw all the points as a single line
87 | scope.drawPoints(
88 | points, pointMode = PointMode.Polygon, Color.White,
89 | strokeWidth = 5f,
90 | cap = StrokeCap.Square
91 | )
92 |
93 | // Keeping the size of points fixed, you can remove this if you want to keep the k5 drawing the lines
94 | // for all points
95 | if (points.size > 200) {
96 | points.removeFirst()
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------